diff --git a/.changeset/9140-changeset-claims-blind-spot-census.md b/.changeset/9140-changeset-claims-blind-spot-census.md new file mode 100644 index 0000000000..5594972906 --- /dev/null +++ b/.changeset/9140-changeset-claims-blind-spot-census.md @@ -0,0 +1,9 @@ +--- +--- + +Count the population `check-changeset-claims` cannot reach. Its `--audit` mode +now reports how many pending bodies publish a CHANGELOG paragraph but spell no +name the gate can resolve — the ones that coordinate themselves by symbol — so +the size of that blind spot is re-derived on every run instead of hand-counted +per incident (objectui#9140). The gate's coordinate is unchanged and it remains +report-only. Tooling and tests only; no package is released by this change. diff --git a/scripts/__tests__/check-changeset-claims.test.ts b/scripts/__tests__/check-changeset-claims.test.ts index 1a36627075..140f7d0201 100644 --- a/scripts/__tests__/check-changeset-claims.test.ts +++ b/scripts/__tests__/check-changeset-claims.test.ts @@ -5,7 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { namedFiles, paragraphNaming, resolveNamed, treeIndex } from '../check-changeset-claims.mjs'; +import { audit, namedFiles, paragraphNaming, resolveNamed, treeIndex } from '../check-changeset-claims.mjs'; /** * objectui#9003 — a pending changeset's prose is judged by nothing. @@ -418,3 +418,97 @@ describe('resolveNamed', () => { expect(resolveNamed(index, 'package.json')).toBeNull(); }); }); + +// ── 6. the blind spot is COUNTED, not hand-counted per incident ────────────── + +/** + * objectui#9140 — the shape this gate cannot see, made measurable. + * + * objectui#9065 was five WENT-FALSE claims at once; the gate named four and was + * blind to the fifth. The fifth's body names no file at all — it coordinates + * itself by SYMBOL — so no diff can make the diff-mode check speak about it. + * That was established by counting backticked spans BY HAND, once, on one card. + * + * `--audit` now counts the population instead, so the size of the blind spot is + * re-derived on every run rather than written down (AGENTS.md rule #9). What is + * pinned here is the COUNTER, in both directions: + * + * - a symbol-coordinated body is counted as outside the reach (it fires), and + * - the FIRING NEGATIVE CONTROL: the same body with one resolvable file name + * added is NOT counted there. Without that leg, "the census sees it" cannot + * be told apart from "the census counts everything". + * + * The limit itself is pinned too: even when the change edits the very file the + * symbol-only body is about, the diff-mode gate still reports nothing about it. + * ⛔ That is the card's finding, pinned AS A LIMIT — not a bug to be fixed by + * loosening the coordinate, which objectui#9140 measured and left open. + */ +describe('objectui#9140 — bodies this gate can never reach', () => { + /** A fixture carrying one symbol-coordinated pending body. */ + function blindFixture(label: string, body: string): Fixture { + const fixture = fixtureRepo(label); + fixture.write('.changeset/9140-symbol-coordinated.md', body); + fixture.commit('a pending declaration that coordinates itself by symbol'); + return fixture; + } + + // The shape of `.changeset/7165-grid-dependent-values.md`: every coordinate it + // offers is a symbol, and not one of them is spelled as a file name. + const SYMBOL_ONLY = + '---\n' + + "'@fixture/alpha': patch\n" + + '---\n\n' + + 'The inline editor now supplies `dependentValues`, so a `dependsOn` column is\n' + + 'no longer permanently uneditable. `renderCellEditor` passes the saved row.\n'; + + it('counts a symbol-coordinated body as outside the reach', () => { + const fixture = blindFixture('blind', SYMBOL_ONLY); + const totals = audit(fixture.root, null); + + expect(totals.silent, 'a body naming no file is outside the diff-mode reach').toBeGreaterThanOrEqual(1); + expect(totals.blind.join('\n')).toContain('.changeset/9140-symbol-coordinated.md'); + }); + + it('FIRING NEGATIVE CONTROL — the same body naming one resolvable file is NOT counted there', () => { + // Known direction, and it must move: the ONLY edit between the two fixtures + // is one added backticked file name that resolves to exactly one tracked + // file. If the census counted every body, this leg would read identically + // to the one above and prove nothing. + const blind = audit(blindFixture('blind-ctl-a', SYMBOL_ONLY).root, null); + const seeing = audit( + blindFixture('blind-ctl-b', SYMBOL_ONLY.replace('the saved row.', 'the saved row, in `reconciliation.test.ts`.')) + .root, + null, + ); + + const named = '.changeset/9140-symbol-coordinated.md'; + expect(blind.blind.join('\n'), 'the symbol-only body IS in the blind list').toContain(named); + expect(seeing.blind.join('\n'), 'adding one resolvable file name takes it OUT').not.toContain(named); + // Non-empty guard: both runs must have looked at the same publishing + // population, so the difference above cannot come from one side reading + // nothing at all. + expect(seeing.publishing, 'both runs read the same publishing population').toBe(blind.publishing); + expect(blind.publishing).toBeGreaterThan(0); + expect(seeing.silent, 'the control body left the silent population').toBe(blind.silent - 1); + }); + + it('pins the LIMIT: the diff-mode gate stays silent even when the change edits the file that body is about', () => { + const fixture = blindFixture('blind-limit', SYMBOL_ONLY); + // `renderCellEditor` is a symbol this body names. Edit the file that + // declares it — the exact shape of the falsifying merge on objectui#9065. + fixture.write('packages/alpha/src/reconciliation.test.ts', 'export const renderCellEditor = () => null;\n'); + fixture.commit('edit the file the symbol-only body is about'); + + const run = runGate(fixture.root, lastCommitRange(fixture)); + + expect(run.status, 'report-only: still exit 0').toBe(0); + expect(run.output, 'the symbol-coordinated body is never reported').not.toContain( + '9140-symbol-coordinated.md', + ); + // Control with a known direction that HITS in this same run: the gate did + // look, and it can still speak about a body that names that same file. + expect(run.output, 'the gate was not simply silent about everything').toContain( + '.changeset/6794-declared-default.md', + ); + }); +}); diff --git a/scripts/check-changeset-claims.mjs b/scripts/check-changeset-claims.mjs index 9cc0211737..7e4fdc08a8 100644 --- a/scripts/check-changeset-claims.mjs +++ b/scripts/check-changeset-claims.mjs @@ -106,6 +106,42 @@ * it, or carves an exemption into it: the two gates are meant to fire together on * a correction, one asking for the read and the other recording the write. * + * ## What objectui#9140 measured about this gate, after five live instances + * + * objectui#9065 was five WENT-FALSE claims at once. The gate was re-run on each + * falsifying merge's own range, and the answer split two ways. Both halves are + * recorded here because each one contradicts something this header assumes. + * + * HALF ONE - it named four of the five, and nobody acted on any of them. All + * four false paragraphs then sat on `main` as pending release-note input until + * objectui#9065 was filed BY HAND, and three had already been folded into the + * standing Changesets release PR's staged CHANGELOGs. ⇒ the "REQUEST TO READ" + * theory of change stated above has a measured delivery record, and on the one + * shape this gate exists for that record is zero for four. ⛔ That is NOT an + * argument for making it block - the measurement against blocking is unchallenged + * and "Why it can never block" still rules. It is the datum whoever revisits that + * trade needs, recorded rather than lost. + * + * HALF TWO - the fifth was invisible, and that is a SECOND historical instance on + * the runner-up's side of the coordinate trade measured above. The one this gate + * could not see is exactly the one whose body names no file at all: it coordinates + * itself by symbol. `--audit` now counts that population - bodies that publish but + * spell no name this gate can resolve - so the blind spot is re-derived on every + * run instead of hand-counted per incident (rule #9 in AGENTS.md). + * + * ⛔ The coordinate was NOT widened, and the reason is a measurement, not taste. + * objectui#9140 asked what it would take to bring that fifth instance into the + * census. Its body reaches a file this gate could resolve through exactly ONE + * spelling - the stem of a file name - and that one spelling is backticked by a + * large fraction of the whole pending queue, because it is the name of one of this + * repo's principal components. Admitting it admits every changeset that mentions + * that component. Re-measured over the same 25-first-parent-commit population the + * table above uses, that widening lands in the same band the runner-up was REJECTED + * in, and it lands there on a channel half one just measured at zero for four. ⇒ + * widening the coordinate is a readability-budget decision, it is open, and it is + * deliberately not taken here. The numbers are on objectui#9140; ⛔ they are not + * copied into this header, which is rule #9 applied to this paragraph. + * * ## Where it runs * * `changeset-presence.yml`, as a second job. That workflow carries NO path @@ -380,19 +416,43 @@ export function audit(root, ref = null) { path.endsWith('.md') && !NOT_A_CHANGESET.has(path.slice(path.lastIndexOf('/') + 1)), ); - const totals = { pending: pendingPaths.length, publishing: 0, naming: 0, resolved: 0, unresolved: 0, dangling: [] }; + const totals = { + pending: pendingPaths.length, + publishing: 0, + naming: 0, + resolved: 0, + unresolved: 0, + dangling: [], + silent: 0, + unreachable: 0, + blind: [], + }; for (const [path, source] of readBlobs(root, ref, pendingPaths)) { const declaration = describeDeclaration(source); if (declaration.kind !== 'frontmatter' || declaration.entries === 0) continue; totals.publishing += 1; const spans = namedFiles(source); - if (spans.length === 0) continue; + if (spans.length === 0) { + // Coordinates itself by SYMBOL, or by nothing. No diff can ever make the + // diff-mode check speak about this body (objectui#9140). + totals.silent += 1; + totals.blind.push(`${path} ~ names no file at all`); + continue; + } totals.naming += 1; + let resolvedHere = 0; for (const span of spans) { if (resolveNamed(index, span) === null) { totals.unresolved += 1; totals.dangling.push(`${path} ~ ${span}`); - } else totals.resolved += 1; + } else { + totals.resolved += 1; + resolvedHere += 1; + } + } + if (resolvedHere === 0) { + totals.unreachable += 1; + totals.blind.push(`${path} ~ every name it spells resolves to none or to many`); } } return totals; @@ -424,10 +484,20 @@ if (isEntrypoint(import.meta.url)) { `File names in those bodies: ${totals.resolved} resolve to exactly one tracked file, ` + `${totals.unresolved} resolve to none or to many.`, ); + console.log( + `\nOutside the diff-mode check's reach ENTIRELY: ${totals.silent} publishing body(ies) name no file ` + + `at all,\nand ${totals.unreachable} spell only names that resolve to none or to many. Those ` + + `${totals.silent + totals.unreachable} cannot be\nreported for ANY diff — that is the size of the ` + + 'blind spot (objectui#9140), re-derived each run.', + ); + if (process.argv.includes('--blind')) for (const line of totals.blind) console.log(` ${line}`); console.log( '\n A name that resolves to nothing is ⛔ NOT a false claim on its own — a changeset that\n' + ' retired a file names it correctly and the file is correctly gone. This count is the\n' + - " reach of the diff-mode check, not a verdict on anybody's prose.", + " reach of the diff-mode check, not a verdict on anybody's prose.\n" + + ' ⛔ Nor is a body that names no file WRONG for coordinating itself by symbol — that is a\n' + + ' normal way to write a changeset. It is simply a body this gate cannot carry a request\n' + + ' to read about, and objectui#9140 measured one WENT-FALSE claim that sat there unseen.', ); process.exit(0); }