@@ -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 ) => / \. y a ? m l $ / . 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