import { DIRECTION_SECTION_ICON, SPINE_SECTION_ICON } from "#configuration/icons/architecture.icons"; import { DIRECTION_SECTION_ID, SPINE_SECTION_ID } from "#core/ids/architecture.ids"; import type { Section } from "#types/document.types"; import { TYPESCRIPT_LANGUAGE } from "#configuration/constants/code.constants"; const LAYER_SHAPE = 'export const SPINE = ["domain", "application", "processing", "runtime", "infrastructure", "operations", "product"] as const;\nexport type Layer = (typeof SPINE)[number];\n\nexport const TIERS = ["engine", "consumer"] as const;\nexport type Tier = (typeof TIERS)[number];\n\nexport interface Classification {\n readonly concern: Concern;\n readonly layer: Layer;\n}\n\nexport interface Direction {\n readonly byPrefix: Readonly>;\n readonly overrides: Readonly>;\n}\n\nexport const tierOf = (path: string, direction: Direction): Tier | null =>\n direction.overrides[path] ?? direction.byPrefix[prefixOf(path)] ?? null;\n\nexport const crossesUpward = (from: string, to: string, direction: Direction): boolean =>\n tierOf(from, direction) === "engine" && tierOf(to, direction) === "consumer";'; const SPINE_DIAGRAM = 'block-beta\n columns 1\n domain["domain · what the system is about"]\n application["application · what it does with that"]\n processing["processing · how data is transformed"]\n runtime["runtime · what runs and when"]\n infrastructure["infrastructure · what everything else stands on"]\n operations["operations · how it is observed and kept alive"]\n product["product · what a person meets"]'; const CONCERN_DIAGRAM = 'flowchart LR\n converter["converter"] --> processing["processing"]\n validator["validator"] --> processing\n registry["registry"] --> infrastructure["infrastructure"]\n factory["factory"] --> infrastructure\n view["view"] --> product["product"]\n renderer["renderer"] --> product\n model["model"] --> domain["domain"]\n policy["policy"] --> domain'; const DIRECTION_DIAGRAM = 'flowchart TB\n engine["The engine tier · knows nothing about any consumer"]\n consumer["The consumer tier · depends on the engine"]\n consumer -- imports --> engine\n engine -. never .-> consumer\n prefix["Classified by container prefix"]\n override["Two trees a prefix cannot decide · classified per file"]\n prefix --> engine\n prefix --> consumer\n override --> engine\n override --> consumer'; const REPAIR_DIAGRAM = 'flowchart LR\n edge["An edge from the engine toward a consumer"]\n trick["A trick · lazy import, direct path, re-export, sort order"]\n move["A move · to the module whose layer and entry point satisfy every dependency"]\n lies["The graph passes and the structure stays wrong"]\n holds["The graph and the structure agree"]\n edge -. tempting .-> trick --> lies\n edge --> move --> holds'; const SPINE_SECTION: Section = { icon: SPINE_SECTION_ICON, id: SPINE_SECTION_ID, intro: "Systems decompose along one spine: domain, application, processing, runtime, infrastructure, operations, product, as the spine draws and concerns to layers tags. The spine is a classification axis and says nothing about who may import whom, which the direction axis holds; placement is a grammar on the methodology page is the practice that parses a tree against it.", subsections: [ { blocks: [ { application: "Tag every concern in the vocabulary to one layer of the spine, and let a file's layer follow from its concern. Classify a file by reading what it does, never by the folder it happens to sit in, and where a file fits two concerns equally well treat it as two files rather than as a tie to break. Hold the tagging in data a check reads, so a layer is a derivation from the concern and never a fact somebody remembers.", boundary: "The spine orders kinds of thing and never orders importance. A product-layer file is not lower than a domain-layer file, and a layer is never a folder: two files in one concern folder sit on the same layer because their concern does, whatever the folder above them is called.", cause: "A layer inferred from a folder name changes when the folder is renamed, and a layer inferred from a file's importance is argued at every review, so only a layer derived from the concern stays true without attention.", decision: "Derive the layer from the concern rather than from the folder or from importance.", failureMode: "A converter sits in a folder named for the feature it serves, the feature is renamed, and every rule that keyed on the folder now sees a file of no layer at all.", kind: "lesson", principle: "The layer spine classifies what a file is, and a file's layer is read from its concern.", problem: "Layering is usually a diagram and a hope: nobody can say which layer a given file is on, because the layer was never derived from anything the file declares.", validation: "Take a file and derive its layer from its concern tag alone, without opening it. A file whose layer cannot be derived is outside the model, and a file whose derived layer surprises you is misclassified, or is two files.", }, { caption: "the spine", kind: "mermaid", text: SPINE_DIAGRAM }, ], title: "Classification", }, { blocks: [ { kind: "text", text: "Every layered architecture has to answer what makes a thing belong to a layer, and most answer it by folder. That is a rule about placement and none about kind, so the layer of a file is whatever its author believed on the day. Clean architecture and hexagonal architecture answer the direction question well and leave this one to taste.", }, { kind: "text", text: "The spine answers it by kind. Every concern in a closed vocabulary is tagged to one layer, the concern is decided by reading what the file does under one concern per file and the narrowest concern that fits, and the layer is a derivation. Concern-folder correspondence makes the derivation visible in the tree: the folder names the concern, the concern names the layer, and nobody remembers anything.", }, ], title: "Belonging by kind, never by folder", }, { blocks: [ { kind: "text", text: "The seven layers map onto how a system decomposes rather than onto how a team is organised. The domain holds what the system is about: its models, records, policies and specifications. The application holds what it does with that: coordinators, behaviours, intents, selectors, stores. Processing holds transformation: converters, normalizers, analyzers, validators, pipelines.", }, { kind: "text", text: "Runtime holds what runs and when: entrypoints, lifecycles, timers, pools. Infrastructure holds what everything else stands on: registries, factories, adapters, resolvers, constants, schemas, the vocabulary itself. Operations holds observation and upkeep: probes, counters, reporters. Product holds what a person meets: views, components, renderers, styles, the strings.", }, { kind: "text", text: "A concern belongs to exactly one layer, and a concern whose layer is contested is two concerns. A file with two concerns is a split, never a tie to break. Layer spine precedence is the one tie-break the canon holds, and it applies only to an irreducible overlap between two tags for one concern: the file classifies to the domain-ward tag, and the rule is a classification rule, never a dependency rule.", }, { caption: "concerns to layers", kind: "mermaid", text: CONCERN_DIAGRAM }, ], title: "The seven layers", }, { blocks: [ { kind: "text", text: "Take a converter. It takes one shape and returns another, so it is processing whatever it converts and whichever feature asked for it. Put it in a folder named for the feature and it has a home but no layer, and the next feature that needs the same conversion either reaches across a boundary or copies the file.", }, { kind: "text", text: "Put it under its concern and the layer follows, the second feature finds it where the concern says it is, and a check can hold that nothing in processing reaches into product. That is separation of concerns with a mechanism behind it. Package by feature answers a different question, how a team navigates, and a feature cuts across every layer as a layer cuts across every feature, so only one of the two can be the folder.", }, ], title: "A converter, placed twice", }, ], title: "The layer spine", }; const DIRECTION_SECTION: Section = { icon: DIRECTION_SECTION_ICON, id: DIRECTION_SECTION_ID, intro: "The layer spine says what kind of thing a file is; this axis says who may depend on whom, as one way draws, and the two are orthogonal, which two axes types. The canon's architecture styles are each one picture of the same rule, and a wrong-way edge has the one repair the repair shows.", subsections: [ { blocks: [ { application: "Name which tier is the engine and which is the consumer. Classify each container by prefix, and classify per file only the trees a prefix cannot decide. Hold both in data a check reads, and let the check refuse an import that runs from the engine toward a consumer.", boundary: "The direction rule governs dependencies and says nothing about classification. A file is not on the engine tier because it is generic, and a consumer is not lower because it is specific; the tier is a fact declared about a tree, and the check reads the fact rather than inferring it.", cause: "A dependency rule that has no check behind it is a diagram, and the first import that crosses the wrong way is the one that was convenient that afternoon.", decision: "Declare the tier as data a check reads, rather than infer it from what a file looks like.", failureMode: "A shared module gains one import from a page, the page changes, the module now breaks on every page, and the layering that was supposed to prevent that never had a rule behind it.", kind: "lesson", principle: "The dependency direction is an orthogonal axis: one way, from consumer to engine, held by its own check.", problem: "Nothing refuses the import that crosses the wrong way, so the engine slowly learns about its consumers one convenient import at a time.", validation: "Take any import and ask which tier each end is on. An import whose ends nobody can tier is outside the model, and an import that runs from the engine toward a consumer is a move waiting to happen.", }, { caption: "one way", kind: "mermaid", text: DIRECTION_DIAGRAM }, ], title: "Engine and consumer", }, { blocks: [ { kind: "text", text: "This is the dependency inversion principle drawn at the scale of a whole tree, and the canon's architecture styles are each one way of drawing it. Hexagonal architecture, ports and adapters architecture and clean architecture put the thing that knows nothing at the centre and let everything specific depend inward. Layered architecture draws the same arrow downward.", }, { kind: "text", text: "What they share is one direction and one rule. What they differ on is a picture, and the picture is not the mechanism. The mechanism is a tier declared for every file, a check that reads the dependency graph and refuses an edge from the engine toward a consumer, and a repair that is always a move.", }, { kind: "text", text: "Inversion of control and dependency injection are the two techniques the rule pushes you toward. The only way an engine uses something specific without knowing it is to be handed it, and extension points with runtime discovery are how the engine finds the consumers it must not import.", }, ], title: "One rule, many pictures", }, { blocks: [ { kind: "text", text: "Most of a tree classifies by where it sits, because a container is built for one tier and everything under it inherits that. A few kinds of file resist that reading. Copy and type declarations serve whichever side names them, so their location says nothing about their tier, and those are classified one file at a time in data that starts empty.", }, { kind: "text", text: "What a file with no entry resolves to is a decision with a reason, not a default that fell out of the code. A type nobody classified resolves to no tier, so the check treats it as unclassified rather than guessing a side. Copy nobody classified resolves to the consumer tier, because copy is nearly always specific to one product. The two defaults differ because the cost of a wrong guess differs, and that is the general rule: a default is chosen by which mistake is cheaper to discover.", }, { code: LAYER_SHAPE, kind: "code", language: TYPESCRIPT_LANGUAGE, title: "two axes" }, ], title: "Where a tier comes from", }, { blocks: [ { kind: "text", text: "The check reads a dependency graph derived from the tree, never the tree's claims about itself, and it fails closed: a missing graph is a refusal, not a pass over nothing. It refuses rather than repairs, because the only honest repair for a wrong-way import is to move the file.", }, { kind: "text", text: "A file that produces a circular dependency, an upward dependency or a bypass of a declared entry point is in the wrong module. Lazy evaluation of an import, a direct path past the entry point, a re-export across modules and an import-sort trick each make the graph pass while the structure stays wrong. Concrete coupling and inappropriate intimacy are what those tricks preserve: the engine still knows a consumer, only through a door nothing watches. Encapsulation and information hiding are what the entry point protects.", }, { caption: "the repair", kind: "mermaid", text: REPAIR_DIAGRAM }, ], title: "The repair is a move", }, ], title: "The direction axis", }; export const LAYER_SECTIONS: readonly Section[] = [SPINE_SECTION, DIRECTION_SECTION];