diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 96669bb01..9f271d496 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,56 @@ +## 2026-09-03 — E-A-CENSUS-IS-A-FUNCTION-OF-ITS-REGEX-SO-GATE-THE-PROPERTY-1 — three numbers, one tree + +**Status:** FINDING (all three counts measured on the same tree, same hour). +**Confidence:** High — the divergence is reproducible by swapping one pattern. + +**The measurement.** A sibling session's census reported **53 of 208** plans +carrying no D-id and proposed a CI gate so the number could not regrow. Before +building it I re-measured, and got a different answer — twice: + +| pattern | untracked / 208 | +|---|---| +| the sibling session's sweep | **53** | +| `supersession_index.py`'s own `DID` | **75** | +| a stricter form requiring a trailing number | **102** | + +Same tree, same hour, same population — the `3DGS` family counts 19 under every +pattern, so nobody was measuring a different thing. **The census is a function +of the regex**, and none of the three numbers is wrong; they answer three +slightly different questions and only one of them (the generator's) is the one +the supersession index actually uses. + +**The design consequence, which is the point.** A gate asserting *"no more than +N untracked plans"* would have been wrong on the day it landed and would have +frozen whichever regex its author happened to hold. So the gate asserts what +regex choice cannot move: **a plan ADDED in this PR cites at least one D-id.** +That stops the backlog regrowing — the stated goal — without requiring the +backlog to be agreed on, counted, or backfilled, which is a cross-session scope +call nobody has made. + +Added-only is deliberate too: gating MODIFIED plans would block whoever next +edits a pre-existing untracked plan, punishing them for a debt they did not +create. A gate that fires on innocent work gets routed around, and a +routed-around gate is worse than none. + +**A second finding, from building it.** The obvious way to share the pattern — +`from supersession_index import DID` — is wrong here: that module has **no +`if __name__ == "__main__"` guard**, so importing it runs the entire generator +and prints the index to stdout. Measured; the first version of the checker did +exactly that. Adding a guard would refactor a CI-gated tool for one caller's +convenience, so the pattern is lifted from its source text instead, with a hard +error if that definition is ever renamed or reshaped. A silent fallback to a +local copy would be the drift +`E-A-CITATION-IS-NOT-A-DEPENDENCY-AND-A-FORCED-COPY-NEEDS-A-GATE-1` was written +about — and note the contrast: there the copy was FORCED by a zero-dep boundary +and the remedy was an equivalence test; here nothing forces it, so the remedy is +to not copy at all. + +**Verified two-sided on real history, not fixtures.** The three plans actually +added in the last 30 commits all carry D-ids, so the gate passes on recent +legitimate work; the `3DGS` family — the largest untracked group — fails it. +A gate that cannot fire and a gate that fires on everything are the same +non-signal, so both halves were run. + ## 2026-09-03 — E-THE-FREE-MITIGATION-WAS-FREE-FOR-TWO-HOURS-1 — the entry's own thesis, applied to the entry **Status:** FINDING (observed on #1160; supersedes the mitigation half of diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 49f738b76..d9ba10f25 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,12 @@ +## 2026-09-03 — branch: new plans must carry D-ids (the gate, not the backfill) — INVENTORY DELTA + +- ADDED `.claude/tools/plan_dids.py` — fails when a plan file carries no D-id. Reads the pattern out of `supersession_index.py`'s SOURCE rather than importing it: that module has no `__main__` guard, so `import` runs the whole generator and prints the index (measured — the first version did this). Hard-errors if the definition is renamed, rather than falling back to a local copy. +- ADDED `.github/workflows/plan-dids.yml` — runs it on plans ADDED by a PR, modelled on `supersession-index.yml` (same trigger shape, same actionable error text). Added-only by design: gating MODIFIED plans would block whoever next edits a pre-existing untracked plan for a debt they did not create. +- MEASURED, and it is why the gate asserts a PROPERTY and not a COUNT: the same tree gives **53** untracked plans (a sibling session's sweep), **75** (the generator's own `DID` pattern) and **102** (a stricter pattern) out of 208. The `3DGS` family is 19 under all three, so the population is identical — the number is a function of the regex. A "no more than N untracked" gate would have been wrong on arrival. +- VERIFIED two-sided on real history: the 3 plans actually added in the last 30 commits all pass; the `3DGS` family fails. The workflow's own `git diff --diff-filter=A` selector was run against a real range and returns exactly those 3. +- NOT DONE, deliberately: the backfill of the existing untracked plans. The sibling session declined it as a cross-session scope call and said the fix is the gate; this is that gate. Their `probe-r2il-live-regfile-v1` remains theirs to id. +- Epiphany: `E-A-CENSUS-IS-A-FUNCTION-OF-ITS-REGEX-SO-GATE-THE-PROPERTY-1`. + ## 2026-09-03 — branch (#1160): both external reviewers are now capped — INVENTORY DELTA - OBSERVED on #1160: Codex answered a review request with "You have reached your Codex usage limits for code reviews"; CodeRabbit is simultaneously at its org spending cap (85 attempts/7d → 1/hour). **No external reviewer can currently see any PR in this repo.** diff --git a/.claude/tools/plan_dids.py b/.claude/tools/plan_dids.py new file mode 100755 index 000000000..b4c3eb6d2 --- /dev/null +++ b/.claude/tools/plan_dids.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Every NEWLY ADDED plan must carry at least one D-id. + +Why a property and not a count +------------------------------ +A census of untracked plans is a function of the regex used to take it. The +same tree, on 2026-09-03, measured **53** (a sibling session's sweep), **75** +(the generator's own pattern) and **102** (a stricter pattern requiring a +trailing number) — identical population, identical `3DGS` family of 19, three +different totals. A gate asserting "no more than N untracked plans" would have +been wrong on the day it landed and would encode whichever regex its author +happened to hold. + +So this gate asserts something regex-choice cannot move: *a plan added in this +PR cites at least one D-id.* That stops the backlog regrowing, which is the +stated goal, without requiring the backlog to be agreed on or backfilled — a +cross-session scope call nobody has made. + +Why it does not fire on MODIFIED plans +-------------------------------------- +Gating modifications would make every edit to a pre-existing untracked plan a +blocked PR, punishing whoever next touches an old file for a debt they did not +create. Added-only is the minimal form that prevents regrowth. + +Why the pattern is READ from the generator and not copied +-------------------------------------------------------- +`supersession_index.py` owns the D-id pattern; the index's coverage column is +computed with it. A second copy here would agree with it exactly until one was +edited, which is when nobody is comparing them — the drift failure this +workspace ruled on in `E-A-CITATION-IS-NOT-A-DEPENDENCY-AND-A-FORCED-COPY-NEEDS-A-GATE-1`. + +`import` was the obvious way to share it and is WRONG here: that module has no +`if __name__ == "__main__"` guard, so importing it runs the whole generator and +prints the index to stdout. (Measured — the first version of this file did +exactly that.) Adding a guard would be a refactor of a CI-gated tool for one +caller's convenience, so instead the pattern is lifted from its source text. +There is exactly one definition and it is still the single source; if that line +is ever renamed or reshaped this raises, which is the correct failure — a silent +fallback to a local copy is the very drift being avoided. +""" + +import pathlib +import re +import sys + +_GEN = pathlib.Path(__file__).resolve().parent / "supersession_index.py" +_DEF = re.compile(r"^DID\s*=\s*re\.compile\(r'(?P.*)'\)\s*$", re.M) + + +def _did_pattern() -> "re.Pattern[str]": + """The generator's own D-id pattern, read from its source.""" + m = _DEF.search(_GEN.read_text(errors="ignore")) + if not m: + raise SystemExit( + f"plan-dids: could not find the `DID = re.compile(r'...')` definition in " + f"{_GEN}. The pattern moved or was renamed. Fix this extractor rather " + f"than copying the pattern here — a second copy is the drift this gate exists to avoid." + ) + return re.compile(m.group("pat")) + + +DID = _did_pattern() + + +def untracked(paths: list[str]) -> list[str]: + """Return the subset of `paths` carrying no D-id.""" + out = [] + for p in paths: + f = pathlib.Path(p) + if not f.is_file(): + continue # deleted or renamed away in the same PR + if not DID.search(f.read_text(errors="ignore")): + out.append(p) + return out + + +def main(argv: list[str]) -> int: + paths = [a for a in argv if a.endswith(".md")] + if not paths: + print("plan-dids: no added plan files in this diff; nothing to check") + return 0 + + missing = untracked(paths) + for p in paths: + print(f" {'MISSING D-id' if p in missing else 'ok '} {p}") + + if not missing: + print(f"plan-dids: {len(paths)} added plan(s), all carry a D-id") + return 0 + + print() + print("::error::A plan added in this PR carries no D-id.") + print("A plan without D-ids is invisible to every discovery path: STATUS_BOARD") + print("has nothing to hold, and the supersession index's coverage column has") + print("nothing to count. Mint ids for its sections and add the rows, the way") + print("#1155 did retroactively for a sibling plan.") + print() + print(f"Pattern (imported from supersession_index.py): {DID.pattern}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/.github/workflows/plan-dids.yml b/.github/workflows/plan-dids.yml new file mode 100644 index 000000000..a8cc709ec --- /dev/null +++ b/.github/workflows/plan-dids.yml @@ -0,0 +1,43 @@ +name: New plans carry D-ids +on: + pull_request: + paths: + - .claude/plans/** + - .claude/tools/plan_dids.py + - .claude/tools/supersession_index.py + - .github/workflows/plan-dids.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + added-plans-have-dids: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # The check compares against the PR's merge base, so it needs history. + fetch-depth: 0 + + - name: Every plan ADDED by this PR must cite a D-id + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # ADDED only, deliberately. Gating MODIFIED plans would block whoever + # next edits one of the pre-existing untracked plans, punishing them + # for a debt they did not create. Added-only is the minimal form that + # stops the backlog regrowing, which is the whole goal; the existing + # backlog is a separate, cross-session scope call. + mapfile -t added < <( + git diff --diff-filter=A --name-only "$BASE_SHA" "$HEAD_SHA" -- '.claude/plans/*.md' + ) + if [ ${#added[@]} -eq 0 ]; then + echo "no plans added by this PR; nothing to check" + exit 0 + fi + python3 .claude/tools/plan_dids.py "${added[@]}"