@@ -1623,17 +1623,34 @@ export function escapableLiteralKey({ check, hint }) {
16231623 * this, so the rule fails rather than needing to be remembered.
16241624 */
16251625const ESCAPABLE_LITERAL_LEDGER = new Set ( [
1626- // DISCHARGED and deleted (#10784): `check:parse-guard scripts`. It declared
1627- // the subtree spelling beside the literal, the row failed as STALE by name,
1628- // and the row came out — the shrink the docblock above describes, walked once
1629- // end to end. Recorded here as the worked instance, not as a row.
1630-
1631- // Reaches `scripts` through a package-relative predicate over tarball
1632- // contents (`rel.startsWith('scripts/')`), not through the repo root it
1633- // appears to name. Its remedy is therefore the other one the idiom allows:
1634- // stop spelling a bare root, rather than declare a subtree the gate does not
1635- // read. Recorded here so that decision is made once, on its own card.
1636- 'check:published-files scripts' ,
1626+ // EMPTY — a verdict, not an absence. Every gate this derivation can SEE
1627+ // spelling a bare top-level root has been discharged. Both instances are
1628+ // recorded here because they took the two DIFFERENT remedies the idiom
1629+ // allows, and a future FRESH row has to CHOOSE between them rather than
1630+ // reach for the first one it is offered:
1631+ //
1632+ // DISCHARGED (#10784): `check:parse-guard scripts`. That gate really does
1633+ // walk the repo's scripts/ tree, so declaring the subtree spelling beside
1634+ // the literal was TRUE. The row then failed as STALE by name and came out —
1635+ // the shrink the docblock above describes, walked once end to end.
1636+ //
1637+ // DISCHARGED (#10875): `check:published-files scripts`. That gate reached
1638+ // `scripts` through a package-relative predicate over would-be tarball
1639+ // contents, NOT through the repo root it appeared to name, so the escape
1640+ // hatch was the wrong remedy for it: a `scripts/**` declaration would have
1641+ // been false, and would have named the gate for every repo-root scripts/
1642+ // edit it does not read — a fabricated lead, which `hintCovers`' docblock
1643+ // prices above a missing one. It took the OTHER remedy the idiom allows,
1644+ // stop spelling a bare root, and the row discharged by CONSTRUCTION rather
1645+ // than by declaration: nothing was added to this list to make it happen,
1646+ // which is the shape a shrink-only list wants.
1647+ //
1648+ // ⚠️ An empty ledger is NOT "the species is gone", and must not be read as
1649+ // one. This list only ever saw the half the derivation can see — a literal
1650+ // that reaches the HINT SET, which needs a separator somewhere in the source
1651+ // spelling. #10840 measures the other half at ~33 gates whose population
1652+ // literal carries no separator at all; those are invisible to the derivation
1653+ // AND to this ledger, and emptying this list moves none of them.
16371654] ) ;
16381655
16391656// ---------------------------------------------------------------------------
@@ -3712,22 +3729,50 @@ function selfTest() {
37123729 // `derive` runs — a second pass built here could enumerate a population no
37133730 // dispatch prompt is derived from.
37143731 const ledgerSwept = trackedFiles ( ) ;
3715- const ledgerRows = escapableLiteralRows ( [ ...discoverFamilies ( ) . byCheck ] , trackedPrefixes ( ledgerSwept ) ) ;
3732+ const ledgerPrefixes = trackedPrefixes ( ledgerSwept ) ;
3733+ const ledgerFamilies = [ ...discoverFamilies ( ) . byCheck ] ;
3734+ const ledgerRows = escapableLiteralRows ( ledgerFamilies , ledgerPrefixes ) ;
37163735 const ledgerKeys = ledgerRows . map ( escapableLiteralKey ) ;
3717- // Zero rows is a broken recognizer, not a clean tree, and it must not read as
3718- // a pass — the same #4690 rule the reachability sweep applies to itself. The
3719- // stale half below would catch it too; this says so in one line rather than
3720- // leaving the reader to derive it from a failure two cases down.
3721- t ( 'the live sweep still reaches this species at all — zero rows is a broken recognizer' , ledgerRows . length > 0 ) ;
3736+ // #4690 at ZERO ROWS: a quiet sweep must still prove it can SPEAK.
3737+ //
3738+ // This guard used to be `ledgerRows.length > 0` — a live-tree count, which
3739+ // read the right rule off the wrong quantity. It conflated two separable
3740+ // claims: "the recognizer still works" and "the tree still owes a row".
3741+ // While a debt existed the two moved together, so the conflation was
3742+ // invisible; paying the LAST row (#10875) is what pulled them apart, and the
3743+ // guard then failed on a clean tree — a shrink-only ledger that could not be
3744+ // allowed to reach zero, which is the one end state it exists to reach.
3745+ //
3746+ // So the recognizer is asked directly instead: splice ONE synthetic family
3747+ // spelling a bare root into the LIVE corpus and require the sweep to find
3748+ // exactly it, and exactly one more row than the tree really owes. That holds
3749+ // at zero live rows and at any other count, it fails loudly if the recognizer
3750+ // or the prefix set goes dead, and unlike the fixture cases above it runs on
3751+ // the live prefixes the real sweep runs on.
3752+ const probeKey = 'check:escapable-literal-probe' ;
3753+ const probedRows = escapableLiteralRows ( [ ...ledgerFamilies , [ probeKey , { hints : [ 'scripts' ] } ] ] , ledgerPrefixes ) ;
3754+ const probedKeys = probedRows . map ( escapableLiteralKey ) ;
3755+ t (
3756+ 'the live sweep still RECOGNISES this species — a probe family spelling a bare root is found,' +
3757+ ' so a quiet sweep means a clean tree rather than a broken recognizer (#4690)' ,
3758+ probedKeys . filter ( ( k ) => k === `${ probeKey } scripts` ) . length === 1 &&
3759+ probedRows . length === ledgerRows . length + 1 ,
3760+ ) ;
37223761 const freshRows = ledgerKeys . filter ( ( k ) => ! ESCAPABLE_LITERAL_LEDGER . has ( k ) ) ;
37233762 const staleRows = [ ...ESCAPABLE_LITERAL_LEDGER ] . filter ( ( k ) => ! ledgerKeys . includes ( k ) ) . sort ( ) ;
37243763 t (
37253764 'no gate has NEWLY joined the escapable-literal species' +
37263765 ( freshRows . length
3727- ? ` — FRESH: ${ freshRows . join ( ' · ' ) } . Declare the subtree spelling beside the literal` +
3728- ' (the ROOT_DIR_WATCH_HINTS idiom — see check-role-word.mjs and check-examples-live-imports.mjs),' +
3729- ' or the gate is unnameable by any dispatch derivation and lands already invisible.' +
3730- ' ⛔ The ledger is SHRINK-ONLY: a new line in it is not the remedy.'
3766+ ? ` — FRESH: ${ freshRows . join ( ' · ' ) } . Unnameable by any dispatch derivation as spelled,` +
3767+ ' so it lands already invisible. TWO remedies, and which one is right depends on what the' +
3768+ ' gate actually READS — pick, do not reach for the first one:' +
3769+ ' (a) it really does walk that repo root ⇒ declare the subtree spelling beside the literal,' +
3770+ ' the ROOT_DIR_WATCH_HINTS idiom (see check-role-word.mjs and check-examples-live-imports.mjs);' +
3771+ ' (b) it does NOT ⇒ stop spelling a bare root, respelling the literal to say what the predicate' +
3772+ ' means (check-published-files.mjs is the worked instance: its predicate is package-relative,' +
3773+ ' over tarball contents, so it took (b) and the row discharged by construction).' +
3774+ ' ⛔ Declaring a root the gate does not read is a FABRICATED lead — worse than the row,' +
3775+ ' at the price hintCovers puts on one. ⛔ The ledger is SHRINK-ONLY: a new line is not a remedy.'
37313776 : '' ) ,
37323777 freshRows . length === 0 ,
37333778 ) ;
@@ -3743,20 +3788,40 @@ function selfTest() {
37433788 // The spelling rule the ledger's docblock states, held mechanically rather
37443789 // than remembered: a row keyed by a direct script path would enter THIS
37453790 // file's own hint set as a path it does not read.
3791+ //
3792+ // Carried on a WITNESS PAIR rather than on the ledger alone. The ledger is
3793+ // empty now, and `[].every(...)` is a pass that proves nothing — precisely
3794+ // the vacuous shape #10784's ablation caught one case over, where a fixture
3795+ // with no subtree hint to strip compared silent to silent and read green. The
3796+ // sample row keeps the positive half exercised at zero rows; the negative
3797+ // witness is what gives the case teeth at all, since the live key format
3798+ // (\`${check} ${hint}\`) always carries a space and the extractor refuses a
3799+ // span with one — so a key in that format cannot build a hint whatever it
3800+ // names, and only the bare-path spelling the docblock warns against can.
3801+ const asHints = ( row ) => extractWatchHints ( `const L = ${ JSON . stringify ( row ) } ;` ) ;
37463802 t (
37473803 'no ledger row enters this file\'s own declared population as a path' ,
3748- [ ...ESCAPABLE_LITERAL_LEDGER ] . every ( ( row ) => extractWatchHints ( `const L = ${ JSON . stringify ( row ) } ;` ) . length === 0 ) ,
3804+ [ ...ESCAPABLE_LITERAL_LEDGER , 'check:sample-gate someroot' ] . every ( ( row ) => asHints ( row ) . length === 0 ) ,
3805+ ) ;
3806+ t (
3807+ '…and that rule can FAIL: the direct-script spelling it forbids does build a hint' ,
3808+ asHints ( 'scripts/check-x.mjs' ) . length === 1 ,
37493809 ) ;
37503810 // The ledger describes the derivation, so it must agree with what the
37513811 // derivation actually reports: every row must name a root the tree HAS and
37523812 // the covering rule refuses — the pair that puts a row in this species rather
37533813 // than in the dead one beside it in the residue block. Asserted over whatever
37543814 // the ledger holds, never over a remembered count of it.
3755- const ledgerPrefixes = trackedPrefixes ( ledgerSwept ) ;
3815+ //
3816+ // Run over the PROBED rows, which are the live rows plus the one synthetic
3817+ // row spliced in above. Two things fall out of that and both are wanted: the
3818+ // property is still checked on every real row, and the case can never go
3819+ // vacuous now that the live half is legitimately empty — the probe row is a
3820+ // row of exactly this species, so `.every` always has something to judge.
37563821 t (
37573822 'every ledger row names a root the tree HAS and the covering rule refuses' ,
3758- ledgerRows . length > 0 &&
3759- ledgerRows . every (
3823+ probedRows . length > 0 &&
3824+ probedRows . every (
37603825 ( { hint, plain } ) =>
37613826 // refused: the gate cannot be named for anything under the root
37623827 ! hintCovers ( hint , `${ plain } /any-file-under-it.mjs` ) &&
@@ -3810,6 +3875,47 @@ function selfTest() {
38103875 ) ;
38113876 }
38123877
3878+ // ── The bare root this gate must NOT spell (#10875) ───────────────────────
3879+ //
3880+ // `check:published-files` asks whether a published package ships a scripts/
3881+ // directory OF ITS OWN — a package-relative predicate over would-be tarball
3882+ // contents. Written as a quoted literal it read to this derivation as a
3883+ // declaration of the repo's own scripts/ tree, which the gate never opens:
3884+ // the last row of ESCAPABLE_LITERAL_LEDGER, discharged by respelling the
3885+ // predicate rather than by declaring a subtree that would have been FALSE.
3886+ //
3887+ // ⛔ The remedy the FRESH message offers first — declare the subtree — is the
3888+ // WRONG one here, and it is the one a reader reaches for. The ledger cannot
3889+ // say so, because by design it says nothing at all once a row is out. So the
3890+ // correct verdict is pinned here, in both directions, against the live tree.
3891+ const publishedFiles = scriptsFamilies . get ( 'check:published-files' ) ;
3892+ t (
3893+ 'check:published-files is discovered at all — the pins below mean nothing without it' ,
3894+ Boolean ( publishedFiles ) ,
3895+ ) ;
3896+ t (
3897+ 'check:published-files declares no bare repo root it does not open' ,
3898+ Boolean ( publishedFiles ) && ! publishedFiles . hints . includes ( 'scripts' ) ,
3899+ JSON . stringify ( { hints : publishedFiles ?. hints } ) ,
3900+ ) ;
3901+ t (
3902+ '…so a brand-new repo-root scripts/ file does NOT name it — a fabricated lead is the costlier error' ,
3903+ Boolean ( publishedFiles ) && classifyEntry ( publishedFiles , [ unwrittenScript ] ) . verdict !== 'matched' ,
3904+ JSON . stringify ( { verdict : publishedFiles && classifyEntry ( publishedFiles , [ unwrittenScript ] ) . verdict } ) ,
3905+ ) ;
3906+ // Non-vacuity for the case above, and the whole difference between a verdict
3907+ // that is CORRECT and one that is merely quiet: it must be passing because
3908+ // the gate reads a different population, never because the gate went dark.
3909+ // Its own source is a population it really does have, and still names it.
3910+ t (
3911+ '…and it is not silent everywhere: the population it really has still names it' ,
3912+ Boolean ( publishedFiles ) &&
3913+ classifyEntry ( publishedFiles , [ 'scripts/check-published-files.mjs' ] ) . verdict === 'matched' ,
3914+ JSON . stringify ( {
3915+ verdict : publishedFiles && classifyEntry ( publishedFiles , [ 'scripts/check-published-files.mjs' ] ) . verdict ,
3916+ } ) ,
3917+ ) ;
3918+
38133919 // ── Telling a WEAK silence from an INVERTED one (#10784) ──────────────────
38143920 //
38153921 // The residue said the same words for a gate that genuinely does not read
0 commit comments