Appearance
Fighting spaghetti code
What this is. How a codebase is kept from turning into spaghetti when most of it is written by an AI that starts every session blank. Here are the ideas, the laws and the loop. The tools that enforce them are harvested raw in inbox/brandmoves/ (full report: inbox/brandmoves/SPAGHETTI-REPORT.md) and have not yet been made generic. Cited from THE-METHOD §1 and §2.4.
Proven in:
- BrandMoves: the health tool, the ratchet and the gates. Health went from 1,417 findings on 26 Sep 2026 to 0 on 27 Sep 2026, over five rounds.
- RookWorlds: the health map, the wiring maps and the fifth question.
- Coordie: the recorded non-merge.
Why it matters more with an AI: spaghetti is gravity for an AI. It writes from the window of code it can see. If the codebase has three colour formulas, the next session adds a fourth, because it found one and not the others. The answer is to make the whole codebase visible at the moment of writing, and to make the right move the cheap one. "Infrastructure and systems over willpower" (Daniel).
1. The shape: the folders are the system
The generic rules live in D:\foundation\docs\ARCHITECTURE.md and are not restated here. In short:
- There are four layers,
interfaces/ → features/ → domain/ → infrastructure/, and imports point down only. - A file's layer is its first folder and its module the second.
- Features never import each other, and neither do interfaces.
- Nothing loops, even inside a layer.
- One door per feature. From outside a feature, only its
service.tsmay be imported. Every interface (app, API, AI connector) calls the same doors. - No side doors. An interface reaches the database or an outside service through the feature that owns it, never directly. Side doors are where "the app and the AI drift apart" (the connector had 13, the web 64; both went to 0).
- The stack changes only by a recorded decision. The layer and module list sits in one marked block in the health tool. A change to it is refused unless a commit carries an
Architecture-Decision: ADR-<n>trailer anddocs/DECISIONS.mdhas that ADR.
What it cost without it (25 Sep 2026): "504 files, 1,784 imports, about twenty jobs done twice, a map that described a structure that did not exist". The first enforced measure found 80 layer violations. Those went 80 → 42 → 22 → 1 → 0.
2. The laws
- One home per fact. A shared fact (a formula, a table of units, a dialog, the way a link is built) is written in one file. The health tool knows each home as (fact, file, a regex signature), and a match anywhere else is a finding. Seven copies of colour maths, three luminance formulas, two cut-offs, and a reader that dropped 3-digit hex.
- Duplication is debt. It is listed, ranked by harm, and folded into one home, and the count only goes down. When folding, keep the copy in the lowest layer, then the one with the shortest path.
- Change a shared piece in place, never by copying it. It is a parallel change: see who depends on it (
--who), expand, migrate every caller, then contract. Never "a second copy to be safe". - The baseline only goes down, finding by finding, never by count.
- A finding key that is not in the baseline fails the gate.
--updatemay only remove keys. - A KIND measured for the first time may adopt the findings already on main, but never a line the branch added (git's diff decides).
- The baseline remembers every kind ever measured.
- The first independent review found both holes in a count-based ratchet: one new finding was let in for every old one fixed, and a new kind swallowed the branch's own workarounds.
- A finding key that is not in the baseline fails the gate.
- Key findings by file and order, not line numbers, so they survive edits (
copy:<a> <-> <b> #k,wa-<kind>:<file> #n). - No workarounds, and the catalogue is code, not judgement.
- Every KIND of workaround the codebase has shown becomes a detector: an id, what it
says, itsfix, a puredetect(), realcaughtexamples and legitimatecleanlook-alikes. - A test runs every example, so recall on known kinds stays at 100%.
- There is no AI judgement in the gate: the same answer every time.
- The first measure found 1,125 workarounds of 38 kinds; the catalogue is now 98 kinds.
- Every KIND of workaround the codebase has shown becomes a detector: an id, what it
- No exceptions list. A finding that is not really a workaround means the DETECTOR is wrong. Sharpen it, and add that case as its
cleanlook-alike. Carve-outs live inside detectors, each held by a test. A list of allowed findings "reeks of workaround". - A remembered rule is a failure the system still allows.
- Every item under a WATCH OUT / Traps / Gotchas heading is counted.
- Each one is either made impossible, deleted with evidence that its failure can no longer happen, or left counted.
- A "what holds it" line must name a real file, script or detector.
- A trap moved under another heading is caught.
- 78 traps kept by memory → 18 → 9.
- Parts we no longer use never shape the design. A retirement goes into a RETIRED registry (what, since, a pattern) the same day. From then on every remaining mention in code, scripts, tests or docs is a finding, except history sections. Call things what they are.
- Does something ELSE already decide this fact? (the fifth question, from RookWorlds). Two live, correct-looking systems computing the same thing drift apart. Opponent strength was calibrated in two layers, five weeks apart, in opposite directions. Never average two theories; measure both and choose one.
- Name the non-merges, too (from Coordie). When an audit decides two similar things must stay separate (different identity models, a prop explosion, risk), write down why beside them, so it is not argued again. "Build once parameterized by role, but keep genuinely different models separate."
- The design questions before building are Congruence's six (
inbox/task-4ce/files/.claude/skills/congruence/, to be sorted intomethods/):- Is there already one of these? Grow it a slot.
- Is this the third variant? Extract, and migrate every caller now.
- Does data narrow in transit?
- Is this one instance of an N-instance disease? Grep.
- Who else expresses this fact?
- Where does the fifth one go?
3. The instrument has to be trustworthy
- A check that cannot be seen failing proves nothing. Every detector carries a caught example. Every gate was once shown red.
- A skipped check is never clean. A check whose input is missing SKIPS, and a skipped blocking check fails the gate.
- Guards fail shut. A hook that crashes lets the call through, so the guard catches its own errors and denies.
- An empty result must not look like a clean one. A CRLF checkout made a detector find zero items and pass. It was found only by running on Linux. Normalize line endings (
.gitattributes: * text=auto eol=lf) and split on/\r?\n/. - A weakened test must not hide. The close prints every changed test expectation, old beside new (
D:\foundation\scripts\test-changes.mjs). A mover once rewrote a CONTROL assertion so that it passed trivially. - A false nag on good work is how a check gets ignored (RookWorlds). When a check fires wrongly, fix the detector and prove it still fires on a control.
4. The loop: health at every step
- Session start. The health reading is printed and said to the human. On main, edits are refused.
- Plan (runbook
plan-a-build).- Health must pass, because no plan starts on messier code.
- A plan names what it is, what existing thing it improves, its proof and its layer.
- Starting a plan runs a "what already exists?" search over the code (
find). - Before close, three questions are answered, with the health findings for the touched files beside each:
- caller: can the caller just ask?
- exists: does code for this job already exist?
- cause: is this a workaround or the cause?
- Build.
- An edit guard refuses code before a plan, edits on main, and a new code file outside a registered module.
- After every edit, a hook injects ONLY what that edit added, with the exact fix (the import line, which copy to keep, which door to ask).
- Mechanical kinds get automatic fixers.
- Touching a file with known findings is the cheapest moment to fold them.
- Prove (runbook
prove-it). - Escape hunt (runbook
escape-hunt). A second agent reads the diff for workarounds the catalogue missed. Each escape is fixed AND becomes a caught example or a new detector, so it can't escape twice. The close refuses a code change with no hunt recorded. The first hunt found 8 escapes; later hunts found 11, 6 and 8. - Close.
- Cheap checks fastest first, ALL of them run even after a failure.
- The expensive check runs only if the cheap ones pass and the change touches what it tests.
- Read the verdict line (
HEALTH EXIT=0,CLOSE EXIT=0), never a wrapper's exit code. - A pass on a clean tree writes a receipt (the SHA) that pre-push trusts.
- A
gauntletruns everything, always, on demand.
- Merge conflicts in the baseline: keep a finding only if BOTH sides still have it;
kindsis the union.
Rounds of folding. To pay down debt, one orchestrating chat runs background agents, one per fenced session folder, and merges them ONE AT A TIME, merging main in first.
5. Seeing the structure
- Measure to a page. The health snapshot (per module: files, lines, uses and used-by, findings) goes to a page, so the structure can be SEEN. BrandMoves'
/workshop/healthand RookWorlds'/admin/healthdid this. - The health map (RookWorlds): a table of systems with statuses, the links between them, and a ranked findings queue. A scanner owns the mechanical columns and its own findings, which resolve themselves; humans own the curated columns. See
inbox/rookworlds-web/files/scripts/health-scan.mjs. - Wiring maps (RookWorlds): god files, hubs, cycles, orphans, and SEAMS (tables, RPCs and routes shared by two clients). They end in a change-axis scorecard: how many files a typical change touches ("a new building type is one table row").
6. Adopting it in a new project, in order
- The four layers from day one (copy the foundation).
.gitattributesLF,test-changes, and the handoff cap check (the foundation has these).- health-core: the layer / loop / door judge, unplaced files, the stack block + ADR check, the per-key ratchet,
--files/--new/--who. It needsdependency-cruiser. - Copies + same-name + HOMES (jscpd, min 70 tokens and 6 lines).
- Health-lite catalogue: the lexer, the detector engine, the example harness, and the ~33 pure-regex generic detectors first. The list is in the report §6.
- Edit guard + post-edit hook + start hook.
plan(local only) +find.- Close / gauntlet / pre-push with a receipt.
move,proc, and a Node port of check-handoff.
Each step is a whole task. The tools are not generic yet: making them generic (their project data turned into a health.config.mjs) is the "organize" task in HANDOFF.md. The README of D:\foundation already names steps 3–6 as "next worth contributing".