Skip to content

Commit 5144b50

Browse files
committed
feat(pm): bound the escapable-literal species with a shrink-only ledger
A gate whose declared population is a bare top-level word the tree HAS is unnameable by any dispatch derivation and lands already invisible: hintCovers refuses a separator-less literal as too generic, a refusal measured at +139084 fabricated pairs that stays. The escape is the ROOT_DIR_WATCH_HINTS subtree declaration, but nothing tracked who still needed to take it -- six instances were found one at a time on six unrelated cards, the sixth a re-discovery of the fourth. Enumerate the species and refuse new members, mirroring KNOWN_IMPORT_UNSAFE: shrink-only, fresh and stale both fail, and the one remedy is named in the failure text. check:pm-dispatch-gates runs the self-test on every PR, so a gate written tomorrow fails at authoring time instead of landing silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
1 parent a7ea328 commit 5144b50

1 file changed

Lines changed: 244 additions & 1 deletion

File tree

scripts/pm/dispatch-gates.mjs

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,127 @@ export function unreachableReason(dead, cap = 3) {
13771377
return dead.length > cap ? `${shown} · …` : shown;
13781378
}
13791379

1380+
// ---------------------------------------------------------------------------
1381+
// The escapable-literal ledger (#10705)
1382+
// ---------------------------------------------------------------------------
1383+
1384+
/**
1385+
* One family's ESCAPABLE literals: the bare separator-less population literals
1386+
* it declares that the tree really HAS, and for which it has NOT declared the
1387+
* subtree spelling.
1388+
*
1389+
* ## The species, and why it is worth enumerating rather than re-finding
1390+
*
1391+
* `hintCovers` refuses a bare single-segment literal as too generic, and that
1392+
* refusal is measured (+139084 fabricated pairs — see its docblock) and stays.
1393+
* The consequence is a gate whose declared population is a bare top-level word
1394+
* the tree DOES have: nothing is wrong with the gate or the tree, but no
1395+
* dispatch derivation can name it, so it scores the same quiet verdict for
1396+
* every card in the tree and, as `check-plugin-teardown-shape.mjs` puts it,
1397+
* "lands already invisible".
1398+
*
1399+
* The escape exists and is a named, copyable idiom — `ROOT_DIR_WATCH_HINTS`,
1400+
* carried by `check-role-word.mjs` (`['skills/**']`) and by
1401+
* `check-examples-live-imports.mjs` (`['examples/**']`), each pinned in its own
1402+
* gate's self-test. What was missing is any record of WHO still needs to take
1403+
* it. Six instances were found one at a time, on six unrelated cards, by
1404+
* someone happening to read the residue block on the way past; the sixth was a
1405+
* re-discovery of the fourth, filed fresh by an agent who did not know the
1406+
* enumeration existed. Discovery-by-coincidence is the failure this ledger
1407+
* closes.
1408+
*
1409+
* ## What counts as ESCAPED, and why the test is the collapsed form
1410+
*
1411+
* A sibling hint escapes the literal only when it declares the SAME population
1412+
* as a subtree — `collapseHint(g) === plain`, with a separator in `g`. A hint
1413+
* that merely reaches INTO the root does not count, and the distinction is
1414+
* live rather than theoretical: `check:published-files` names
1415+
* `scripts/check-published-files.mjs`, which `hintCovers` accepts against the
1416+
* bare directory `scripts` through its reverse-containment branch while
1417+
* covering no other file under that root. Treating that as an escape would
1418+
* retire a ledger row for a gate that is still unnameable for every card under
1419+
* the root it appears to declare.
1420+
*
1421+
* ## What this does NOT see
1422+
*
1423+
* Only literals that reach the HINT SET, which requires a separator somewhere
1424+
* in the source spelling (`'scripts/'` trims to `scripts`). A gate that spells
1425+
* its root with no separator at all (`const POPULATION = 'packages'`) builds no
1426+
* hint, so it is invisible to the derivation AND to this ledger — the same
1427+
* shape #10107 recorded for the directory half. That remainder is bounded only
1428+
* by each gate's own `ROOT_DIR_WATCH_HINTS` declaration and self-test, because
1429+
* only the gate knows its real population; this tool cannot read intent out of
1430+
* a bare word, and a sweep that tried was measured at 73 (family, word) pairs
1431+
* across 52 of 128 families — overwhelmingly `join(ROOT, 'packages', …)` path
1432+
* components, which is the +139084 fabrication re-introduced one level up.
1433+
*/
1434+
export function escapableLiteralRows(entries, prefixes) {
1435+
const rows = [];
1436+
for (const [check, entry] of entries) {
1437+
const hints = [...new Set(entry.hints ?? [])];
1438+
for (const hint of hints) {
1439+
const plain = collapseHint(hint);
1440+
// Exactly `hintCovers`' refusal, read off the same two conditions rather
1441+
// than a paraphrase of them: a literal it does NOT refuse is nameable and
1442+
// is no part of this species.
1443+
if (plain.length < 2) continue;
1444+
if (hint.includes('/') || plain.startsWith('.')) continue;
1445+
// …and the tree HAS the whole literal. A bare word the tree does not have
1446+
// (`node_modules`, `@objectstack`) is the genuinely-dead species instead,
1447+
// which no declaration can fix and which `unreachableReason` already
1448+
// separates by exactly this test.
1449+
if (deepestTrackedPrefix(hint, prefixes) !== plain) continue;
1450+
if (hints.some((g) => g !== hint && g.includes('/') && collapseHint(g) === plain)) continue;
1451+
rows.push({ check, hint, plain });
1452+
}
1453+
}
1454+
return rows;
1455+
}
1456+
1457+
/** The ledger key for one row — see the ledger's docblock for the spelling rule. */
1458+
export function escapableLiteralKey({ check, hint }) {
1459+
return `${check} ${hint}`;
1460+
}
1461+
1462+
/**
1463+
* ⛔ SHRINK-ONLY. The gates whose declared population is a bare top-level word
1464+
* the tree HAS, and which have not declared the subtree spelling for it.
1465+
*
1466+
* It is a DEBT list, not an exception list, and the same property makes it safe
1467+
* that makes `KNOWN_IMPORT_UNSAFE` safe (#10665): every entry has one remedy —
1468+
* declare the subtree spelling beside the literal, the `ROOT_DIR_WATCH_HINTS`
1469+
* idiom — and no entry records a judgement anyone has to re-make later. There
1470+
* is no supported route in the other direction: a family this rule newly
1471+
* reaches is a FAILURE with that one remedy, never a new line in here. An entry
1472+
* whose gate has since taken the escape fails as STALE and names itself, which
1473+
* is what stops the list from rotting into an allowlist nobody re-reads.
1474+
*
1475+
* Both halves are asserted in this file's self-test, against the live tree,
1476+
* and `check:pm-dispatch-gates` runs that self-test on every pull request. So a
1477+
* gate written tomorrow that spells a bare root word fails at AUTHORING time
1478+
* rather than landing invisible — which is the half of this class the six
1479+
* historical instances could not fix, because each of them was archaeology.
1480+
*
1481+
* ⚠️ Spelling rule for a new row: it must not become a watch hint of THIS file.
1482+
* `extractWatchHints` reads any quoted span carrying a separator, so a family
1483+
* keyed by a direct script path (`node scripts/check-x.mjs`) would enter this
1484+
* file's own declared population as a path it does not read — the same trap
1485+
* `DEFAULT_BASE_REF` is assembled in two halves to avoid. Spell such a row so
1486+
* it carries no separator, or join it at runtime. A self-test case below holds
1487+
* this, so the rule fails rather than needing to be remembered.
1488+
*/
1489+
const ESCAPABLE_LITERAL_LEDGER = new Set([
1490+
// Its ONLY hint, so this family is unnameable for every card in the tree —
1491+
// including a card about its own blind spot, which is how it was found.
1492+
'check:parse-guard scripts',
1493+
// Reaches `scripts` through a package-relative predicate over tarball
1494+
// contents (`rel.startsWith('scripts/')`), not through the repo root it
1495+
// appears to name. Its remedy is therefore the other one the idiom allows:
1496+
// stop spelling a bare root, rather than declare a subtree the gate does not
1497+
// read. Recorded here so that decision is made once, on its own card.
1498+
'check:published-files scripts',
1499+
]);
1500+
13801501
// ---------------------------------------------------------------------------
13811502
// Change-kind derivation — the gates a path match can never reach
13821503
// ---------------------------------------------------------------------------
@@ -2529,7 +2650,20 @@ export function tierLines(result) {
25292650
// Live derivation
25302651
// ---------------------------------------------------------------------------
25312652

2532-
function derive(paths, { showResidue = false } = {}) {
2653+
/**
2654+
* Discover every check family in the tree: the workflows that invoke it, the
2655+
* `paths:` triggers that schedule it, the script files it resolves to, and the
2656+
* watch hints those files declare.
2657+
*
2658+
* Lifted out of `derive` so the escapable-literal ledger can ask the SAME
2659+
* question the derivation asks, from the same implementation. The ledger's
2660+
* whole claim is about what the derivation can and cannot name, so a second
2661+
* discovery pass built beside this one would let the two disagree — and a
2662+
* ledger enumerating a population no dispatch prompt is actually derived from
2663+
* is the drift this file's header refuses everywhere else. The self-test is
2664+
* the only other caller, and it calls THIS.
2665+
*/
2666+
export function discoverFamilies() {
25332667
const wfDir = join(ROOT, '.github/workflows');
25342668
const workflows = readdirSync(wfDir).filter((f) => /\.ya?ml$/.test(f));
25352669
if (workflows.length === 0) throw new Error('no workflow files found under .github/workflows');
@@ -2582,6 +2716,11 @@ function derive(paths, { showResidue = false } = {}) {
25822716
if (existsSync(abs)) entry.hints.push(...extractWatchHints(readFileSync(abs, 'utf8')));
25832717
}
25842718
}
2719+
return { byCheck, workflows };
2720+
}
2721+
2722+
function derive(paths, { showResidue = false } = {}) {
2723+
const { byCheck, workflows } = discoverFamilies();
25852724

25862725
// The reachability sweep runs BEFORE a line is printed, so its refusals
25872726
// (#4690: an empty corpus, or an all-unreachable answer) come out as a
@@ -3344,6 +3483,110 @@ function selfTest() {
33443483
t('the bare root words the gate spells in ROOTS are still refused as too generic', !hintCovers('docs', 'docs/qa/platform-checklist/RUNNER.md') && !hintCovers('content', 'content/docs/deployment/cli.mdx'));
33453484
t('while the declared subtrees cover those same paths', hintCovers('docs/**', 'docs/qa/platform-checklist/RUNNER.md') && hintCovers('content/**', 'content/docs/deployment/cli.mdx'));
33463485

3486+
// ── The escapable-literal ledger (#10705) ────────────────────────────────
3487+
//
3488+
// Every case above pins ONE gate that took the escape. What none of them can
3489+
// say is who still has not — and that is the whole finding: six instances
3490+
// were found one at a time, on six unrelated cards, the sixth a re-discovery
3491+
// of the fourth by an agent who did not know the enumeration existed. These
3492+
// cases turn that into a bounded list with a verdict.
3493+
//
3494+
// The predicate is pinned on FIXTURES first, because a tree-only assertion
3495+
// cannot show which of its conditions is doing the work; the live halves
3496+
// follow.
3497+
const fx = (hints) => [['check:fixture', { hints }]];
3498+
const fxPrefixes = new Set(['scripts', 'scripts/pm', 'examples', 'skills']);
3499+
t(
3500+
'a bare root literal the tree HAS, undeclared, is a ledger row',
3501+
escapableLiteralRows(fx(['scripts']), fxPrefixes).length === 1,
3502+
);
3503+
t(
3504+
'the same literal beside its subtree spelling is NOT — that gate escaped',
3505+
escapableLiteralRows(fx(['scripts', 'scripts/**']), fxPrefixes).length === 0,
3506+
);
3507+
// The live distinction `check:published-files` forced: a hint that reaches
3508+
// INTO the root covers the bare directory through hintCovers' reverse
3509+
// containment, while covering no other file under it. If this case ever goes
3510+
// green the ledger has started retiring rows for gates that are still
3511+
// unnameable.
3512+
t(
3513+
'a hint that merely reaches into the root does not escape it',
3514+
escapableLiteralRows(fx(['scripts', 'scripts/check-x.mjs']), fxPrefixes).length === 1,
3515+
);
3516+
t(
3517+
'a literal the covering rule never refused is no part of the species',
3518+
escapableLiteralRows(fx(['scripts/pm']), fxPrefixes).length === 0,
3519+
);
3520+
t(
3521+
'nor is a dotted root, which hintCovers admits as written',
3522+
escapableLiteralRows(fx(['.claude']), new Set(['.claude'])).length === 0,
3523+
);
3524+
// The other species the residue block names, kept out by exactly the test
3525+
// `unreachableReason` uses to tell them apart: no declaration can fix a
3526+
// literal the tree does not have, so it is not a debt anyone can pay.
3527+
t(
3528+
'a bare word the tree does NOT have is the genuinely-dead species, not this one',
3529+
escapableLiteralRows(fx(['node_modules']), fxPrefixes).length === 0,
3530+
);
3531+
3532+
// The live halves, over the real tree and through the SAME discovery pass
3533+
// `derive` runs — a second pass built here could enumerate a population no
3534+
// dispatch prompt is derived from.
3535+
const ledgerSwept = trackedFiles();
3536+
const ledgerRows = escapableLiteralRows([...discoverFamilies().byCheck], trackedPrefixes(ledgerSwept));
3537+
const ledgerKeys = ledgerRows.map(escapableLiteralKey);
3538+
// Zero rows is a broken recognizer, not a clean tree, and it must not read as
3539+
// a pass — the same #4690 rule the reachability sweep applies to itself. The
3540+
// stale half below would catch it too; this says so in one line rather than
3541+
// leaving the reader to derive it from a failure two cases down.
3542+
t('the live sweep still reaches this species at all — zero rows is a broken recognizer', ledgerRows.length > 0);
3543+
const freshRows = ledgerKeys.filter((k) => !ESCAPABLE_LITERAL_LEDGER.has(k));
3544+
const staleRows = [...ESCAPABLE_LITERAL_LEDGER].filter((k) => !ledgerKeys.includes(k)).sort();
3545+
t(
3546+
'no gate has NEWLY joined the escapable-literal species' +
3547+
(freshRows.length
3548+
? ` — FRESH: ${freshRows.join(' · ')}. Declare the subtree spelling beside the literal` +
3549+
' (the ROOT_DIR_WATCH_HINTS idiom — see check-role-word.mjs and check-examples-live-imports.mjs),' +
3550+
' or the gate is unnameable by any dispatch derivation and lands already invisible.' +
3551+
' ⛔ The ledger is SHRINK-ONLY: a new line in it is not the remedy.'
3552+
: ''),
3553+
freshRows.length === 0,
3554+
);
3555+
t(
3556+
'and no ledger row is stale' +
3557+
(staleRows.length
3558+
? ` — STALE: ${staleRows.join(' · ')}. Good news, and the list must say so:` +
3559+
' delete each one from ESCAPABLE_LITERAL_LEDGER. A stale line is how this would start' +
3560+
' drifting into an allowlist nobody re-reads.'
3561+
: ''),
3562+
staleRows.length === 0,
3563+
);
3564+
// The spelling rule the ledger's docblock states, held mechanically rather
3565+
// than remembered: a row keyed by a direct script path would enter THIS
3566+
// file's own hint set as a path it does not read.
3567+
t(
3568+
'no ledger row enters this file\'s own declared population as a path',
3569+
[...ESCAPABLE_LITERAL_LEDGER].every((row) => extractWatchHints(`const L = ${JSON.stringify(row)};`).length === 0),
3570+
);
3571+
// The ledger describes the derivation, so it must agree with what the
3572+
// derivation actually reports. Both current rows name the root `scripts`,
3573+
// which the tree has and the covering rule refuses — the pair that puts them
3574+
// in this species rather than in the dead one.
3575+
const ledgerPrefixes = trackedPrefixes(ledgerSwept);
3576+
t(
3577+
'every ledger row names a root the tree HAS and the covering rule refuses',
3578+
ledgerRows.length > 0 &&
3579+
ledgerRows.every(
3580+
({ hint, plain }) =>
3581+
// refused: the gate cannot be named for anything under the root
3582+
!hintCovers(hint, `${plain}/any-file-under-it.mjs`) &&
3583+
// …and the root is really there, which is what separates this species
3584+
// from the dead literals beside it in the residue block
3585+
ledgerPrefixes.has(plain) &&
3586+
deepestTrackedPrefix(hint, ledgerPrefixes) === plain,
3587+
),
3588+
);
3589+
33473590
// ── A trailing sentence period is not part of the path (#8534, half two) ──
33483591
//
33493592
// Coupled to the rule above: the raw-prefix comparison reached the real file

0 commit comments

Comments
 (0)