@@ -11160,6 +11160,57 @@ function selfTest() {
1116011160 console.log(` ${cond ? '✓' : '✗'} ${name}`);
1116111161 };
1116211162
11163+ // ── A subject this TREE cannot decide is not a passing case (#15255) ──────
11164+ //
11165+ // A handful of cases below read the REAL corpus rather than a fixture,
11166+ // because a fixture cannot show that a live specimen still reaches the tree
11167+ // it names. That is the right shape, and it carries one hazard a fixture does
11168+ // not: the specimen's population is a property of the tree, and a tree may
11169+ // legitimately hold none of it. `.changeset/*.md` is the measured instance —
11170+ // a changesets version pass consumes the whole population by design, so the
11171+ // Version Packages PR carries a tree the corpus assertions cannot be
11172+ // evaluated over, and `main` carries one for as long as it takes the next
11173+ // changesets to land.
11174+ //
11175+ // The control that guarded them asserted the population itself
11176+ // (`length >= 100`), which is a claim about the release cycle rather than
11177+ // about this tool, and it made a REQUIRED context red on the one PR whose
11178+ // merge IS the release. ⛔ The repair is not a quiet `if` around them either:
11179+ // a silent skip is exactly how a specimen rots unnoticed, which is the
11180+ // failure the population control was reaching for in the first place.
11181+ //
11182+ // So an undecidable subject is neither: it prints its own line, states WHY
11183+ // this tree cannot decide it in words a reader can check, and is counted
11184+ // apart from `cases` in the verdict. It is never pushed into `cases` — there
11185+ // it would be one more `✓`, indistinguishable from a case that ran.
11186+ const notMeasured = [];
11187+ const unmeasurable = (subject, why) => {
11188+ notMeasured.push([subject, why]);
11189+ console.log(` ⊘ NOT MEASURED — ${subject}`);
11190+ console.log(` ${why}`);
11191+ };
11192+
11193+ /**
11194+ * The verdict's NOT-MEASURED suffix — a pure renderer so the two properties
11195+ * that matter can be pinned on fixtures instead of on a run of this tree,
11196+ * which by construction skips nothing: it is EMPTY when nothing was skipped,
11197+ * so a fully-measured run's verdict line is byte-identical to the one this
11198+ * file printed before the tally existed; and it NAMES every skipped subject
11199+ * when there is one, so no skip can reach a reader as a bare pass count.
11200+ */
11201+ const notMeasuredSuffix = (entries) =>
11202+ entries.length ? ` ⊘ ${entries.length} subject(s) NOT MEASURED on this tree — ${entries.map(([s]) => s).join(' · ')}.` : '';
11203+
11204+ /**
11205+ * The `.changeset/*.md` live specimen's population in a corpus, and the whole
11206+ * decision behind the block far below: with one member the corpus assertions
11207+ * are real, with none they are vacuous. Named and pure so both of its call
11208+ * sites ask the same question and so the boundary can be pinned at the sizes
11209+ * the release cycle really produces, rather than only at the one this tree
11210+ * happens to be at today.
11211+ */
11212+ const changesetSpecimenPop = (corpus) => corpus.filter((f) => /^\.changeset\/[^/]+\.md$/.test(f));
11213+
1116311214 const wf = [
1116411215 'jobs:',
1116511216 ' lint:',
@@ -13045,14 +13096,86 @@ function selfTest() {
1304513096 // `.changeset/*.md` into `.changeset/.md`, so the hint reached ZERO of 548
1304613097 // tracked changesets while reading as an ordinary literal, and the residue
1304713098 // then named a directory rename as the cause. Read from the REAL corpus: a
13048- // fixture cannot show that the tree still holds the population the trap needs,
13049- // and this one grows with every merged PR.
13099+ // fixture cannot show that the hint still reaches the population the trap was
13100+ // sprung on. What that corpus is NOT is something this file may require: it
13101+ // grows with every merged PR and a version pass takes all of it back (#15255,
13102+ // and the docblock under `changesetPop` below).
1305013103 const suffixCorpus = trackedFiles();
13051- const changesetPop = suffixCorpus.filter((f) => /^\.changeset\/[^/]+\.md$/.test(f));
13052- t('the tree really does hold a large `.changeset/*.md` population', changesetPop.length >= 100);
13053- t('the live specimen reaches every changeset it names', changesetPop.every((f) => hintCovers('.changeset/*.md', f)));
13054- t('and claims nothing else in the whole tree', suffixCorpus.filter((f) => hintCovers('.changeset/*.md', f)).length === changesetPop.length);
13055- t('so it is nobody\'s dead literal any more', hintReachesTree('.changeset/*.md', suffixCorpus));
13104+ const changesetPop = changesetSpecimenPop(suffixCorpus);
13105+ // ⛔ NOT `changesetPop.length >= 100` (#15255). That control was reaching for
13106+ // something real — the two assertions under it pass VACUOUSLY over an empty
13107+ // population, `every` on nothing and `0 === 0` — but it bought the guard with
13108+ // a claim this tool has no standing to make. The size of that population is
13109+ // owned by the release cycle: a changesets version pass consumes all of it,
13110+ // so it is 1 on the Version Packages PR (measured on #11336's head
13111+ // b8573e843: `.changeset/` holds README.md and config.json and nothing else),
13112+ // it is whatever has landed since on `main` for the days after, and it is 865
13113+ // here. A required context that reds at 1 and at 3 blocks the release itself,
13114+ // and then blocks `main` behind it.
13115+ //
13116+ // What a gate CAN require is the specimen's HOME. `.changeset/config.json` is
13117+ // the changesets tool's own configuration: while it is tracked, an empty
13118+ // population is this repo mid-cycle and the specimen is merely resting; when
13119+ // it goes, the specimen has no population to come back to and somebody must
13120+ // pick a new one for this species. That distinction is the whole difference
13121+ // between "not measurable today" and "rotted", and it is the one the count
13122+ // could not draw.
13123+ //
13124+ // The vacuity the count was guarding is closed by construction instead: the
13125+ // three corpus assertions run only where there is a corpus, and where there
13126+ // is not, `unmeasurable` says so out loud rather than letting them green.
13127+ // Note the population is 1, not 0, on the real release PR — `README.md`
13128+ // survives a version pass — so a guard written at `=== 0` would have left the
13129+ // gate red on the very tree it was written for. It is written at "empty" and
13130+ // measured at 0, 1, 3 and 865.
13131+ t('the `.changeset/*.md` specimen still has a home in this tree', suffixCorpus.includes('.changeset/config.json'));
13132+ if (changesetPop.length === 0) {
13133+ unmeasurable(
13134+ 'the `.changeset/*.md` live-specimen corpus assertions',
13135+ 'this tree tracks no `.changeset/*.md` at all — that is what a changesets version pass produces, and the ' +
13136+ 'population comes back as changesets land. The specimen still has its home (see the case above); the three ' +
13137+ 'assertions that need a population are the only thing skipped, and every literal case in this block ran. ' +
13138+ "Check it yourself: git ls-files '.changeset/'",
13139+ );
13140+ } else {
13141+ t('the live specimen reaches every changeset it names', changesetPop.every((f) => hintCovers('.changeset/*.md', f)));
13142+ t('and claims nothing else in the whole tree', suffixCorpus.filter((f) => hintCovers('.changeset/*.md', f)).length === changesetPop.length);
13143+ t('so it is nobody\'s dead literal any more', hintReachesTree('.changeset/*.md', suffixCorpus));
13144+ }
13145+ // The branch above is a decision this tree can only exercise one way — it
13146+ // holds a population today and will hold one on almost every run — so the
13147+ // sizes the release cycle really produces are pinned on fixtures, at the four
13148+ // states named in the docblock. A boundary written at the wrong one is the
13149+ // defect that shipped: `>= 100` is green at 865 and red at every size a
13150+ // version pass leaves behind.
13151+ const csTree = (...names) => ['AGENTS.md', '.changeset/config.json', ...names];
13152+ t('at 865 the corpus assertions run, which is this tree and every ordinary day',
13153+ changesetSpecimenPop(csTree(...Array.from({ length: 865 }, (_, i) => `.changeset/c${i}.md`))).length === 865);
13154+ t('at 3 they still run — the state `main` is in for days after a release lands, and where `>= 100` was red',
13155+ changesetSpecimenPop(csTree('.changeset/a.md', '.changeset/b.md', '.changeset/c.md')).length === 3);
13156+ t('at 1 they still run, and 1 is what the Version Packages PR really carries — README.md survives a version pass',
13157+ changesetSpecimenPop(csTree('.changeset/README.md')).length === 1);
13158+ t('only an EMPTY population is undecidable, and that is the only state that skips',
13159+ changesetSpecimenPop(csTree()).length === 0);
13160+ // The home discriminator, both directions: it is what separates "resting" from
13161+ // "rotted", so it must not answer the same way for a tree that has retired
13162+ // changesets altogether.
13163+ t('a tree mid-cycle still has the specimen home, so an empty population reads as resting',
13164+ csTree().includes('.changeset/config.json'));
13165+ t('...while a tree that retired changesets has no home, and the case above reds instead of skipping',
13166+ !['AGENTS.md', 'package.json'].includes('.changeset/config.json'));
13167+ // The tally the skip is reported through. Pinned here rather than at the
13168+ // verdict because a green run of this file never reaches the non-empty branch
13169+ // of it, so nothing else in this program can show that a skip is visible.
13170+ t('a run that skipped nothing prints the verdict it always printed', notMeasuredSuffix([]) === '');
13171+ t('...and a run that skipped something names it, so no skip reaches a reader as a bare pass count',
13172+ notMeasuredSuffix([['a subject', 'a reason']]).includes('NOT MEASURED') &&
13173+ notMeasuredSuffix([['a subject', 'a reason']]).includes('a subject'));
13174+ t('...naming every one of them, never just a count',
13175+ notMeasuredSuffix([['first', 'x'], ['second', 'y']]).includes('first') &&
13176+ notMeasuredSuffix([['first', 'x'], ['second', 'y']]).includes('second'));
13177+ t('and a skipped subject is never a case, so the pass count cannot absorb one',
13178+ !cases.some(([name]) => name.includes('NOT MEASURED')));
1305613179 t('the extension the glob names is honoured', !hintCovers('.changeset/*.md', '.changeset/config.json'));
1305713180 t('a single `*` matches exactly one segment here too', !hintCovers('.changeset/*.md', '.changeset/pre/x.md'));
1305813181 t('a directory surface above it still derives the gate', hintCovers('.changeset/*.md', '.changeset'));
@@ -16739,7 +16862,25 @@ function selfTest() {
1673916862 t('...and its reason names the prefix that went missing', unreachableReason(patternPrefixGone).includes('packages/gone-away'));
1674016863 // The live half: the specimen is gone from the residue entirely, which is
1674116864 // what the card was filed for. A fixture cannot show that.
16742- t('the live specimen is not a dead literal on this tree at all', hintReachesTree('.changeset/*.md', trackedFiles()));
16865+ //
16866+ // It reads the same live population as the `globCarriesLiteralSuffix` block
16867+ // above, so it takes the same exit when a version pass has consumed it
16868+ // (#15255) — the second site, and the reason `unmeasurable` is a primitive
16869+ // rather than one `if` written once. Left unguarded, this one case would have
16870+ // kept the release-blocking red after the block above stopped producing it,
16871+ // which is the shape a repair applied at only the site that was measured red
16872+ // always has.
16873+ const residueCorpus = trackedFiles();
16874+ if (residueCorpus.some((f) => /^\.changeset\/[^/]+\.md$/.test(f))) {
16875+ t('the live specimen is not a dead literal on this tree at all', hintReachesTree('.changeset/*.md', residueCorpus));
16876+ } else {
16877+ unmeasurable(
16878+ "the residue block's live `.changeset/*.md` specimen",
16879+ 'a hint reaches the tree by matching a FILE, and a tree whose version pass has consumed the population has ' +
16880+ 'no file for this one to match. The judgment this case guards — that a pattern-judged hint is not read as a ' +
16881+ 'dead literal — is pinned on fixtures a few lines above and ran.',
16882+ );
16883+ }
1674316884
1674416885 // ── A slash is not proof of a path (#10097, option C) ─────────────────────
1674516886 //
@@ -19041,11 +19182,19 @@ function selfTest() {
1904119182 for (const [, cond] of cases) {
1904219183 if (!cond) failed++;
1904319184 }
19185+ // The NOT-MEASURED tally rides on BOTH verdicts (#15255). A reader who sees
19186+ // only the pass count cannot tell a run that measured everything from one
19187+ // that skipped a subject, and that indistinguishability is the whole failure
19188+ // mode a silent skip introduces — so the count is repeated here, next to the
19189+ // number it would otherwise be hiding inside. It never moves the exit code:
19190+ // "this tree cannot decide it" is not a defect in this tool, and a gate that
19191+ // reds for it is the release blocker this replaced.
19192+ const skipped = notMeasuredSuffix(notMeasured);
1904419193 if (failed) {
19045- console.error(`✗ dispatch-gates self-test: ${failed} of ${cases.length} case(s) failed.`);
19194+ console.error(`✗ dispatch-gates self-test: ${failed} of ${cases.length} case(s) failed.${skipped} `);
1904619195 process.exit(1);
1904719196 }
19048- console.log(`✓ dispatch-gates self-test: ${cases.length} cases pass.`);
19197+ console.log(`✓ dispatch-gates self-test: ${cases.length} cases pass.${skipped} `);
1904919198
1905019199 return SELF_TEST_VERDICT;
1905119200}
0 commit comments