From d946eaaa7e383936ec136be23f4bb97daae99726 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sat, 25 Jul 2026 00:18:36 +0200 Subject: [PATCH] docs(solutions): compound the doc-reconciliation learnings; grow CONCEPTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the durable learnings from the 2026-07-25 whole-repo documentation reconciliation as a knowledge-track learning, plus the vocabulary it introduced. New learning — conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md: - Tier docs by TENSE, not only by content type. claude-md-content-tiering.md established "descriptions drift, rules don't" and tiered by content type; that axis could not say what to do with 41 completed plans, which are neither rules nor current descriptions. Present-tense docs are defects when stale; past-tense records are supposed to be stale and get annotated, never rewritten. - Ledger AND inline notes, because each covers the other's failure: a ledger alone is a pointer you may not follow, inline notes alone give no changelog view. Prefer annotation patterns the repo already uses. - Record what was NOT reversed, or a ledger of only reversals makes every old doc look suspect and settled decisions get re-litigated. - Separate "docs are wrong" from "code is wrong" — route code-side findings to a recommendations doc so a docs diff stays reviewable. - Verify against the artifact that decides behaviour: the Makefile for commands (including macro-generated targets a grep misses), the build script for outputs, imports for source-vs-output, branch history for whether a plan shipped. An audit that never withdraws a finding has not been checking itself — one finding here was withdrawn after reading package.json. - Audit the state that actually runs: a fresh worktree checks out the submodule PIN, which lagged real HEAD and would have hidden a whole merged feature. Three structural lessons in "Why This Matters": - An index describing another document's role is a factual claim that can rot, and it is worse than the stale document itself — it defeats the reader's judgement before it engages. This was the tree's single most misleading line. - Wrong beats absent again, now for commands: README's server runbook documented every remote-* target without the -test/-prod suffix guard-env requires. deploy-cycle.md had it right — the defect was a second copy drifting. - Promoting a doc to "the authoritative list of X" creates a completeness obligation it did not have as prose, and nothing enforces it. - A removal is not finished when the code is gone, but when every consumer and every description of it is gone — travel-memories left a compose service behind, hidden by a cached Docker image. Local state can mask a breakage indefinitely, so "it works here" is not evidence. Overlap with conventions/claude-md-content-tiering.md scored MODERATE (2 of 5 dimensions: same root-cause thesis, overlapping files; different tiering axis and different prevention), so a new doc was written rather than folding into it. Flagged in the Related section as a consolidation candidate if a third documentation learning appears. CONCEPTS.md — new Documentation cluster (Historical record, Superseded decision, Plan status) and one flagged ambiguity recording that a present-tense historical record is not a claim about the current system. These three are now referenced by CLAUDE.md and both doc READMEs, so they needed defining. Discoverability check: no edit needed — CLAUDE.md's entry-point table already surfaces docs/solutions/ with its frontmatter fields and CONCEPTS.md. Co-Authored-By: Claude Opus 5 (1M context) --- CONCEPTS.md | 18 ++ ...tense-tiering-and-a-supersession-ledger.md | 241 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 docs/solutions/conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md diff --git a/CONCEPTS.md b/CONCEPTS.md index 1e400b8..065de72 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -48,6 +48,24 @@ A server's per-host configuration overlay. Once it exists, Grav's Admin writes * ### Remote-only plugin One of the project's three plugin-management categories, alongside GPM-managed (declared in the shared install list and restored by the standard install flow) and custom-in-repo (code tracked in the Content repo). A Remote-only plugin is installed explicitly on servers and restored by **no** standard flow — if its code goes missing it stays missing until someone reinstalls it deliberately, even while its configuration persists in the Env tree. +## Documentation + +### Historical record +A document that states what was decided or built at a past moment, not what is true now — plans, specs, milestone scopes, and session write-ups. Its going out of date is expected and is what makes it a record; it is corrected only by annotation, never by rewriting, because the value is the reasoning at the time. + +Distinguished from *current documentation*, which asserts how the system is today and is simply wrong when it drifts. A Historical record often reads in present tense, so the distinction is carried by an explicit marker rather than by tone. + +### Superseded decision +Something the project planned or built and then deliberately reversed, recorded so the reversal is discoverable from the document that still describes the original. Each one names what was planned, what replaced it, when, and why. + +The record exists because a reversal is otherwise invisible: the old document keeps asserting the old thing, and the reasoning that killed it lives only in whoever remembers. A Superseded decision is the standing answer to "may I re-create this?" — usually no, and often the prohibition is also a hard rule. + +### Plan status +The single recorded state of a plan, carried on the plan itself rather than in a separate tracker. **Deferred** and **Abandoned** are deliberately distinct: Deferred means still wanted but not now, Abandoned means decided against, kept so the decision is not re-litigated. + +A status that lags reality is worse than no status, because it is trusted — so it moves when the work lands, not later. + ## Flagged ambiguities - "daily" / "entry" / "journal post" all refer to the same concept (a dated journal post). Canonical term: **Entry**. The section/folder is named "dailies" and the nav label is "Journal" — these name the *collection*, not a different entity. +- A **Historical record** written in present tense is **not** a claim about the current system. Staleness there is correct; staleness in current documentation is a defect. When the two disagree, the code decides, and the gap is recorded as a **Superseded decision**. diff --git a/docs/solutions/conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md b/docs/solutions/conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md new file mode 100644 index 0000000..3ea8a81 --- /dev/null +++ b/docs/solutions/conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md @@ -0,0 +1,241 @@ +--- +title: Reconciling drifted docs — tier by tense, and record reversals in a ledger +date: 2026-07-25 +category: conventions +module: documentation +problem_type: convention +component: documentation +severity: high +applies_when: + - Auditing documentation against the code after a period of undocumented change + - Deciding whether a stale document should be corrected, annotated, or deleted + - A plan or milestone describes a feature that was later dropped or replaced + - Writing or reviewing an index that describes what another document is for + - Asked whether the docs would pass a review, or to make them pass one + - A decision is being reversed and the old rationale needs to survive the reversal +tags: [documentation-conventions, staleness, tiering, drift, supersession, decision-log, audit, verification, indexes] +--- + +# Reconciling drifted docs — tier by tense, and record reversals in a ledger + +## Context + +Five weeks after the last documentation pass, this repo's docs described a site that partly no longer +existed. `/map`, `/stats`, `/tracker`, Leaflet, a light theme, and `hero_image` on entries had all been +deliberately removed — but several documents still presented them in confident present tense, and +nothing marked those documents as historical. + +The trigger question was *"straighten this out so a repeat review returns ok."* The answer depended on +a distinction the tree did not encode. + +[`claude-md-content-tiering.md`](claude-md-content-tiering.md) established that **descriptions drift +and rules do not**, and tiered content by *type* (rules stay in `CLAUDE.md`, descriptions move to +`docs/`). This pass confirmed that thesis again — every one of 20 verified defects was a description +of code, config, or a command; not one was a rule that had gone wrong on its own. But content-type +tiering alone did not answer what to *do* with 41 completed plans and 4 milestone specs, because those +are neither rules nor current descriptions. + +The missing axis was **tense**. + +## Guidance + +### 1. Tier by tense, then treat the halves oppositely + +| Kind | Files here | Claims | Staleness is | Treatment | +|---|---|---|---|---| +| **Present-tense** | `CLAUDE.md`, `docs/reference/`, `docs/guides/`, `README.md`, `CONCEPTS.md` | "this is how it *is*" | a **defect** | correct against the code | +| **Past-tense** | `docs/working/plans/`, `specs/`, `milestones/`, `summary.md`, `pm-analysis.md` | "this is what we decided *then*" | **correct and expected** | annotate only, never rewrite | + +A completed plan *should* be stale — that is what makes it a record. Rewriting 41 plans to match +today's code would destroy the audit trail of *why* each thing changed, and the work is unbounded. +The defect was never their staleness; it was that nothing told a reader they were history. + +`docs/solutions/` straddles the split deliberately: past-tense incident, present-tense guidance. That +is why its `applies_when` frontmatter matters more than its narrative — the frontmatter is the part +that must stay true. + +### 2. Ledger plus inline notes — neither alone is enough + +Two mechanisms, because each covers the other's failure: + +- **A supersession ledger** (`docs/reference/superseded-decisions.md`) — one table: what was planned, + where it was planned, what is true now, when it changed, why. This is the only thing that answers + *"what did I change my mind about?"* in one place, which is the question a review actually asks. + Alone, it has an indirection problem: a pointer you might not follow. +- **Inline `> **Superseded …**` notes** at each stale claim, so the claim cannot be read + un-corrected. Alone, it has a completeness problem: no changelog view, and coverage is only as good + as the annotation pass was. + +**Prefer the annotation patterns the repo already uses.** Here, `architecture.md` already carried +`> History:` notes and `trip-switching.md` already carried `> **Changed 2026-07:**`. Inventing a third +convention would have been worse than adopting either. + +**Add the ledger row in the same commit as the reversal.** A ledger that lags is worse than no ledger, +because it is trusted. This is the same failure mode as a lagging plan `Status:` line — and one was +found during this pass: a plan read `📋 Not started` while its feature was merged. + +### 3. Also record what was *not* reversed + +A ledger of only reversals makes every old document look suspect. This one ends with a short +"decisions that were *not* reversed" section — the `pm-analysis.md` SKIP list still stands, the +weather integration shipped exactly as specified, the stats computation survived and only *moved*. +Without it, a future reader re-litigates settled decisions because the surrounding docs looked old. + +### 4. Separate "the docs are wrong" from "the code is wrong" + +An audit against code finds both. Mixing them makes the diff unreviewable and stalls the documentation +fix behind a behaviour decision. Route code-side findings to a separate recommendations document and +**explicitly do not act on them**. Here that kept a 300-line docs diff clean while still capturing that +`make start` is broken on any clean checkout. + +Documenting a trap is not the same as fixing it — and is the right move when the fix is someone else's +call. Per the tiering doc, a gotcha has no natural trigger that opens a file, so a live trap belongs in +`CLAUDE.md` even while its fix stays unscheduled. + +### 5. Verify against the artifact that decides behaviour, not the prose about it + +Every finding must come from the thing that actually determines behaviour: + +| To check | Read | +|---|---| +| What a command does | the `Makefile` — including macro-generated targets, which a grep for literal target names will miss | +| What a build produces | the build script (`package.json`), not a prose asset table | +| Whether a file is a source or an output | which file *imports* it, and how it reaches the page | +| Whether a feature exists | the absence of its mechanism, not the absence of a mention | +| Whether a plan shipped | the branch history, not the plan's own `Status:` line | + +This is also where an audit catches *itself*. One draft finding here claimed the asset table was +missing four source files; reading `package.json` showed the table lists esbuild **entry points**, so +imported-only sources were correctly absent. The finding was withdrawn. **An audit that never +withdraws a finding has not been checking itself.** + +### 6. Audit against the state that is actually current + +The outer repo pinned a `user/` commit that lagged the submodule's real HEAD, and that pin was the +default checkout in a fresh worktree. Auditing it would have produced findings against a state nobody +runs — and would have missed a whole merged feature. Move to the real HEAD first, and keep the +gitlink out of the commit (see +[`dual-repo-submodule-workflow.md`](../architecture-patterns/dual-repo-submodule-workflow.md) — `M user` +is normal and must not be "fixed"). + +## Why This Matters + +**An index describing another document's role makes a factual claim that can rot — and it is worse +than the stale document itself.** The single most misleading line in this tree was +`docs/working/README.md` advertising `summary.md` as *"Project summary / current state"*, while +`summary.md` described Leaflet, `/tracker`, `/map` and `/stats`. A stale document is survivable — a +reader may notice the date, the tone, the odd claim. An index that vouches for it as authoritative +**defeats that judgement before it engages.** When writing an index, treat every "what this file is +for" phrase as an assertion with an expiry date. + +**Wrong beats absent, again — now for commands.** The tiering doc found this for paths: an absent fact +makes an agent go look; a wrong one makes it act confidently in the wrong place. The same held for +`README.md`'s server runbook, where every `remote-*` command was documented without the `-test`/`-prod` +suffix `guard-env` requires. Every documented command failed on the first line. `deploy-cycle.md` had +the rule right the whole time — the defect was a **second copy** of the knowledge drifting from the +first. Fewer copies would have prevented it outright. + +**Promoting a doc to "the authoritative list of X" creates a completeness obligation it did not have +as prose.** `CLAUDE.md` pointed at `README.md` for "the full `make` command list"; README then held 7 +of ~20 `remote-*` targets. The pointer was added by a well-intentioned earlier tiering pass. Routing +content out of an always-loaded file is right, but **the destination inherits a duty to be complete**, +and nothing enforces that. + +**Deliberate removals leak.** `travel-memories` was extracted to its own project, its source deleted +and `services/` gitignored — but `docker-compose.yml` still declared the service, and `CLAUDE.md` still +claimed it ran on :8082. `make start` has therefore been broken on every clean checkout since, hidden +only because a pre-removal Docker image stayed cached locally. **A removal is not finished when the +code is gone; it is finished when every consumer and every description of it is gone too.** The cached +image is the general lesson: local state can mask a breakage indefinitely, so "it works here" is not +evidence. + +## When to Apply + +- After any stretch of change that outpaced its documentation, or when asked whether the docs would + survive a review +- Before rewriting a stale plan, spec, or milestone — annotate it instead; the record is the value +- When reversing a decision: add the ledger row and the inline note in the reversal's own commit +- When writing an index, a folder README, or any "read X for Y" pointer — that pointer is a claim +- When removing a service, route, feature, or dependency: sweep for consumers *and* for prose that + describes it, including compose files, always-loaded instruction files, and demo fixtures +- When promoting any document to authoritative for a list — decide who keeps it complete +- Before auditing a repo with submodules: confirm you are on the state that actually runs + +## Examples + +### Tense-marking a historical spec, without rewriting it + +`milestones/milestone-2.md` still opens with its original goal — that is the record. The banner sits +directly beneath it, so the stale claim cannot be read alone: + +```markdown +**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, … + +> **Superseded — written 2026-06-21. Neither the `/map` page nor Leaflet exists.** +> +> - **No `/map` route.** The map renders inline on the trip page via the single shared partial +> `templates/partials/entry-map.html.twig` (R1, retired 2026-07-04). +> - **Leaflet + OpenStreetMap tiles → MapLibre GL JS** (R4, 2026-06-20). +> +> The *substance* of this spec survived — markers per entry, chronological route line, popups, +> bounds fitting — it all lives in `MapUtils.initEntryMap()`. Only the page and the library changed. +``` + +Separating "the idea won" from "this implementation lost" is what stops a future reader concluding the +whole spec was a dead end. + +### An index that vouched for a stale document + +```diff +-| `summary.md` | Project summary / current state | ++| `summary.md` | **Historical** wrap-up of the original four-milestone branch (2026-06-21). ++ *Not* the current state — for that read [`../reference/architecture.md`](../reference/architecture.md) | +``` + +### A source relationship that never existed + +`CLAUDE.md` asserted a build dependency between two unrelated things. `css/` is hand-authored and +served *directly*; `css-compiled/` is esbuild output from the CSS imports inside `js/src/*.js`: + +```diff +-- `css-compiled/` and `fonts/` are generated (sources: `css/style.css`, `css/tokens.css`) ++- `css-compiled/` and `fonts/` are **esbuild output from the imports inside `js/src/`** — *not* ++ from `css/`. Everything in `css/` is hand-authored and served directly (`assets.addCss` in ++ `partials/base.html.twig`), never compiled. +``` + +The failure this invited: an agent wanting to change a font edits `css-compiled/main.css` — a +generated bundle — because the rule named `css/style.css` as its source and that file does not contain +it. The next `make build-assets` silently reverts the edit. + +### Proving a breakage instead of inferring it + +Reasoning that a missing directory *would* break a build is not evidence. Running it is: + +```console +$ docker compose build travel-memories +unable to prepare context: path ".../services/travel-memories" not found +``` + +The follow-up mattered more than the failure: a cached `travel-blog-intotheeast-travel-memories:latest` +image explained why `make start` still worked on the main checkout but failed in every new worktree. +Without that check the finding would have been reported as "broken everywhere" and been wrong. + +## Related + +- [`claude-md-content-tiering.md`](claude-md-content-tiering.md) — the content-type tiering axis and + the "descriptions drift, rules don't" thesis this learning extends with a tense axis. **Consolidation + candidate:** the two overlap on root cause and on the files they touch; if a third documentation + learning appears, consider merging all three into one documentation-maintenance doc. +- [`../architecture-patterns/retiring-a-consolidated-grav-sub-page.md`](../architecture-patterns/retiring-a-consolidated-grav-sub-page.md) + — the mechanics of the retirement that produced ledger rows R1, R2 and R5. That doc covers removing + the *page*; this one covers removing the *claims about* the page. +- [`../architecture-patterns/dual-repo-submodule-workflow.md`](../architecture-patterns/dual-repo-submodule-workflow.md) + — why a fresh worktree's `user/` sits at the pin rather than at HEAD, which is the audit-baseline trap + in §6. +- [`../integration-issues/stale-grav-version-blocks-api-plugin-install.md`](../integration-issues/stale-grav-version-blocks-api-plugin-install.md) + — the same rot in the deploy-config domain: a version number that went stale and broke an install. +- `docs/working/specs/2026-07-25-docs-reconciliation-design.md` — the design and the verification + table for this pass. +- `docs/working/2026-07-25-doc-drift-recommendations.md` — the code-side findings deliberately not + acted on, including the compose breakage and a proposed repeatable `make docs-check`.