diff --git a/CHANGELOG.md b/CHANGELOG.md index 84bea04f..ae69f27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to Massing. Releases are signed, auto-updating desktop build (Windows / macOS / Linux); the updater always serves the latest. Format loosely follows [Keep a Changelog](https://keepachangelog.com/). +## Unreleased — the app.ts figure was wrong in four places, and the gate that was supposed to stop that reached one of them + +**Corrects `docs/roadmap.md`, and widens `services/api/test_claude_md_gates.py` to the class rather +than the instance.** + +The R39-DECOMP-VIEWER entry carried **three different figures for one file**: a header saying +`app.ts` is "5,160 → **3,311**, a 36% cut", a shipped block saying "5,160 → **2,944**, a 43% cut", +and the ratchet pinning **2,508**. A fourth sat elsewhere in the file — "5,064 → 3,444 → and thirteen +slices since". The header also claimed "seven slices shipped" directly above a list naming eleven. + +**The two baselines were both real, and neither was labelled.** `CLAUDE.md` measures from 5,064 — +the file before the first extraction commit of 2026-08-06 — while the roadmap measured from 5,160, a +**same-day peak** partway through it: the file grew before it shrank. Nothing on the page could tell +a reader the two were counted from different points. The baseline is now stated, matches `CLAUDE.md`, +and the current figure is the ratchet's own count. + +**The gate is the real subject here.** The previous release added a check binding `CLAUDE.md`'s +narrative `app.ts` figure to the `test_file_sizes.py` pin, on the reasoning that *prose is a copy of +a gated value and a copy is what drifts*. That reasoning was right and the implementation was too +narrow: it gated **one copy of a number that lives in four places**, and the other three were already +wrong when it shipped. *Gating the instance you happened to be looking at is not gating the class.* +The check now scans every doc that carries the baseline-anchored form. + +**It immediately earned that.** Having corrected the two figures I found by reading, the widened +check failed on a **third** I had missed entirely. + +**And mutation testing found a defect in the widened check itself.** Its first draft used a summed +floor — "at least 2 figures across the docs" — and reported "3 figures across 2 docs" while **all +three came from the roadmap**: `CLAUDE.md` words it "5,064 lines to", so the arrow pattern never +matched it and its contribution was zero. A mutation that reformatted only the roadmap took the +count to 0 and exposed it. That is precisely the per-doc vacuity failure this same file's citation +ratchet was built to prevent, repeated one screen further down. The floor is now **per-doc**, and +`CLAUDE.md` is covered by name rather than by assumption. + ## Unreleased — SCALE-SEAM (104): coverage maps Extracts `spineTraceability` and `scopeRegister` from `client.ts` (**572 → 554**) into diff --git a/docs/roadmap.md b/docs/roadmap.md index dd5d4d46..0281d072 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1129,7 +1129,7 @@ four phantom entries — one of them a bare `SCALE-SEAM` with its `㉘` silently exact failure `roadmapLanes.test.ts` documents in its `MARKS` note. The gates caught all four.)* * **Not R39-DECOMP-VIEWER ③.** It ranked second last time on a size-ceiling argument that had - already gone false. `app.ts` is decomposing steadily (5,064 → 3,444 → and thirteen slices since), the ratchet + already gone false. `app.ts` is decomposing steadily (5,064 → 2,508, slices still landing), the ratchet is pinned, and it moves on its own whenever a feature pushes it. It does not need a sprint; it needs to keep being interleaved. **Re-measure the ceiling before ever promoting it again** — that is the specific error row 2 made. @@ -2546,8 +2546,10 @@ re-open): the converter build stage moved to the supported Node LTS with a pinne refusal (`services/api/src/aec_api/main.py`), and full-history checkout for the secret-scan job. -- 🟡 **R39-DECOMP-VIEWER ③** *(L, Lane E — **seven slices shipped; `app.ts` is 5,160 → 3,311, a 36% - cut. The `builders` map is entirely gone.** The paragraph below saying the extraction "is NOT begun" +- 🟡 **R39-DECOMP-VIEWER ③** *(L, Lane E — **`app.ts` is 5,064 → 2,508, a 50% cut. The `builders` + map is entirely gone.** Slice count deliberately not restated here: `services/api/test_file_sizes.py` + is the record, and a count repeated in prose is a copy that drifts — this header said "seven slices" + while the list immediately below it named eleven. The paragraph below saying the extraction "is NOT begun" was true on 2026-08-06 and stayed on the page until 2026-08-17, through six shipped slices — a roadmap entry describing work as un-started while the work is being done is worse than a missing entry, because it sends the next reader to re-derive a plan that was already executed. Kept only @@ -2559,10 +2561,18 @@ refusal (`services/api/src/aec_api/main.py`), and full-history checkout for the ⑤ project-browser panel (216) · ⑥ `loadProjectModel` (37) · ⑦ **drawings & sheets (142, v0.3.978)** · ⑨ **fabrication detail (65)** · ⑩ **MEP / fire / life safety (169)** — both v0.3.981 · ⑫ **envelope & free-form geometry (75, v0.3.982)** · ⑬ **model federation & version compare - (88, v0.3.1043)**. `app.ts` 5,160 → **2,944**, a **43% cut**. + (88, v0.3.1043)**. `app.ts` 5,064 → **2,508**, a **50% cut**. Each ratcheted `services/api/test_file_sizes.py` down, never reset. `services/api/test_file_sizes.py` carries the per-slice history; that comment, not this list, is the record. + *(**Three figures for one file, two of them stale — corrected 2026-09-05.** This block said + `5,160 → 2,944` and the header said `5,160 → 3,311`, while the ratchet pinned **2,508**. The two + BASELINES also disagreed with `CLAUDE.md`, which says 5,064, and both were real: 5,064 is the file + before the first extraction commit of 2026-08-06, and **5,160 was a same-day peak** partway through + it — the file grew before it shrank. Neither number was ever labelled, so nothing could tell a + reader they were measuring from different points. Baseline is now stated and matches `CLAUDE.md`; + the current figure is the ratchet's own count, which is the only one under a gate.)* + **"never reset" was false for two days, and the way it was found is the transferable part.** Slices ⑭⑮⑯ walked the pin 2_865 → 2_757 → 2_630 → 2_571 on 2026-08-27, and a REL-4 portal commit to the same shared file later that afternoon carried the pre-⑭ line back in — value *and* comment trail, so diff --git a/services/api/test_claude_md_gates.py b/services/api/test_claude_md_gates.py index 4fce2ed8..5c1cb2c0 100644 --- a/services/api/test_claude_md_gates.py +++ b/services/api/test_claude_md_gates.py @@ -289,6 +289,53 @@ def resolve(name): # Both halves must be FOUND, not just agree. If either pattern stops matching — the sentence is # reworded, the ratchet key is renamed — this check would otherwise pass on two Nones, which is the # vacuous-green failure this file's own header calls worse than no gate. +# The SAME figure lives in `docs/roadmap.md` twice, and the gate above did not reach it. Added +# 2026-09-05, one PR after that gate: the roadmap's R39-DECOMP-VIEWER entry carried THREE figures for +# `app.ts` — a header saying "5,160 -> 3,311", a shipped block saying "5,160 -> 2,944", and the truth, +# 2,508. It also used a DIFFERENT baseline from CLAUDE.md (5,160 vs 5,064) with neither labelled, so +# nothing could tell a reader they were measured from different points; both were real, 5,064 being the +# file before the first extraction commit and 5,160 a same-day peak partway through it. +# +# The lesson is about the gate, not the numbers. The check above was written to stop a narrative copy of +# a pinned value from drifting, and it gated ONE copy of a number that lives in three places. **Gating +# the instance you happened to be looking at is not gating the class.** So this scans every doc for the +# baseline-anchored form and requires each to equal the pin — which also holds the BASELINE steady, +# since a figure measured from somewhere else no longer matches the pattern at all. +# +# Historical mentions are deliberately still matchable only by their own baseline: the corrected text +# quotes "5,160 -> 2,944" as the thing that was wrong, and that does not match this pattern, so the +# record of the error survives without failing the build. +APP_TS_BASELINE = "5,064" +_arrow = re.compile(re.escape(APP_TS_BASELINE) + r"\s*(?:->|\u2192)\s*\*{0,2}([\d,]+)") + +#: Docs carrying the ARROW form, and the minimum each must contribute. PER-DOC, never summed — the +#: first draft of this check used a summed floor of 2 and reported "3 figures across 2 docs" while +#: **all three came from the roadmap**: CLAUDE.md words it "5,064 lines to", so the arrow pattern +#: never matched it and its contribution was ZERO. A mutation that reformatted only the roadmap took +#: the count to 0 and revealed it. That is the exact failure this file's own citation ratchet was +#: built to avoid, repeated one screen further down — **a summed floor lets one doc hold the number +#: up while another silently contributes nothing.** CLAUDE.md is not listed here because the two +#: checks below cover its wording directly; a doc is either in this map or covered by name, never +#: assumed. +ARROW_DOCS = {"docs/roadmap.md": 3} + +_bad, _thin = [], [] +for _doc, _min in ARROW_DOCS.items(): + with open(os.path.join(ROOT, _doc), encoding="utf-8") as fh: + _hits = _arrow.findall(fh.read()) + if len(_hits) < _min: + _thin.append(f"{_doc}={len(_hits)} (min {_min})") + for _h in _hits: + if _pinned and int(_h.replace(",", "")) != int(_pinned.group(1).replace("_", "")): + _bad.append(f"{_doc}: {APP_TS_BASELINE} -> {_h}") + +check( + "every doc that quotes the app.ts figure quotes the pinned one", + not _bad and not _thin, + (f"below the floor: {', '.join(_thin)}; " if _thin else "") + + (f"DISAGREE: {', '.join(_bad)}" if _bad else "all figures match the pin"), +) + check( "CLAUDE.md's app.ts sentence and the ratchet pin are both readable", bool(_stated) and bool(_pinned),