Uh oh!
There was an error while loading. Please reload this page.
fix(devx): pin metadata-lifecycle's two prose registry counts to the AST parse - #11796
Conversation
…AST parse The sentence under the overlay whitelist table states two registry-derived quantities by hand -- "Those five are the complete allowOrgOverride: true set: of the 27 types in DEFAULT_METADATA_TYPE_REGISTRY" -- and nothing checked either. Registering a 28th type left the page asserting 27 with every gate green, including the one that shipped alongside the sentence. Adds LEG 3 (prose -> registry) to check-overlay-whitelist-table: it reads both numbers out of the page and compares them with entries.length and the size of the `true` filter, the two counts the gate already prints in --list. No count is written into the script -- a literal 27 there would have moved the hand-kept copy one file to the left. The prose is untouched: parseCountToken reads digits AND English number-words 0..99, so "five" is pinned as written rather than digitised to suit the parser. Above 99, or on any token the maps do not cover, the gate refuses rather than guessing. An absent or reworded claim is RED, not a silent no-op. Also derives the self-test's refusal-case count from the case arrays instead of the hand-kept literal 21 -- the same defect class, in this gate's own summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT. Verified by content on ⭐ The extra control is the best vacuity demonstration this lane has producedYou ran the pre-fix gate against direction 1's mutated tree. Verified it is genuinely the pre-fix gate:
A green gate stating 28, about a page asserting 27. That is not an argument that the hole existed — it is the hole, printed, on the real code. It also quietly proves something stronger than the card claimed: the old gate did not merely fail to check the prose, it had the correct number in hand and printed it next to an uncorrected page. Nobody would have caught that by reading. Both Zone 2 calls decided better than either option I offeredCall 1 — the prose is unchanged, and the reason kills my framing. I offered "parse the word" or "change the prose to a digit, and say so". You found that changing the prose would violate this gate's own VERDICT block, which already argues that a gate "fixed" by editing what it measures has inverted its point. Editing the sentence to suit the parser is the same move as flipping a registry flag to make the table agree — and I had just written that ⛔ into the card two levels up without noticing it applied here. The second half is as good: English style spells small numbers, so a digit would have been reverted by the next prose editor — a fix that loses to normal writing is not a fix. And the feared long tail is bounded by refusing: above 99, or on any uncovered token, the gate goes red rather than guessing, with Call 2 — the numbers stay, and your reason is sharper than mine. I said the sentence exists to make the claim checkable by a human. You put it precisely: the drift risk was never the numbers, it was that nothing held them — and leg 3 holds them. Making an absent, reworded or unreadable claim RED, matching the existing missing-heading doctrine, is what keeps "delete the numbers later" a reversible, explicit decision rather than a silent erosion. Non-vacuity: the isolation is what makes it evidence
Legs 1–2 reading 0 on direction 1 is the point — that is the unchanged code that used to report this clean. And on direction 2 the total claim stays green while the true-set claim reds, so each red is produced by the claim under test and not by collateral. Mutations proven on disk by anchor count and sha256 before the gate ran; restore byte-identical both legs; final unmutated control Verified independently: No rebuild owed, and you said why rather than claiming one: the gate reads the registry as text via The in-place fix you took is the same class, found inside the gate
That is a fourth instance of one measurement written down twice, and it was sitting inside the gate that #11759 added to close exactly that class two days' work ago. Taking it under the four-condition exemption, with the evidence named in the PR body, is correct — and worth noting as the pattern's persistence rather than as a tidy-up. Flipping to ready. Arming once every check is green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11763
What was unguarded
content/docs/concepts/metadata-lifecycle.mdx:119, the sentence directly under the whitelist table:Two registry-derived quantities, both hand-kept, both correct today, neither checked. Legs 1 and 2 structurally cannot see them: leg 2 only demands a table row for each
truetype, so a newly registeredfalsetype moves neither leg and the total goes stale in silence.That is measured here, not inferred. Registering a 28th type and running the gate as it stands on
origin/mainprints:Exit 0 — a green line stating
out of 28 declaredabout a page asserting 27, one directory over.What this adds — LEG 3, prose → registry
readProseCounts()reads both numbers out of the page;compareProse()compares them withentries.lengthand the size of thetruefilter — the same two counts--listhas printed since day one. No count is written into the script. A literal27in the gate would have moved the hand-kept copy one file to the left and closed nothing.Every match is checked, not just the first, so a second stale copy of the same claim elsewhere on the page is caught too. An absent, reworded, or unreadable claim is RED, matching this gate's existing doctrine for a missing heading: a leg whose subject sentence vanished must not keep printing a clean line about it.
The green line now carries the claims and where they were read, so the leg is visible as having run:
Zone 2, decision 1 — "five" stays a word, and the docs are NOT changed
This PR makes no change to
metadata-lifecycle.mdx. The cheap route was to rewrite the sentence to "Those 5 are …" and match\d+. Measured cost of the alternative:parseCountTokenplus its two Maps is 35 lines including its doc comment, 22 lines of code, and covers digits and English number-words 0–99 (five,twenty-seven).Three reasons that is the better 22 lines:
nullis the refusal channel and is pinned in the self-test as never colliding with0.Zone 2, decision 2 — the numbers stay in the sentence
Deleting them would also have been a fix, and it is the cheaper one. I kept them. The sentence was added to make the completeness claim checkable by a human without opening the registry, which a count does and a bare "every other type is
false" does not: the bare form is unfalsifiable by a reader, who can only take it on faith. The drift risk that argued for deletion was never the numbers — it was that nothing held them, and leg 3 is what holds them. Removing a reader-facing fact to avoid writing 22 lines of gate is the wrong side of that trade when the gate is the cheaper half.Leg 3's refusal path keeps that decision reversible and explicit: if a future editor concludes the counts should go, the gate goes red and the claim has to be removed from
proseClaimPatterns()in the same edit, rather than the leg quietly draining to vacuum.Non-vacuity — both directions, on the real tree
Mutations applied to
packages/spec/src/kernel/metadata-plugin.zod.tsundertrap … EXIT INT TERM, each proven on disk by anchor count and sha256 before the gate ran, each restored byte-identically after.allowOrgOverride: false, not named in the table (zz_ablation_probe)LEG 3: 1—the prose states the number of types in `DEFAULT_METADATA_TYPE_REGISTRY` as "27" (27); the registry declares 28.LEG 1: 0, LEG 2: 0 — the isolation is the point: this is the card's exact scenario, and legs 1–2 are the unchanged code that used to report it clean.agentflippedfalse→true(total stays 27)LEG 3: 1—the prose states the size of the `allowOrgOverride: true` set as "five" (5); the registry declares 6.LEG 1: 1 (the expectedagentrow mismatch), LEG 2: 0. The total claim stays green, isolating the true-set half.Each red is produced by the claim under test while the other claim stays right, so neither reading can be the wrong claim firing. Restore proven
identical=YESon both; the final unmutated control is byte-identical to the pre-ablation baseline (diff … IDENTICAL), andgit statuson the registry shows 0 modifications.No rebuild is involved and none is owed: this gate reads
packages/spec/src/kernel/metadata-plugin.zod.tsas text viareadFileSyncand parses it with the TypeScript compiler API. Its subject does not resolve through any package'sexportsordist/, so the stale-distfailure mode does not apply here.Controls
flow/permission/position, leg 2translation), and all 14 existing structural/parser refusal cases still refuse.origin/main: 0 and 0, over 13 types in 8 rows and 5truetypes out of 27.FIXTURE_REGISTRY's 14/5 counts drifts zero) paired with every red.In-place fix, named: the self-test's own hand-kept
21The self-test summary line ended with the literal
"21 structural/parser cases are refused". That literal is the same defect class this card is about — a derived count written down by hand — sitting in the summary line of the gate making the argument. It is also unverifiable: the case arrays hold 7 + 7 = 14, so21did not match any count in the file.It is now derived:
structural.length + registryStructural.length + proseStructural.length, printing 19 with the 5 new prose cases added. Taken in place under the four-condition exemption (same defect class; mechanical, with the correct shape pinned by the arrays themselves; no other claim on this file; same gate family, no new verification surface).Verification
All at
adf8f1105, the branch head.pnpm check:overlay-whitelist-tablepnpm check:entry-guard144 scripts/ file(s), 52 self-test casespnpm check:parse-guardpnpm check:agent-test-spellingpnpm check:cross-package-test-inputspnpm check:pnpm-filter-targetspnpm check:nul-bytespnpm check:pm-dispatch-gatespnpm check:type-check-coveragepnpm check:where-matcherpnpm lint(whole repo,eslint . --no-inline-config)Gate families derived with
node scripts/pm/dispatch-gates.mjs(no path list passed, so the changeset comes from the merge base): 7 matched. The last four rows above are the two roster-shaped families the residue flags as not evidence in either direction for ascripts/path, run rather than assumed.pnpm check:type-check-debtrefuses on an environment precondition, not on this diff: its--re-measurehalf requires the built workspace closure (packages/core/distand 55 siblings are absent in a fresh worktree), whichlint.ymlbuilds before that step. Its own error text names this.scripts/check-type-check-coverage.mjscontains 0 references to the one file this PR touches, and the pairedcheck:type-check-coveragepasses.No changeset: the diff is one CI gate script and publishes nothing.
skip-changesetapplied.Generated by Claude Code