Skip to content

Commit 32e57e5

Browse files
claude[bot]claude
andauthored
tooling(pm): derive the line ratchet's lane roster from the lanes/ directory (#16437)
The self-test's lane-roster case was an `.every` over eight literal lane names, so the ninth lane file was pinned by nothing: a ceiling row put there for it kept the case green without the case ever naming it, and deleting that row again was caught only by the map-wide cases, which say nothing about a row that is simply gone. The roster is now read from the directory (`readdirSync`, every `*.md`, sorted) and held against the ceiling map, so the tree and the map are two independent sources checked against one another. A new lane file is pinned by construction: no list to extend, and no count in the label to keep in step with the list. A derived roster has one failure mode of its own, and it is the same shape as the defect above -- `[].every(...)` is `true`, so an unreadable directory would report perfect coverage of nothing. The verdict refuses an empty roster outright, and each red path carries a fixture case beside the live one, because a red path that stopped working runs green forever. Moves named: the pinned case label and its expectation-table entry move together (duplicate-label refusal); one case becomes three, so the self-test runs 155 -> 157 cases and SELF_TEST_BATTERY_FLOOR moves 155 -> 157, with the two prose readings of that same number moved with it. No ceiling row changed; no other file. Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8047911 commit 32e57e5

1 file changed

Lines changed: 75 additions & 6 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@
253253
* choose between preserving a defect and hiding it.
254254
*/
255255

256-
import { readFileSync } from 'node:fs';
256+
import { readdirSync, readFileSync } from 'node:fs';
257257
import process from 'node:process';
258258
import { isEntrypoint } from '../invoked-as.mjs';
259259

