Skip to content

Commit 3628b4e

Browse files
committed
fix(scripts): the doc-authoring cross-package leg gets the explicit seen floor its header prescribes once the baseline is empty
The prose-id baseline is now {}, so the stale arm can no longer catch a walker or prefilter that goes blind: 0 measured against 0 pinned reads green. The leg now reds when the real tree's parsed-source or string count falls below PACKAGES_PROSE_SEEN_FLOOR (600 sources, 40000 strings, about half of today's 1252 / 86276), each measure judged on its own. A new self-test battery proves a below-floor reading reds; the header sentence says the floor exists and why. No other behaviour moves. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0edca88 commit 3628b4e

1 file changed

Lines changed: 97 additions & 9 deletions

File tree

‎scripts/check-doc-authoring.mjs‎

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -697,12 +697,14 @@ function scanBoundaryLines() {
697697
// `--census-ledger` in the same PR, so burn-down
698698
// is recorded where review can see it.
699699
//
700-
// While the baseline is non-empty, the ratchet IS this leg's blindness floor:
701-
// a walker or prefilter that goes blind reads 0 sites against 632 pinned
702-
// pairs and reds as stale. The id regex itself is shared with the spec leg,
703-
// whose per-bucket floors guard it independently. If the baseline is ever
704-
// burned to empty, add an explicit seen-floor here in the same PR — at that
705-
// point the stale arm can no longer catch a dormant walker.
700+
// While the baseline was non-empty, the ratchet WAS this leg's blindness
701+
// floor: a walker or prefilter that went blind read 0 sites against the pinned
702+
// pairs (632 at census time) and redded as stale. The baseline has since been
703+
// burned to empty (`{}`), so the stale arm can no longer catch a dormant
704+
// walker — 0 measured against 0 pinned is green. The explicit seen floor,
705+
// PACKAGES_PROSE_SEEN_FLOOR below, landed in the same PR that emptied it and
706+
// does that job now. The id regex itself is shared with the spec leg, whose
707+
// per-bucket floors guard it independently.
706708
//
707709
// ## What this leg deliberately does NOT do
708710
//
@@ -714,6 +716,33 @@ const PACKAGES_PROSE_ROOT = 'packages';
714716
const PACKAGES_PROSE_EXCLUDED = 'packages/spec';
715717
const PACKAGES_PROSE_LEDGER = 'scripts/doc-authoring-prose-id.baseline.json';
716718

719+
/**
720+
* The leg's explicit SEEN FLOOR — the blindness floor the stale arm stopped
721+
* being when the baseline was burned to empty (see "The ratchet" above).
722+
*
723+
* With `{}` pinned there is nothing left to go stale, so a scan that went
724+
* blind would read 0 ids against 0 pinned and print green. This floor reds it
725+
* instead: the REAL tree's reading must stay at or above both numbers, each
726+
* judged on its own, so one measure can never cover for the other.
727+
*
728+
* filesParsed sources the prefilter admitted and the parser read
729+
* stringsSeen string literals and templates the walker visited in them
730+
*
731+
* Reading when the floor was pinned (the PR that emptied the baseline, at
732+
* 0edca886c): 1252 parsed sources, 86276 strings. The floor sits at about half
733+
* of each — 600 and 40000, a margin of 652 sources (52%) and 46276 strings
734+
* (54%) — so ordinary churn (a package added or retired, a file split or
735+
* merged, ids moved out of comments) never trips it, while a dormant walker,
736+
* a blind prefilter or a root that lost most of its population does. Both
737+
* numbers are measured AFTER the prefilter, which admits a file on any
738+
* tracker-shaped token, comments included, so they also fall as ids leave
739+
* comments; the margin is sized for that too.
740+
*
741+
* Raising a number after re-measuring is the landing author's to take;
742+
* lowering one is ⛔ MAINTAINER-ONLY, the same split as SELF_TEST_BATTERIES.
743+
*/
744+
const PACKAGES_PROSE_SEEN_FLOOR = Object.freeze({ filesParsed: 600, stringsSeen: 40000 });
745+
717746
/**
718747
* Cheap byte-level prefilter: a SUPERSET of {@link INTERNAL_ID} (no
719748
* lookarounds), deliberately non-global so `.test()` is stateless. A file this
@@ -849,6 +878,18 @@ function comparePackageProseLedger(counts, ledger) {
849878
return { growth, stale };
850879
}
851880

881+
/**
882+
* The seen-floor verdict for one reading: every measure of
883+
* {@link PACKAGES_PROSE_SEEN_FLOOR} the reading falls below, each judged on its
884+
* own. Empty means the reading clears the floor. A measure the reading does
885+
* not carry at all is a breach, never a pass (`undefined >= n` is false).
886+
*/
887+
function packageProseSeenFloorBreaches(reading, floor = PACKAGES_PROSE_SEEN_FLOOR) {
888+
return Object.keys(floor)
889+
.filter((measure) => !(reading[measure] >= floor[measure]))
890+
.map((measure) => ({ measure, seen: reading[measure], floor: floor[measure] }));
891+
}
892+
852893
const posix = (p) => p.split(sep).join('/');
853894

854895
function walk(dir, out) {
@@ -1507,13 +1548,14 @@ const SELF_TEST_BATTERIES = Object.freeze({
15071548
'spec text: boundary output': 8,
15081549
'cross-package prose ids': 11,
15091550
'cross-package ledger arithmetic': 7,
1551+
'cross-package seen floor': 8,
15101552
});
15111553

15121554
// The registry is shrink-only in the same sense its counts are: DELETING an
15131555
// entry silences that battery's floor exactly as effectively as zeroing it, so
15141556
// the registry's own size is pinned too. Adding a battery raises this number;
15151557
// removing one is the same ⛔ MAINTAINER-ONLY edit as lowering a count.
1516-
const SELF_TEST_BATTERY_FLOOR = 16;
1558+
const SELF_TEST_BATTERY_FLOOR = 17;
15171559

15181560
// The key an assertion is filed under when no battery is open. It is not a
15191561
// declared battery, so it reds by the same set difference rather than silently
@@ -1945,7 +1987,7 @@ function selfTest() {
19451987
}
19461988
process.exit(1);
19471989
}
1948-
console.log('✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees and docs/{audits,handoff,plans} out), detection, the dead-root hard error (red when a ROOT is renamed, green when restored), the empty-scan hard error (red when a root yields nothing and when the whole scan does, green when restored), the published-catalog internal-id rule (red on a planted id in prose, in a fenced comment and in the repo#NNNN spelling, green when removed; hex colours, version numbers, HTTP codes, array indices and the "#1" ordinal all pass; references/ reached, generated artifacts and the internal roots out; the `#<n>` placeholder passes while the concrete ids it replaced stay red, with no exemption to reach for), the spec customer-facing-text internal-id rule (red on an id planted on a LATER line of a concatenated message — the shape a line-oriented census cannot see, proven here — and in a template chain, a positional validator message, the repo#NNNN spelling, a nested strictObject `guidance` prescription, a HOISTED guidance const, a `KeySetGuidance` const consumed only CROSS-MODULE in both the annotated and the `as const satisfies` spelling, a HOISTED refusal message, a `retiredKey()` tombstone, `new Map` and `Object.freeze` guidance tables, `.describe()` prose, and the nested `guidance` of a whole options table written `satisfies StrictObjectOptions`; green when removed; an ADR id on a tombstone, a `.default()` VALUE, `history`/`guidance` outside a strictObject options position, `extraKeys` key names and an inferred local that merely MENTIONS `KeySetGuidance` all pass; test bodies out; the seen floor is PER BUCKET so one matcher rotting while the others carry the total still reds; and the two TYPE ANCHORS are pinned on the predicate itself — the annotation, `satisfies` and `as const satisfies` spellings all read as a strictObject options position while some other satisfied type does not, and the `*_STRICT_OPTIONS` NAME branch still fires where no type is written at all — which is the only place they can be told apart, since end to end they are redundant), the fourth population — customer-facing text BUILT INSIDE A FUNCTION (red on an id in an inline `error: () =>` callback, in a const the callback only dispatches to, inside a `message:` builder function, RETURNED from a tombstone-prescription builder, in a `: StrictObjectOptions` options factory, and in a plain `error:` string; ⛔ the body of an ordinary helper and a local inside a recognised factory stay unswept, because the climb crosses a function only when the FUNCTION sits in a recognised position; and `functionBuilt` carries its own blindness floor, since an unrecognised spelling produces no flag SILENTLY), the FIFTH population — prose built inside plain `function` DECLARATIONS (#13156: red on an id in a declaration consumed by `message:`, RETURNED to a `retiredKey()` argument, and in a const the declaration only dispatches to; its own `functionDeclared` bucket with its own floor, so the declaration clause rotting cannot hide behind the arrows; ⛔ an unconsumed declaration and one consumed only by an unrecognised call stay unswept — the clause is the fourth population\'s, one declaration form over, never an unconditional crawl), the GENERATED table — a prescription filed under each of a list of keys by `Object.fromEntries(keys.map(…))` rather than written as an object literal (red both HOISTED into a const spread into an options factory\'s `guidance` and generated INLINE at the `guidance:` key itself, green when the id is removed; ⛔ and a generated VALUE table reaching no sink stays unswept, because `.map()` is TRANSPARENT to the climb and never a position of its own), the Rule 3 boundary OUTPUT (names the position-based root AND the ledgered cross-package leg\'s root, exclusion and baseline, no longer claims siblings are unscanned, and lists every floored bucket — derived from the same constants the scans read), the CROSS-PACKAGE prose-id leg (#13297: a concatenation-split id in a plain helper is counted — total-string coverage, no position climb to rot; a `//` comment, a test body and the spec subtree are out; an id inside a template\'s embedded expression counts exactly once; a 6-digit colour never matches while the cross-repo spelling\'s id half does; the prefilter is a superset of the id regex on every counted site; and the ledger arithmetic answers all three verdicts from one measurement — exact baseline green, empty baseline all-growth, over-pinned baseline stale without invented growth) and the dispatch-gates declaration (every separator-less walked root declared as a subtree — `packages/**` included since #13297 — nothing declared this gate does not walk, the over-claim bounded to SKIP_PATHS) all hold.'
1990+
console.log('✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees and docs/{audits,handoff,plans} out), detection, the dead-root hard error (red when a ROOT is renamed, green when restored), the empty-scan hard error (red when a root yields nothing and when the whole scan does, green when restored), the published-catalog internal-id rule (red on a planted id in prose, in a fenced comment and in the repo#NNNN spelling, green when removed; hex colours, version numbers, HTTP codes, array indices and the "#1" ordinal all pass; references/ reached, generated artifacts and the internal roots out; the `#<n>` placeholder passes while the concrete ids it replaced stay red, with no exemption to reach for), the spec customer-facing-text internal-id rule (red on an id planted on a LATER line of a concatenated message — the shape a line-oriented census cannot see, proven here — and in a template chain, a positional validator message, the repo#NNNN spelling, a nested strictObject `guidance` prescription, a HOISTED guidance const, a `KeySetGuidance` const consumed only CROSS-MODULE in both the annotated and the `as const satisfies` spelling, a HOISTED refusal message, a `retiredKey()` tombstone, `new Map` and `Object.freeze` guidance tables, `.describe()` prose, and the nested `guidance` of a whole options table written `satisfies StrictObjectOptions`; green when removed; an ADR id on a tombstone, a `.default()` VALUE, `history`/`guidance` outside a strictObject options position, `extraKeys` key names and an inferred local that merely MENTIONS `KeySetGuidance` all pass; test bodies out; the seen floor is PER BUCKET so one matcher rotting while the others carry the total still reds; and the two TYPE ANCHORS are pinned on the predicate itself — the annotation, `satisfies` and `as const satisfies` spellings all read as a strictObject options position while some other satisfied type does not, and the `*_STRICT_OPTIONS` NAME branch still fires where no type is written at all — which is the only place they can be told apart, since end to end they are redundant), the fourth population — customer-facing text BUILT INSIDE A FUNCTION (red on an id in an inline `error: () =>` callback, in a const the callback only dispatches to, inside a `message:` builder function, RETURNED from a tombstone-prescription builder, in a `: StrictObjectOptions` options factory, and in a plain `error:` string; ⛔ the body of an ordinary helper and a local inside a recognised factory stay unswept, because the climb crosses a function only when the FUNCTION sits in a recognised position; and `functionBuilt` carries its own blindness floor, since an unrecognised spelling produces no flag SILENTLY), the FIFTH population — prose built inside plain `function` DECLARATIONS (#13156: red on an id in a declaration consumed by `message:`, RETURNED to a `retiredKey()` argument, and in a const the declaration only dispatches to; its own `functionDeclared` bucket with its own floor, so the declaration clause rotting cannot hide behind the arrows; ⛔ an unconsumed declaration and one consumed only by an unrecognised call stay unswept — the clause is the fourth population\'s, one declaration form over, never an unconditional crawl), the GENERATED table — a prescription filed under each of a list of keys by `Object.fromEntries(keys.map(…))` rather than written as an object literal (red both HOISTED into a const spread into an options factory\'s `guidance` and generated INLINE at the `guidance:` key itself, green when the id is removed; ⛔ and a generated VALUE table reaching no sink stays unswept, because `.map()` is TRANSPARENT to the climb and never a position of its own), the Rule 3 boundary OUTPUT (names the position-based root AND the ledgered cross-package leg\'s root, exclusion and baseline, no longer claims siblings are unscanned, and lists every floored bucket — derived from the same constants the scans read), the CROSS-PACKAGE prose-id leg (#13297: a concatenation-split id in a plain helper is counted — total-string coverage, no position climb to rot; a `//` comment, a test body and the spec subtree are out; an id inside a template\'s embedded expression counts exactly once; a 6-digit colour never matches while the cross-repo spelling\'s id half does; the prefilter is a superset of the id regex on every counted site; and the ledger arithmetic answers all three verdicts from one measurement — exact baseline green, empty baseline all-growth, over-pinned baseline stale without invented growth; and, the baseline now being empty, the SEEN FLOOR that took over the stale arm\'s blindness-floor job reds a below-floor reading on each measure independently, a dormant walker and a missing measure included, while a reading at the floor stays green) and the dispatch-gates declaration (every separator-less walked root declared as a subtree — `packages/**` included since #13297 — nothing declared this gate does not walk, the over-claim bounded to SKIP_PATHS) all hold.'
19491991
+ ` — ${declaredBatteries.length} declared batteries, ${totalCases} cases registered, every`
19501992
+ ' battery at or above its pinned floor.');
19511993
return SELF_TEST_VERDICT;
@@ -3012,6 +3054,33 @@ function selfTestPackagesProse(expect, battery) {
30123054
stale.stale.length, 1);
30133055
expect('...named precisely', stale.stale[0]?.file, 'packages/widgets/src/gone.ts');
30143056
expect('...without inventing growth', stale.growth.length, 0);
3057+
3058+
battery('cross-package seen floor');
3059+
// ── The seen floor: the blindness floor once the baseline is empty ────────
3060+
// The fixture tree above is a handful of files, so its OWN reading is the
3061+
// below-floor case: judged against the real floor it must red on both
3062+
// measures, from the same function main() asks.
3063+
const fixtureBreaches = packageProseSeenFloorBreaches(r).map((b) => b.measure).sort().join(',');
3064+
expect('a below-floor reading reds on both measures (the fixture tree against the real floor)',
3065+
fixtureBreaches, 'filesParsed,stringsSeen');
3066+
expect('a dormant walker (0 sources, 0 strings) reds on both measures',
3067+
packageProseSeenFloorBreaches({ filesParsed: 0, stringsSeen: 0 }).length, 2);
3068+
const atFloor = { ...PACKAGES_PROSE_SEEN_FLOOR };
3069+
expect('a reading exactly AT the floor is green', packageProseSeenFloorBreaches(atFloor).length, 0);
3070+
expect('one string short reds on that measure alone — the other cannot cover for it',
3071+
packageProseSeenFloorBreaches({ ...atFloor, stringsSeen: atFloor.stringsSeen - 1 })
3072+
.map((b) => b.measure).join(','), 'stringsSeen');
3073+
expect('one source short reds on that measure alone',
3074+
packageProseSeenFloorBreaches({ ...atFloor, filesParsed: atFloor.filesParsed - 1 })
3075+
.map((b) => b.measure).join(','), 'filesParsed');
3076+
expect('a measure the reading does not carry is a breach, never a pass',
3077+
packageProseSeenFloorBreaches({ stringsSeen: atFloor.stringsSeen }).map((b) => b.measure).join(','),
3078+
'filesParsed');
3079+
expect('the breach names the reading and the floor it fell under',
3080+
JSON.stringify(packageProseSeenFloorBreaches({ ...atFloor, filesParsed: 3 })[0]),
3081+
JSON.stringify({ measure: 'filesParsed', seen: 3, floor: atFloor.filesParsed }));
3082+
expect('the pinned floor is a real floor on both measures (a zero floor passes a blind scan)',
3083+
PACKAGES_PROSE_SEEN_FLOOR.filesParsed > 0 && PACKAGES_PROSE_SEEN_FLOOR.stringsSeen > 0, true);
30153084
}
30163085

30173086
function main() {
@@ -3132,6 +3201,7 @@ function main() {
31323201
}
31333202
const prosePinned = JSON.parse(readFileSync(PACKAGES_PROSE_LEDGER, 'utf8'));
31343203
const { growth: proseGrowth, stale: proseStale } = comparePackageProseLedger(packageProse.counts, prosePinned);
3204+
const proseFloorBreaches = packageProseSeenFloorBreaches(packageProse);
31353205

31363206
// The boundary is stated on EVERY verdict, red or green — a reader of the
31373207
// green line must be able to see what the clean bill covers, and a reader of
@@ -3237,6 +3307,22 @@ function main() {
32373307
);
32383308
}
32393309

3310+
if (proseFloorBreaches.length > 0) {
3311+
failed = true;
3312+
console.error(`\n✗ doc authoring guard: the cross-package leg read LESS of the tree than its seen floor allows:\n`);
3313+
for (const b of proseFloorBreaches) console.error(` ${b.measure}: ${b.seen} measured, floor ${b.floor}`);
3314+
console.error(
3315+
`\nThe prose-id baseline (${PACKAGES_PROSE_LEDGER}) is empty, so its verdict is only`
3316+
+ `\nworth something if the scan actually read the tree: a walker or prefilter that goes`
3317+
+ `\nblind reads 0 ids against 0 pinned, which the ledger alone would print as green. Find`
3318+
+ `\nwhat stopped reaching the sources under ${PACKAGES_PROSE_ROOT}/ — the walk in`
3319+
+ `\ncollectPackageProseFiles(), the ${PACKAGES_PROSE_EXCLUDED} exclusion, PACKAGES_PROSE_PREFILTER,`
3320+
+ `\nthe string visit in scanPackageProseIds() — and restore it. If the population really`
3321+
+ `\nshrank, re-measure with --census and state both readings in the PR; lowering`
3322+
+ `\nPACKAGES_PROSE_SEEN_FLOOR is ${RATCHET_AUTHORITY_MARKER}, NOT a co-equal option.\n`,
3323+
);
3324+
}
3325+
32403326
if (proseGrowth.length > 0) {
32413327
failed = true;
32423328
console.error(`\n✗ NEW internal issue-id reference(s) in sibling-package string prose:\n`);
@@ -3291,7 +3377,9 @@ function main() {
32913377
console.log(
32923378
`✓ doc authoring guard: sibling-package prose ids hold the baseline — `
32933379
+ `${packageProse.sites.length} pinned site(s) across ${packageProse.counts.size} file(s), `
3294-
+ `${packageProse.stringsSeen} string(s) read in ${packageProse.filesParsed} parsed source(s), `
3380+
+ `${packageProse.stringsSeen} string(s) read in ${packageProse.filesParsed} parsed source(s) `
3381+
+ `(seen floor ${PACKAGES_PROSE_SEEN_FLOOR.stringsSeen} strings / `
3382+
+ `${PACKAGES_PROSE_SEEN_FLOOR.filesParsed} sources), `
32953383
+ `no growth, no burn-down unrecorded.`,
32963384
);
32973385
}

0 commit comments

Comments
 (0)