Files
intotheeast-com/docs/solutions/conventions/reconciling-drifted-docs-tense-tiering-and-a-supersession-ledger.md
T
m038andClaude Opus 5 7c9c140a1b Merge main into feat/docs-reconcile; defer to main on two overlapping fixes
main advanced 13 commits while this documentation audit ran — the
location-override work was merged into the outer repo — and it independently
fixed two of the audit's findings. Without this merge the branch would have
REVERTED them, which is the worst outcome for a cleanup pass because it arrives
disguised as an improvement.

Both conflicts resolved in main's favour; main's wording was better informed:

- CLAUDE.md, the single-map-path rule: main's carve-out (829325c) states the
  exception as its own top-level bullet, names MAP_STYLE as the one shared
  thing, and spells out both prohibitions ("do not fold it into initEntryMap",
  "do not add a third path"). Taken verbatim over the version drafted here.
- 2026-07-23-post-form-location-override.md: main (a517331) had already set the
  status to Complete, with far richer detail — the multi-agent review findings,
  the green-run numbers, the DEL4 regression still open, and the merge SHAs.
  Taken in full; the audit's claim that the status "lagged" was dropped, since
  it was true only of this branch's older branch point.

Submodule pin: main bumped user/ to dd19995 and this merge preserves that. The
audit's own no-gitlink-commit discipline applies to bumping the pin as a side
effect of routine work, not to discarding a bump main already made.

main touched none of the other nine corrected documents, so the remaining 18
findings stand unchanged.

Audit notes corrected to match reality rather than left overstated:

- superseded-decisions.md R13 now dates the carve-out to 2026-07-24 (829325c)
  rather than implying this pass introduced it.
- The reconciliation spec gains an "audit baseline moved twice" section: the
  submodule pin lagged real HEAD, and then the base branch advanced mid-audit.
- The compounded learning's section 6 is rewritten from "audit the current
  state" to "re-check the baseline before publishing, not only before starting",
  with the two habits that actually follow: merge the base branch in before
  opening the PR and read conflicts as findings, and when the incoming version
  is better, take it wholesale. An audit has no special authority over the work
  it audits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 00:21:47 +02:00

254 lines
15 KiB
Markdown

---
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 — the same failure mode as a lagging plan `Status:` line.
### 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. Re-check the baseline before publishing, not only before starting
A long audit **races the work it is auditing**. This one had its baseline move twice, and each time the
convenient state was the wrong one:
- **The submodule pin lagged.** A fresh worktree checks out the commit the outer repo pins, not the
submodule's real HEAD. Auditing the pin would have reported a shipped feature as unbuilt. 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").
- **The base branch advanced 13 commits mid-audit**, independently fixing two findings. Merging the
base branch in before opening the PR is what surfaced that. Without it, the branch would have
**reverted** work that was already correct — the worst possible outcome for a cleanup pass, because it
arrives disguised as an improvement.
Two habits fall out of this. **Merge the base branch in before publishing, and read the conflicts as
findings rather than chores** — each conflict is the codebase telling you someone else already reasoned
about this line. And **when the incoming version is better, take it wholesale**: here the base branch's
map-doctrine wording and plan status were both more informed than the replacements drafted during the
audit, so they were kept in full and the audit's own notes were corrected to match. An audit has no
special authority over the work it audits.
## 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`.