@@ -1716,14 +1716,14 @@ function run() {
17161716
// case is attributed to the row actually being run. There is no `battery()`
17171717
// opener: for a table-driven self-test the ROW is the battery.
17181718
//
1719-
// ⭐ ALL 155 rows are floored, the four `...(() => { ... })()` spreads included.
1719+
// ⭐ ALL 157 rows are floored, the four `...(() => { ... })()` spreads included.
17201720
// Those spreads were flagged in the batch-8 census as an IIFE-produced block
17211721
// whose rows could not take a literal roster key. Measured here, that premise
17221722
// does not hold for this file: each IIFE is a SCOPING device that declares
17231723
// local fixture consts and then `return [...]`s an array of LITERAL
17241724
// `[label, actual, expected]` rows. No row label is a template string, none is
17251725
// computed, and no row is produced by a `map`/`push`/loop. Three independent
1726-
// readings agree on 155 -- the source labels extracted by indentation, the
1726+
// readings agree on 157 -- the source labels extracted by indentation, the
17271727
// literal row starts, and the `cases.length` the green line prints on a run --
17281728
// so nothing here is the `extra`-call residue of PR #15286, and leaving any row
17291729
// outside the roster would have been the lossy reading.
@@ -1748,7 +1748,9 @@ const SELF_TEST_BATTERIES = Object.freeze({
17481748
'SKILL.md is covered': 1,
17491749
'the dev-agent definition is covered': 1,
17501750
'all five compressed references are covered': 1,
1751-
'all eight lane/seat job descriptions are covered': 1,
1751+
'every lane/seat job description in the tree carries a ceiling row': 1,
1752+
'...and one carrying no row is RED, with the message naming that file': 1,
1753+
'...and an EMPTY roster is RED, not vacuously green: a derived roster checks nothing when the read fails': 1,
17521754
'the other four skills are covered (#9473)': 1,
17531755
'root AGENTS.md is covered (#9792)': 1,
17541756
'root CLAUDE.md is covered (#9965)': 1,
@@ -1900,16 +1902,81 @@ const SELF_TEST_BATTERIES = Object.freeze({
19001902
// the literal above, so the roster falls below this number; the table
19011903
// cross-check in the floor block is the other half, and names WHICH label
19021904
// collided.
1903-
const SELF_TEST_BATTERY_FLOOR = 155;
1905+
const SELF_TEST_BATTERY_FLOOR = 157;
19041906

19051907
// Returned by `selfTest()` only after its verdict is printed. The dispatch
19061908
// refuses anything else: a `return` that leaves the function above that line
19071909
// prints nothing and still exits 0 — a self-test that never finished, reported
19081910
// as one that passed (#13798).
19091911
const SELF_TEST_VERDICT = 'check-skill-line-ratchet self-test reached its verdict';
19101912

1913+
// -- The lane roster, DERIVED from the tree (#15965) -------------------------
1914+
//
1915+
// The case this replaced was an `.every` over eight literal lane names, so the
1916+
// ninth lane file was pinned by nothing: a ceiling row put there for it kept
1917+
// that case green without the case ever naming it, and deleting that row again
1918+
// was caught only by the map-wide cases, which say nothing about a row that is
1919+
// simply GONE. The roster is now read from the DIRECTORY, so the tree and the
1920+
// map are two independent sources checked against one another: a new lane file
1921+
// is pinned by construction, there is no list to keep in step, and no count in
1922+
// the label to keep in step with the list. (This is NOT the derivation the
1923+
// battery-floor note above refuses. That one would read the roster from the
1924+
// very table it checks, so a deleted row would delete its own floor; this one
1925+
// reads a DIFFERENT source from the one it checks, which is the whole repair.)
1926+
//
1927+
// Deriving a roster introduces one failure mode of its own, and it is the same
1928+
// shape as the defect above: `[].every(...)` is `true`, so a lanes/ directory
1929+
// that cannot be read would report perfect coverage of nothing. The verdict
1930+
// therefore refuses an EMPTY roster outright -- #4690's cannot-read rule, on a
1931+
// directory rather than a file. Each red path carries a fixture case beside the
1932+
// live one, because enforcement holds neither: a red path that stopped working
1933+
// runs green forever, which is the same reason the case exists at all.
1934+
const LANES_DIR = '.claude/skills/pm-dispatch/references/lanes/';
1935+
1936+
/**
1937+
* The lane/seat job descriptions present in the tree, sorted. An unreadable
1938+
* directory yields `[]`, which the verdict below reads as RED.
1939+
*
1940+
* @returns {string[]}
1941+
*/
1942+
export function laneFilesOnDisk() {
1943+
try {
1944+
return readdirSync(new URL(LANES_DIR, REPO_ROOT)).filter((f) => f.endsWith('.md')).sort();
1945+
} catch {
1946+
return [];
1947+
}
1948+
}
1949+
1950+
/**
1951+
* Every lane file in the tree carries a ceiling row keyed by its repo-relative
1952+
* path -- the key spelling `CEILINGS` itself uses, forward slashes and all.
1953+
*
1954+
* @param {string[]} laneFiles lane file names, as read from the tree
1955+
* @param {Map<string, number>} ceilings the ceiling map to hold them against
1956+
* @returns {{ok: boolean, msg: string}}
1957+
*/
1958+
export function laneRosterVerdict(laneFiles, ceilings) {
1959+
if (laneFiles.length === 0) {
1960+
return {
1961+
ok: false,
1962+
msg: `${LANES_DIR} yielded no *.md lane job description — an empty roster checks nothing (every() over nothing is true), so it is red, not a skip.`,
1963+
};
1964+
}
1965+
const uncovered = laneFiles.filter((f) => !ceilings.has(`${LANES_DIR}${f}`));
1966+
if (uncovered.length > 0) {
1967+
return {
1968+
ok: false,
1969+
msg: `${uncovered.map((f) => `${LANES_DIR}${f}`).join(', ')} — lane job description(s) in the tree carrying no ceiling row, so the ratchet does not read them at all. Give each one a row keyed by that path, or take the lane file out of the tree.`,
1970+
};
1971+
}
1972+
return { ok: true, msg: `all ${laneFiles.length} lane/seat job descriptions in the tree carry a ceiling row.` };
1973+
}
1974+
19111975
function selfTest() {
19121976
const rel = '.claude/skills/pm-dispatch/SKILL.md';
1977+
// Fixture for the derived roster's naming red path: a tree that carries
1978+
// triage.md, against a map holding a row for engine.md only.
1979+
const laneGap = laneRosterVerdict(['engine.md', 'triage.md'], new Map([[`${LANES_DIR}engine.md`, 32]]));
19131980
const cases = [
19141981
['under the ceiling -> green', verdict(rel, 2900, 3050).ok, true],
19151982
['at the ceiling -> green', verdict(rel, 3050, 3050).ok, true],
@@ -1922,7 +1989,9 @@ function selfTest() {
19221989
['SKILL.md is covered', CEILINGS.has('.claude/skills/pm-dispatch/SKILL.md'), true],
19231990
['the dev-agent definition is covered', CEILINGS.has('.claude/agents/os-dev.md'), true],
19241991
['all five compressed references are covered', ['dispatch-runbook', 'platform-readings', 'review-checklist', 'landing-operations', 'seat-post-protocol'].every((n) => CEILINGS.has(`.claude/skills/pm-dispatch/references/${n}.md`)), true],
1925-
['all eight lane/seat job descriptions are covered', ['engine', 'services', 'cli', 'devx', 'skills', 'spec', 'hotcrm', 'director'].every((n) => CEILINGS.has(`.claude/skills/pm-dispatch/references/lanes/${n}.md`)), true],
1992+
['every lane/seat job description in the tree carries a ceiling row', laneRosterVerdict(laneFilesOnDisk(), CEILINGS).ok, true],
1993+
['...and one carrying no row is RED, with the message naming that file', !laneGap.ok && laneGap.msg.includes('triage.md'), true],
1994+
['...and an EMPTY roster is RED, not vacuously green: a derived roster checks nothing when the read fails', laneRosterVerdict([], CEILINGS).ok, false],
19261995
['the other four skills are covered (#9473)', ['checklist-test', 'checklist-author', 'dogfood-verification', 'spec-property-retirement'].every((n) => CEILINGS.has(`.claude/skills/${n}/SKILL.md`)), true],
19271996
['root AGENTS.md is covered (#9792)', CEILINGS.has('AGENTS.md'), true],
19281997
['root CLAUDE.md is covered (#9965)', CEILINGS.has('CLAUDE.md'), true],

0 commit comments

Comments
 (0)