From 417ba79d5fdf858eb3a85cbf4ec794730c816f20 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 02:20:34 +0000 Subject: [PATCH] gate(checklist): bind the platform-checklist corpus to the ONE symbol resolver (#16898) `scripts/check-platform-checklist.mjs` resolved symbol anchors with a rule of its own -- mask comments, then ask whether the bare token appears anywhere in the file -- in the place `scripts/symbol-anchors.mjs` states the #13556 ruling it implements: "One resolver." The private rule was the LOOSER of the two, so it resolved anchors the shared resolver refuses and the checklist reported coverage it did not have; a second resolver that is greener than the shared one is never the one anybody points at. `absentAnchorSegments` now delegates every segment to `symbol-anchors.mjs#symbolResolutionClass` and holds no matcher at all. The per-segment split stays -- that is this corpus's anchor grammar, not a rule about what "present" means -- and a self-test assertion reads this file's own source to pin the body against re-forking. Withdrawing the permissive match is the deliverable, not a regression: 56 of 633 anchor occurrences stopped resolving (577 resolve -- 516 declaration, 61 literal). All 56 are carried in `SHARED_RESOLVER_RESIDUAL`, one closed row per (family file, anchor), each with the shape that used to resolve it and a verdict: 47 bad-citation, 8 accept-set. Every row must fire (a stale row reds), an unlisted failure is an ordinary red, and a grow-never ceiling sits beside it -- so the ledger is a closed list that shrinks by repair, never a rule. No floor is lowered and the maintainer-only baseline is untouched. `scripts/symbol-anchors.mjs` is untouched too: widening the shared core to keep this ledger green would export the looser reading to the ADR, `scripts/**`, spec-docblock and system-context corpora. Two self-test assertions that pinned the permissive behaviour are RE-JUDGED in place with the reason recorded, never deleted: P3 (an inline object-literal member) and P5 (a `.json` value). The directions they really covered are kept as P3b and P5b on shapes the shared rule accepts. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --- docs/qa/platform-checklist/README.md | 32 ++- scripts/check-platform-checklist.mjs | 363 +++++++++++++++++++++++---- 2 files changed, 344 insertions(+), 51 deletions(-) diff --git a/docs/qa/platform-checklist/README.md b/docs/qa/platform-checklist/README.md index f5961f29e0..be6258a7a6 100644 --- a/docs/qa/platform-checklist/README.md +++ b/docs/qa/platform-checklist/README.md @@ -173,13 +173,31 @@ Why this shape: ``` `check:platform-checklist` resolves every anchor: the file must be in this repo and the - symbol must still be in it, **comments masked** — a symbol surviving only in a docblock - is ABSENT, because prose about a symbol is not a symbol. A symbol inside a string - literal counts as present: much of what this ledger cites *is* a string (capability - names, error codes, `sys_*` machine names). Dotted anchors resolve segment by segment. - This is presence-in-code, not a declaration analysis — what it catches is the rename or - deletion that leaves the pointer naming nothing, which is exactly the rot the line - numbers used to carry. + symbol must still be **declared** in it. ⭐ The rule is **not this gate's** and not this + paragraph's — it is the one shared resolver every anchor corpus in this tree uses + (`scripts/symbol-anchors.mjs#symbolResolutionClass`), and that module's own + `THE RESOLUTION RULE` block is the authority. What an author needs from it: + + - **comments are stripped first** — a symbol surviving only in a docblock is ABSENT, + because prose about a symbol is not a symbol; + - a **declaration site** resolves (a `function` / `class` / `interface` / `type` / + `const` / `let` / `var`, a member or object-literal key written **at the start of a + line**, a named re-export). For a `.json` target that means a **key**, never a value; + - a **complete quoted string token** resolves — `'sys_metadata'` — which is what keeps + the machine names this ledger is full of anchorable (capability names, error codes, + `sys_*` object and field names); + - ⚠️ and nothing else does. A **call site**, an **import**, a **member access on some + other object**, a **local parameter**, or the symbol appearing only as a **substring + of a longer string** (`saveItem` inside `'meta.saveItem'`, `:shareId` inside a route + pattern) is **NOT** resolution. If that is all the cited file has, the citation is + naming the file that *uses* the symbol, not the file that *declares* it — re-point + it, or cite the file bare. + + Dotted anchors resolve segment by segment. ⛔ An anchor the shared resolver refuses is + either a bad citation or a case for widening that shared core, and the second is its + own card against that module — ⛔ never a local rule here: the core is shared with the + ADR, `scripts/**`, spec-docblock and system-context corpora, and widening it to suit + this ledger would export the looser reading to all four (#16898). - **A citation the gate cannot resolve stays BARE — that is honest, not a gap.** Anchors are only for extensions the gate reads symbols out of (`.ts .tsx .mts .cts .js .mjs .cjs .json`); an anchor on a `.md` or a `.yml` is an ERROR rather than a silent skip, diff --git a/scripts/check-platform-checklist.mjs b/scripts/check-platform-checklist.mjs index 71ad13dedc..7be4b5ddef 100644 --- a/scripts/check-platform-checklist.mjs +++ b/scripts/check-platform-checklist.mjs @@ -47,9 +47,12 @@ // Both directions together are what the trap vocabulary beside them has // always had — used implies documented, documented implies used. // - every SYMBOL ANCHOR (`/.ts#`, the spelling -// `scripts/symbol-anchors.mjs#ANCHOR_GRAMMAR` defines) resolves: the cited -// file is in the tree and the cited symbol is still in it, comments masked -// — a symbol surviving only in a docblock is ABSENT. A shrink-never floor +// `scripts/symbol-anchors.mjs#ANCHOR_GRAMMAR` defines) resolves — and it is +// `scripts/symbol-anchors.mjs#symbolResolutionClass` that says so, the ONE +// resolver the #13556 ruling allows. This gate detects anchors and owns no +// rule about what "the symbol is in that file" means (#16898); the 56 +// anchors that stopped resolving when the private permissive rule was +// withdrawn are a named, closed, grow-never residual. A shrink-never floor // per family file keeps the population from being emptied one deleted // `#symbol` at a time (see the symbol-anchor block below); // - and no `call` string — the one field in this ledger a runner REPLAYS — @@ -65,6 +68,7 @@ import { readdirSync, readFileSync, existsSync } from 'node:fs'; import { maskComments } from './js-comment-mask.mjs'; +import { symbolResolutionClass } from './symbol-anchors.mjs'; import { join, basename } from 'node:path'; const ROOT = new URL('..', import.meta.url).pathname; @@ -780,7 +784,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ [BATTERY_UNREFERENCED_RECIPES]: 19, [BATTERY_META_CALL_SPELLING]: 53, [BATTERY_SOURCE_LINE_CITATIONS]: 19, - [BATTERY_SYMBOL_ANCHORS]: 29, + [BATTERY_SYMBOL_ANCHORS]: 40, }); const SELF_TEST_BATTERY_FLOOR = 6; @@ -1387,23 +1391,48 @@ function selfTestSourceLineCitations() { // untouched — a citation carries its reason, and a rewrite into a bare object // per citation would have thrown that away across the whole ledger. // -// ## What "the symbol is there" means here — deliberately dumb, again +// ## What "the symbol is there" means here — NOT THIS FILE'S CALL (#16898) // -// PRESENCE IN CODE, at identifier boundaries, with comments masked. NOT a -// declaration analysis: a symbol this file imports and merely calls counts as -// present, and this check will never tell you the citation names the wrong -// KIND of thing. What it catches is the rot the ruling named — the rename or -// the deletion that leaves the anchor pointing at nothing. Two consequences -// worth stating rather than discovering: +// It is `scripts/symbol-anchors.mjs#symbolResolutionClass`'s call, and that is +// the whole point. This gate used to answer the question itself, with a token +// match: mask comments, then ask whether the bare token appears anywhere in the +// file. That was a SECOND implementation of the resolution rule, in the place +// the #13556 ruling this module's header quotes says there is to be exactly +// one — and it was the LOOSER of the two, which is the dangerous direction: a +// second resolver that is greener than the shared one is never the resolver +// anybody points at, so the drift only ever gets discovered by census. // -// - a symbol surviving only in a COMMENT is ABSENT. Comments are masked -// before the scan (the house `maskComments`, for the reason its own header -// gives), so a deleted export whose name lingers in a docblock still reds. -// This is the direction that matters: prose about a symbol is not a symbol. -// - a symbol inside a STRING literal counts as present. That is deliberate — -// a large share of what this ledger cites IS a string (capability names, -// error codes, route literals, `sys_*` machine names) and a rule that -// refused them would push the ledger back to unanchored prose. +// Measured on the ledger at the moment of binding: of 633 anchor occurrences +// the permissive rule resolved all 633, and the shared resolver resolves 577 +// (516 `declaration`, 61 `literal`). The 56 it refuses are the coverage this +// checklist was reporting and did not have; they are enumerated, one per +// (family file, anchor), in `SHARED_RESOLVER_RESIDUAL` below. +// +// The shared rule, restated only so far as a reader here needs it — the +// authority is that module's own `## THE RESOLUTION RULE` block, ⛔ never this +// paragraph: +// +// - comments are stripped before anything is matched, so a symbol surviving +// only in a COMMENT is ABSENT. Prose about a symbol is not a symbol. (That +// half is unchanged; it is the only half the old rule had right.) +// - `declaration` — a declaration site in the target's own language, which +// for a `.json` target means a KEY and never a value. +// - `literal` — a COMPLETE quoted string token (`'sys_metadata'`). This is +// what keeps the ledger's DATA identifiers (capability names, error codes, +// `sys_*` machine names) anchorable. ⚠️ COMPLETE: a symbol that is only a +// SUBSTRING of a longer string — `saveItem` inside `'meta.saveItem'`, +// `:shareId` inside a route pattern — does not resolve, and that refusal +// is most of the 56. +// - a CALL SITE, an IMPORT and a LOCAL PARAMETER are none of those, so they +// do not resolve. A citation whose symbol survives only that way is naming +// a file that uses the symbol, not the file that declares it. +// +// ⛔ Do NOT answer any of this locally again, and ⛔ do not widen the shared +// core to make a checklist citation green: the core is shared with the ADR, +// `scripts/**`, `packages/spec/src/**` and system-context corpora, and widening +// it here would export this defect to all four. An anchor the core refuses is +// either a bad citation (re-point it) or a case for widening the core, and the +// second is its own card against `scripts/symbol-anchors.mjs`. // // ## Why anchors are not authored on every citation // @@ -1442,24 +1471,162 @@ function findSymbolAnchors(text) { * the same rot as `Foo` being dropped, and reporting the whole anchor as * present because its first half survived is the fail-open one level down. * + * ⛔ THE VERDICT IS NOT TAKEN HERE. Each segment goes to + * `scripts/symbol-anchors.mjs#symbolResolutionClass`, which owns the rule for + * every anchor corpus in this tree; the per-segment split above it is this + * corpus's anchor GRAMMAR (the shared grammar's `#symbol` carries no dot), not + * a rule about what "present" means. A matcher of any kind inside this body is + * the fork the ruling forbids, and `selfTestSymbolAnchors`'s N5 reads this + * source to say so. + * + * The target extension is derived by the shared module from `filePath`, so the + * `.json`-is-not-masked special case this function used to carry is gone with + * the rest of the private rule: JSON resolves against KEYS there. + * * @param {string} source raw file text + * @param {string} filePath repo-relative path of the cited file * @param {string} symbol the anchor's symbol, dots allowed - * @param {string} ext lower-cased extension of the cited file * @returns {string[]} */ -function absentAnchorSegments(source, symbol, ext) { - // JSON has no comment syntax, and masking a `.json` would blank whatever a - // `//`-carrying string value holds — a route literal, a URL. Every other - // anchorable extension is JS-family and goes through the house masker. - const src = ext === 'json' ? source : maskComments(source); +function absentAnchorSegments(source, filePath, symbol) { const absent = []; for (const seg of symbol.split('.')) { - const esc = seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - if (!new RegExp(`(? `${doc}::${anchor}`; +const SHARED_RESOLVER_RESIDUAL_INDEX = new Map( + SHARED_RESOLVER_RESIDUAL.map((r) => [residualKey(r.doc, r.anchor), r]), +); + // ── The anchor floor ──────────────────────────────────────────────────────── // // A resolver nobody can dodge is a resolver whose subject population cannot be @@ -1568,23 +1735,98 @@ function selfTestSymbolAnchors() { "const ROUTE = '/api/v1/meta/object';", 'export function hasPlatformAdminStanding(x) { return RESOLVED.mode === x; }', ].join('\n'); + /* The same shape with the member at the START OF A LINE, which is where the + * shared rule reads one. Kept as a second fixture rather than folded into + * `src`, because `src`'s inline member is exactly what P3 now pins. */ + const blockSrc = [ + 'export const PolicyShape = {', + " mode: 'strict',", + '};', + ].join('\n'); t('R1 a symbol that is gone from the file is reported absent', - absentAnchorSegments(src, 'parsePlatformAdminEmails', 'ts').length === 1); + absentAnchorSegments(src, 'x.ts', 'parsePlatformAdminEmails').length === 1); t('R2 a symbol surviving ONLY in a comment is ABSENT — prose about a symbol is not a symbol', - absentAnchorSegments(src, 'renamed', 'ts').length === 1); + absentAnchorSegments(src, 'x.ts', 'renamed').length === 1); t('R3 a dotted anchor whose SECOND segment is gone is reported, not passed on the first', - absentAnchorSegments(src, 'RESOLVED.strictness', 'ts').join(',') === 'strictness'); + absentAnchorSegments(src, 'x.ts', 'RESOLVED.strictness').join(',') === 'strictness'); t('R4 a substring of a present identifier does not count as present', - absentAnchorSegments(src, 'PlatformAdmin', 'ts').length === 1); + absentAnchorSegments(src, 'x.ts', 'PlatformAdmin').length === 1); // ── and STAYS SILENT ────────────────────────────────────────────────────── - t('P1 a declared export resolves', absentAnchorSegments(src, 'hasPlatformAdminStanding', 'ts').length === 0); - t('P2 a const resolves', absentAnchorSegments(src, 'RESOLVED', 'ts').length === 0); - t('P3 both segments of a live dotted anchor resolve', absentAnchorSegments(src, 'RESOLVED.mode', 'ts').length === 0); - t('P4 a symbol inside a STRING literal counts as present — this ledger cites machine names, not only exports', - absentAnchorSegments(src, 'strict', 'ts').length === 0); - t('P5 `.json` is scanned unmasked, so a `//`-carrying string value is never blanked away', - absentAnchorSegments('{ "route": "https://x/y", "kind": "live" }', 'live', 'json').length === 0); + t('P1 a declared export resolves', absentAnchorSegments(src, 'x.ts', 'hasPlatformAdminStanding').length === 0); + t('P2 a const resolves', absentAnchorSegments(src, 'x.ts', 'RESOLVED').length === 0); + /* P3 and P5 are RE-JUDGED IN PLACE (#16898), ⛔ not deleted. Both asserted a + * resolution only the permissive token match this gate used to carry could + * produce, so both had to be re-read once the verdict moved to + * `scripts/symbol-anchors.mjs#symbolResolutionClass` — and keeping the case + * with its verdict inverted is what pins the narrowing, where deleting it + * would leave the tree unable to say the narrowing ever happened. + * + * P3 asserted `RESOLVED.mode` resolves. `{ mode: 'strict' }` is an + * object-literal key written INLINE; the shared rule admits a member or + * object-literal key written at the START OF A LINE. Six of the ledger's + * own residual rows are this exact shape, and whether the shared accept + * set should reach an inline key is a card against that module — ⛔ never + * a widening made here. The direction P3 used to cover (both segments of + * a live dotted anchor resolving) is P3b, on a member the rule accepts. + * P5 asserted a `.json` VALUE resolves, because this gate used to scan JSON + * unmasked and ask only whether the token appeared. The shared rule reads + * JSON KEYS, which is what keeps a value of the same spelling out. The + * half P5 really cared about — that a `//`-carrying string value is never + * blanked away — is P5b, which resolves a key while such a value sits + * beside it. + */ + t('P3 a dotted anchor whose member is written INLINE is now ABSENT — the shared rule takes a member key at the start of a line (re-judged, #16898)', + absentAnchorSegments(src, 'x.ts', 'RESOLVED.mode').join(',') === 'mode'); + t('P3b both segments of a live dotted anchor resolve when the member IS at the start of a line', + absentAnchorSegments(blockSrc, 'x.ts', 'PolicyShape.mode').length === 0); + t('P4 a symbol that is a COMPLETE quoted token counts as present — this ledger cites machine names, not only exports', + absentAnchorSegments(src, 'x.ts', 'strict').length === 0); + t('P5 a `.json` VALUE is ABSENT — the shared rule resolves JSON KEYS (re-judged, #16898)', + absentAnchorSegments('{ "route": "https://x/y", "kind": "live" }', 'x.json', 'live').join(',') === 'live'); + t('P5b a `.json` KEY resolves, and a `//`-carrying string value beside it is never blanked away', + absentAnchorSegments('{ "route": "https://x/y", "live": true }', 'x.json', 'live').length === 0); + + // ── the BINDING itself (#16898) ─────────────────────────────────────────── + // + // The four shapes the withdrawn rule accepted and the shared resolver + // refuses, measured on one fixture — and, in the same fixture, the POSITIVE + // CONTROL that says the binding narrowed rather than simply broke. Without + // N4 a green here cannot tell "bound correctly" from "everything stopped + // resolving", which is the only way a resolver swap can look fine and be + // worthless. + const bindSrc = [ + "import { importedOnly } from './elsewhere.js';", + 'export function declaredHere(paramOnly) { return importedOnly(paramOnly); }', + "export const HOST = { list: ['whole_token', 'dotted_head.actions.go'] };", + ].join('\n'); + t('N1 a CALL SITE / IMPORT only is ABSENT — the file uses the symbol, it does not declare it', + absentAnchorSegments(bindSrc, 'x.ts', 'importedOnly').length === 1); + t('N2 a LOCAL PARAMETER only is ABSENT', + absentAnchorSegments(bindSrc, 'x.ts', 'paramOnly').length === 1); + t('N3 a SUBSTRING of a longer string token is ABSENT — `literal` is a COMPLETE quoted token', + absentAnchorSegments(bindSrc, 'x.ts', 'dotted_head').length === 1); + t('N4 POSITIVE CONTROL — a declaration and a complete quoted token BOTH still resolve, so a green above is a narrowing and not a dead resolver', + absentAnchorSegments(bindSrc, 'x.ts', 'declaredHere').length === 0 + && absentAnchorSegments(bindSrc, 'x.ts', 'whole_token').length === 0); + const ownResolverBody = /function absentAnchorSegments\([^)]*\)\s*\{[\s\S]*?\n\}/ + .exec(readFileSync(new URL(import.meta.url).pathname, 'utf8'))?.[0] ?? ''; + t('N5 this gate carries NO resolution rule of its own — the body delegates and holds no matcher, which is the fork the #13556 ruling forbids', + ownResolverBody.includes('symbolResolutionClass(') + && !/new RegExp|RegExp\(|\.test\(|maskComments|indexOf\(/.test(ownResolverBody)); + + // ── the residual ledger, both directions ───────────────────────────────── + t('D1 the residual is a CLOSED ledger — every row carries a doc, an anchor, a shape and one of exactly two verdicts', + SHARED_RESOLVER_RESIDUAL.length > 0 + && SHARED_RESOLVER_RESIDUAL.every((r) => r.doc && r.anchor.includes('#') && r.shape + && (r.verdict === 'bad-citation' || r.verdict === 'accept-set'))); + t('D2 no row is written twice — a duplicate would let one repair leave a live excuse behind', + SHARED_RESOLVER_RESIDUAL_INDEX.size === SHARED_RESOLVER_RESIDUAL.length); + t('D3 the ledger is inside its grow-never ceiling — a residual that can grow is the permissive rule coming back a row at a time', + SHARED_RESOLVER_RESIDUAL.length <= SHARED_RESOLVER_RESIDUAL_CEILING); + t('D4 the `accept-set` rows are the minority and are named as a DIFFERENT card — ⛔ nothing here widens the shared core', + SHARED_RESOLVER_RESIDUAL.filter((r) => r.verdict === 'accept-set').length + < SHARED_RESOLVER_RESIDUAL.filter((r) => r.verdict === 'bad-citation').length); // ── the floor, both directions ──────────────────────────────────────────── const floors = { 'areas/a.json': 10, 'areas/b.json': 4 }; @@ -1634,7 +1876,7 @@ if (process.argv.slice(2).includes('--self-test')) { ' the unreferenced-recipe direction fires on a recipe nobody uses while leaving a cross-area consumer, a retired consumer and a `$`-annotation alone;' + ' and the `/meta` call-spelling refusal reads its vocabulary out of the live generated contract, fires on every folded spelling a `call` can instruct, and stays silent on the canonical singular, on parameter placeholders, and on the `why`/`expect`/`source`/`requires` prose that narrates the fold;' + ' and the source-line-citation refusal fires on every spelling this ledger carried (file-anchored, range, bare continuation, parenthesised, `~:`, comma/slash-chained) while staying silent on HTTP status, config literals, URL ports, clock times, JSON quoted in prose and the README placeholder that documents the ban;' + - ' and the symbol-anchor resolver finds every anchor spelling an author writes while reading no issue reference, URL fragment, ADR section or bare citation as one, reports a symbol that survives only in a comment as ABSENT, resolves both segments of a dotted anchor, and holds the per-file anchor floor in both directions.', + ' and the symbol-anchor limb finds every anchor spelling an author writes while reading no issue reference, URL fragment, ADR section or bare citation as one, and then RESOLVES NOTHING ITSELF: the verdict is `scripts/symbol-anchors.mjs#symbolResolutionClass` (#16898), pinned here by a body that holds no matcher, by a call site / import / local parameter / string-substring all reading ABSENT, by the positive control that a declaration and a complete quoted token still resolve, by a `.json` key resolving where a `.json` value no longer does, and by the closed, grow-never residual that names what the binding withdrew — with the per-file anchor floor held in both directions beside it.', ); process.exit(0); } @@ -2094,9 +2336,12 @@ function anchorSource(absPath) { const anchorCounts = new Map(); let anchorsScanned = 0; let anchorsResolved = 0; +let anchorsResidual = 0; +const residualFired = new Set(); for (const rel of familyFiles(CHECKLIST_DIR)) { const text = readFileSync(join(CHECKLIST_DIR, rel), 'utf8'); let resolved = 0; + let residual = 0; for (const anchor of findSymbolAnchors(text)) { anchorsScanned++; if (!ANCHORABLE_EXTENSIONS.has(anchor.ext)) { @@ -2108,15 +2353,44 @@ for (const rel of familyFiles(CHECKLIST_DIR)) { err(rel, null, `ANCHOR FILE NOT FOUND — \`${anchor.anchor}\`: ${anchor.file} is not in this repo. A path this gate cannot open is not an anchor; a sibling-repo citation (objectui/cloud) stays BARE, and a moved file needs the pin re-pointed.`); continue; } - const absent = absentAnchorSegments(anchorSource(abs), anchor.symbol, anchor.ext); + const absent = absentAnchorSegments(anchorSource(abs), anchor.file, anchor.symbol); if (absent.length) { - err(rel, null, `ABSENT SYMBOL — \`${anchor.anchor}\`: ${absent.map((s) => `\`${s}\``).join(' and ')} ${absent.length > 1 ? 'are' : 'is'} not in ${anchor.file} outside its comments. The symbol was renamed or removed and the citation kept reading as "verified against source" — re-point it at what the file carries now, or drop the \`#symbol\` half and cite the file bare.`); + // On the measured residual? Then this is one of the 56 the binding + // withdrew (#16898) — recorded, counted apart from `resolved`, and NOT a + // red. Anything else that fails to resolve is an ordinary red, which is + // what keeps the ledger closed instead of permissive. + const known = SHARED_RESOLVER_RESIDUAL_INDEX.get(residualKey(rel, anchor.anchor)); + if (known) { + residualFired.add(residualKey(rel, anchor.anchor)); + residual++; + anchorsResidual++; + continue; + } + err(rel, null, `ABSENT SYMBOL — \`${anchor.anchor}\`: ${absent.map((s) => `\`${s}\``).join(' and ')} ${absent.length > 1 ? 'are' : 'is'} not declared in ${anchor.file} by \`scripts/symbol-anchors.mjs#symbolResolutionClass\` — no declaration site and no complete quoted string token, comments stripped. A call site, an import, a member access or a substring of a longer string is NOT resolution. Re-point the anchor at what the file declares now, or drop the \`#symbol\` half and cite the file bare. ⛔ Do not widen \`scripts/symbol-anchors.mjs\` to make this green: it is shared with four other corpora.`); continue; } resolved++; anchorsResolved++; } - anchorCounts.set(rel, resolved); + // The floor's population is every anchor this gate holds BOUND — resolved + // plus the named residual. ⛔ That is deliberately not the same number as + // `resolved`, and the console line prints both so the coverage claim stays + // honest: folding the residual in silently is the exact move this card + // exists to undo, and no floor in the maintainer-only baseline is touched. + anchorCounts.set(rel, resolved + residual); +} + +// A residual row that did not fire has been repaired, re-authored or removed — +// and a ledger that outlives its defect is a standing excuse for whatever +// anchor next happens to match it. Deleting the row is the remedy. +for (const r of SHARED_RESOLVER_RESIDUAL) { + if (residualFired.has(residualKey(r.doc, r.anchor))) continue; + err(r.doc, null, `STALE RESIDUAL ROW — \`${r.anchor}\` no longer fails to resolve here (repaired, re-authored, or gone). Delete its row from \`SHARED_RESOLVER_RESIDUAL\` in this file, in the same edit; the ledger is shrink-only and every row must fire.`); +} +if (SHARED_RESOLVER_RESIDUAL.length > SHARED_RESOLVER_RESIDUAL_CEILING) { + console.error(`check-platform-checklist: SHARED_RESOLVER_RESIDUAL carries ${SHARED_RESOLVER_RESIDUAL.length} rows but the grow-never ceiling is ${SHARED_RESOLVER_RESIDUAL_CEILING}.`); + console.error('\nThis is a REFUSAL, not a pass: a residual that can grow is the permissive rule coming back one row at a time. Rows leave by repairing the citation, never by raising this number.'); + process.exit(1); } // The census mode: how the baseline beside this gate is authored, and the one @@ -2127,7 +2401,7 @@ if (process.argv.slice(2).includes('--anchor-census')) { const census = {}; for (const [rel, n] of [...anchorCounts].sort(([a], [b]) => a.localeCompare(b))) if (n > 0) census[rel] = n; console.log(JSON.stringify(census, null, 2)); - console.error(`\ncheck-platform-checklist --anchor-census: ${anchorsResolved}/${anchorsScanned} anchors resolved across ${Object.keys(census).length} family files. This is a CENSUS, not a verdict — run the gate with no flags for that.`); + console.error(`\ncheck-platform-checklist --anchor-census: ${anchorsResolved}/${anchorsScanned} anchors resolved through \`scripts/symbol-anchors.mjs#symbolResolutionClass\`, plus ${anchorsResidual} on the named residual, across ${Object.keys(census).length} family files. ⚠️ The per-file counts printed above are the FLOOR population (resolved + residual), which is what the baseline beside this gate pins; they are NOT a coverage figure on their own. This is a CENSUS, not a verdict — run the gate with no flags for that.`); process.exit(0); } @@ -2174,6 +2448,7 @@ console.log( ` provisioning: ${recipeTotal} area recipes, ${recipeRefs} item references resolved (${qualifiedRefs} area-qualified), ${recipesReferenced}/${recipeTotal} recipes referenced;` + ` meta-URL spelling: ${metaCallsScanned} \`call\` strings scanned against ${FOLDED_META_SPELLINGS.size} folded spellings;` + ` source citations: ${citationsScanned} family files carry no \`file:line\` pin;` + - ` symbol anchors: ${anchorsResolved}/${anchorsScanned} resolved against ${anchorSourceCache.size} cited sources, ${Object.keys(anchorFloors).length} file floors held;` + + ` symbol anchors: ${anchorsResolved}/${anchorsScanned} resolved by \`symbol-anchors.mjs\` (the ONE resolver) against ${anchorSourceCache.size} cited sources` + + `, ${anchorsResidual} on the named #16898 residual, ${Object.keys(anchorFloors).length} file floors held;` + ` (self-checks: ${trapControl.checked} trap-vocabulary + ${provisioningControl.checked} provisioning-resolve + ${unreferencedControl.checked} unreferenced-recipe + ${metaCallControl.checked} meta-call-spelling + ${citationControl.checked} source-line-citation + ${symbolAnchorControl.checked} symbol-anchor assertions).`, );