SYS-009: Cascade documentation by altitude — one body of work, a distinct artifact per surface¶
Status: Accepted (amended 2026-07-05: reframings cascade too; decision records name their blast radius; volatility rule for outward surfaces. Amended 2026-07-18: guarantees name their enforcer, everything else is a dated observation) Date: 2026-06-27 (accepted 2026-06-29, amended 2026-07-05, amended 2026-07-18) Deciders: San Lee
Context¶
The system has outgrown a single audience. A piece of work — say the classifier's prompt-optimization loop (classifier/ADR-005) — now needs to appear on several surfaces at once:
- the repo that implements it (spec, ADR, code, run log),
- this architecture repo, where cross-repo patterns and decisions live,
- the portfolio, where the work is shown to a skeptical external reviewer,
- learning-notes, where the underlying concepts are explained in plain language.
Each surface has a different audience and a different altitude. Without a convention, two failure modes show up by default. Either the same document is copied onto every surface (and the copies drift), or each surface slowly accretes whatever lands on it — learning-notes degrades from a concept library into a project changelog, and the portal duplicates repo internals.
Several of these surfaces are also generated or aggregated: the portal (build_portal.py, architecture/ADR-001), the portfolio site's navigation, the learning-notes index and concept map. Generated files cannot be merged across parallel sessions, so the same work touching several of them creates conflicts unless the wiring is owned in one place.
Decision¶
Treat each landing as one body of work, transformed into a distinct artifact at each altitude — never the same document duplicated up a chain.
| Altitude | Surface | Artifact | Audience |
|---|---|---|---|
| Ground truth | the implementing repo | spec, ADR, code, run log | builder |
| System | this architecture repo |
the cross-repo pattern / decision (SYS-*) |
the system view, future-me |
| Showcase | portfolio (/lab, writeups) |
the artifact + a Decision→Why→Tradeoff narrative | skeptical external reviewer |
| Concept | learning-notes | plain-language concept notes, project-independent | a learner (incl. future-me) |
Three disciplines hold it together:
- Transform, don't duplicate. Each surface gets an artifact written at its own altitude and for its own audience. A spec is a contract; a portfolio writeup is a narrative; a SYS ADR is a pattern. The same prose never appears on two surfaces, because coupling them guarantees drift.
- learning-notes are concept-first, not changelogs. A note explains a concept (e.g. loop engineering, Goodhart in eval-driven optimization, held-out test sets) and must stand on its own even if the project that motivated it never existed. Project specifics stay in the repo and the portfolio.
- Stack changes cascade back, not just forward. The table above is the forward path — one landing fans out into per-altitude artifacts. The same edges run in reverse. When a repo's ground truth changes in a way the derivative artifacts describe — a language or framework port, a dropped dependency, a schema migration — the work is not done until every surface that references it is swept. The implementing repo's PR names its dependent surfaces and updates them, or files a fast-follow. The motivating instance: notes-api's Java/Spring + Kafka → Python/FastAPI port left learning-notes and the portal describing a stack that no longer existed, a full architecture behind the code. Two traps this rule exists to prevent, both observed: a partial sweep (updating a README banner but leaving the note bodies stale) reads as "done" while the substance rots; and verifying against a local clone that is behind
mainre-confirms the stale version as correct — check the current remote, the same sync-first discipline each repo's CLAUDE.md already demands.
Wiring rule for generated/aggregated surfaces. When a landing must register itself in a generated index (the portal, portfolio nav, the learning-notes index/map), that registration + rebuild is done by a single integrator, once, after the content lands — never hand-edited in parallel branches. This extends the parallel-session rule already in each repo's CLAUDE.md, and binds the portal (architecture/ADR-001) specifically: its portal/ and site/ trees are gitignored build outputs, regenerated by build_portal.py, never edited by hand.
De-scope sweep procedure¶
When something is cut, renamed, ported, or otherwise scrapped — a dropped dependency (Kafka), a superseded metric (v1 → v2 numbers), a renamed contract, a removed framework — the work is not done until every surface that names it is reconciled. The mechanical drill, in order:
- Grep the old term across every repo, from the remote
main— not a local clone. Cast wide: not just READMEs, but ADRs and decision records,.env.exampleand config samples, the sources of generated docs (KB stubs, portal source, learning-notes bodies, diagram data), code comments, and repo metadata (descriptions, topics). The slips hide in the long tail. - Reconcile each hit explicitly. Update it to the current truth, or mark it as labeled history. A
v1/ "earlier" mention is fine; an unlabeled, present-tense claim of the old reality is not. - A partial sweep is a failure. Updating a README banner while an ADR, a KB stub, or a metric cell still asserts the old truth reads as "done" while the substance rots. The slip that motivated writing this down: the classifier's v1.1.0 doc sweep fixed the README/PRD but missed the two ADRs, which kept claiming API-layer enum validation the code never did.
- Verify against the current remote. A local clone that is behind
mainwill re-confirm the stale version as correct — the same sync-first discipline each repo's CLAUDE.md already demands. - The PR names the surfaces it swept (or files a fast-follow for the rest), so the cascade is auditable.
A periodic cross-repo consistency audit — grep the canonical facts (metrics, stack, app names, firm name) across every public surface and diff them — is the backstop that catches whatever a single sweep missed. The audit checks meaning, not just terms: alongside the greps, read the outward surfaces' claims against the current decision records and flag restatements of a superseded framing, which no grep for an old term will catch.
Reframings cascade the same way (amendment, 2026-07-05)¶
A de-scope (something cut, renamed, ported) is not the only ground-truth change that rots
derivative surfaces. A reframing — a new decision that changes the organizing story
without removing anything — carries the same sweep obligation. The motivating instance:
classifier/ADR-006 reframed the portfolio spine as a four-level autonomy ladder; nothing
was cut, every old fact stayed true, yet the GitHub profile README went on telling the
pre-ladder story. A reframing is harder to catch than a de-scope because there is no old
term to grep for; the stale surface is accurate but superseded. That is why the two rules
below exist.
Decision records name their blast radius (amendment, 2026-07-05)¶
Every new decision record — SYS-* here and repo-local ADR-* alike — includes a
Downstream surfaces section: the list of surfaces that restate or depend on what the
decision changes (READMEs, portfolio copy, sync registries, setup scripts, generated-doc
sources). "None" is a valid entry, but it must be written, because the failure mode is not
listing the wrong surfaces — it is never asking the question. The landing PR sweeps the
listed surfaces or files a fast-follow, same as the de-scope drill. This is the prose analog
of a sync script's consumer registry: the registry is what turns "we decided X" into "every
copy of X moved." Both misses on 2026-07-05 shared this root cause — dotfiles PR #6 decided
a skill stays repo-local but did not update the sync registry that decision touched, and
ADR-006 landed the ladder without touching the profile README that restates the spine.
Volatility rule for outward surfaces (amendment, 2026-07-05)¶
Outward-facing surfaces that restate the system (the GitHub profile README, portfolio landing copy) restate shipped facts only, and link to the canonical source for the evolving story. Shipped facts are immutable history — "v2 added BM25 and the lift was marginal" ages perfectly. Plans and organizing framings move with every ADR, so every surface that restates one becomes a standing sweep obligation. Restate the slow-moving, link the fast-moving. This is prevention where the blast-radius rule is mitigation: prose that does not exist cannot drift.
Guarantees name their enforcer; everything else is a dated observation (amendment, 2026-07-18)¶
The 2026-07-18 audit found six confidently-wrong claims across the system. Sorting them by cause gave one rule, and it is narrower and more useful than "be careful."
Some had been true when written and were outrun by change — published accuracy figures, a roadmap listing shipped versions, an autonomy rung whose technique was later retired. Those are cheap failures: the sentence was honest, the world moved, and nothing pointed back at it.
The dangerous ones had never been true, and read as observations because they were written
beside the thing they described. SYS-004 said "both sides now carry contract tests, so a
drift is caught by a red build." Tests did exist on both sides. The leap from "tests exist on
both sides" to "the tests observe each other" is one word wide, invisible unless specifically
checked, and it stood for a month while the seam it promised to guard was broken. ADR-002
called its redline guard a mechanical backstop; the guard had no behavioural tests. A
portfolio page claimed CI catches a renamed field; nothing did.
The pattern: a claim about system behaviour, written at the same moment as the system, feels like a description and is actually a prediction. So:
- A sentence asserting a guarantee must name the mechanism that produces it — the check,
the gate, the test, the generated artifact. If you cannot name one, you do not get to write
the guarantee. "Drift fails the build (
scripts/check_classify_contract.py, CI)" is a claim a reader can go verify; "drift is caught by a red build" is a hope in the present tense. - A claim you cannot make executable is written as a dated observation, not a guarantee. "As of 2026-07-18, each side pins the shape in its own suite" ages visibly and stays true as history. Most of what drifted would have been harmlessly stale in that voice.
- Prefer generating over restating. A number retyped from a report is a copy that will
eventually disagree with its source; a number asserted against a published artifact cannot.
This is
SYS-018applied to prose, and it is why the classifier now publishesevals/metrics.jsonand the portfolio checks against it rather than proofreading itself.
This is enforced where it can be. scripts/lint_decision_log.py runs in CI over this log:
table rows match disk, statuses match headers, cross-links resolve, and no decision claims to
have foreclosed something with an empty Alternatives table. The rest is a writing convention,
and it is honest to say so — a convention is exactly the kind of thing this amendment is about
not trusting, which is why the checkable parts were made checkable first.
Consequences¶
- Every surface has a clear, discoverable home for its version of the work, and the four artifacts each evolve at their own pace without dragging the others.
- The public surfaces stay legible. learning-notes remains a concept library; the portfolio stays a curated showcase; the portal stays a faithful aggregate, not a pile of duplicates.
- Cost: more authoring. A single landing can imply up to four derivative artifacts, each needing a deliberate "what belongs here, at this altitude?" judgment. Accepted — the alternative is drift or rot.
- Forecloses the convenience of one canonical doc reused everywhere, deliberately. That convenience is exactly what produces stale copies and wrong-altitude content.
- Not every landing hits all four surfaces. A pure bugfix may never leave its repo. The convention says where each kind of artifact goes when it is warranted, not that every change must fan out.
- A ground-truth change carries a sweep obligation. Because each surface holds a transformed copy, moving the substrate (a port, a migration, a dropped dependency) silently rots those copies. Naming the dependent surfaces in the change's PR is what keeps the cascade honest in both directions; skipping it is how learning-notes drifted a full architecture behind the notes-api code.
- Every decision record now costs a "Downstream surfaces" pass (2026-07-05 amendment). Accepted: it is a one-list tax at authoring time, paid instead of an unbounded drift-hunt later, and it forces the "who restates this?" question at the only moment the author reliably knows the answer.
Alternatives Considered¶
| Option | Reason Not Chosen |
|---|---|
| One canonical doc, linked or embedded everywhere | Wrong altitude and audience per surface; a builder's spec is not a reviewer's showcase, and embedding couples them so they drift together |
| Copy the doc onto each surface | Duplication that diverges the moment one copy is edited — the failure this convention exists to prevent |
| Document only in-repo; skip portfolio + learning-notes | Discards the showcase and the concept value, the entire reason the system has those surfaces |
| No convention; let each surface grow organically | The observed default: learning-notes rots into a changelog, the portal duplicates repo internals, indexes conflict across sessions |
Cross-references: architecture/ADR-001 (the portal this wiring rule binds), SYS-001 (the two-tier ADR practice this follows), and the motivating instance classifier/ADR-005.