Skip to content

Commit 9965c88

Browse files
committed
test(scripts): floor check-skill-line-ratchet's self-test on all 155 table rows
Recipe A per the pilot PR #15271: every row label is a declared battery with a floor of 1, `registerCase(name)` is the first statement of the driving loop, the roster is a LITERAL cross-checked against the table, size pinned at 155. The only deleted line is the red-run verdict, reworded to the recipe's `N failure(s) (cases and floor)`; the green line stays byte-identical. The batch-8 census flagged this table's four `...(() => { ... })()` spreads as an IIFE-produced block whose rows could not take a literal roster key. Measured, that premise does not hold here: each IIFE is a SCOPING device that declares local fixture consts and then returns an array of LITERAL [label, actual, expected] rows. No label is a template string or computed, and no row comes from a map/push/loop. Three independent readings agree on 155 -- labels extracted from source, literal row starts, and the `cases.length` the green line prints on a run -- so ALL 155 rows are floored with no residue, rather than the partition the row anticipated. Part of #13799 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
1 parent 6f2fc4e commit 9965c88

1 file changed

Lines changed: 267 additions & 1 deletion

File tree

‎scripts/pm/check-skill-line-ratchet.mjs‎

Lines changed: 267 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,209 @@ function run() {
14881488
if (failed) process.exit(1);
14891489
}
14901490

