import { ALGO_DOMAIN_FACE, ALGO_FACE, KIND_FACE, LAYER_FACE } from "@govlab/constants"; import { CONSEQUENCE_SECTION_ICON, CONTROL_SECTION_ICON, DECAY_SECTION_ICON, PAIR_SECTION_ICON, } from "#configuration/icons/architecture.icons"; import { CONSEQUENCE_SECTION_ID, CONTROL_SECTION_ID, DECAY_SECTION_ID, PAIR_SECTION_ID, } from "#core/ids/architecture.ids"; import type { Section } from "#types/document.types"; import { TYPESCRIPT_LANGUAGE } from "#configuration/constants/code.constants"; import { faceLink } from "#domain/converters/ontology.converter"; const DECAY_SHAPE = 'export const CONTROLS = ["boundary", "contract", "ownership", "versioning", "observability", "state-isolation", "enforcement"] as const;\nexport type Control = (typeof CONTROLS)[number];\n\nexport const PROPAGATION = ["shortcut", "repetition", "normalisation", "dependency-formation", "institutionalisation", "high-cost-repair"] as const;\nexport type Stage = (typeof PROPAGATION)[number];\n\nexport const REMEDIATION = ["missing-control", "inverse-control", "migration", "absence-check", "prevention-gate"] as const;\nexport type Repair = (typeof REMEDIATION)[number];\n\nexport interface AntiPattern {\n readonly id: AntiPatternId;\n readonly absentControl: Control;\n readonly path: readonly { readonly stage: Stage; readonly change: string }[];\n readonly presentsAs: string;\n readonly violates: readonly PrincipleId[];\n readonly detectedBy: readonly SignalId[];\n readonly repairs: readonly { readonly step: Repair; readonly action: string }[];\n}'; const PAIR_SHAPE = "export interface Inversion {\n readonly refused: { readonly construct: Construct; readonly borrowsAgainst: Consequence };\n readonly required: { readonly construct: Construct; readonly buys: Consequence };\n}\n\nexport const inversionOf = (refused: Construct, required: Construct, debt: Consequence, leverage: Consequence): Inversion => ({\n refused: { borrowsAgainst: debt, construct: refused },\n required: { buys: leverage, construct: required },\n});\n\nexport const asFinding = (rule: Inversion, at: Location): Finding => ({\n at,\n reported: rule.refused.construct,\n remediation: rule.required.construct,\n reason: rule.refused.borrowsAgainst,\n});"; const DECAY_DIAGRAM = 'flowchart TB\n control["A control is absent · boundary, contract, ownership, versioning, observability, state isolation, enforcement"]\n step1["A local shortcut lands · nothing refuses it"]\n step2["It is repeated under pressure and becomes convention"]\n step3["Dependent code forms around it · a boundary or contract erodes"]\n presents["Systemic fragility · the anti-pattern, reproducible, named, detectable"]\n repair["The repair · install the inverse control, migrate the dependents, check the absence, gate the recurrence"]\n control --> step1 --> step2 --> step3 --> presents --> repair'; const PROPAGATION_DIAGRAM = "stateDiagram-v2\n [*] --> Shortcut : a control is absent\n Shortcut --> Repetition : nothing refuses it\n Repetition --> Convention : delivery pressure\n Convention --> Dependency : code forms around it\n Dependency --> Fragility : the boundary erodes\n Fragility --> Repair : the pattern is named\n Repair --> [*] : inverse control installed · dependents migrated · recurrence gated\n Repair --> Shortcut : one call site removed · the path stays open"; const CONTROL_DIAGRAM = 'flowchart LR\n boundary["boundary absent · a foreign model leaks in"]\n contract["contract absent · a consumer depends on a promise nobody made"]\n ownership["ownership absent · two writers, the last one wins"]\n versioning["versioning absent · a breaking change ships as ordinary"]\n observability["observability absent · the first signal is a person"]\n isolation["state isolation absent · a side effect crosses a boundary"]\n enforcement["enforcement absent · a rule decays at the rate of attention"]\n decay["One decay class per control · placed by what was missing, never by how it looked"]\n boundary --> decay\n contract --> decay\n ownership --> decay\n versioning --> decay\n observability --> decay\n isolation --> decay\n enforcement --> decay'; const PAIR_DIAGRAM = 'flowchart TB\n refused["A construct is refused · with the debt it borrows against"]\n required["Its replacement is required · with the leverage it buys"]\n refused -- one inversion, two consequences --> required\n check["A check reports the refused construct where it lands"]\n refused -.-> check\n remedy["The finding names the required construct as its remediation"]\n check --> remedy\n required -.-> remedy'; const CONSEQUENCE_DIAGRAM = 'flowchart TB\n construct["A construct under review"]\n lender{"Whom does it borrow from?"}\n reader["the next reader"]\n operator["the operator, later and elsewhere"]\n change["every future change"]\n nobody["nobody · it is not a debt"]\n replacement["What stands in its place, and what does that buy?"]\n rule["A rule · two constructs, two consequences"]\n construct --> lender\n lender --> reader --> replacement\n lender --> operator --> replacement\n lender --> change --> replacement\n lender --> nobody\n replacement --> rule'; const DECAY_SECTION: Section = { icon: DECAY_SECTION_ICON, id: DECAY_SECTION_ID, intro: `A decay starts as a local shortcut that nothing refused, and it becomes dangerous the moment the shortcut becomes infrastructure, the path a decay path draws and the propagation states. The canon carries the class as a ${faceLink(ALGO_FACE, "architecture-anti-pattern", "contract of its own")}, and every anti-pattern it names is a ${faceLink(KIND_FACE, "anti-pattern", "kind")} a principle may point at and nothing else may.`, subsections: [ { blocks: [ { application: "Name an anti-pattern by the control whose absence causes it, and record the decay path as the sequence of stages that produces it, so it can be reproduced rather than argued about. Record how it presents, which principles it violates, which signals detect it, which repairs reverse it and which gate prevents its return.", boundary: "A decay path is a class, never an incident. The catalogue carries the shape, how it starts, how it presents and what was missing, and never which file or who, because a class transfers to a tree with nothing else in common and an incident makes the next project inherit someone else's history as law.", cause: "A shortcut is cheap where it is taken and expensive where it is depended on, so blaming the first change finds a small thing, and the control whose absence let it through is invisible because absent things do not appear in reviews.", decision: "Repair the control rather than the change that revealed it, because the next change will reveal it again.", failureMode: "A module gains a second writer, then a third, each with a good reason, and by the time two of them disagree about its state nobody can say which change was the wrong one because none of them was.", kind: "lesson", principle: "An anti-pattern is a reproducible decay path caused by the absence of a specific control.", problem: "Anti-patterns are usually catalogued by how they look, and how they look is the last stage of a process that started with something missing.", validation: "Take an anti-pattern you have met and name the control whose presence would have made the first step of its decay path fail. If you cannot name one, you have named the symptom rather than the pattern.", }, { caption: "a decay path", kind: "mermaid", text: DECAY_DIAGRAM }, ], title: "An absent control, a reproducible path", }, { blocks: [ { kind: "text", text: `A decay path is recorded as reproduction rather than description, and the ${faceLink(ALGO_FACE, "anti-pattern-propagation-kernel", "propagation")} runs through the same stages every time. A local shortcut lands and nothing refuses it, which is where the missing control is named. The shortcut is repeated under delivery pressure until it is a convention, dependent code forms around it, and the repair is now expensive in proportion to what depends on it.`, }, { kind: "text", text: "Recorded that way, the pattern is a test: install the control, replay the path, and the first stage should now fail. Lava flow is what a path looks like once nobody can replay it, code kept because nobody knows what depends on it, and temporal coupling is a path whose stages are hidden in the order things happen to be called.", }, { caption: "the propagation", kind: "mermaid", text: PROPAGATION_DIAGRAM }, ], title: "The path", }, { blocks: [ { kind: "text", text: `The ${faceLink(ALGO_FACE, "anti-pattern-remediation-algebra", "repair")} follows the same algebra in reverse. Name the missing control, introduce its inverse, migrate the dependents off the shortcut, verify the old shape is absent, and gate its recurrence.`, }, { kind: "text", text: "A repair that removes one call site leaves the propagation path open and the pattern returns somewhere else. Shotgun surgery is the shape of that failed repair, one change spread across many files because the control was never installed, and divergent change is its mirror, one file changed for many reasons because its concern was never split.", }, ], title: "The repair", }, ], title: "An anti-pattern is a decay path", }; const CONTROL_SECTION: Section = { icon: CONTROL_SECTION_ICON, id: CONTROL_SECTION_ID, intro: `The controls are few: a boundary, a contract, an ownership, a versioning, an observability, a state isolation, an enforcement. Each owns a decay class, as seven controls draws and an anti-pattern record types, because the ${faceLink(ALGO_FACE, "architecture-anti-pattern", "anti-pattern record")} holds as its invariant that every decay path is caused by the absence of one of those seven. The same idea generalises past architecture: a ${faceLink(ALGO_FACE, "smell-taxonomy", "taxonomy of smells")} groups any bad practice by the control it lacks rather than by how it looks, and a catalogue organised that way answers a question a gallery of symptoms cannot: what to install.`, subsections: [ { blocks: [ { application: "Place every anti-pattern under the one control whose absence lets it start, and read the catalogue as seven classes rather than as a list. When a new pattern appears, ask which of the seven was missing before asking what it looks like, because the answer names the repair.", boundary: "The seven controls classify absence and never presence. A tree that has all seven is not thereby well designed; it is a tree in which the decay classes have something to refuse them, and what it refuses is still decided by the principles the controls hold.", cause: "A catalogue of symptoms grows without bound and teaches recognition, while a catalogue of absent controls stays small and teaches repair, because the number of ways to look wrong is unbounded and the number of things that can be missing is not.", decision: "Keep a catalogue of absences rather than a gallery of symptoms.", failureMode: "A team keeps a growing list of things that went wrong, each with its own fix, and the next failure fits none of the entries because it was filed by appearance and appears differently this time.", kind: "lesson", principle: "Seven controls own seven decay classes, and an anti-pattern is placed by the control it lacks.", problem: "A catalogue organised by appearance grows without bound and never says what to install.", validation: "Take any three anti-patterns you know and name the control each lacks. If two of them lack the same control, they are one class with two faces, and one check covers both.", }, { caption: "seven controls", kind: "mermaid", text: CONTROL_DIAGRAM }, ], title: "Placed by what was missing", }, { blocks: [ { kind: "text", text: "A missing boundary lets a foreign model leak into yours until the two are one tangled model: boundary leakage, framework leakage, the anemic domain model that follows. A missing contract lets a consumer depend on a behaviour nobody promised, an implicit contract, so the next change breaks it silently. A missing ownership lets two parties write shared mutable state, and the write that lands last wins with no report, which is the lost update.", }, { kind: "text", text: "A missing versioning lets an unversioned breaking change ship as an ordinary one, and schema drift follows. A missing observability lets a system run with opaque runtime behavior, so the first sign of an unobservable failure is a person. A missing state isolation lets a hidden side effect reach across a boundary it should not see, and action at a distance is its name once nobody can find the cause. A missing enforcement is manual-only governance, a rule everyone agreed to decaying at the rate of attention.", }, ], title: "The seven, each with its class", }, { blocks: [ { kind: "text", text: `Two questions decide whether a catalogue entry is real. Can the path be replayed from its first stage, so that a reader with a different tree reproduces the decay rather than recognising the picture? And does the entry name what it violates by identity, so that a finding on the ${faceLink(LAYER_FACE, "enforcement-core", "enforcement layer")} resolves to the principle, its severity and the repair in one lookup?`, }, { kind: "text", text: `An anti-pattern held as a ${faceLink(ALGO_FACE, "anti-pattern-relationship-record", "typed relationship record")} answers both, and a smell ${faceLink(ALGO_FACE, "anti-pattern-rule-compiler", "compiles into a rule")} only once it does. Pattern cargo cult and the golden hammer are what a catalogue of appearances produces: a shape applied because it was recognised, never because its absent control was named.`, }, { code: DECAY_SHAPE, kind: "code", language: TYPESCRIPT_LANGUAGE, title: "an anti-pattern record" }, ], title: "When an entry is real", }, { blocks: [ { kind: "text", text: "For a model-authored codebase the decay classes are the same and the rate is different. A model will tend to take a shortcut as readily as a person under a deadline, and it rarely objects to its own, so a system with an absent control decays at the model's speed rather than a team's.", }, { kind: "text", text: "A control that exists only as a reviewer's habit is absent for every change the reviewer did not see. Installing the seven as checks before the model writes anything is what makes them present for every change rather than for the ones somebody happened to read.", }, ], title: "The same classes at a model's speed", }, ], title: "Seven controls, seven classes", }; const PAIR_SECTION: Section = { icon: PAIR_SECTION_ICON, id: PAIR_SECTION_ID, intro: `A rule that only says never leaves an empty action set the moment the forbidden thing is the obvious thing, and an empty action set is how the forbidden thing gets written anyway, which is why every rule is the pair one inversion draws. The canon holds every inversion as a ${faceLink(ALGO_DOMAIN_FACE, "architectural-rules", "contract with an invariant")}, and fail at the boundary on the methodology page is the practice for the pairs that recur most.`, subsections: [ { blocks: [ { application: "Give every prohibition its replacement, and give both halves their consequence: the debt the refused construct borrows against, and the leverage the required one buys. Keep the consequences as the reason, so the rule carries its own justification and is never re-argued. Let a check report the refused construct where it lands and name the required one as the remediation, and delete what the replacement supersedes in the same change, because a marker on the old path keeps two paths alive under one label.", boundary: "An inversion governs living code on one forward path: anything something else depends on, where a shortcut taken today is read as a decision by whoever finds it next. It says nothing about how strict a rule is, and it is not a threshold; a bound lives where the check reads it, and the inversion only says that the bound exists.", cause: "A prohibition without its replacement names what to avoid and not what to do, so under pressure the avoided thing is the only thing anyone knows how to write.", decision: "Write the replacement before the prohibition, so the rule is never a bare never.", failureMode: "A rule says no fallbacks, a value is missing at boot, the author has nowhere to go, and the fallback is written with a comment apologising for it.", kind: "lesson", principle: "An avoidance rule is an inversion: a refused construct with the debt it borrows against, and a required construct with the leverage it buys.", problem: "Avoidance rules written as a list of don'ts are obeyed until the first moment the don't is convenient.", validation: "Cover the second half of any rule you hold and ask what you would write instead. If nothing comes, the rule was a prohibition and the debt is already in the tree somewhere.", }, { caption: "one inversion", kind: "mermaid", text: PAIR_DIAGRAM }, ], title: "Half a rule", }, { blocks: [ { kind: "text", text: "The generalised form is a small derivation, and it can be run on any construct in any tree. Take the construct, ask what it borrows against and from whom, name the construct that would stand in its place, and ask what that one buys.", }, { kind: "text", text: "If the second half comes, the pair is a rule and the consequences are its reason. If it does not, the construct is either not a debt or not yet understood, and either way it is not a rule.", }, ], title: "The derivation", }, { blocks: [ { kind: "text", text: "Only living code on a single forward path survives that reading. A deprecation marker, a tombstone, a compatibility shim and zombie code are each a refused construct whose replacement is deletion in the same edit. Backward compatibility is a contract a boundary declares, never a second path kept alive inside one.", }, { kind: "text", text: "An export exists only while another file imports it now, unless it is staged behind a drift check that holds it to that promise. A utility dump and a repository dump are the same debt at the scale of a folder: a home for constructs whose replacement nobody named, and so a place where every one of them is kept.", }, ], title: "One forward path", }, ], title: "Never and always", }; const CONSEQUENCE_SECTION: Section = { icon: CONSEQUENCE_SECTION_ICON, id: CONSEQUENCE_SECTION_ID, intro: "The consequence on each side of an inversion is what lets the rule survive the moment it is inconvenient, derived by the two questions lender and replacement draws, and it is what turns the pair into a finding a check can print, the record an inversion record types.", subsections: [ { blocks: [ { application: "Derive the refused consequence by asking whom the construct borrows from: the next reader, the operator who meets the failure later, every future change that has to keep two paths alive. Derive the required consequence by asking what holds without attention once the replacement is in place. Name both as a lender and a purchase rather than as an adjective, so the rule can be pointed at when it is questioned, and let the same two names become the message a check prints and the remediation it proposes.", boundary: "A consequence names a lender or a purchase and never a severity. How strictly a rule binds is a separate fact held beside it, so two rules with the same consequence can bind differently, and a consequence never argues for its own rule's rank.", cause: "A rule whose reason is an adjective is re-argued because an adjective cannot be pointed at, while a rule whose reason names a lender ends the argument by naming who pays.", decision: "Justify a rule with who pays rather than with an adjective.", failureMode: "A rule is defended as good practice, the defence convinces nobody under deadline, and the construct it refused is written because no one could say who would pay for it.", kind: "lesson", principle: "A consequence is a named lender or a named purchase, and a pair with both is already a finding.", problem: "Rules are justified with adjectives, and an adjective is re-argued every time the rule is inconvenient.", validation: "Take any rule you hold and name who pays when it is broken and what is bought when it is kept. A rule where either answer is an adjective has no consequence yet, and it will lose the next argument it is in.", }, { caption: "lender and replacement", kind: "mermaid", text: CONSEQUENCE_DIAGRAM }, ], title: "A lender and a purchase", }, { blocks: [ { kind: "text", text: `The same question derives the refused consequence every time: whom does this construct borrow from? A shortcut borrows from the next reader. A ${faceLink(ALGO_FACE, "no-fallback", "fallback pattern")} borrows from the operator, because it converts a loud failure into a quiet wrong answer that surfaces later and elsewhere. A ${faceLink(ALGO_FACE, "no-deprecation", "deprecation")} borrows from every future change, because it keeps two paths alive so nobody has to choose now.`, }, { kind: "text", text: `A ${faceLink(ALGO_FACE, "no-dual-path", "second path")} borrows from every reader, who now has to decide which one is real. Deferring borrows from the person who will not remember. ${faceLink(ALGO_FACE, "no-shared-ownership", "Shared mutable state")} borrows from whoever has to find out which writer won, and silent data corruption from whoever meets the failure it hid.`, }, ], title: "The lender", }, { blocks: [ { kind: "text", text: `The required consequence names what the replacement buys, and the purchase is what makes the rule worth keeping. A ${faceLink(ALGO_FACE, "no-shortcuts", "constraint")} is leverage because it holds without attention. Fail fast is clarity, because the failure lands where its cause is. Explicit removal is coherence, because a removed path cannot be taken.`, }, { kind: "text", text: `A single path is determinism. A single owner is auditability. A bounded lifetime is deterministic release. ${faceLink(ALGO_FACE, "no-callbacks", "An emitted event")} is loose coupling, and ${faceLink(ALGO_FACE, "no-convention-enforcement", "policy as code")} is an automated gate. That shape is why the same record serves the person reading the rule and the check enforcing it, and why the two never drift apart.`, }, { code: PAIR_SHAPE, kind: "code", language: TYPESCRIPT_LANGUAGE, title: "an inversion record" }, ], title: "The purchase", }, ], title: "Debt and leverage", }; export const FAILURE_SECTIONS: readonly Section[] = [DECAY_SECTION, CONTROL_SECTION, PAIR_SECTION, CONSEQUENCE_SECTION];