import { CORE_SECTION_ICON, EXECUTION_SECTION_ICON, STRUCTURAL_SECTION_ICON, } from "#configuration/icons/architecture.icons"; import { CORE_SECTION_ID, EXECUTION_SECTION_ID, STRUCTURAL_SECTION_ID } from "#core/ids/architecture.ids"; import { LAYER_FACE } from "@govlab/constants"; import type { Section } from "#types/document.types"; import { TYPESCRIPT_LANGUAGE } from "#configuration/constants/code.constants"; import { faceLink } from "#domain/converters/ontology.converter"; const OWNERSHIP_SHAPE = 'export interface Owned {\n readonly acquire: () => Handle;\n readonly release: (handle: Handle) => void;\n}\n\nexport interface Lifecycle {\n readonly init: () => void;\n readonly run: () => void;\n readonly shutdown: () => void;\n}\n\nexport const withOwned = (owned: Owned, use: (handle: Handle) => Result): Result => {\n const handle = owned.acquire();\n try {\n return use(handle);\n } finally {\n owned.release(handle);\n }\n};\n\nexport interface Bounded {\n readonly capacity: number;\n readonly evict: "lru" | "fifo";\n readonly items: readonly Item[];\n}'; const EVENT_SHAPE = 'export interface Emitted {\n readonly ordinal: number;\n readonly intent: Intent;\n}\n\nexport interface Child {\n readonly emit: (intent: Intent) => Emitted;\n}\n\nexport interface Parent {\n readonly subscribe: (react: (event: Emitted) => "accept" | "refuse") => Unsubscribe;\n}\n\nexport type Failure =\n | { readonly ok: true }\n | { readonly ok: false; readonly code: Code; readonly halts: boolean };'; const CORE_DIAGRAM = 'flowchart TB\n subgraph computation["Computation · what happens to data"]\n pure["pure functions · immutable · idempotent"]\n marked["uncertainty marked, never hidden"]\n end\n subgraph resource["Resource · where data lives"]\n owner["one owner · bounded lifetime"]\n symmetric["open then close · start then stop · guaranteed"]\n halt["invariant broken · halt"]\n end\n subgraph execution["Execution · control flow"]\n events["children emit · parents subscribe"]\n monotonic["append only · never retract"]\n snapshot["backtrack by reinstantiation"]\n end\n structural["Structural · applies to all three · observes itself"]\n resource -- observe --> computation\n computation --> execution\n resource --> execution\n execution -- feeds --> structural\n structural -- feedback --> execution'; const LIFETIME_DIAGRAM = "stateDiagram-v2\n [*] --> Acquired : the owner opens it\n Acquired --> InUse : initialise\n InUse --> InUse : use · one owner writes\n InUse --> Released : shutdown · by scope or explicit destroy\n InUse --> Halted : invariant broken · fail fast\n Halted --> Released : the owner still releases\n Released --> [*]"; const EXCHANGE_DIAGRAM = "sequenceDiagram\n participant Child\n participant Bus as Event bus\n participant Parent\n Child->>Bus: emit intent · ordinal 41\n Bus->>Parent: deliver\n Parent-->>Bus: accept\n Child->>Bus: emit intent · ordinal 42\n Bus->>Parent: deliver\n Parent-->>Bus: refuse · typed error, halts: false\n Note over Child,Parent: the child never calls the parent · the log only grows"; const RELEASE_DIAGRAM = 'flowchart TB\n resource["A resource is acquired"]\n who{"Who releases it?"}\n discipline["A person remembering · it leaks"]\n structure["The structure · scope, a finally, an explicit destroy"]\n owner{"Exactly one owner?"}\n weak["Every other reference is weak or ephemeral"]\n shared["Shared ownership · ambiguity, then a leak"]\n resource --> who\n who -- discipline --> discipline\n who -- structure --> structure --> owner\n owner -- yes --> weak\n owner -- no --> shared'; const STRUCTURAL_DIAGRAM = 'flowchart TB\n classical["A classical principle · separation of concerns, simplicity, one responsibility"]\n sharpened["The practice that sharpens it · the whole system in view, compression, an invariant"]\n pair["One pair · the principle holds, the practice says how"]\n human["Human factors · bounded by what a person can hold"]\n evolution["Evolution · state protected, room left, nothing built unneeded"]\n classical --> pair\n sharpened --> pair\n pair --> human\n pair --> evolution'; const CORE_SECTION: Section = { icon: CORE_SECTION_ICON, id: CORE_SECTION_ID, intro: `The core of the canon is a split between what happens to data, ${faceLink(LAYER_FACE, "computation-core", "computation")}, and where data lives, a ${faceLink(LAYER_FACE, "resource-core", "resource")}. The split is the first question to ask of any unit, because the two halves are answered by different rules on the same question: who releases decides whether a resource leaks, as who releases draws, and a lifetime runs from one owner to one release, as a lifetime draws and ownership as types states.`, subsections: [ { blocks: [ { application: "Sort every unit into computation or resource before designing it. Freeze what computation produces, assign each variable once, and let a computation carry no persistent state, so it can be replayed and its rollback restores meaning rather than bytes. Give every resource one owner, bound its lifetime to that owner's, pair every open with a close and every start with a stop, and release by scope or by an explicit destroy rather than by anyone remembering. Bound every cache and pool with a declared capacity and an eviction policy, and register it where it can be seen.", boundary: "Computation and resource are answered by different rules on the same question, and the split is what stops the rules from colliding. A computation is stateless and a resource is snapshotted before mutation. Computed data is immutable and resource state is mutable but managed. A broken resource invariant halts, and computation uncertainty is marked and carried on.", cause: "A stateful thing treated as stateless leaks, because nothing owns its release, and a stateless thing treated as stateful drifts, because it acquires a lifetime nothing bounds.", decision: "Release by structure rather than by discipline, and freeze rather than manage wherever a unit can be a computation.", failureMode: "A cache with no capacity grows until the process dies, a listener nobody unsubscribes fires against a component that was removed an hour ago, and both were written correctly by their authors.", kind: "lesson", principle: "Computation is stateless and frozen, a resource has one owner and a bounded lifetime, and release is structural.", problem: "Most code mixes the two, so data that should have been frozen is mutated and handles that should have been owned are shared, and the leaks are found in production.", validation: "Take any resource and name its owner and the structure that releases it. A resource with two owners, or with a release that depends on a person, will leak, and the only question is when.", }, { caption: "who releases", kind: "mermaid", text: RELEASE_DIAGRAM }, ], title: "What happens to data, where data lives", }, { blocks: [ { kind: "text", text: "The computation half is the canon's computation core read as one rule. Immutability freezes what a computation produces. Pure functions give it no effect but its return value, and referential transparency lets any call be replaced by its result. Determinism makes the same input give the same output, which is what makes repeatability and reproducibility properties rather than hopes, and testability follows from all of them at once.", }, { kind: "text", text: "Statelessness is the same rule seen from the outside: nothing is retained between calls, so a computation can run anywhere and be replayed. Idempotency is its consequence at the edge, because a retry of a stateless step has one effect however many times it lands. Validation and verification are then cheap, since a frozen output can be compared against an expected one without a running system around it.", }, ], title: "The computation half", }, { blocks: [ { kind: "text", text: "Reachable is not useful. A collector frees what nothing reaches and keeps what something still points at, so an architectural leak survives collection because it is reachable: a cache entry nobody will read, an observer on a dead subject, a handle held by an injection container.", }, { kind: "text", text: "That is why every non-owning reference is weak or ephemeral, why hidden retention in injection, mapping and observer machinery is made observable and bounded, and why anything that outlives a single call carries an initialise, a run and a shutdown. Graceful shutdown is the resource rule at the scale of a process. A long-lived component with no shutdown is a leak by construction, and the leak is a hole in the contract rather than a bug in the code.", }, { caption: "a lifetime", kind: "mermaid", text: LIFETIME_DIAGRAM }, { code: OWNERSHIP_SHAPE, kind: "code", language: TYPESCRIPT_LANGUAGE, title: "ownership as types" }, ], title: "The resource half", }, { blocks: [ { kind: "text", text: "Caching is where the two halves meet most often and where the split is most often lost. A cache holds computed data, so its entries are immutable, and it is a resource, so it has one owner, a declared capacity and an eviction policy. Cache poisoning by design is what happens when the first half is forgotten, and a cache with no capacity is what happens when the second is.", }, { kind: "text", text: "Configuration externalization is the same split at the boundary of the process. What the process reads at boot is a resource with one source and a validation at the door, and what it computes from that is frozen for the run. Environment parity follows: the same computation over a different resource behaves the same, or the difference is in the resource and can be named.", }, ], title: "Where the halves meet", }, ], title: "Computation and resource", }; const EXECUTION_SECTION: Section = { icon: EXECUTION_SECTION_ICON, id: EXECUTION_SECTION_ID, intro: `${faceLink(LAYER_FACE, "execution-core", "Execution")} is how the two halves meet, as the core split draws, and it has its own rules, each the refusal of one improvised join: the exchange one exchange draws and execution as types states.`, subsections: [ { blocks: [ { application: "Let a child announce what happened as an event and let the parent decide what to do with it, so the child never holds a reference to who reacts. Append a correction as a new record that supersedes rather than editing history, and restore an earlier state by reinstantiating from a snapshot rather than by patching in place. Order by a monotonic sequence the system owns, never by the host's clock. Make every error a typed value in the system's own vocabulary, decide per error whether it halts or is carried, and let a guard that fails do so closed.", boundary: "Execution rules govern how computation and resources interact at runtime, and they do not decide what either half is. A resource that halts on a broken invariant is obeying the resource rule, and a computation that marks an uncertainty is obeying the computation rule; execution only carries the result between them.", cause: "A callback couples a child to a parent it should not know, a retraction is a second path every reader must handle, a clock is a dependency on the host, and a sentence in an error is a contract nobody can dispatch on.", decision: "Own the sequence and the vocabulary rather than borrow the host's clock and prose.", failureMode: "A child calls back into its parent, the parent is replaced, and the child keeps calling into something that no longer exists, while the error that would have said so was a string nobody parsed.", kind: "lesson", principle: "Execution joins the halves through events rather than callbacks, appends rather than retracts, and treats errors as part of the language.", problem: "Control flow is usually improvised per call site, so the same two halves are joined a different way in every place they meet.", validation: "Follow one event from the child that emits it to every parent that reacts, and one error from where it is raised to where it is handled. A callback that runs upward, a retraction, or an error handled by string comparison is a place where execution has no rule.", }, { caption: "the core split", kind: "mermaid", text: CORE_DIAGRAM }, ], title: "Events, growth, order, errors", }, { blocks: [ { kind: "text", text: "Event-driven architecture is the shape the first rule produces. Domain events are what a child emits, the publish/subscribe pattern is how a parent hears them without the child knowing who listens, and an event bus is the mechanism that carries them. The observer pattern is the same relation inside one process. Execution may counter-propose by emitting an intent a parent can refuse, which is asynchronous communication with the refusal kept explicit.", }, { kind: "text", text: "An append-only log is the growth rule as a store, and event sourcing is it as a whole architecture, where the current state is a fold over the events and a correction is a new event that supersedes. Backtracking is by snapshot and reinstantiation rather than by patching state in place, which is the memento pattern held as a rule rather than a trick.", }, { kind: "text", text: "Ordering is by a monotonic sequence or an append-only id, never a wall clock. A clock is a dependency on the host and a sequence is a dependency on nothing, which is why causality is tracked with Lamport clocks, vector clocks or hybrid logical clocks rather than timestamps, and why event ordering is a constraint the system owns. A happens-before relationship is derivable from a sequence and never from two clocks.", }, { caption: "one exchange", kind: "mermaid", text: EXCHANGE_DIAGRAM }, ], title: "Events and growth", }, { blocks: [ { kind: "text", text: "Errors are part of the language. Error handling uses the system's own vocabulary and the errors are machine-processable, so a consumer dispatches on an error rather than parsing a sentence. An inconsistent error model, one boundary raising and another returning a code, is the anti-pattern this rule refuses, and exception control flow is its twin.", }, { kind: "text", text: "A resource invariant that breaks fails fast and halts, because carrying on with a corrupt handle is worse than any crash. A computation that is uncertain marks the uncertainty and carries on, because temporary inconsistency in data is acceptable exactly while it is visible. Defensive programming is the wrong reflex here, and fail at the boundary on the methodology page is the practice that replaces it. Fail safe and fail secure are the same decision made once for a domain rather than per call site.", }, { code: EVENT_SHAPE, kind: "code", language: TYPESCRIPT_LANGUAGE, title: "execution as types" }, ], title: "Errors as language", }, ], title: "Execution joins the halves", }; const STRUCTURAL_SECTION: Section = { icon: STRUCTURAL_SECTION_ICON, id: STRUCTURAL_SECTION_ID, intro: `The ${faceLink(LAYER_FACE, "structural-core", "structural")} domain applies to all three cores and observes itself. Its principles are the classical ones, each paired the way principle and practice draws, and this chapter says what the canon adds to each.`, subsections: [ { blocks: [ { application: "Take each classical principle and pair it with the practice that makes it recognisable in a tree, then write the violation as a shape a check can match. Where the pairing yields no shape, the principle is not yet held by anything.", boundary: "A pairing sharpens a principle and never replaces it. The classical name still carries the intent a reader recognises, and the practice only says how that intent shows in a tree; a practice with no principle behind it is a house rule, and a principle with no practice is a wish.", cause: "A classical principle is stated at a level where everyone agrees and nobody can check, so the disagreement moves to what the principle means in this file, and that is the argument the pairing settles in advance.", decision: "Settle what a principle means in a file in advance, as a shape, rather than at each review.", failureMode: "Two reviewers agree that a module should have one responsibility and disagree about whether this one does, because responsibility was never derived from anything either of them could point at.", kind: "lesson", principle: "A structural principle is a classical principle paired with the practice that makes it recognisable.", problem: "Classical principles are agreed at a level nobody can check, so what they mean in a given file is argued every time.", validation: "Take any structural principle you hold and state, in one sentence, what a violation of it looks like in a file. If the sentence names a shape, the principle has its practice. If it names an opinion, the pairing is still missing.", }, { caption: "principle and practice", kind: "mermaid", text: STRUCTURAL_DIAGRAM }, ], title: "A principle and the practice that sharpens it", }, { blocks: [ { kind: "text", text: "Separation of concerns is paired with reasoning about the whole system, so one module holds one concern while the design is still read as one thing. Simplicity is paired with compression: the simplest solution is a rule or a generator, never an enumeration, and a repeated shape is compressed by its type. A literal becomes a constant, a structure a composition, a behaviour one orchestrator, a fact one source. Do not repeat yourself is that compression named for the case of knowledge.", }, { kind: "text", text: "The single responsibility principle is derived from an invariant rather than a feature. The open/closed principle is met through few composable primitives, so extension arrives without editing what is tested. The Liskov substitution principle makes substitutable parts order-independent. The interface segregation principle cuts an interface by usage, and the dependency inversion principle addresses it by meaning rather than by location. Composition over inheritance is the practice all five share, because a composed part can be replaced and an inherited one cannot.", }, { kind: "text", text: "Code as data makes code, data and state one interchangeable structure, and homoiconicity is the degree to which a system has that property. One truth lives in versioned, queryable symbols, which is single source of truth with a location. Placement in a hierarchy reflects meaning, time is a logical sequence rather than a wall clock, and the system observes its own execution as data it can query, which is where introspection and observability meet.", }, ], title: "The pairs", }, { blocks: [ { kind: "text", text: `${faceLink(LAYER_FACE, "human-factors", "Human factors")} bound the whole by what a person can hold. Related logic stays together, which is high cohesion read as a limit on attention. Complexity stays under a declared bound, and bounded nesting depth is one such bound made checkable. Uncertainty stops the work rather than passing silently. Internals are hidden but shipped with a live inspector, so encapsulation is traded against debuggability at a point somebody chose.`, }, { kind: "text", text: `${faceLink(LAYER_FACE, "evolution-principles", "Evolution")} says how the whole changes. Units are independent and swappable, with state protected before code. Features are added externally, through extension points, with room left deliberately unspecified. Nothing is built that nothing needs, which refuses speculative generality and premature abstraction, and what the system will not do is written down in architecture decision records rather than remembered. Evolutionary architecture with fitness functions is that domain held by checks rather than by review.`, }, ], title: "Beneath the core", }, ], title: "The structural domain", }; export const RESOURCE_SECTIONS: readonly Section[] = [CORE_SECTION, EXECUTION_SECTION, STRUCTURAL_SECTION];