@@ -1386,17 +1386,141 @@ export function runnableInvocation({ check, filter, direct }) {
13861386 */
13871387/**
13881388 * A hint with its globs collapsed and its trailing separators dropped — the
1389- * form `hintCovers` compares against, extracted so the reachability sweep can
1390- * describe a dead hint in the SAME terms the comparison judged it by. The
1391- * transformation is carried verbatim from where it was written inline; a
1392- * second, separately-maintained copy of it is exactly the drift this file
1393- * refuses everywhere else.
1389+ * form `hintCovers` compares against for every hint whose globs are TRAILING,
1390+ * extracted so the reachability sweep can describe a dead hint in the SAME
1391+ * terms the comparison judged it by. The transformation is carried verbatim
1392+ * from where it was written inline; a second, separately-maintained copy of it
1393+ * is exactly the drift this file refuses everywhere else.
1394+ *
1395+ * One strip, not two. The trailing-separator cleanup used to be written
1396+ * `.replace(/\/+$/, '').replace(/\/$/, '')`, and the second call was
1397+ * unreachable: `/\/+$/` is greedy and anchored, so after it runs no trailing
1398+ * separator survives for the second to find. Measured over all 754 distinct
1399+ * hints in the fleet — and over every probe string the shape admits (`a///`,
1400+ * `a/`, `a//`, `a/**\/`, `**\/`, `/`) — it changed the answer for zero of them.
1401+ * Deleted here rather than left as decoration, because a redundant strip reads
1402+ * as defence against a case the first one misses and there is no such case:
1403+ * what the anchored strips genuinely cannot touch is a separator left in the
1404+ * MIDDLE, which is the defect the branch below fixes rather than one more
1405+ * trailing pass would.
13941406 */
13951407export function collapseHint ( hint ) {
1396- return hint . replace ( / \* \* ? / g, '' ) . replace ( / \/ + $ / , '' ) . replace ( / \/ $ / , '' ) ;
1408+ return hint . replace ( / \* \* ? / g, '' ) . replace ( / \/ + $ / , '' ) ;
13971409}
13981410
1411+ /**
1412+ * ## A glob in a NON-FINAL segment is not collapsible, and must not be collapsed
1413+ *
1414+ * Collapse-by-deletion is sound for a glob in the LAST segment, which is every
1415+ * shape the idioms above use: `packages/**` → `packages`, `packages/spec/src/**`
1416+ * → `packages/spec/src`, `packages/client*` → `packages/client`. Each names a
1417+ * subtree root the tree really has, and `ROOT_DIR_WATCH_HINTS` depends on
1418+ * exactly that reduction (`scripts/check-published-files.mjs:215-248` declares
1419+ * the workspace globs VERBATIM so "the glob collapse reduces each back to the
1420+ * root it names", justified there at 91.3%).
1421+ *
1422+ * A glob in a non-final segment is the one case where deletion does not reduce
1423+ * — it MANGLES. `skills/*\/references/_index.md` collapses to
1424+ * `skills//references/_index.md`, a double separator no tree can hold, so the
1425+ * hint matches nothing BY CONSTRUCTION while looking like an ordinary literal.
1426+ * `check:skill-refs` was the live specimen: `packages/spec/scripts/
1427+ * build-skill-references.ts` generates nine `_index.md` files, `git ls-files
1428+ * 'skills/*\/references/_index.md'` returns those nine, and the derivation
1429+ * reached zero of them.
1430+ *
1431+ * It also produced the worst row this output can print. `unreachableClass`
1432+ * calls a family "THE LAYOUT MOVED … a real miss, worth triaging" when a dead
1433+ * hint's `deepest` differs from its collapsed form, and the mangled form makes
1434+ * that true for a gate whose layout did not move at all — with the three
1435+ * "reasons" printed beside it drawn from OTHER inert hints, TypeScript
1436+ * `paths`-mapped module specifiers that were never repo paths. Both halves of
1437+ * the row were wrong: the classification and its evidence.
1438+ *
1439+ * No spelling of `collapseHint` can fix this, which is why the branch is here
1440+ * rather than there, and the impossibility is structural rather than a matter
1441+ * of a cleverer string. For a family to be classed "by construction" its dead
1442+ * hint needs `deepest(P) === P`, i.e. P must be a tracked prefix — but any
1443+ * tracked prefix P makes the three comparisons below reach every file beneath
1444+ * it, so the hint goes LIVE and never enters `dead` to be classified at all.
1445+ * The two requirements are mutually exclusive. Re-checked on this tree against
1446+ * every candidate P the specimen admits: `skills` is a tracked prefix and
1447+ * reaches 50 files (live, never classified); `skills/*`,
1448+ * `skills/*\/references`, `skills/*\/references/_index.md`, `skills/references`
1449+ * and the mangled `skills//references/_index.md` are none of them tracked
1450+ * prefixes and all reach 0.
1451+ *
1452+ * ## The rule, and why it reuses `triggerCovers`
1453+ *
1454+ * When — and ONLY when — a glob sits in a non-final segment, the hint is
1455+ * matched as the filter pattern it visibly is, through `triggerCovers`. That is
1456+ * not a second matching language: `triggerCovers` is `triggerPatternRegex` plus
1457+ * the literal-prefix directory reach, both of which already live in this file
1458+ * for this exact job, and the docblock above refuses a second one on principle.
1459+ * It also lines the two directions up with the ones the collapse gives: the
1460+ * regex answers "does this pattern match the file" (`plain`-equality and
1461+ * subtree descent), and the literal prefix answers "is the input a DIRECTORY
1462+ * the pattern reaches into" (reverse containment).
1463+ *
1464+ * ## Measured, both directions, on 170 families × 754 distinct hints × 6816
1465+ * ## tracked files
1466+ *
1467+ * Exactly SIX distinct hints in the whole fleet carry a glob in a non-final
1468+ * segment, so the blast radius is enumerable rather than estimated:
1469+ *
1470+ * packages/**\/*.ts 0 → 4693 check:cross-package-test-inputs (+2)
1471+ * packages/**\/*.object.ts 0 → 79 the same three families
1472+ * skills/*\/references/_index.md 0 → 9 check:skill-refs
1473+ * src/**\/* 0 → 0 check:type-source-resolution
1474+ * spec/src/*\/index.ts 0 → 0 check:type-source-resolution
1475+ * src/**\/*.zod.ts 0 → 0 check:published-files
1476+ *
1477+ * watch-hint (gate, file) pairs 72482 → 86807
1478+ * families gaining coverage 4 rows / 3 gates, listed above; ZERO losing
1479+ * (check, hint) newly live 7; newly inert 0
1480+ * unreachable families 12 → 11, and "layout moved" 1 → 0
1481+ * `skills/*\/references/_index.md` claims the 9 real files and 0 others
1482+ *
1483+ * The narrower rule is deliberate. Matching ALL whole-segment globs this way
1484+ * was measured and REFUSED: it takes `check:test-source-alias` 7511 → 107,
1485+ * `check:type-source-resolution` 8682 → 1278 and `check:published-files` 7407 →
1486+ * 3 (−7404 each), because it stops collapsing the trailing `**` the
1487+ * `ROOT_DIR_WATCH_HINTS` idiom is built on. That is a regression, not a
1488+ * correction, and this branch keeps the idiom bit-for-bit: `packages/*` 5228,
1489+ * `examples/*` 241, `skills/**` 50, `content/**` 442, all unchanged. So is the
1490+ * DECIDED partial-segment trade — `packages/client*` still refuses
1491+ * `packages/client-react/src/index.ts` and still covers `packages/client`,
1492+ * because a partial-segment glob in the LAST segment is not this case.
1493+ *
1494+ * ## What the +14325 pairs are, and why they are not a widening
1495+ *
1496+ * All of the residual cost is two hints: `packages/**\/*.ts` and
1497+ * `packages/**\/*.object.ts`, declared in `scripts/cross-package-test-inputs.mjs`
1498+ * and inherited by three family rows. They reach 0 today ONLY because of this
1499+ * defect, so the question is not whether to widen a gate but whether a
1500+ * declaration that finally means what it says is wanted — and it is, checked at
1501+ * the declaration site rather than assumed: `packages/**\/*.ts` is held by
1502+ * `packages/core/src/security/operation-private-keys.pin.test.ts`, whose scan
1503+ * surface is spelled `git ls-files` over "every `.ts`/`.tsx` file under
1504+ * `packages/`", and `packages/**\/*.object.ts` by the two repo-wide
1505+ * `*.object.ts` walkers in `packages/spec`. Those tests really do read every
1506+ * matching file, so every recovered pair is a TRUE lead, not a fabricated one —
1507+ * which is the provenance criterion the docblock above prices, never volume.
1508+ *
1509+ * Its derivation cost was measured rather than feared, because "22 leads is the
1510+ * same as none" is the failure this file exists to avoid. Reading each tracked
1511+ * file as a one-file card surface: `check:cross-package-test-inputs` is named by
1512+ * 3323 cards today and 5561 after — 2238 additional cards, 32.8% of the tree.
1513+ * It is NOT "nearly every card touching `packages/**`": of the 4693
1514+ * `packages/**\/*.ts` files, 2455 already name the family through some other
1515+ * hint and 2238 do not. Mean matched families per card goes 12.05 → 13.51 for a
1516+ * `packages/**\/*.ts` card and 11.99 → 12.89 fleet-wide — about one extra
1517+ * lead, against the +139084-pair explosion that took one card from 7 families
1518+ * to 34 and is the number this file calls unaffordable. So the two hints stay
1519+ * as declared; narrowing them at their declaration site was considered and is
1520+ * refused, because the narrowing would be the false statement.
1521+ */
13991522export function hintCovers ( hint , inputPath ) {
1523+ if ( globInNonFinalSegment ( hint ) ) return triggerCovers ( hint , inputPath ) ;
14001524 const plain = collapseHint ( hint ) ;
14011525 if ( plain . length < 2 ) return false ;
14021526 // `hint`, not `plain`: glob collapse destroys the separator this refusal is
@@ -1409,6 +1533,20 @@ export function hintCovers(hint, inputPath) {
14091533 ) ;
14101534}
14111535
1536+ /**
1537+ * Does a glob sit anywhere but this hint's LAST segment? The one question that
1538+ * decides which of the two rules above judges a hint, kept as its own named
1539+ * predicate so the branch reads as the case it is rather than as an inline
1540+ * condition. Segment-wise on purpose: `packages/client*` is a glob in the last
1541+ * segment and stays with the collapse (the DECIDED trade), while
1542+ * `packages/**\/*.ts` is not.
1543+ */
1544+ export function globInNonFinalSegment ( hint ) {
1545+ const segments = hint . split ( '/' ) ;
1546+ for ( let i = 0 ; i < segments . length - 1 ; i ++ ) if ( segments [ i ] . includes ( '*' ) ) return true ;
1547+ return false ;
1548+ }
1549+
14121550/**
14131551 * Translate ONE GitHub filter pattern into an anchored regex, following the
14141552 * documented filter-pattern semantics rather than approximating them:
@@ -4661,6 +4799,48 @@ function selfTest() {
46614799 t ( 'the same glob still covers the package it names' , hintCovers ( 'packages/client*' , 'packages/client/src/index.ts' ) ) ;
46624800 t ( 'a segment-boundary glob is untouched by the trade' , hintCovers ( 'packages/client/**' , 'packages/client/src/index.ts' ) ) ;
46634801
4802+ // ── A glob in a NON-FINAL segment is matched, not collapsed (#12246) ──────
4803+ //
4804+ // Collapse-by-deletion mangles this one shape into a double separator no tree
4805+ // can hold, so the hint matched nothing BY CONSTRUCTION and its family was
4806+ // then misfiled as "THE LAYOUT MOVED". Both halves are pinned: what the rule
4807+ // now reaches, and — the larger half — everything it deliberately does NOT
4808+ // disturb, because the refused alternative (matching ALL whole-segment globs)
4809+ // breaks the ROOT_DIR_WATCH_HINTS idiom by −7404 pairs on each of three gates.
4810+ t ( 'the predicate reads the LAST segment, so a trailing glob is not this case' , ! globInNonFinalSegment ( 'packages/**' ) ) ;
4811+ t ( 'nor is a partial-segment glob in the last segment' , ! globInNonFinalSegment ( 'packages/client*' ) ) ;
4812+ t ( 'a glob in a middle segment IS' , globInNonFinalSegment ( 'skills/*/references/_index.md' ) ) ;
4813+ t ( 'and a `**` in a middle segment IS' , globInNonFinalSegment ( 'packages/**/*.ts' ) ) ;
4814+ // The live specimen: `packages/spec/scripts/build-skill-references.ts` emits
4815+ // these nine files and the derivation reached zero of them. If the skills
4816+ // layout ever changes, re-point this case at whatever mid-segment glob the
4817+ // fleet then declares rather than deleting it.
4818+ t ( 'a mid-segment glob reaches the file it names' , hintCovers ( 'skills/*/references/_index.md' , 'skills/objectstack-formula/references/_index.md' ) ) ;
4819+ t ( 'and does NOT claim the rest of the subtree it passes through' , ! hintCovers ( 'skills/*/references/_index.md' , 'skills/objectstack-formula/SKILL.md' ) ) ;
4820+ t ( 'a `**` crosses separators, a single `*` does not' , hintCovers ( 'packages/**/*.ts' , 'packages/spec/src/data/filter.zod.ts' ) ) ;
4821+ t ( 'so a single `*` segment matches exactly one segment' , ! hintCovers ( 'skills/*/references/_index.md' , 'skills/a/b/references/_index.md' ) ) ;
4822+ t ( 'the extension the glob names is honoured' , ! hintCovers ( 'packages/**/*.object.ts' , 'packages/spec/src/index.ts' ) ) ;
4823+ // Reverse containment, the direction a coarse card surface needs: the literal
4824+ // prefix before the first wildcard still reaches back to a directory surface,
4825+ // exactly as the collapsed form used to.
4826+ t ( 'a directory surface above the glob still derives the gate' , hintCovers ( 'skills/*/references/_index.md' , 'skills' ) ) ;
4827+ t ( 'but an unrelated root does not' , ! hintCovers ( 'skills/*/references/_index.md' , 'packages' ) ) ;
4828+ // ⛔ The refused alternative, pinned as the loss it would be. Each of these
4829+ // is a trailing glob and MUST keep going through the collapse.
4830+ t ( 'a trailing `**` still collapses to the root it names' , hintCovers ( 'packages/**' , 'packages/spec/src/index.ts' ) ) ;
4831+ t ( 'the ROOT_DIR_WATCH_HINTS idiom is untouched' , hintCovers ( 'skills/**' , 'skills/objectstack-formula/SKILL.md' ) ) ;
4832+ t ( 'and so is a trailing single `*`' , hintCovers ( 'examples/*' , 'examples/app-showcase/src/x.ts' ) ) ;
4833+ t ( 'the DECIDED partial-segment trade still refuses the sibling' , ! hintCovers ( 'packages/client*' , 'packages/client-react/src/index.ts' ) ) ;
4834+
4835+ // The trailing-separator strip is ONE call, not two: `/\/+$/` is greedy and
4836+ // anchored, so nothing survives for a second `/\/$/` to remove. Measured at
4837+ // zero of 754 hints; pinned on the probes that could tell them apart, so a
4838+ // future reader does not restore the redundant call as defence-in-depth.
4839+ t ( 'the greedy trailing strip removes every trailing separator' , collapseHint ( 'a///' ) === 'a' ) ;
4840+ t ( 'including the one a trailing `**` leaves behind' , collapseHint ( 'a/**/' ) === 'a' ) ;
4841+ t ( 'and a hint that is nothing but separators collapses to empty' , collapseHint ( '**/' ) === '' ) ;
4842+ t ( 'what it cannot touch is a separator left in the MIDDLE' , collapseHint ( 'skills/*/references/_index.md' ) === 'skills//references/_index.md' ) ;
4843+
46644844 // ── A declared SUBTREE is not a bare word (#9626) ─────────────────────────
46654845 //
46664846 // The genericity refusal reads the hint as the author wrote it. Both
0 commit comments