Skip to content

Commit 2695a8c

Browse files
os-zhuangclaude
andauthored
fix(devx): stop check:published-files spelling a bare repo root it never opens (#10890)
`check:published-files` reached the literal `scripts` through a package-relative predicate over would-be tarball contents (`rel.startsWith('scripts/')`), not through the repo root of the same name. The dispatch derivation read that literal as a declaration of the repo's own `scripts/` tree, and `hintCovers` refuses a bare top-level word as too generic — so the gate was unnameable for every card while appearing to name a root it never opens. It was the last row of `ESCAPABLE_LITERAL_LEDGER`. Respell the predicate as an anchored regex, the idiom the file's three other FORBIDDEN entries already use. The row then discharges by CONSTRUCTION rather than by declaration, and the ledger empties. The ROOT_DIR_WATCH_HINTS escape — declaring `scripts/**` — is the wrong remedy for this gate: it would be a false declaration, and would name the gate for all 226 repo-root scripts/ files it does not read. Pinned against, since the ledger cannot say so once its row is out. Re-anchor the ledger's #4690 anti-vacuity guard. It was `ledgerRows.length > 0`, a live-tree count that conflated "the recognizer works" with "the tree still owes a row"; paying the last row pulled those apart and left a shrink-only ledger that could not reach zero. A probe family spelling a bare root is now spliced into the live corpus and must be recognized, which holds at zero rows and at any other count. Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7e75dc7 commit 2695a8c

2 files changed

Lines changed: 159 additions & 26 deletions

File tree

‎scripts/check-published-files.mjs‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,31 @@ const FORBIDDEN = [
117117
// The arm #4206 leans on: scripts/ is build-time tooling by convention,
118118
// never runtime code. If that ever stops being true it must stop being true
119119
// loudly here, rather than silently in the docs-drift classifier.
120+
//
121+
// Spelled as an anchored REGEX, like the three entries above, and that is
122+
// load-bearing rather than stylistic (#10875). `rel` is package-relative,
123+
// over the contents of a would-be tarball: this asks whether THIS package
124+
// ships a scripts/ directory of its own, and says nothing whatever about
125+
// the repo root of the same name. Written as the quoted literal it used to
126+
// be -- `rel.startsWith('scripts/')` -- the dispatch derivation
127+
// (scripts/pm/dispatch-gates.mjs, `extractWatchHints`) reads it as this
128+
// gate DECLARING the repo's own scripts/ tree as its population. That
129+
// declaration is false, and it collapses to a bare top-level word
130+
// `hintCovers` refuses as too generic, so the gate scored `silent` for
131+
// every card in the tree while appearing to name a root it never opens.
132+
//
133+
// Two things not to do here, both of which look like the fix and are not:
134+
// - do NOT respell this as a quoted `'scripts/'`. check:pm-dispatch-gates
135+
// fails if this gate declares that bare root again, and it decides with
136+
// the derivation's own extractor rather than a copy of it;
137+
// - do NOT reach for the ROOT_DIR_WATCH_HINTS escape (`['scripts/**']`,
138+
// as check-parse-guard and check-role-word legitimately use). For THIS
139+
// gate that declaration would be a lie, and a load-bearing one: the
140+
// derivation would name this gate for every repo-root scripts/ edit,
141+
// which it does not read. A fabricated lead costs more than a missing
142+
// one -- see the +139084 measurement in `hintCovers`' docblock.
120143
label: 'build-time script',
121-
test: (rel) => rel.startsWith('scripts/'),
144+
test: (rel) => /^scripts\//.test(rel),
122145
},
123146
];
124147

@@ -285,6 +308,10 @@ function selfTest() {
285308
['tsup.config.ts', 'build tooling'],
286309
['tsconfig.build.json', 'build tooling'],
287310
['scripts/i18n-extract.config.ts', 'build-time script'],
311+
// Anchored at the PACKAGE root, which is the whole content of the
312+
// package-relative claim above: a scripts/ directory nested anywhere else
313+
// is ordinary source and must not be classified as build tooling.
314+
['src/scripts/helper.ts', null],
288315
['src/index.ts', null],
289316
['src/latest.ts', null],
290317
['dist/index.js', null],

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 131 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,17 +1623,34 @@ export function escapableLiteralKey({ check, hint }) {
16231623
* this, so the rule fails rather than needing to be remembered.
16241624
*/
16251625
const 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

Comments
 (0)