1491+
// -- The self-test's own battery roster and floor (#13489) ------------------
1492+
//
1493+
// `--self-test` reaching its verdict used to be this self-test's ONLY success
1494+
// condition, so "every case held" and "the cases never ran" printed the same
1495+
// line. Closed the PR #13487 way: what is pinned is the registered NAMES, not a
1496+
// number.
1497+
//
1498+
// This self-test is TABLE-DRIVEN -- one `cases` table, one loop over it, and a
1499+
// sink (`failed++`) that writes only when a case FAILS. Routing THAT sink
1500+
// through `registerCase()` would register a case only when it fails: a fully
1501+
// green run would register 0 and every battery would read DID NOT RUN, the
1502+
// floor inverted rather than installed. So the roster is the table's own rows.
1503+
// Each row LABEL is a declared battery, verbatim, with a floor of 1, and
1504+
// `registerCase(name)` is the first statement of the driving loop body -- so the
1505+
// case is attributed to the row actually being run. There is no `battery()`
1506+
// opener: for a table-driven self-test the ROW is the battery.
1507+
//
1508+
// ⭐ ALL 155 rows are floored, the four `...(() => { ... })()` spreads included.
1509+
// Those spreads were flagged in the batch-8 census as an IIFE-produced block
1510+
// whose rows could not take a literal roster key. Measured here, that premise
1511+
// does not hold for this file: each IIFE is a SCOPING device that declares
1512+
// local fixture consts and then `return [...]`s an array of LITERAL
1513+
// `[label, actual, expected]` rows. No row label is a template string, none is
1514+
// computed, and no row is produced by a `map`/`push`/loop. Three independent
1515+
// readings agree on 155 -- the source labels extracted by indentation, the
1516+
// literal row starts, and the `cases.length` the green line prints on a run --
1517+
// so nothing here is the `extra`-call residue of PR #15286, and leaving any row
1518+
// outside the roster would have been the lossy reading.
1519+
//
1520+
// A pinned TOTAL is not the repair, and neither is a roster DERIVED from the
1521+
// table: `cases.length` moves with the table, so a deleted row would delete its
1522+
// own floor. The roster below is a LITERAL the table is checked against, which
1523+
// is what lets a deleted or renamed row name ITSELF in the refusal.
1524+
//
1525+
// The counts are a FLOOR, not an equality -- a row that grows into several
1526+
// registrations must not red. 1 is the honest floor for a table row: the loop
1527+
// reaches it exactly once per run.
1528+
const SELF_TEST_BATTERIES = Object.freeze({
1529+
'under the ceiling -> green': 1,
1530+
'at the ceiling -> green': 1,
1531+
'over the ceiling -> red': 1,
1532+
'red message names the file': 1,
1533+
'red message names the remedy': 1,
1534+
'red message names the authoring rule': 1,
1535+
'empty read -> red, not a skip': 1,
1536+
'every covered file has a positive ceiling': 1,
1537+
'SKILL.md is covered': 1,
1538+
'the dev-agent definition is covered': 1,
1539+
'all five compressed references are covered': 1,
1540+
'all eight lane/seat job descriptions are covered': 1,
1541+
'the other four skills are covered (#9473)': 1,
1542+
'root AGENTS.md is covered (#9792)': 1,
1543+
'root CLAUDE.md is covered (#9965)': 1,
1544+
'references/compile-surfaces.md is covered (#12098)': 1,
1545+
'every separator-less ceiling declares a root-file watch hint': 1,
1546+
'and the declaration names no file the map does not cover': 1,
1547+
'both root instruction files are declared': 1,
1548+
'the declared form is NOT a CEILINGS key': 1,
1549+
'the published skills/ catalog is deliberately uncovered': 1,
1550+
'budget is 120 bytes': 1,
1551+
'a short ASCII line -> green': 1,
1552+
'a long ASCII line -> RED': 1,
1553+
'a short CJK line -> green': 1,
1554+
'a long CJK line -> RED': 1,
1555+
'the RED message names the budget': 1,
1556+
'the RED message names the line number': 1,
1557+
'the RED message offers NO allowlist to add a line to': 1,
1558+
'no offenders -> green verdict': 1,
1559+
'a long line inside a fence is exempt': 1,
1560+
'...and the SAME line outside one is RED': 1,
1561+
'scan: a fenced long line yields no offender': 1,
1562+
'scan: the fence closes again': 1,
1563+
'a long table row is exempt': 1,
1564+
'...and the same cells as prose are RED': 1,
1565+
'a long heading is exempt': 1,
1566+
'...and the same text as a paragraph is RED': 1,
1567+
'front matter is exempt': 1,
1568+
'scan: front matter closes at the second ---': 1,
1569+
'an anchored Blocked-by: line is exempt': 1,
1570+
'Restart-when: too': 1,
1571+
'Restart-touch: too': 1,
1572+
'but a MID-PROSE mention is not exempt — the escape hatch is line-anchored only': 1,
1573+
'a long blockquote line is exempt': 1,
1574+
'...including one indented inside a list': 1,
1575+
'...and the same quotation unquoted is RED': 1,
1576+
'a bare over-long URL is exempt': 1,
1577+
'a single over-long code span is exempt': 1,
1578+
'...but prose LEADING to that URL is RED (wrap first, URL lands alone)': 1,
1579+
'wrapLine splits a long CJK line': 1,
1580+
'every wrapped CJK segment is within budget': 1,
1581+
'wrapping a CJK line changes NOTHING but whitespace': 1,
1582+
'wrapping an ASCII line changes NOTHING but whitespace': 1,
1583+
'a list continuation is indented under the marker': 1,
1584+
'no continuation line opens a new markdown block': 1,
1585+
'wrapLine is idempotent — its output is the canonical form': 1,
1586+
'wrapLine leaves a short line untouched': 1,
1587+
'wrapLine never breaks inside a code span': 1,
1588+
'wrapLine never breaks inside a 「…」 ruling quote': 1,
1589+
'...nor inside a 『…』 one': 1,
1590+
'a quote longer than the budget makes its line unbreakable, not RED': 1,
1591+
'a line OPENING an unterminated 「 is exempt': 1,
1592+
'a line INSIDE an open quote is exempt': 1,
1593+
'...and the same line outside one is RED — the exemption is the quote, not the text': 1,
1594+
'a SHORT line inside a quote is simply green, not counted exempt': 1,
1595+
'advanceState opens on an unmatched 「': 1,
1596+
'advanceState closes on the matching 」': 1,
1597+
'a quote opened and closed on ONE line does not open the state': 1,
1598+
'advanceState does not track quotes inside a fence': 1,
1599+
'scan: a two-line quote yields quotation exemptions, not offenders': 1,
1600+
'wrapLine never strands a closing 。 at a line head': 1,
1601+
'an ASCII , after a Han character is a no-break-after mark': 1,
1602+
'...and ; and : are the same mark class': 1,
1603+
'...but after a LATIN word it stays an ordinary break point': 1,
1604+
'...and a mark with a space before it follows nothing': 1,
1605+
'...and an atom not ending in one is never the mark': 1,
1606+
'breakLegal refuses the break after a Han+ASCII mark': 1,
1607+
'...a mark with no following space was never a break point to begin with': 1,
1608+
'...still allows the CJK-to-CJK break one atom earlier': 1,
1609+
'...and still allows an ordinary ASCII space break': 1,
1610+
'wrapLine splits the trap line': 1,
1611+
'no wrapped line ends on a Han+ASCII mark — the break is no longer OFFERED': 1,
1612+
'...the line it took instead is still within budget': 1,
1613+
'...and it moved only whitespace, as every wrap must': 1,
1614+
'the SAME shape with a Latin word before the mark still breaks there': 1,
1615+
'wrapLine is still idempotent under the new rule': 1,
1616+
'a long table row is EXEMPT from the 120-byte line rule': 1,
1617+
'...and the same row is METERED by its file pin': 1,
1618+
'at exactly the pin -> green': 1,
1619+
'one byte wider -> RED (widening a cell is the measured defect)': 1,
1620+
'narrower than the pin -> green': 1,
1621+
'a pin of 0 is a measurement — a file with no table row passes it': 1,
1622+
'...and the FIRST table row in such a file is RED': 1,
1623+
'a missing pin is RED, not a skip (#4690)': 1,
1624+
'...and says so rather than naming a width': 1,
1625+
'the RED message names the width': 1,
1626+
'the RED message names the line': 1,
1627+
'the RED message names the remedy': 1,
1628+
'the RED message names consolidation as the way to pay it down': 1,
1629+
'the RED message offers NO allowlist': 1,
1630+
'the RED message says raising needs a maintainer ruling': 1,
1631+
'scanTableRows finds the widest row': 1,
1632+
'...and reports its line number': 1,
1633+
'a `|` line inside a FENCE is not a table row': 1,
1634+
'a `|` line in FRONT MATTER is not a table row': 1,
1635+
'a file with no table row measures 0': 1,
1636+
'every ceilinged file carries a pin': 1,
1637+
'and the pin map names no file the ceiling map does not cover': 1,
1638+
'every pin is a non-negative integer': 1,
1639+
'the published skills/ catalog is uncovered here too': 1,
1640+
'the citation is the ruling this file was given': 1,
1641+
'cross-file move — a raise covered by its sources\' net decrease PASSES': 1,
1642+
'...and the green verdict states the arithmetic it read': 1,
1643+
'cross-file move — a raise whose sources did NOT shrink is RED (the whole defect: a `move` that licenses an ordinary raise would run green forever)': 1,
1644+
'...and the RED verdict names the raise and the net decrease it fell short of': 1,
1645+
'...and sends the author to the ordinary path rather than to a bigger declaration': 1,
1646+
'cross-file move — a raise EXCEEDING the net decrease is RED, even by one line': 1,
1647+
'...while a raise exactly equal to it is legal': 1,
1648+
'cross-file move — a declaration citing no ruling is RED, however sound its arithmetic': 1,
1649+
'...and the RED verdict spells the citation it wanted': 1,
1650+
'cross-file move — declarations whose net movement is 0 leave the map total unchanged': 1,
1651+
'cross-file move — a declaration whose participants net POSITIVE fails the total (+2 here)': 1,
1652+
'...and the RED total names the lines it grew by': 1,
1653+
'...and a net-negative move reports the corpus shrinking': 1,
1654+
'cross-file move — one source may not pay for two destinations': 1,
1655+
'cross-file move — a destination lowered back to or below its pre-move ceiling reads as PAID DOWN, never as red (lowering is always legitimate)': 1,
1656+
'...and says so rather than reporting an arithmetic it can no longer measure': 1,
1657+
'cross-file move — a SOURCE grown back past what it paid re-opens the move (the loophole: the destination keeps the lines while the payers grow back)': 1,
1658+
'cross-file move — an unknown destination is RED, not a skip (#4690)': 1,
1659+
'cross-file move — an unknown source is RED, not a skip (#4690)': 1,
1660+
'cross-file move — a file may not pay its own raise': 1,
1661+
'cross-file move — a declaration naming no source at all is RED': 1,
1662+
'every live declaration cites the ruling': 1,
1663+
'every live declaration names its destination\'s pre-move ceiling and at least one source': 1,
1664+
'every live participant is a file this map covers': 1,
1665+
'ruled raise — a destination that later took an ordinary ruled raise PASSES with `was` at its literal pre-move value': 1,
1666+
'...and the verdict prices the MOVE, not the ruling: +10 against the sources\' net 11': 1,
1667+
'...and names the ruled lines it took out, so the arithmetic can be read back': 1,
1668+
'...while the SAME tree with the raise unrecorded is the double red this record ends': 1,
1669+
'...whose first half reads the ruling as the move\'s: +44 against 11': 1,
1670+
'...and which now sends the author to the record rather than to `was`': 1,
1671+
'ruled raise — the map-wide total subtracts it too, and reads the corpus DOWN 1': 1,
1672+
'...where the unrecorded twin reports the corpus growing by 33': 1,
1673+
'ruled raise — a record quoting NO ruling is RED (the licence is the maintainer\'s or it does not exist)': 1,
1674+
'...and the RED verdict says what it wanted': 1,
1675+
'...while an ENGLISH ruling quoted the way this map already quotes one passes': 1,
1676+
'ruled raise — a record with no line count is RED': 1,
1677+
'ruled raise — a NEGATIVE line count is RED: lowering is always legitimate and is never recorded here': 1,
1678+
'ruled raise — a record with no ruling DATE is RED': 1,
1679+
'ruled raise — records claiming MORE lines than the ceiling stands above `was` are RED': 1,
1680+
'...which is the loophole that closes: an inflated record would otherwise read the move as paid down and pass forever': 1,
1681+
'ruled raise — a destination whose whole rise is ruled keeps none of it for the move, and reads as paid down': 1,
1682+
'ruled raise — a declaration carrying no record behaves exactly as before (every case above this group is one)': 1,
1683+
'every live ruled-raise record quotes its ruling, dates it, and names a positive line count': 1,
1684+
});
1685+
1686+
// DELETING an entry silences that battery's floor exactly as effectively as
1687+
// zeroing it, so the roster's own size is pinned too. This pin is also half of
1688+
// the duplicate-label refusal: two rows sharing a label collapse to ONE key in
1689+
// the literal above, so the roster falls below this number; the table
1690+
// cross-check in the floor block is the other half, and names WHICH label
1691+
// collided.
1692+
const SELF_TEST_BATTERY_FLOOR = 155;
1693+
14911694
// Returned by `selfTest()` only after its verdict is printed. The dispatch
14921695
// refuses anything else: a `return` that leaves the function above that line
14931696
// prints nothing and still exits 0 — a self-test that never finished, reported
@@ -1786,14 +1989,77 @@ function selfTest() {
17861989
];
17871990
})(),
17881991
].map((c) => (Array.isArray(c[1]) || (c[1] && typeof c[1] === 'object') ? [c[0], JSON.stringify(c[1]), JSON.stringify(c[2])] : c));
1992+
// The ledger this self-test's floor is evaluated against (#13489).
1993+
const batterySeen = new Map();
1994+
const registerCase = (name) => {
1995+
batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1);
1996+
};
1997+
17891998
let failed = 0;
17901999
for (const [name, actual, expected] of cases) {
2000+
registerCase(name);
17912001
const ok = actual === expected;
17922002
if (!ok) failed++;
17932003
console.log(` ${ok ? '✓' : '✗'} ${name}`);
17942004
}
2005+
// -- The floor: every declared row RAN, and ran its case (#13489) --------
2006+
//
2007+
// Evaluated after every row has had its chance and BEFORE the verdict, so the
2008+
// success line below can only be printed by a run in which the set of rows
2009+
// that registered EQUALS the set declared. A set difference names WHICH row
2010+
// stopped; a count says only that something did.
2011+
const floorFailure = (message) => {
2012+
console.error(`✗ self-test floor: ${message}`);
2013+
failed++;
2014+
};
2015+
const declaredBatteries = Object.keys(SELF_TEST_BATTERIES);
2016+
let floorBreached = false;
2017+
if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) {
2018+
floorBreached = true;
2019+
floorFailure(
2020+
`SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned `
2021+
+ `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`,
2022+
);
2023+
}
2024+
const rowLabels = cases.map(([name]) => name);
2025+
const duplicated = [...new Set(rowLabels.filter((name, i) => rowLabels.indexOf(name) !== i))];
2026+
if (duplicated.length > 0) {
2027+
floorBreached = true;
2028+
floorFailure(
2029+
`the cases table uses ${duplicated.map((n) => JSON.stringify(n)).join(', ')} as a row label more than once — `
2030+
+ 'two rows sharing a label are ONE battery, so the second can stop running while the first keeps the floor met.',
2031+
);
2032+
}
2033+
for (const [name, count] of batterySeen) {
2034+
if (declaredBatteries.includes(name)) continue;
2035+
floorBreached = true;
2036+
floorFailure(
2037+
`self-test battery "${name}" registered ${count} case(s) but is not declared in `
2038+
+ 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.',
2039+
);
2040+
}
2041+
for (const name of declaredBatteries) {
2042+
const count = batterySeen.get(name) ?? 0;
2043+
if (count >= SELF_TEST_BATTERIES[name]) continue;
2044+
floorBreached = true;
2045+
floorFailure(
2046+
count === 0
2047+
? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. `
2048+
+ 'The verdict below would have claimed that case holds.'
2049+
: `self-test battery "${name}" registered ${count} case(s), below its pinned floor of `
2050+
+ `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`,
2051+
);
2052+
}
2053+
if (floorBreached) {
2054+
floorFailure(
2055+
'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the '
2056+
+ 'number. Find what stopped registering (a deleted row, a renamed label, a loop that no longer '
2057+
+ 'reaches it) and restore it.',
2058+
);
2059+
}
2060+
17952061
if (failed) {
1796-
console.error(`✗ check-skill-line-ratchet self-test: ${failed} of ${cases.length} case(s) failed.`);
2062+
console.error(`✗ check-skill-line-ratchet self-test: ${failed} failure(s) (cases and floor).`);
17972063
process.exit(1);
17982064
}
17992065
console.log(`✓ check-skill-line-ratchet self-test: ${cases.length} cases pass.`);

0 commit comments

Comments
 (0)