docs(#3308): ADR-3180 §8.1 Enforced, Amendment 9 — Phase 10 validation

Updates docs/adr/3180-planning-semantic-model-single-owner.md to
reflect Phase 10 shipping: §8.1 status Required -> Enforced (Phase 10,
#3308), guard-roster row contract only -> enforced, phase-index table
issue/status backfilled, and a new Amendment 9 recording the guard's
real baseline (15 distinct raw-read sites, 21 total acknowledged
occurrences in cmdValidateHealth) against the issue's own vaguer
estimate, per Amendment 4a's standing "N found by the guard, never
per the epic" rule. Also records the intended reading of an absent
STATE.md as UNREADABLE-without-diagnostic, symmetric with every other
§7 owner's absence-vs-corruption distinction.
This commit is contained in:
sim
2026-08-12 21:52:50 -04:00
parent 2538fd6344
commit 97876a77a4

View File

@@ -900,7 +900,7 @@ Two rows join the roster (declared here rather than inserted above):
| Derivation | Owner | Guard | Scan surface | Status |
|---|---|---|---|---|
| Diagnostic subject (8.1) | `planning-snapshot.cts` (Phase 10) | `lint-planning-snapshot-bypass-drift.cjs` | `src/` | contract only |
| Diagnostic subject (8.1) | `planning-snapshot.cts` (Phase 10) | `lint-planning-snapshot-bypass-drift.cjs` | `src/` | enforced |
| Planning-artifact registration (8.4) | `artifacts.cts` | `lint-planning-artifact-writer-drift.cjs` (Phase 12) | `src/` | contract only |
**The second row is a different shape, recorded as such rather than filed under a contract it does
@@ -969,7 +969,7 @@ is a further amendment to this ADR. Decision 8 **consumes** §7.1–7.7 and does
of them — in particular §7.7 already governs `state validate`'s unconditional `{valid: true}`, and
Decision 8 does not re-decide it.
#### 8.1 The subject a rule may read — *Required — Phase 10*
#### 8.1 The subject a rule may read — *Enforced (Phase 10, #3308)*
**Question.** What may a diagnostic rule look at?
@@ -1107,7 +1107,7 @@ apply.
| Phase | Issue | Deliverable | Status |
|---|---|---|---|
| 9 | #3287 | this design lock (Decision 8) | docs-only |
| 10 | to file | `src/planning-snapshot.cts` (8.1) + `lint-planning-snapshot-bypass-drift.cjs`, ratcheted | ready — Phase 5 merged |
| 10 | #3308 | `src/planning-snapshot.cts` (8.1) + `lint-planning-snapshot-bypass-drift.cjs`, ratcheted | PR pending (Amendment 9) |
| 11 | to file | `src/health-diagnostic.cts` (8.2/8.3/8.5), `validate.health` migrated, W021/W017 second subjects take new codes, `health.md` tables generated | follows Phase 10 |
| 12 | to file | `validate.consistency` + `state.validate` onto the envelope (8.4); `lint-planning-artifact-writer-drift.cjs` | follows Phase 11 |
@@ -1314,3 +1314,49 @@ narrowly.
`.changeset/bold-otters-scope.md` is updated to disclose this write-path change alongside the two
Amendment 7 already recorded.
### Amendment 9 — Phase 10 (#3308) validation: the guard's real baseline, not the issue's estimate
Phase 10 (`src/planning-snapshot.cts`, PR pending) shipped the diagnostic subject §8.1 specifies:
`buildPlanningSnapshot(cwd)`, a parsed projection of `.planning/` composed **exclusively** from the
already-consolidated §7 owners — `getMilestoneInfo`, `listMilestonePhaseDirs`, `isPhaseComplete`,
`scanPhasePlans`, `stateFieldValue`, `planningPaths`. It introduces exactly one new piece of
coordination logic: `worstScope(...scopes)`, a severity-ordered combinator (`COMPLETE` best,
`UNREADABLE` worst) folding several independently-scoped owner answers into one composite `Scope`
per phase record. This is not a re-derivation of any owner — each input `scope` is already that
owner's final verdict; `worstScope` only picks the worst of several finals, which is new coordination
no single §7 owner has visibility to express on its own.
**The guard's real baseline, per Amendment 4a's standing rule ("N found by the guard, never N per
the epic").** The ratcheted guard `scripts/lint-planning-snapshot-bypass-drift.cjs`, scoped to
`DIAGNOSTIC_RULE_FUNCTIONS = {src/verify.cts: {cmdValidateHealth}}`, found **15 distinct (file, text)
raw-read sites, 21 total acknowledged occurrences** inside `cmdValidateHealth`
(`scripts/baselines/planning-snapshot-bypass-baseline.json`). Contrast this against the epic's own
code-COUNT estimate: `cmdValidateHealth` is described, both in the issue and in this ADR's own
Amendment 6 (§ *Why the diagnostic layer is the same failure class*), as emitting "30+" diagnostic
codes through one nested `addIssue` closure — a figure about how many **codes** the function emits,
not how many **raw-read call sites** produce them. The two are different measures, exactly as
Amendments 2/3/4/7 found for their own derivations: a code-count estimate is not a call-site count,
and the whole-repo, function-scoped guard is what makes the real number visible instead of assumed.
The gap runs the expected direction — several codes share a read (`configRaw`'s
`fs.readFileSync(configPath, 'utf-8')` alone accounts for 4 of the 21 occurrences) — so 15 sites
covering 21 occurrences behind 30+ codes is consistent with, not contradictory to, the epic's figure.
**The contract held on the first pass.** No amendment to §8.1 rules 1–4 was needed. Rule 2 — parsed
values only, never raw text — is what the guard now mechanically enforces going forward for any
**new** diagnostic-rule-shaped code: an unrecorded raw-read site inside a `DIAGNOSTIC_RULE_FUNCTIONS`
entry fails lint immediately. `cmdValidateHealth`'s existing 15 sites are ratcheted debt explicitly
owned by Phase 11 (#3309), not silently left unwatched — the baseline can only shrink, and a site that
stops firing without being pruned from the baseline also fails, per Decision 4(e)'s invariants.
**One open judgment call, surfaced for a maintainer's eyes rather than silently resolved — not a
defect, per this ADR's own "written rule, not silent implementation choice" philosophy.** §8.1 rule 4's
text ("Read failures are reported via `warnUnusableInput`... and the field's `scope` is `UNREADABLE`")
could read as implying every `UNREADABLE` scope correlates with a reported diagnostic. Phase 10's
`currentPhaseLabel` field (`buildCurrentPhaseLabel`, `src/planning-snapshot.cts`) treats a genuinely
**absent** STATE.md as `UNREADABLE` too, but does **not** call `warnUnusableInput` for that case — only
an actual read error (e.g. EISDIR) fires it. This mirrors §7's own absence-vs-corruption distinction
elsewhere in this ADR (e.g. the `unusable-input.cts` glossary entry's `#1881` note on ROADMAP.md): a
project that never ran `state.init` legitimately has no STATE.md yet, and that is a non-answer, not
corruption. Recorded as the intended reading rather than a gap, since it is symmetric with how every
other §7 owner already treats absence vs. unreadable.