docs(solutions): compound the doc-reconciliation learnings; grow CONCEPTS.md
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) <noreply@anthropic.com>
This commit is contained in:
+241
@@ -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`.
|
||||
Reference in New Issue
Block a user