@@ -1129,7 +1129,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
11291129 // case that a citation both grammars already agreed on keeps its verdict, the
11301130 // over-firing refusal on this ledger's own colon-then-digit neighbours, and
11311131 // the live reading with its control.
1132- [ BATTERY_LINE_CITATION_BINDING ] : 9 ,
1132+ //
1133+ // 9 → 18 at #24894: the tilde-range declaration (D4), the card's own pins
1134+ // through this corpus's declarations (C1 to C3), and the append-only filter
1135+ // driven both ways on a synthetic document (E1 to E5), its ⛔ not-wider case
1136+ // included.
1137+ [ BATTERY_LINE_CITATION_BINDING ] : 18 ,
11331138 // 40 → 42 at #18107, and the SHAPE of the battery changed under it: the 25
11341139 // detector/resolver cases that pinned this file's own forked grammar moved
11351140 // into `scripts/symbol-anchors.mjs`'s battery (93 → 122 there) and 27 took
@@ -1700,6 +1705,79 @@ export const NEIGHBOURING_MAP: Readonly<Record<string, string>> = Object.freeze(
17001705// carries none of its own. `selfTestLineCitationBinding` below is that, driven
17011706// live in both directions on a synthetic corpus — the citation is a finding
17021707// with the declaration, and is not one without it.
1708+ //
1709+ // ⭐ Two spellings still went through (#24894), and the fix took the same exit:
1710+ // the core was widened, ⛔ no detector came back here. The COMMA form — a
1711+ // path, a comma, a line spec, no space — is now a row of the shared path-
1712+ // anchored grammar for every corpus, and the bare TILDE RANGE is the core's
1713+ // fourth corpus declaration, `tildeRangeCitations`, which this corpus makes.
1714+ // This ledger had accumulated 42 comma citations before a hand sweep moved
1715+ // them to symbol anchors; nothing refused the 43rd.
1716+ //
1717+ // What stays LOCAL is one thing the core cannot know: which text in THIS
1718+ // ledger is append-only. A `history[*]` row quotes what an item used to say,
1719+ // and `FOLLOW-UPS.md` is the sweeps' decision register ("append, never
1720+ // rewrite"); both were written before any gate read these two spellings, and
1721+ // rewriting a record to pass a gate falsifies the record. So a finding in the
1722+ // two NEW spellings inside that text is counted and NOT judged
1723+ // (`appendOnlyLineCitation`). ⛔ It is a filter on the sweep's findings, keyed
1724+ // on WHERE a finding sits — never a grammar, and never wider than the two
1725+ // spellings: a colon-form citation in a history row is judged exactly as
1726+ // before, because no measured population needs it excused.
1727+
1728+ /* The documents of this family that are append-only as a whole. A path under
1729+ * `CHECKLIST_REL`, compared against a finding's `doc`. */
1730+ const APPEND_ONLY_DOCS = Object . freeze ( [ `${ CHECKLIST_REL } /FOLLOW-UPS.md` ] ) ;
1731+
1732+ /**
1733+ * The 1-based line ranges of every `"history"` array in one JSON document,
1734+ * inclusive at both ends. A character walk that tracks string state, so a
1735+ * bracket or the word inside a string value is payload — never a parse that
1736+ * would lose the line numbers the sweep reports findings against.
1737+ */
1738+ function historyLineRanges ( text ) {
1739+ const ranges = [ ] ;
1740+ const open = [ ] ;
1741+ let line = 1 ;
1742+ let inString = false ;
1743+ let escaped = false ;
1744+ let stringStart = - 1 ;
1745+ let lastString = null ;
1746+ let key = null ;
1747+ for ( let i = 0 ; i < text . length ; i += 1 ) {
1748+ const c = text [ i ] ;
1749+ if ( c === '\n' ) line += 1 ;
1750+ if ( inString ) {
1751+ if ( escaped ) escaped = false ;
1752+ else if ( c === '\\' ) escaped = true ;
1753+ else if ( c === '"' ) { inString = false ; lastString = text . slice ( stringStart + 1 , i ) ; }
1754+ continue ;
1755+ }
1756+ if ( c === '"' ) { inString = true ; stringStart = i ; continue ; }
1757+ if ( c === ':' ) { key = lastString ; continue ; }
1758+ if ( c === '[' || c === '{' ) { open . push ( { history : c === '[' && key === 'history' , start : line } ) ; key = null ; continue ; }
1759+ if ( c === ']' || c === '}' ) {
1760+ const top = open . pop ( ) ;
1761+ if ( top ?. history ) ranges . push ( [ top . start , line ] ) ;
1762+ key = null ;
1763+ continue ;
1764+ }
1765+ if ( c === ',' ) key = null ;
1766+ }
1767+ return ranges ;
1768+ }
1769+
1770+ /**
1771+ * Is this sweep finding one of the two #24894 spellings, sitting in text this
1772+ * ledger keeps append-only? `readDoc` answers a finding's `doc` with its text,
1773+ * so the self-test can drive the rule on a synthetic document.
1774+ */
1775+ function appendOnlyLineCitation ( finding , readDoc ) {
1776+ if ( finding . kind !== 'line-anchor' || ! ( finding . comma || finding . tildeRange ) ) return false ;
1777+ if ( APPEND_ONLY_DOCS . includes ( finding . doc ) ) return true ;
1778+ if ( ! finding . doc . endsWith ( '.json' ) ) return false ;
1779+ return historyLineRanges ( readDoc ( finding . doc ) ) . some ( ( [ from , to ] ) => finding . line >= from && finding . line <= to ) ;
1780+ }
17031781
17041782/**
17051783 * ⛔ NOT a grammar, and not a detector — the two things this limb used to be.
@@ -1735,13 +1813,19 @@ function selfTestLineCitationBinding() {
17351813 && ! / \b f u n c t i o n \s + f i n d L i n e C i t a t i o n s \w * \b / . test ( OWN_SOURCE ) ) ;
17361814 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"' ,
17371815 / p a t h l e s s L i n e C i t a t i o n s : \s * t r u e / . test ( OWN_SOURCE ) && / s w e e p C o r p u s \( C O R P U S , R O O T \) / . test ( OWN_SOURCE ) ) ;
1816+ t ( 'D4 the corpus DECLARES the tilde range too — this ledger wrote one, and without the declaration the shared grammar does not read it' ,
1817+ CORPUS . tildeRangeCitations === true ) ;
17381818
17391819 // ── the binding, BOTH DIRECTIONS on the SAME text ────────────────────────
17401820 //
17411821 // ⭐ The declaration is read off `CORPUS` rather than written as a literal,
17421822 // so deleting it from the registration fails B1 instead of leaving a case
17431823 // that passes on a flag nothing reads.
1744- const declared = { unspannedAnchors : CORPUS . unspannedAnchors , pathlessLineCitations : CORPUS . pathlessLineCitations } ;
1824+ const declared = {
1825+ unspannedAnchors : CORPUS . unspannedAnchors ,
1826+ pathlessLineCitations : CORPUS . pathlessLineCitations ,
1827+ tildeRangeCitations : CORPUS . tildeRangeCitations ,
1828+ } ;
17451829 const cited = 'ManifestSchema id :140 and the leg ~L7246-7331' ;
17461830 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' ,
17471831 extractAnchors ( cited , declared ) . lineAnchors . length === 2 ,
@@ -1759,11 +1843,60 @@ function selfTestLineCitationBinding() {
17591843 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' ,
17601844 extractAnchors ( 'status:409 {maxRetries:3} http://localhost:3000/_console/ 08:00 {"scannedTypes":1}' , declared ) . lineAnchors . length === 0 ) ;
17611845
1846+ // ── #24894: the two spellings that went through, in THIS ledger's shape ──
1847+ // The card's own pins, through this corpus's declarations. The grammar's
1848+ // full battery (every neighbour, both directions) is the core's.
1849+ const lineRaws = ( text , opts = declared ) => extractAnchors ( text , opts ) . lineAnchors . map ( ( l ) => l . raw ) ;
1850+ t ( 'C1 the COMMA form is refused — a path, a comma and a line, and a path, a comma and a range' ,
1851+ lineRaws ( 'see foo.ts,123 and foo.ts,123-140' ) . join ( ) === 'foo.ts,123,foo.ts,123-140' ,
1852+ JSON . stringify ( lineRaws ( 'see foo.ts,123 and foo.ts,123-140' ) ) ) ;
1853+ t ( 'C2 the TILDE RANGE is refused through this corpus’s declaration, and is NOT read without it' ,
1854+ lineRaws ( 'createColumn ~7354-7368' ) . join ( ) === '~7354-7368'
1855+ && lineRaws ( 'createColumn ~7354-7368' , { ...declared , tildeRangeCitations : false } ) . length === 0 ) ;
1856+ t ( 'C3 a COUNT after a file name and a history index are admitted — the loose comma is prose' ,
1857+ lineRaws ( 'pinned by foo.test.ts, 12 cases; see history[1]' ) . length === 0 ) ;
1858+
1859+ // ── #24894: append-only text is counted, not judged ──────────────────────
1860+ // ⭐ Driven on a synthetic document through the SAME rule the live loop
1861+ // uses, in both directions: the same spelling one array over IS judged.
1862+ const fixtureDoc = `${ CHECKLIST_REL } /areas/fixture.json` ;
1863+ const fixture = [
1864+ '{' , // 1
1865+ ' "items": [' , // 2
1866+ ' {' , // 3
1867+ ' "fixtures": { "knownGaps": ["createColumn ~7354-7368"] },' , // 4 judged
1868+ ' "history": [' , // 5
1869+ ' { "revision": 1, "change": "had foo.ts,123 [sic]" },' , // 6 quoted
1870+ ' { "revision": 2, "change": "cut createColumn ~7354-7368" }' , // 7 quoted
1871+ ' ],' , // 8
1872+ ' "verify": "then foo.ts,123 again"' , // 9 judged
1873+ ' }' , // 10
1874+ ' ]' , // 11
1875+ '}' , // 12
1876+ ] . join ( '\n' ) ;
1877+ const readFixture = ( doc ) => ( doc === fixtureDoc ? fixture : '' ) ;
1878+ const findingAt = ( line , extra ) => ( { kind : 'line-anchor' , doc : fixtureDoc , line, raw : 'x' , comma : false , tildeRange : false , ...extra } ) ;
1879+ t ( 'E1 the history array is found by its lines, and a bracket inside a string value does not end it' ,
1880+ JSON . stringify ( historyLineRanges ( fixture ) ) === '[[5,8]]' , JSON . stringify ( historyLineRanges ( fixture ) ) ) ;
1881+ t ( 'E2 a comma citation and a tilde range INSIDE a history row are not judged' ,
1882+ appendOnlyLineCitation ( findingAt ( 6 , { comma : true } ) , readFixture )
1883+ && appendOnlyLineCitation ( findingAt ( 7 , { tildeRange : true } ) , readFixture ) ) ;
1884+ t ( 'E3 CONTROL — the same two spellings OUTSIDE the history array ARE judged' ,
1885+ ! appendOnlyLineCitation ( findingAt ( 4 , { tildeRange : true } ) , readFixture )
1886+ && ! appendOnlyLineCitation ( findingAt ( 9 , { comma : true } ) , readFixture ) ) ;
1887+ t ( 'E4 FOLLOW-UPS.md is append-only as a whole, for the two new spellings only' ,
1888+ appendOnlyLineCitation ( { kind : 'line-anchor' , doc : APPEND_ONLY_DOCS [ 0 ] , line : 1 , comma : true } , readFixture )
1889+ && ! appendOnlyLineCitation ( { kind : 'line-anchor' , doc : `${ CHECKLIST_REL } /README.md` , line : 1 , comma : true } , readFixture ) ) ;
1890+ t ( 'E5 ⛔ NOT wider than the two spellings — a colon-form citation in a history row is still judged' ,
1891+ ! appendOnlyLineCitation ( findingAt ( 6 , { } ) , readFixture )
1892+ && ! appendOnlyLineCitation ( { kind : 'line-anchor' , doc : APPEND_ONLY_DOCS [ 0 ] , line : 1 } , readFixture ) ) ;
1893+
17621894 // ── the LIVE reading, so the zero above the console line prints is a
17631895 // reading and not an instrument that stopped ──────────────────────────
17641896 const liveSweep = sweepCorpus ( CORPUS , ROOT ) ;
1765- const liveLineAnchors = liveSweep . findings . filter ( ( f ) => f . kind === 'line-anchor' ) ;
1766- t ( 'B5 the live family carries NO line citation — the class the migration deleted has not come back' ,
1897+ const readLive = ( doc ) => readFileSync ( join ( ROOT , doc ) , 'utf8' ) ;
1898+ const liveLineAnchors = liveSweep . findings . filter ( ( f ) => f . kind === 'line-anchor' && ! appendOnlyLineCitation ( f , readLive ) ) ;
1899+ t ( 'B5 the live family carries NO judged line citation — the class the migration deleted has not come back' ,
17671900 liveLineAnchors . length === 0 ,
17681901 liveLineAnchors . map ( ( f ) => `${ f . doc } :${ f . line } ${ f . raw } ` ) . join ( ', ' ) ) ;
17691902 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() {
19162049// projection is a corpus saying its judged text is authored PROSE; this
19172050// one declares none and sweeps its documents raw. That test is
19182051// mechanical, and this registration passes it.
2052+ // `tildeRangeCitations: true` (#24894) The third path-less spelling, a bare
2053+ // approximate range continuing a filename already named; this ledger
2054+ // carried one in an item's fixtures until a hand sweep re-pointed it.
2055+ // Legal here by the same registration test as the option above. The
2056+ // measurement of what it reads in the corpora that do NOT declare it is
2057+ // with the grammar, `scripts/symbol-anchors.mjs#TILDE_RANGE_CITATION`.
19192058//
19202059// The dotted `#Outer.member` form this ledger's grammar carried is now the
19212060// shared grammar's, and `sweepCorpus` requires EVERY segment to resolve — the
@@ -1934,6 +2073,7 @@ const CORPUS = defineCorpus({
19342073 excludeDirs : [ 'runs' ] ,
19352074 unspannedAnchors : true ,
19362075 pathlessLineCitations : true ,
2076+ tildeRangeCitations : true ,
19372077} ) ;
19382078
19392079// ── The residual the binding measured (#16898) ────────────────────────
@@ -3534,8 +3674,14 @@ for (const [doc, c] of sweep.byDoc) {
35343674 anchorCounts . set ( rel , c . declaration + c . literal ) ;
35353675}
35363676
3677+ // The two #24894 spellings inside append-only text: counted and printed, NOT
3678+ // judged (see `appendOnlyLineCitation`).
3679+ let appendOnlyCitations = 0 ;
3680+ const readSweptDoc = ( doc ) => readFileSync ( join ( ROOT , doc ) , 'utf8' ) ;
3681+
35373682for ( const f of sweep . findings ) {
35383683 const rel = relToChecklist ( f . doc ) ;
3684+ if ( appendOnlyLineCitation ( f , readSweptDoc ) ) { appendOnlyCitations += 1 ; continue ; }
35393685 if ( f . kind === 'unresolved-symbol' ) {
35403686 const anchor = `${ f . path } #${ f . symbol } ` ;
35413687 // On the measured residual? Then this is one of the 56 the binding
@@ -3640,7 +3786,7 @@ console.log(
36403786 ` traps: ${ TRAPS . size } documented, ${ usedTraps . size } in use;` +
36413787 ` provisioning: ${ recipeTotal } area recipes, ${ recipeRefs } item references resolved (${ qualifiedRefs } area-qualified), ${ recipesReferenced } /${ recipeTotal } recipes referenced;` +
36423788 ` meta-URL spelling: ${ metaCallsScanned } \`call\` strings scanned against ${ FOLDED_META_SPELLINGS . size } folded spellings;` +
3643- ` 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;` +
3789+ ` 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) ;` +
36443790 ` 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` +
36453791 `, ${ anchorsResidual } on the named #16898 residual, ${ Object . keys ( anchorFloors ) . length } file floors held;` +
36463792 ` (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).` ,
0 commit comments