From ce79cd85dfedcc2566c6ee0786e0d91534a5da1d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 01:17:11 +0000 Subject: [PATCH 1/2] wip(scripts): symbol-anchors reads the comma line citation and an opt-in tilde range Claude-Session: https://claude.ai/code/session_01PPd7QqomCKXskDb8wkVgDV Co-authored-by: Claude --- scripts/symbol-anchors.mjs | 178 ++++++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 22 deletions(-) diff --git a/scripts/symbol-anchors.mjs b/scripts/symbol-anchors.mjs index 7ec98fe5035..a16a56ff916 100644 --- a/scripts/symbol-anchors.mjs +++ b/scripts/symbol-anchors.mjs @@ -88,9 +88,9 @@ * * (`scripts/symbol-anchors.mjs#extractAnchors`, `#defineCorpus`) * - * ### Three corpus-declared variations on the SAME three forms (#18107, #18592) + * ### Four corpus-declared variations on the SAME three forms (#18107, #18592, #24894) * - * None changes what an anchor means; all three are DELIMITER and POPULATION + * None changes what an anchor means; all four are DELIMITER and POPULATION * declarations a corpus makes about its own documents, defaulted OFF so a * corpus that says nothing reads exactly the text it read before. * @@ -121,6 +121,18 @@ * text is prose and in PROSE a colon before digits is punctuation. What * the option admits there is re-measured below. * + * `tildeRangeCitations: true` A FOURTH declaration, and a third path-less + * spelling kept apart from the two above on purpose (#24894): the bare + * approximate RANGE, `createColumn ~7354-7368`, a tilde and two numbers + * with no colon and no `L`. In a data ledger it continues the filename + * named before it; in prose it is a quantity (`~60-120` seconds) far more + * often than a line, which is why it is opt-in and why `defineCorpus` + * refuses it beside a `docProjection` exactly as it refuses the option + * above. It is its OWN declaration, ⛔ not a third row inside + * `pathlessLineCitations`, because a corpus gate that prints which + * path-less spellings it leaves unjudged (`docs/adr/**` does) names that + * option's two; folding a third in would make that sentence false. + * * ⭐ A dotted `#Outer.member` is also part of the grammar, and `sweepCorpus` * requires EVERY segment to resolve. Admitting it was additive: a dotted symbol * could not be written inside a code span at all before, so nothing that @@ -138,9 +150,12 @@ * * ⛔ A LINE NUMBER IS NOT AN ANCHOR FORM. `.ts:4901`, `.ts:341-400`, * `.ts:459–463` (en dash), `.tsx:~605-615` (the approximation - * tilde) and a bare continuation -- a backticked `:` followed by a line spec, - * `2933` -- are all findings, and so are the two PATH-LESS spellings a corpus - * declaring `pathlessLineCitations` admits. ⚠️ The angle brackets are + * tilde), the same spec after a COMMA in place of the colon (`.ts`, a + * comma, `1159-1161`, written with no space) and a bare continuation -- a + * backticked `:` followed by a line spec, `2933` -- are all findings, and so + * are the two PATH-LESS spellings a corpus declaring `pathlessLineCitations` + * admits and the tilde range one declaring `tildeRangeCitations` admits. + * ⚠️ The angle brackets are * deliberate: this file is itself inside the `scripts/**` corpus, so an * illustration written path-shaped would be a citation of its own. * @@ -365,8 +380,22 @@ const LINE_SPEC = '\\d+(?:\\s*[-–—]\\s*\\d+)?(?:\\s*[,/]\\s*\\d+(?:\\s*[-– * ⛔ The tilde is the ONLY thing admitted here, and ⛔ no whitespace with it: a * `:` followed by a SPACE and then digits is ordinary prose punctuation * (`**Date**: 2026-04-19`, `exit contract: 1`), and admitting it was measured - * to fire 26 times across `docs/adr/**` alone, every one a false positive. */ -const LINE_ANCHOR = new RegExp('(?/.ts:378` — a comment that is nothing but a path. */ @@ -448,6 +477,32 @@ const PATHLESS_LINE_SPEC = '\\d+(?:-\\d+)?'; const PATHLESS_COLON_CITATION = new RegExp('(?]*-->/; export const EXEMPT_CLASSES = ['HISTORICAL', 'EXTERNAL']; @@ -627,17 +682,19 @@ export function symbolSegmentResolution(source, filePath, symbol) { * * @param {string} markdown the document text (already projected, if the corpus * declares a `docProjection`) - * @param {{unspannedAnchors?: boolean, pathlessLineCitations?: boolean}} [options] + * @param {{unspannedAnchors?: boolean, pathlessLineCitations?: boolean, tildeRangeCitations?: boolean}} [options] * `unspannedAnchors` admits the UNSPANNED form beside the spanned one, for a * corpus whose documents are data rather than markdown. * `pathlessLineCitations` admits the two line citations that carry no path of * their own -- the bare `:NNN` continuation and the `L` pin -- for a corpus * that writes a second pointer into a file it has already named. - * ⛔ Both default OFF: a prose corpus that never opted in reads exactly the - * text it read before. + * `tildeRangeCitations` admits the third, the bare tilde range, for the same + * kind of corpus. + * ⛔ All three default OFF: a prose corpus that never opted in reads exactly + * the text it read before. */ export function extractAnchors(markdown, options = {}) { - const { unspannedAnchors = false, pathlessLineCitations = false } = options; + const { unspannedAnchors = false, pathlessLineCitations = false, tildeRangeCitations = false } = options; const anchors = []; const lineAnchors = []; const lines = markdown.split('\n'); @@ -654,7 +711,7 @@ export function extractAnchors(markdown, options = {}) { if (inFence) { if (!FENCED_HEADER.test(text)) return; for (const m of text.matchAll(LINE_ANCHOR)) { - lineAnchors.push({ line: lineNo, index: m.index, raw: m[0], path: m[1], cited: parseInt(m[2], 10), exempt: null, fencedHeader: true }); + lineAnchors.push({ line: lineNo, index: m.index, raw: m[0], path: m[1], cited: parseInt(m[3], 10), exempt: null, fencedHeader: true, comma: m[2] === ',' }); } return; } @@ -731,7 +788,7 @@ export function extractAnchors(markdown, options = {}) { }; for (const m of text.matchAll(LINE_ANCHOR)) { const ex = exemptionFor(m.index + m[0].length); - pushLineAnchor({ line: lineNo, index: m.index, raw: m[0], path: m[1], cited: parseInt(m[2], 10), exempt: ex?.class ?? null, exemptRaw: ex?.raw ?? null }, m[0].length); + pushLineAnchor({ line: lineNo, index: m.index, raw: m[0], path: m[1], cited: parseInt(m[3], 10), exempt: ex?.class ?? null, exemptRaw: ex?.raw ?? null, comma: m[2] === ',' }, m[0].length); } for (const m of text.matchAll(TILDE_LINE)) { const ex = exemptionFor(m.index + m[0].length); @@ -757,6 +814,17 @@ export function extractAnchors(markdown, options = {}) { pushLineAnchor({ line: lineNo, index: m.index, raw: m[0], path: lastPath, cited: parseInt(m[1], 10), exempt: ex?.class ?? null, exemptRaw: ex?.raw ?? null, pathless: true }, m[0].length); } } + /* Same de-duplication as the pass above, and the same inheritance: the + * range continues the filename named before it. A second number BELOW the + * first is not a line range (see `TILDE_RANGE_CITATION`) and is skipped. */ + if (tildeRangeCitations) { + for (const m of text.matchAll(TILDE_RANGE_CITATION)) { + if (lineAnchorRanges.some(([s, e]) => m.index < e && m.index + m[0].length > s)) continue; + if (parseInt(m[2], 10) < parseInt(m[1], 10)) continue; + const ex = exemptionFor(m.index + m[0].length); + pushLineAnchor({ line: lineNo, index: m.index, raw: m[0], path: lastPath, cited: parseInt(m[1], 10), exempt: ex?.class ?? null, exemptRaw: ex?.raw ?? null, pathless: true, tildeRange: true }, m[0].length); + } + } }); return { anchors, lineAnchors }; } @@ -812,7 +880,7 @@ export function defineCorpus(spec) { const { id, label, docRoots, docPattern = /\.mdx?$/, crossRepos = {}, checkBarePaths = false, docProjection = null, judgeUntrackedLineAnchors = true, excludeDirs = [], - unspannedAnchors = false, pathlessLineCitations = false, + unspannedAnchors = false, pathlessLineCitations = false, tildeRangeCitations = false, } = spec; if (!id || !label) throw new Error('defineCorpus: `id` and `label` are required'); if (!Array.isArray(docRoots) || docRoots.length === 0) throw new Error('defineCorpus: `docRoots` must be a non-empty array'); @@ -848,19 +916,28 @@ export function defineCorpus(spec) { * by the next projection of the same kind, silently and with nobody deciding * it; this one over-refuses a hypothetical PROJECTED DATA corpus loudly, at * registration, and the ruling this module implements already names that exit - * -- widen the core rather than fork the rule. */ - if (docProjection !== null && pathlessLineCitations) { + * -- widen the core rather than fork the rule. + * + * ⭐ `tildeRangeCitations` is refused beside a projection by the SAME rule + * (#24894): it is the third path-less spelling, and in prose a tilde range is + * a quantity. The message names whichever option was declared. */ + const pathlessOptions = [ + pathlessLineCitations && 'pathlessLineCitations', + tildeRangeCitations && 'tildeRangeCitations', + ].filter(Boolean); + if (docProjection !== null && pathlessOptions.length) { + const named = pathlessOptions.map((o) => `\`${o}: true\``).join(' and '); throw new Error( - `defineCorpus: corpus \`${id}\` declares a \`docProjection\` AND \`pathlessLineCitations: true\`. ` - + 'A projection declares the judged text is authored PROSE, where a colon before digits is punctuation; ' - + 'the two path-less spellings are for a DATA corpus writing a second pointer beside a filename it has ' + `defineCorpus: corpus \`${id}\` declares a \`docProjection\` AND ${named}. ` + + 'A projection declares the judged text is authored PROSE, where a colon or a tilde before digits is punctuation or a quantity; ' + + 'the path-less spellings are for a DATA corpus writing a second pointer beside a filename it has ' + 'already named. Drop one of the two: sweep the raw document if this corpus is data, or leave ' - + '`pathlessLineCitations` off and keep citing a path.', + + `${pathlessOptions.map((o) => `\`${o}\``).join(' and ')} off and keep citing a path.`, ); } return { id, label, docRoots, docPattern, crossRepos, checkBarePaths, docProjection, - judgeUntrackedLineAnchors, excludeDirs, unspannedAnchors, pathlessLineCitations, + judgeUntrackedLineAnchors, excludeDirs, unspannedAnchors, pathlessLineCitations, tildeRangeCitations, }; } @@ -1005,6 +1082,7 @@ export function sweepCorpus(corpus, root = process.cwd()) { { unspannedAnchors: corpus.unspannedAnchors ?? false, pathlessLineCitations: corpus.pathlessLineCitations ?? false, + tildeRangeCitations: corpus.tildeRangeCitations ?? false, }, ); @@ -1048,6 +1126,11 @@ export function sweepCorpus(corpus, root = process.cwd()) { findings.push({ kind: la.exemptRaw ? 'bad-exemption' : 'line-anchor', doc: rel, line: la.line, raw: la.raw, + /* The two spellings #24894 added, carried so a corpus can tell them + * apart without re-parsing `raw` -- the checklist exempts them inside + * its append-only text, where they were written before any gate read + * them. */ + comma: la.comma === true, tildeRange: la.tildeRange === true, detail: la.exemptRaw ? `\`${la.exemptRaw}\` names no valid exemption class — expected one of ${EXEMPT_CLASSES.join(' / ')}` : `a line number is not an anchor form — cite the symbol (\`${la.path ?? 'path'}#symbol\`) or drop to a file-level anchor`, @@ -1186,8 +1269,13 @@ function assert(cond, msg) { if (!cond) { console.error(`❌ symbol-anchors --se // projection. ⛔ The cases ride the file's ONE battery on purpose: // the roster is per SECTION BANNER and this file carries none, which // is why a single hoisted battery is its landed shape. +// 156 → 175 when the comma form joined the shared row and the tilde range +// became its own opt-in (#24894): each firing spelling beside the +// silent neighbour that bounds it, the default-off pair, the +// inheritance row, the de-duplication pair, and the projection +// refusal extended to the new option with its data-corpus control. const SELF_TEST_BATTERIES = Object.freeze({ - 'symbol-anchors self-test': 156, + 'symbol-anchors self-test': 175, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -1606,6 +1694,43 @@ export function selfTest() { check(extractAnchors('a template at packages/a/page.html:42 here').lineAnchors.length === 1, '`html` is in the shared anchorable vocabulary — a citation into a template page is read like any other'); + // 11e'. ⭐ THE COMMA FORM (#24894) — the colon's separator swapped for a + // comma, read by EVERY corpus because it carries its own path. Each + // firing row has its silent neighbour beside it: the tight/loose line + // is the whole rule. + const commaOf = (s) => extractAnchors(s).lineAnchors; + check(commaOf('see packages/a/foo.ts,123 for it').length === 1 && commaOf('see packages/a/foo.ts,123 for it')[0].comma === true + && commaOf('see packages/a/foo.ts,123 for it')[0].cited === 123, + `a COMMA citation is a line anchor for every corpus, marked as the comma form — got ${JSON.stringify(commaOf('see packages/a/foo.ts,123 for it'))}`); + check(commaOf('see foo.ts,123-140 there').map((l) => l.raw).join() === 'foo.ts,123-140', 'a comma citation of a RANGE is caught, bare filename or not'); + check(commaOf('see doctor.ts,1159–1161 there').length === 1, 'a comma citation with an EN DASH range is caught'); + check(commaOf('pinned by foo.test.ts, 12 cases').length === 0, + '⛔ a comma followed by a SPACE is a count after a file name, not a citation — the loose form was measured to admit three of those in the checklist'); + check(commaOf('both a.ts,b.ts and x.json,y.json').length === 0, 'a comma LIST of files is not a citation'); + check(commaOf('the pin packages/a/foo.ts:123 here')[0]?.comma === false, 'CONTROL: the colon form is still read, and is NOT marked as the comma form'); + + // 11e''. ⭐ THE TILDE RANGE (#24894), its own opt-in. Both directions on + // the SAME text, and every quantity-shaped neighbour pinned silent. + const trDoc = 'fixtures.knownGaps cite createColumn ~7354-7368 in it'; + check(extractAnchors(trDoc).lineAnchors.length === 0 + && extractAnchors(trDoc, { pathlessLineCitations: true }).lineAnchors.length === 0, + 'DEFAULT: a tilde range is NOT read without `tildeRangeCitations` — not even beside `pathlessLineCitations`, whose two spellings a corpus gate may name'); + const tr = (s) => extractAnchors(s, { tildeRangeCitations: true }).lineAnchors; + check(tr(trDoc).map((l) => l.raw).join() === '~7354-7368' && tr(trDoc)[0].tildeRange === true && tr(trDoc)[0].pathless === true, + `OPT-IN: a tilde range is caught, marked path-less and as the tilde range — got ${JSON.stringify(tr(trDoc))}`); + check(tr('arms in applyTenantScope (~12016–12066)').length === 1, 'an EN DASH tilde range in parentheses is caught'); + const trInherit = tr('`packages/a/b.ts#createColumn` ~7354-7368'); + check(trInherit.length === 1 && trInherit[0].path === 'packages/a/b.ts', 'a tilde range inherits the path named before it'); + check(tr('a run of ~2026-09 ships').length === 0, '⛔ a DESCENDING pair is not a line range — an approximate month stays silent'); + check(tr('takes ~10-20% of the run').length === 0, 'a percentage range is not a citation'); + check(tr('a ~ 100-200 budget and ~100 - 200 rows').length === 0, '⛔ a tilde range with WHITESPACE in it is prose, not a citation'); + check(tr('version 1~12-34 and v~12-34').length === 0, 'a tilde inside an identifier-shaped token is not a citation'); + check(tr('one ~5-9 item').length === 0, 'a one-digit range is under the two-digit floor'); + check(tr('computeAuthGate :~5084-5160').length === 0 && tr('a pin at packages/a/x.tsx:~605-615 here').length === 1, + 'a tilde after a COLON is the colon form\'s own spelling — not read a second time as a tilde range'); + check(extractAnchors('computeAuthGate ~:5084-5160 and ~7354-7368', { pathlessLineCitations: true, tildeRangeCitations: true }).lineAnchors.length === 2, + 'with both options on, the colon form and the tilde range each report ONCE'); + // 11f. ⭐ WHO MAY DECLARE `pathlessLineCitations` (#18845). The option is a // DATA corpus's declaration, and a corpus declaring a `docProjection` // has already said its documents are prose — so the pair is refused at @@ -1631,6 +1756,15 @@ export function selfTest() { 'CONTROL: a projected corpus that never mentions the option registers exactly as it did before'); check(defineCorpus({ ...proseSpec, pathlessLineCitations: false }).pathlessLineCitations === false, 'CONTROL: the refusal is about the PAIR — declaring the option OFF beside a projection is not a refusal'); + // ⭐ The tilde range rides the same rule (#24894), and the message names it. + let tildePairError = null; + try { defineCorpus({ ...proseSpec, tildeRangeCitations: true }); } catch (err) { tildePairError = err; } + check(tildePairError !== null && String(tildePairError.message).includes('tildeRangeCitations') + && !String(tildePairError.message).includes('pathlessLineCitations'), + `a projected corpus declaring \`tildeRangeCitations\` is refused, and the message names THAT option — got ${JSON.stringify(String(tildePairError && tildePairError.message).slice(0, 160))}`); + check(defineCorpus({ id: 'data-corpus', label: 'data', docRoots: ['a'], tildeRangeCitations: true }).tildeRangeCitations === true + && defineCorpus({ id: 'x', label: 'x', docRoots: ['a'] }).tildeRangeCitations === false, + 'CONTROL: a DATA corpus may declare `tildeRangeCitations`, and a corpus that says nothing has it OFF'); // ⭐ Keyed on ANY projection, ⛔ never on one function's identity: an identity // test is evaded by a wrapper around the very same projection, silently. let wrappedError = null; From 35c435837febb60c21c98f5e2690360caa714cc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 01:20:52 +0000 Subject: [PATCH 2/2] wip(scripts): the checklist declares the tilde range and counts append-only hits unjudged Claude-Session: https://claude.ai/code/session_01PPd7QqomCKXskDb8wkVgDV Co-authored-by: Claude --- scripts/check-platform-checklist.mjs | 156 ++++++++++++++++++++++++++- scripts/symbol-anchors.mjs | 17 +-- 2 files changed, 161 insertions(+), 12 deletions(-) diff --git a/scripts/check-platform-checklist.mjs b/scripts/check-platform-checklist.mjs index 34ba917d219..9e4b2cc317c 100644 --- a/scripts/check-platform-checklist.mjs +++ b/scripts/check-platform-checklist.mjs @@ -1129,7 +1129,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ // case that a citation both grammars already agreed on keeps its verdict, the // over-firing refusal on this ledger's own colon-then-digit neighbours, and // the live reading with its control. - [BATTERY_LINE_CITATION_BINDING]: 9, + // + // 9 → 18 at #24894: the tilde-range declaration (D4), the card's own pins + // through this corpus's declarations (C1 to C3), and the append-only filter + // driven both ways on a synthetic document (E1 to E5), its ⛔ not-wider case + // included. + [BATTERY_LINE_CITATION_BINDING]: 18, // 40 → 42 at #18107, and the SHAPE of the battery changed under it: the 25 // detector/resolver cases that pinned this file's own forked grammar moved // into `scripts/symbol-anchors.mjs`'s battery (93 → 122 there) and 27 took @@ -1700,6 +1705,79 @@ export const NEIGHBOURING_MAP: Readonly> = Object.freeze( // carries none of its own. `selfTestLineCitationBinding` below is that, driven // live in both directions on a synthetic corpus — the citation is a finding // with the declaration, and is not one without it. +// +// ⭐ Two spellings still went through (#24894), and the fix took the same exit: +// the core was widened, ⛔ no detector came back here. The COMMA form — a +// path, a comma, a line spec, no space — is now a row of the shared path- +// anchored grammar for every corpus, and the bare TILDE RANGE is the core's +// fourth corpus declaration, `tildeRangeCitations`, which this corpus makes. +// This ledger had accumulated 42 comma citations before a hand sweep moved +// them to symbol anchors; nothing refused the 43rd. +// +// What stays LOCAL is one thing the core cannot know: which text in THIS +// ledger is append-only. A `history[*]` row quotes what an item used to say, +// and `FOLLOW-UPS.md` is the sweeps' decision register ("append, never +// rewrite"); both were written before any gate read these two spellings, and +// rewriting a record to pass a gate falsifies the record. So a finding in the +// two NEW spellings inside that text is counted and NOT judged +// (`appendOnlyLineCitation`). ⛔ It is a filter on the sweep's findings, keyed +// on WHERE a finding sits — never a grammar, and never wider than the two +// spellings: a colon-form citation in a history row is judged exactly as +// before, because no measured population needs it excused. + +/* The documents of this family that are append-only as a whole. A path under + * `CHECKLIST_REL`, compared against a finding's `doc`. */ +const APPEND_ONLY_DOCS = Object.freeze([`${CHECKLIST_REL}/FOLLOW-UPS.md`]); + +/** + * The 1-based line ranges of every `"history"` array in one JSON document, + * inclusive at both ends. A character walk that tracks string state, so a + * bracket or the word inside a string value is payload — never a parse that + * would lose the line numbers the sweep reports findings against. + */ +function historyLineRanges(text) { + const ranges = []; + const open = []; + let line = 1; + let inString = false; + let escaped = false; + let stringStart = -1; + let lastString = null; + let key = null; + for (let i = 0; i < text.length; i += 1) { + const c = text[i]; + if (c === '\n') line += 1; + if (inString) { + if (escaped) escaped = false; + else if (c === '\\') escaped = true; + else if (c === '"') { inString = false; lastString = text.slice(stringStart + 1, i); } + continue; + } + if (c === '"') { inString = true; stringStart = i; continue; } + if (c === ':') { key = lastString; continue; } + if (c === '[' || c === '{') { open.push({ history: c === '[' && key === 'history', start: line }); key = null; continue; } + if (c === ']' || c === '}') { + const top = open.pop(); + if (top?.history) ranges.push([top.start, line]); + key = null; + continue; + } + if (c === ',') key = null; + } + return ranges; +} + +/** + * Is this sweep finding one of the two #24894 spellings, sitting in text this + * ledger keeps append-only? `readDoc` answers a finding's `doc` with its text, + * so the self-test can drive the rule on a synthetic document. + */ +function appendOnlyLineCitation(finding, readDoc) { + if (finding.kind !== 'line-anchor' || !(finding.comma || finding.tildeRange)) return false; + if (APPEND_ONLY_DOCS.includes(finding.doc)) return true; + if (!finding.doc.endsWith('.json')) return false; + return historyLineRanges(readDoc(finding.doc)).some(([from, to]) => finding.line >= from && finding.line <= to); +} /** * ⛔ NOT a grammar, and not a detector — the two things this limb used to be. @@ -1735,13 +1813,19 @@ function selfTestLineCitationBinding() { && !/\bfunction\s+findLineCitations\w*\b/.test(OWN_SOURCE)); t('D3 CONTROL for D2 — the same source read DOES find the declaration, so a green above is "no fork" and not "the read returned nothing"', /pathlessLineCitations:\s*true/.test(OWN_SOURCE) && /sweepCorpus\(CORPUS, ROOT\)/.test(OWN_SOURCE)); + t('D4 the corpus DECLARES the tilde range too — this ledger wrote one, and without the declaration the shared grammar does not read it', + CORPUS.tildeRangeCitations === true); // ── the binding, BOTH DIRECTIONS on the SAME text ──────────────────────── // // ⭐ The declaration is read off `CORPUS` rather than written as a literal, // so deleting it from the registration fails B1 instead of leaving a case // that passes on a flag nothing reads. - const declared = { unspannedAnchors: CORPUS.unspannedAnchors, pathlessLineCitations: CORPUS.pathlessLineCitations }; + const declared = { + unspannedAnchors: CORPUS.unspannedAnchors, + pathlessLineCitations: CORPUS.pathlessLineCitations, + tildeRangeCitations: CORPUS.tildeRangeCitations, + }; const cited = 'ManifestSchema id :140 and the leg ~L7246-7331'; t('B1 ON — a path-less citation in THIS ledger’s shape is read as a line anchor by the shared grammar, through the declarations this corpus makes', extractAnchors(cited, declared).lineAnchors.length === 2, @@ -1759,11 +1843,60 @@ function selfTestLineCitationBinding() { t('B4 the neighbours this ledger is full of stay silent under the declaration — an HTTP status, a config literal, a URL port, a clock time and JSON quoted in prose', extractAnchors('status:409 {maxRetries:3} http://localhost:3000/_console/ 08:00 {"scannedTypes":1}', declared).lineAnchors.length === 0); + // ── #24894: the two spellings that went through, in THIS ledger's shape ── + // The card's own pins, through this corpus's declarations. The grammar's + // full battery (every neighbour, both directions) is the core's. + const lineRaws = (text, opts = declared) => extractAnchors(text, opts).lineAnchors.map((l) => l.raw); + t('C1 the COMMA form is refused — a path, a comma and a line, and a path, a comma and a range', + lineRaws('see foo.ts,123 and foo.ts,123-140').join() === 'foo.ts,123,foo.ts,123-140', + JSON.stringify(lineRaws('see foo.ts,123 and foo.ts,123-140'))); + t('C2 the TILDE RANGE is refused through this corpus’s declaration, and is NOT read without it', + lineRaws('createColumn ~7354-7368').join() === '~7354-7368' + && lineRaws('createColumn ~7354-7368', { ...declared, tildeRangeCitations: false }).length === 0); + t('C3 a COUNT after a file name and a history index are admitted — the loose comma is prose', + lineRaws('pinned by foo.test.ts, 12 cases; see history[1]').length === 0); + + // ── #24894: append-only text is counted, not judged ────────────────────── + // ⭐ Driven on a synthetic document through the SAME rule the live loop + // uses, in both directions: the same spelling one array over IS judged. + const fixtureDoc = `${CHECKLIST_REL}/areas/fixture.json`; + const fixture = [ + '{', // 1 + ' "items": [', // 2 + ' {', // 3 + ' "fixtures": { "knownGaps": ["createColumn ~7354-7368"] },', // 4 judged + ' "history": [', // 5 + ' { "revision": 1, "change": "had foo.ts,123 [sic]" },', // 6 quoted + ' { "revision": 2, "change": "cut createColumn ~7354-7368" }', // 7 quoted + ' ],', // 8 + ' "verify": "then foo.ts,123 again"', // 9 judged + ' }', // 10 + ' ]', // 11 + '}', // 12 + ].join('\n'); + const readFixture = (doc) => (doc === fixtureDoc ? fixture : ''); + const findingAt = (line, extra) => ({ kind: 'line-anchor', doc: fixtureDoc, line, raw: 'x', comma: false, tildeRange: false, ...extra }); + t('E1 the history array is found by its lines, and a bracket inside a string value does not end it', + JSON.stringify(historyLineRanges(fixture)) === '[[5,8]]', JSON.stringify(historyLineRanges(fixture))); + t('E2 a comma citation and a tilde range INSIDE a history row are not judged', + appendOnlyLineCitation(findingAt(6, { comma: true }), readFixture) + && appendOnlyLineCitation(findingAt(7, { tildeRange: true }), readFixture)); + t('E3 CONTROL — the same two spellings OUTSIDE the history array ARE judged', + !appendOnlyLineCitation(findingAt(4, { tildeRange: true }), readFixture) + && !appendOnlyLineCitation(findingAt(9, { comma: true }), readFixture)); + t('E4 FOLLOW-UPS.md is append-only as a whole, for the two new spellings only', + appendOnlyLineCitation({ kind: 'line-anchor', doc: APPEND_ONLY_DOCS[0], line: 1, comma: true }, readFixture) + && !appendOnlyLineCitation({ kind: 'line-anchor', doc: `${CHECKLIST_REL}/README.md`, line: 1, comma: true }, readFixture)); + t('E5 ⛔ NOT wider than the two spellings — a colon-form citation in a history row is still judged', + !appendOnlyLineCitation(findingAt(6, {}), readFixture) + && !appendOnlyLineCitation({ kind: 'line-anchor', doc: APPEND_ONLY_DOCS[0], line: 1 }, readFixture)); + // ── the LIVE reading, so the zero above the console line prints is a // reading and not an instrument that stopped ────────────────────────── const liveSweep = sweepCorpus(CORPUS, ROOT); - const liveLineAnchors = liveSweep.findings.filter((f) => f.kind === 'line-anchor'); - t('B5 the live family carries NO line citation — the class the migration deleted has not come back', + const readLive = (doc) => readFileSync(join(ROOT, doc), 'utf8'); + const liveLineAnchors = liveSweep.findings.filter((f) => f.kind === 'line-anchor' && !appendOnlyLineCitation(f, readLive)); + t('B5 the live family carries NO judged line citation — the class the migration deleted has not come back', liveLineAnchors.length === 0, liveLineAnchors.map((f) => `${f.doc}:${f.line} ${f.raw}`).join(', ')); t('B6 CONTROL for B5 — the same sweep over the same family DID read anchors, so the zero above is a reading and not a sweep that reached nothing', @@ -1916,6 +2049,12 @@ function selfTestLineCitationBinding() { // projection is a corpus saying its judged text is authored PROSE; this // one declares none and sweeps its documents raw. That test is // mechanical, and this registration passes it. +// `tildeRangeCitations: true` (#24894) The third path-less spelling, a bare +// approximate range continuing a filename already named; this ledger +// carried one in an item's fixtures until a hand sweep re-pointed it. +// Legal here by the same registration test as the option above. The +// measurement of what it reads in the corpora that do NOT declare it is +// with the grammar, `scripts/symbol-anchors.mjs#TILDE_RANGE_CITATION`. // // The dotted `#Outer.member` form this ledger's grammar carried is now the // shared grammar's, and `sweepCorpus` requires EVERY segment to resolve — the @@ -1934,6 +2073,7 @@ const CORPUS = defineCorpus({ excludeDirs: ['runs'], unspannedAnchors: true, pathlessLineCitations: true, + tildeRangeCitations: true, }); // ── The residual the binding measured (#16898) ──────────────────────── @@ -3534,8 +3674,14 @@ for (const [doc, c] of sweep.byDoc) { anchorCounts.set(rel, c.declaration + c.literal); } +// The two #24894 spellings inside append-only text: counted and printed, NOT +// judged (see `appendOnlyLineCitation`). +let appendOnlyCitations = 0; +const readSweptDoc = (doc) => readFileSync(join(ROOT, doc), 'utf8'); + for (const f of sweep.findings) { const rel = relToChecklist(f.doc); + if (appendOnlyLineCitation(f, readSweptDoc)) { appendOnlyCitations += 1; continue; } if (f.kind === 'unresolved-symbol') { const anchor = `${f.path}#${f.symbol}`; // On the measured residual? Then this is one of the 56 the binding @@ -3640,7 +3786,7 @@ console.log( ` traps: ${TRAPS.size} documented, ${usedTraps.size} in use;` + ` 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;` + - ` line citations: 0 survive across ${sweep.counts.docs} swept documents — \`file:line\`, a bare \`:NNN\` continuation and an \`L\` pin are all judged by \`symbol-anchors.mjs\`, through the same registration;` + + ` line citations: 0 survive across ${sweep.counts.docs} swept documents — \`file:line\`, the comma form \`file,line\`, a bare \`:NNN\` continuation, an \`L\` pin and a tilde range \`~NNN-MMM\` are all judged by \`symbol-anchors.mjs\`, through the same registration (${appendOnlyCitations} comma or tilde-range citation(s) in append-only text — \`history\` rows and FOLLOW-UPS.md — counted, not judged);` + ` symbol anchors: ${anchorsResolved}/${anchorsScanned} resolved by \`symbol-anchors.mjs\` (the ONE resolver, reached as a REGISTERED corpus) across ${sweep.counts.docs} swept documents against ${sweep.counts.citedSources} 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} line-citation-binding + ${symbolAnchorControl.checked} symbol-anchor + ${plannedStatusControl.checked} planned-status assertions).`, diff --git a/scripts/symbol-anchors.mjs b/scripts/symbol-anchors.mjs index a16a56ff916..8a82922046c 100644 --- a/scripts/symbol-anchors.mjs +++ b/scripts/symbol-anchors.mjs @@ -389,8 +389,9 @@ const LINE_SPEC = '\\d+(?:\\s*[-–—]\\s*\\d+)?(?:\\s*[,/]\\s*\\d+(?:\\s*[-– * colon. ⛔ Tight, exactly as the tilde: a comma followed by a SPACE is a list * or a count after a file name (a test file, a comma, a space, `12 cases`), and * the loose form was measured to admit three such counts in that ledger. The - * tight form was measured over all five registered corpora before it became a - * row for every corpus, not an option: 11 hits, all in the checklist's + * tight form was measured over all five registered corpora, on `7b047b4483` + * in this repository, before it became a row for every corpus rather than an + * option: 11 hits, all in the checklist's * `FOLLOW-UPS.md`, and ZERO in `docs/adr/**`, `scripts/**`, * `packages/spec/src/**` and the `isSystem` census page -- so nothing that * passed anywhere else can start failing on this row alone, and a citation @@ -485,11 +486,13 @@ const PATHLESS_L_CITATION = new RegExp('(?