158158 * assertion reads it rather than adding to it.
159159 */
160160
161- import { readFileSync , writeFileSync } from 'node:fs' ;
161+ import { createHash } from 'node:crypto' ;
162+ import { mkdtempSync , readFileSync , rmSync , writeFileSync } from 'node:fs' ;
162163import { execFileSync } from 'node:child_process' ;
164+ import { tmpdir } from 'node:os' ;
163165import { join } from 'node:path' ;
164166import { fileURLToPath } from 'node:url' ;
165167
@@ -201,11 +203,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
201203 'the refusal has to SHOW its work (both counts, both classes, the diff)' : 2 ,
202204 'WIRING: this gate, and its self-test, really run in CI' : 2 ,
203205 'POPULATION DECLARATION: what the dispatch derivation is told this gate reads' : 6 ,
206+ '⭐ ROW REFERENCES: held by seam, and the insertion that was silent (#15869)' : 23 ,
204207} ) ;
205208
206209// DELETING an entry silences that battery's floor exactly as effectively as
207210// zeroing it, so the roster's own size is pinned too.
208- const SELF_TEST_BATTERY_FLOOR = 17 ;
211+ const SELF_TEST_BATTERY_FLOOR = 18 ;
209212
210213// The key an assertion is filed under when no battery is open. It is not a
211214// declared battery, so it reds by the same set difference rather than silently
@@ -1642,6 +1645,104 @@ function fixturePage({ anchor = 'pkg/a.ts:2', helper = 'pkg/a.ts:7' } = {}) {
16421645 ] . join ( '\n' ) ;
16431646}
16441647
1648+ /**
1649+ * ── The ROW REFERENCE fixture (#15869) ──────────────────────────────────────
1650+ *
1651+ * A miniature of the real page's shape, and only its shape: one behaviour table
1652+ * whose numbering runs on into a `### 6.` carry-onward table, then the four
1653+ * spellings the real page uses to point back into them -- `Row N`,
1654+ * `row N's`, `row N`, and the two `rows N–M` extents.
1655+ *
1656+ * ⛔ Deliberately NOT a copy of the real page: the cases below drive one edit at
1657+ * a time, and a fixture small enough to read whole is the only way a reader can
1658+ * see that the edit is the only difference. The real page is exercised too, in
1659+ * the same battery, by the ablation.
1660+ */
1661+ const ROW_FIXTURE_PAGE = [
1662+ '---' ,
1663+ 'title: row fixture' ,
1664+ '---' ,
1665+ '' ,
1666+ '| # | Behaviour | Anchor |' ,
1667+ '|:--|:---|:---|' ,
1668+ '| 1 | **The short-circuit** runs first | `pkg/a.ts:2` |' ,
1669+ '| 2 | **`owner_id` is not stamped** on INSERT | `pkg/a.ts:3` |' ,
1670+ '| 3 | `revoke()` deletes directly, before the guard | `pkg/a.ts:4` |' ,
1671+ '' ,
1672+ '### 6. Reads that only carry the flag onward' ,
1673+ '' ,
1674+ '| # | Site | What it does |' ,
1675+ '|:--|:---|:---|' ,
1676+ '| 4 | `pkg/b.ts:5` | Propagates the flag onward |' ,
1677+ '| 5 | `pkg/b.ts:6` | Rebuilds the context |' ,
1678+ '' ,
1679+ '1. **`revoke()` skips its own conflict guard.** Row 3 is correct for the rule.' ,
1680+ "2. The shared verdict is the one function all of row 2's doors consult." ,
1681+ '3. The step 3.5 guard is inside the block row 1 skips.' ,
1682+ '' ,
1683+ '| — behaviour-bearing (rows 1–3 above) | 9 |' ,
1684+ '| — carry the flag onward only (rows 4–5 above) | 2 |' ,
1685+ '' ,
1686+ ] . join ( '\n' ) ;
1687+
1688+ /** The fixture's own `PAGE_ROW_REFERENCES`: one entry per reference above. */
1689+ const ROW_FIXTURE_REFS = [
1690+ {
1691+ context : '**`revoke()` skips its own conflict guard.**' ,
1692+ seam : '`revoke()` deletes directly' ,
1693+ why : 'the bolded subject is `revoke()`' ,
1694+ } ,
1695+ { context : 'doors consult' , seam : '**`owner_id` is not stamped**' , why : 'the doors are the stamp doors' } ,
1696+ { context : 'is inside the block' , seam : '**The short-circuit** runs first' , why : 'the block is the short-circuit' } ,
1697+ { context : 'behaviour-bearing (rows' , section : 'behaviour' , why : 'the behaviour extent' } ,
1698+ { context : 'carry the flag onward only (rows' , section : 'carry-onward' , why : 'the carry-onward extent' } ,
1699+ ] ;
1700+
1701+ /**
1702+ * The fixture's own `NON_READ_ANCHORS`, carrying all three `why:` shapes: a plain
1703+ * `row N`, a `why` naming TWO rows in one string, and the hyphenated `row-N`.
1704+ */
1705+ const ROW_FIXTURE_LEDGER = [
1706+ {
1707+ file : 'pkg/a.ts' ,
1708+ needle : 'a' ,
1709+ why : 'row 3 -- the guard `revoke()` deletes in front of' ,
1710+ rowSeams : [ '`revoke()` deletes directly' ] ,
1711+ } ,
1712+ {
1713+ file : 'pkg/a.ts' ,
1714+ needle : 'b' ,
1715+ why : 'row 2 -- the stamp guard the row-1 short-circuit skips' ,
1716+ rowSeams : [ '**`owner_id` is not stamped**' , '**The short-circuit** runs first' ] ,
1717+ } ,
1718+ { file : 'pkg/a.ts' , needle : 'c' , why : 'rough edge 5 -- names no row at all' , rowSeams : [ ] } ,
1719+ ] ;
1720+
1721+ /**
1722+ * Insert one numbered row above row `n` and renumber every row at or below it --
1723+ * the edit PR #15687 made, which this gate used to be green through.
1724+ *
1725+ * @returns {{ text: string, inserted: boolean } }
1726+ */
1727+ function insertRowAbove ( pageText , n , cell ) {
1728+ let inserted = false ;
1729+ const out = [ ] ;
1730+ for ( const line of pageText . split ( '\n' ) ) {
1731+ const m = / ^ \| \s * ( \d + ) \s * \| ( .* ) $ / . exec ( line ) ;
1732+ if ( ! m ) {
1733+ out . push ( line ) ;
1734+ continue ;
1735+ }
1736+ const number = Number ( m [ 1 ] ) ;
1737+ if ( number === n && ! inserted ) {
1738+ out . push ( `| ${ n } |${ cell } ` ) ;
1739+ inserted = true ;
1740+ }
1741+ out . push ( number >= n ? `| ${ number + 1 } |${ m [ 2 ] } ` : line ) ;
1742+ }
1743+ return { text : out . join ( '\n' ) , inserted } ;
1744+ }
1745+
16451746/**
16461747 * The page's UNENFORCED decomposition, as a fixture: the six rows plus the dated
16471748 * marker. Every knob is a way the page could decay -- a number going stale, a row
@@ -1760,6 +1861,7 @@ function selfTest() {
17601861 // ── ledger ─────────────────────────────────────────────────────────────────
17611862 battery ( 'ledger' ) ;
17621863 const ledgerStale = evaluate ( {
1864+ pageRowReferences : [ ] ,
17631865 pageText : fixturePage ( ) ,
17641866 census : FIXTURE_CENSUS ,
17651867 tracked : FIXTURE_TRACKED ,
@@ -1770,6 +1872,7 @@ function selfTest() {
17701872 } ) ;
17711873 t ( 'LEDGER: a needle that matches nothing is a finding' , ledgerStale . problems . some ( ( p ) => p . startsWith ( '[ledger-stale]' ) ) ) ;
17721874 const ledgerAmbig = evaluate ( {
1875+ pageRowReferences : [ ] ,
17731876 pageText : fixturePage ( ) ,
17741877 census : FIXTURE_CENSUS ,
17751878 tracked : FIXTURE_TRACKED ,
@@ -1780,6 +1883,7 @@ function selfTest() {
17801883 } ) ;
17811884 t ( 'LEDGER: a needle matching two lines is a finding' , ledgerAmbig . problems . some ( ( p ) => p . startsWith ( '[ledger-ambiguous]' ) ) ) ;
17821885 const ledgerUnused = evaluate ( {
1886+ pageRowReferences : [ ] ,
17831887 pageText : fixturePage ( { helper : 'pkg/a.ts:2' } ) ,
17841888 census : FIXTURE_CENSUS ,
17851889 tracked : FIXTURE_TRACKED ,
@@ -1828,6 +1932,7 @@ function selfTest() {
18281932 t ( 'COUNTS: the carry-onward split is read from section 6 itself' , carryOnwardRowCount ( sectionPage ) === 2 ) ;
18291933 t ( 'COUNTS: a renamed section 6 is underivable, not zero' , carryOnwardRowCount ( 'nothing here' ) === - 1 ) ;
18301934 const underivable = evaluate ( {
1935+ pageRowReferences : [ ] ,
18311936 pageText : fixturePage ( ) ,
18321937 census : FIXTURE_CENSUS ,
18331938 tracked : FIXTURE_TRACKED ,
@@ -2001,6 +2106,7 @@ function selfTest() {
20012106 // touched cannot come back with a missing site, an unexplained anchor or an
20022107 // unused ledger row. Pinned behaviourally rather than argued in a comment.
20032108 const afterFix = evaluate ( {
2109+ pageRowReferences : [ ] ,
20042110 pageText : bothShifted . text ,
20052111 census : FIXTURE_CENSUS ,
20062112 tracked : FIXTURE_TRACKED ,
@@ -2110,6 +2216,223 @@ function selfTest() {
21102216 // `check-aggregator-roster` and `check-ci-filter-parity` set -- and, like the second
21112217 // docs root that gate added, this needed NO workflow edit: `lint.yml` already invokes
21122218 // both legs, and it is the repo's busiest file.
2219+ // ── ⭐ ROW REFERENCES: held by SEAM, and the insertion that used to be silent ─
2220+ //
2221+ // The shape this battery exists for (#15869): a row INSERTED into the behaviour
2222+ // table renumbers every row below it, so every `row N` past the insertion point
2223+ // becomes false while this gate stays green -- because nothing compared a number
2224+ // to a row. It happened (#15687, three references), and it un-happened by
2225+ // coincidence (#15395, the same three), with nothing observing either event.
2226+ //
2227+ // ⭐ The ablation is run on a COPY of the REAL page, in a temp dir, because a
2228+ // toy fixture cannot show that the check reaches the references that actually
2229+ // rotted. The real page is never written -- asserted below, by bytes.
2230+ battery ( '⭐ ROW REFERENCES: held by seam, and the insertion that was silent (#15869)' ) ;
2231+
2232+ const rowRefs = ( page , refs = ROW_FIXTURE_REFS , ledger = ROW_FIXTURE_LEDGER ) =>
2233+ checkRowReferences ( { pageText : page , ledger, pageRefs : refs } ) ;
2234+ const codes = ( result ) => result . problems . map ( ( p ) => p . slice ( 0 , p . indexOf ( ']' ) + 1 ) ) ;
2235+
2236+ const rowGreen = rowRefs ( ROW_FIXTURE_PAGE ) ;
2237+ t (
2238+ '⭐ POSITIVE CONTROL: every reference whose number MATCHES its keyed row passes' ,
2239+ rowGreen . problems . length === 0 ,
2240+ rowGreen . problems . join ( ' | ' )
2241+ ) ;
2242+ t (
2243+ 'the control really exercised all five page references and both `why:` mentions' ,
2244+ rowGreen . held . page === 5 && rowGreen . held . why === 3 && rowGreen . held . unheld === 0 ,
2245+ JSON . stringify ( rowGreen . held )
2246+ ) ;
2247+
2248+ // ── one case per SPELLING the page uses ────────────────────────────────────
2249+ const falsified = ( page , expect ) => {
2250+ const result = rowRefs ( page ) ;
2251+ return (
2252+ result . problems . some ( ( p ) => p . startsWith ( '[row-ref-falsified]' ) && p . includes ( expect ) ) &&
2253+ result . problems . length === 1
2254+ ) ;
2255+ } ;
2256+ t (
2257+ 'SPELLING `Row N` (sentence-initial): a number that no longer matches its seam is falsified' ,
2258+ falsified ( ROW_FIXTURE_PAGE . replace ( 'Row 3 is correct' , 'Row 2 is correct' ) , '`Row 2`' )
2259+ ) ;
2260+ t (
2261+ "SPELLING `row N's` (possessive): same" ,
2262+ falsified ( ROW_FIXTURE_PAGE . replace ( "row 2's doors" , "row 1's doors" ) , '`row 1`' )
2263+ ) ;
2264+ t (
2265+ 'SPELLING `row N` (lowercase, mid-sentence): same' ,
2266+ falsified ( ROW_FIXTURE_PAGE . replace ( 'the block row 1 skips' , 'the block row 3 skips' ) , '`row 3`' )
2267+ ) ;
2268+ t (
2269+ 'SPELLING `rows N–M` (range): the behaviour extent is held to the section, not to a seam' ,
2270+ falsified ( ROW_FIXTURE_PAGE . replace ( '(rows 1–3 above)' , '(rows 1–4 above)' ) , '`rows 1–4`' )
2271+ ) ;
2272+ t (
2273+ 'SPELLING `rows N–M` (range): and the carry-onward extent to ITS section' ,
2274+ falsified ( ROW_FIXTURE_PAGE . replace ( '(rows 4–5 above)' , '(rows 4–6 above)' ) , '`rows 4–6`' )
2275+ ) ;
2276+
2277+ // ── the incident shape, on the fixture: ONE insertion, several falsehoods ───
2278+ const inserted = insertRowAbove ( ROW_FIXTURE_PAGE , 3 , ' **An inserted row** | `pkg/a.ts:9` |' ) ;
2279+ const afterInsert = rowRefs ( inserted . text ) ;
2280+ t (
2281+ '⭐ THE INCIDENT SHAPE: inserting one row above row 3 falsifies every reference below it, ' +
2282+ 'and the refusal names the reference, its number and the row the key resolves to' ,
2283+ inserted . inserted &&
2284+ afterInsert . problems . filter ( ( p ) => p . startsWith ( '[row-ref-falsified]' ) ) . length === 3 &&
2285+ afterInsert . problems . some ( ( p ) => p . includes ( '`Row 3`' ) && p . includes ( 'is row 4' ) ) ,
2286+ afterInsert . problems . join ( ' | ' )
2287+ ) ;
2288+
2289+ // ── keys that stop resolving ───────────────────────────────────────────────
2290+ t (
2291+ 'a key that resolves to NO row is a refusal, not a pass -- the reference is now held by nothing' ,
2292+ codes ( rowRefs ( ROW_FIXTURE_PAGE . replace ( '`revoke()` deletes directly, before the guard' , 'reworded' ) ) ) . includes (
2293+ '[row-ref-key-unresolved]'
2294+ )
2295+ ) ;
2296+ t (
2297+ 'a key that resolves to TWO rows refuses and names both candidates' ,
2298+ ( ( ) => {
2299+ const twice = ROW_FIXTURE_PAGE . replace (
2300+ '| 2 | **`owner_id` is not stamped** on INSERT | `pkg/a.ts:3` |' ,
2301+ '| 2 | **`owner_id` is not stamped** and `revoke()` deletes directly | `pkg/a.ts:3` |'
2302+ ) ;
2303+ const result = rowRefs ( twice ) ;
2304+ return result . problems . some ( ( p ) => p . startsWith ( '[row-ref-key-ambiguous]' ) && p . includes ( 'rows 2, 3' ) ) ;
2305+ } ) ( )
2306+ ) ;
2307+ t (
2308+ 'a context that no longer matches any line refuses rather than dropping the reference' ,
2309+ codes ( rowRefs ( ROW_FIXTURE_PAGE , [ { context : 'a sentence nobody wrote' , seam : 'x' , why : 'y' } ] ) ) . includes (
2310+ '[row-ref-context-stale]'
2311+ )
2312+ ) ;
2313+
2314+ // ── POPULATION: the reference nobody declared ──────────────────────────────
2315+ t (
2316+ '⭐ POPULATION: a `row N` the ledger does not claim is a refusal -- a ledger of what exists ' +
2317+ 'today cannot see the reference someone writes tomorrow' ,
2318+ ( ( ) => {
2319+ const grown = `${ ROW_FIXTURE_PAGE } \n4. And a brand new sentence about row 2.\n` ;
2320+ const result = rowRefs ( grown ) ;
2321+ return result . problems . some ( ( p ) => p . startsWith ( '[row-ref-undeclared]' ) && p . includes ( '`row 2`' ) ) ;
2322+ } ) ( )
2323+ ) ;
2324+ t (
2325+ 'an `unheld` entry is tolerated -- but still LOCATED, so it cannot decay into "not there"' ,
2326+ ( ( ) => {
2327+ const grown = `${ ROW_FIXTURE_PAGE } \n4. And a sentence with no derivable key about row 2.\n` ;
2328+ const declared = [ ...ROW_FIXTURE_REFS , { context : 'no derivable key about' , unheld : 'no key' } ] ;
2329+ const ok = rowRefs ( grown , declared ) . problems . length === 0 ;
2330+ const gone = rowRefs ( ROW_FIXTURE_PAGE , declared ) . problems ;
2331+ return ok && gone . some ( ( p ) => p . startsWith ( '[row-ref-context-stale]' ) ) ;
2332+ } ) ( )
2333+ ) ;
2334+ t (
2335+ 'ZERO numbered rows refuses instead of passing over a table it could not find' ,
2336+ codes ( rowRefs ( ROW_FIXTURE_PAGE . replace ( / ^ \| \d + \| / gm, '| x |' ) ) ) . includes ( '[no-table-rows]' )
2337+ ) ;
2338+
2339+ // ── the ledger's own `why:` strings ────────────────────────────────────────
2340+ t (
2341+ "a `why:` string whose row number disagrees with its seam is falsified, naming the `why`" ,
2342+ ( ( ) => {
2343+ const drifted = ROW_FIXTURE_LEDGER . map ( ( row ) =>
2344+ row . why . startsWith ( 'row 3' ) ? { ...row , why : 'row 2 -- the guard `revoke()` deletes in front of' } : row
2345+ ) ;
2346+ const result = rowRefs ( ROW_FIXTURE_PAGE , ROW_FIXTURE_REFS , drifted ) ;
2347+ return result . problems . some ( ( p ) => p . startsWith ( '[why-row-falsified]' ) && p . includes ( 'is row 3' ) ) ;
2348+ } ) ( )
2349+ ) ;
2350+ t (
2351+ "SPELLING `row-N` (hyphenated, inside a `why:`): held by its own seam like any other" ,
2352+ rowGreen . problems . length === 0 &&
2353+ ROW_FIXTURE_LEDGER . some ( ( row ) => row . why . includes ( 'row-1' ) ) &&
2354+ ( ( ) => {
2355+ const drifted = ROW_FIXTURE_LEDGER . map ( ( row ) =>
2356+ row . why . includes ( 'row-1' ) ? { ...row , why : row . why . replace ( 'row-1' , 'row-3' ) } : row
2357+ ) ;
2358+ return rowRefs ( ROW_FIXTURE_PAGE , ROW_FIXTURE_REFS , drifted ) . problems . some ( ( p ) =>
2359+ p . startsWith ( '[why-row-falsified]' )
2360+ ) ;
2361+ } ) ( )
2362+ ) ;
2363+ t (
2364+ 'a `why:` that names a row and declares NO seam is refused -- an unkeyed number reads as current forever' ,
2365+ ( ( ) => {
2366+ const unkeyed = [ { file : 'pkg/a.ts' , needle : 'n' , why : 'row 3 -- unkeyed' } ] ;
2367+ return codes ( rowRefs ( ROW_FIXTURE_PAGE , ROW_FIXTURE_REFS , unkeyed ) ) . includes ( '[why-row-unkeyed]' ) ;
2368+ } ) ( )
2369+ ) ;
2370+
2371+ // ── ⭐ THE REAL PAGE, and the ablation on a COPY of it ──────────────────────
2372+ let realPage = null ;
2373+ try {
2374+ realPage = readFileSync ( join ( ROOT , PAGE ) , 'utf8' ) ;
2375+ } catch ( err ) {
2376+ t ( '⭐ the real page is readable' , false , err . code ?? err . message ) ;
2377+ }
2378+ if ( realPage !== null ) {
2379+ const realResult = checkRowReferences ( { pageText : realPage } ) ;
2380+ t (
2381+ "⭐ TODAY'S TREE: every row reference on the real page and in the real ledger resolves to its keyed row" ,
2382+ realResult . problems . length === 0 ,
2383+ realResult . problems . join ( ' | ' )
2384+ ) ;
2385+ t (
2386+ 'and the real run really resolved them (10 page references + 9 `why:` mentions, 2 declared unheld)' ,
2387+ realResult . held . page === 10 && realResult . held . why === 9 && realResult . held . unheld === 2 ,
2388+ JSON . stringify ( realResult . held )
2389+ ) ;
2390+
2391+ // ⛔ The ablation is written to a TEMP DIR and read back from disk. The real
2392+ // page is never opened for writing -- the byte assertion below is the proof.
2393+ const before = createHash ( 'sha256' ) . update ( realPage ) . digest ( 'hex' ) ;
2394+ const dir = mkdtempSync ( join ( tmpdir ( ) , 'census-row-ablation-' ) ) ;
2395+ try {
2396+ const mutated = insertRowAbove (
2397+ realPage ,
2398+ 34 ,
2399+ ' **An inserted row, for the ablation** | plugin-sharing | Get: nothing | `sharing-service.ts:1` |'
2400+ ) ;
2401+ const copy = join ( dir , 'system-context.mdx' ) ;
2402+ writeFileSync ( copy , mutated . text ) ;
2403+ const readBack = readFileSync ( copy , 'utf8' ) ;
2404+ t (
2405+ 'ABLATION: the mutated COPY really reached the disk, one row heavier and renumbered' ,
2406+ mutated . inserted &&
2407+ readBack !== realPage &&
2408+ extractTableRows ( readBack ) . length === extractTableRows ( realPage ) . length + 1 ,
2409+ `${ extractTableRows ( realPage ) . length } -> ${ extractTableRows ( readBack ) . length } rows`
2410+ ) ;
2411+ const ablated = checkRowReferences ( { pageText : readBack } ) ;
2412+ const falsifiedRefs = ablated . problems . filter ( ( p ) => p . startsWith ( '[row-ref-falsified]' ) ) ;
2413+ const falsifiedWhy = ablated . problems . filter ( ( p ) => p . startsWith ( '[why-row-falsified]' ) ) ;
2414+ t (
2415+ '⭐ ABLATION: one row inserted above row 34 turns the gate RED, naming the falsified page ' +
2416+ 'references -- this is the exact edit #15687 made under a green gate' ,
2417+ falsifiedRefs . some ( ( p ) => p . includes ( '`Row 34`' ) && p . includes ( 'is row 35' ) ) &&
2418+ falsifiedRefs . some ( ( p ) => p . includes ( '`rows 1–61`' ) ) ,
2419+ ablated . problems . join ( ' | ' )
2420+ ) ;
2421+ t (
2422+ '⭐ ABLATION: and the `why:` strings for rows 34 and 60 -- the other two references #15687 falsified' ,
2423+ falsifiedWhy . some ( ( p ) => p . includes ( '`row 34`' ) && p . includes ( 'is row 35' ) ) &&
2424+ falsifiedWhy . some ( ( p ) => p . includes ( '`row 60`' ) && p . includes ( 'is row 61' ) ) ,
2425+ falsifiedWhy . join ( ' | ' )
2426+ ) ;
2427+ t (
2428+ '⛔ ABLATION SAFETY: the REAL page was never written -- same bytes before and after' ,
2429+ createHash ( 'sha256' ) . update ( readFileSync ( join ( ROOT , PAGE ) , 'utf8' ) ) . digest ( 'hex' ) === before
2430+ ) ;
2431+ } finally {
2432+ rmSync ( dir , { recursive : true , force : true } ) ;
2433+ }
2434+ }
2435+
21132436 battery ( 'WIRING: this gate, and its self-test, really run in CI' ) ;
21142437 const SELF = 'scripts/check-system-context-census.mjs' ;
21152438 let lintYml = null ;
0 commit comments