From 87d239b128b3a8312b3aa8763932dfd0a9c64b65 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 22:49:13 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(gate):=20read=20BORN-FALSE=20claims=20?= =?UTF-8?q?=E2=80=94=20an=20address=20this=20change's=20own=20diff=20moves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check-changeset-claims.mjs` covered WENT FALSE only and said so in its own output. This adds the second reading, over a corpus of its own: the prose this change publishes about ITSELF — the pull request body (from the event payload the job already receives) plus the changeset bodies this change adds. The coordinate is an unbound backticked LINE ADDRESS resolving to one tracked file this change touches, and the question asked of it is arithmetic rather than semantic: does this diff's own hunk map the cited base line somewhere else, or is the file one this change creates, so the number can only have been read from a tree that exists nowhere outside this pull request? An address whose own SENTENCE names the tree it was read from is never reported — that is the durable form both carded pull requests converged on, and a gate that reported it would teach authors to unbind. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- .github/workflows/changeset-presence.yml | 9 +- scripts/check-changeset-claims.mjs | 528 +++++++++++++++++++- scripts/render-changeset-claims-comment.mjs | 88 +++- 3 files changed, 594 insertions(+), 31 deletions(-) diff --git a/.github/workflows/changeset-presence.yml b/.github/workflows/changeset-presence.yml index 67e6af2b90..462de968e1 100644 --- a/.github/workflows/changeset-presence.yml +++ b/.github/workflows/changeset-presence.yml @@ -246,9 +246,16 @@ jobs: ); } + // BOTH halves (objectui#9509). `findings` is the went-false half — + // somebody else's pending changeset — and `bornFalse` is this pull + // request's own prose. A born-false-only run must still create the + // comment, or the one half nothing later will ever turn red is + // delivered to the job log the ruling measured at zero answers out + // of four. let findings = 0; try { - findings = JSON.parse(fs.readFileSync('claims.json', 'utf8')).findings.length; + const measured = JSON.parse(fs.readFileSync('claims.json', 'utf8')); + findings = measured.findings.length + (measured.bornFalse ?? []).length; } catch (error) { core.warning(`Could not read the finding count: ${error.message}`); } diff --git a/scripts/check-changeset-claims.mjs b/scripts/check-changeset-claims.mjs index 6cd26b02dd..baea62269a 100644 --- a/scripts/check-changeset-claims.mjs +++ b/scripts/check-changeset-claims.mjs @@ -30,10 +30,73 @@ * WENT FALSE - true when written, falsified by a LATER merge * (objectui#7721, objectui#8617). * - * ⛔ This gate covers WENT FALSE ONLY, and says so in its own output. It cannot - * see a born-false claim: a changeset this change ADDS is excluded by - * construction, and objectui#8759's defect was a cardinal in prose, which is the - * kind of reading the next section refuses to attempt. + * This gate opened covering WENT FALSE ONLY, and said so in its own output. It + * now carries a SECOND reading for BORN FALSE (objectui#9509), over a different + * corpus and on a different coordinate — see "The born-false reading" below. + * ⛔ The two readings are never merged: the went-false reading still excludes a + * changeset this change adds, because a body naming its own pull request's files + * is the normal case and reporting it would fire on nearly every change here. + * + * ## The born-false reading (objectui#9509) — corpus, coordinate, carve-out + * + * objectui#9509 measured three instances in one day across two pull requests. + * ⛔ None of them was in a pending changeset, and that is the whole reason this + * reading needed a corpus of its own: + * + * 1. objectui#9496, in its PULL REQUEST BODY: a test frame cited as `:281:75` + * that the same branch's later edit moved to `:283:75` — and then to + * `:299:75`. + * 2. objectui#9496, in its PULL REQUEST BODY: "recurs exactly twice: `:223` + * ... and `:246`", falsified by the 28-line comment block THE SAME DIFF + * inserts at `imported-defaults.ts:221`. At the head `:223` is rewritten and + * `:246` is at `:274` — and both numbers then land inside the block that + * displaced them, so a reader following the citation reaches prose ABOUT + * the claim instead of the code it is about. + * 3. objectui#9495, in a SOURCE DOCBLOCK: "a grep for `shortcut` finds that + * member first", falsified by the same diff's own 28-line insertion above + * it. ⛔ Out of reach here and deliberately — see the limits below. + * + * CORPUS: the prose this change PUBLISHES ABOUT ITSELF. That is the pull request + * body (read from `GITHUB_EVENT_PATH`, or `--pr-body ` locally) plus the + * `.changeset/*.md` bodies this change adds or modifies. ⛔ Not the tree at + * large: a citation written into an ordinary source file is the differential + * citation gate's population (`check-new-cross-file-line-citations.mjs`), and + * this repository treats a second reader over one population as a defect. + * + * COORDINATE: a backticked LINE ADDRESS — `some-file.ts:246`, or a bare `:246` + * continuing a file named earlier in the same paragraph — resolving to exactly + * one tracked file THIS CHANGE TOUCHES. Same resolution rule as the went-false + * reading, and for the same reason: an ambiguous spelling names nothing. + * + * The question asked of it is arithmetic, and is still not "is this sentence + * true?": + * + * MOVED the file is MODIFIED here, and this change's own hunks map the + * cited base line to a different head line, or delete it outright. + * The number was read from a tree this diff replaced. + * UNANCHORED the file is ADDED here, so the address can only ever have been + * read from a tree that exists nowhere but inside this pull request, + * and moves again on the next push. Instance 1 is this shape: the + * frame moved twice, between revisions of one branch. + * + * CARVE-OUT: an address whose own SENTENCE names the tree it was read from is + * ⛔ never reported. Both pull requests converged on that form independently and + * objectui#9509 carded it as the durable one: `` `:246` at `b8a006883d`, `:274` + * at this head ``. A number bound to a sha cannot re-stale, so a gate that + * reported it would be teaching authors to unbind. ⚠️ The binding is read per + * SENTENCE, not per paragraph: a sha floating three sentences away binds + * nothing, and a paragraph-wide window would have exempted instance 2. + * + * ⛔ What the born-false reading does NOT cover, so nobody reads it as more: + * - A claim with no line address at all. Instance 3 coordinates itself by + * ORDINAL ("finds that member first"), and deciding that requires reading + * what the sentence means — the one question triage fenced off, below. + * - An address into a file this change does not touch. That address may be + * false, but nothing about THIS diff made it so; it is the citation census's + * population and not this one's. + * - A pull request body EDITED without a push. `pull_request` fires on + * `opened`/`synchronize`/`reopened`, so a body rewritten on its own is read + * at the next push and not before. * * ## What it judges, and the one thing it refuses to judge * @@ -363,9 +426,11 @@ export function readBlobs(root, ref, paths) { * * 1. Changesets this change ADDS OR MODIFIES. A changeset naming files from its * own pull request is the normal case, not a finding - it would fire on - * every change that carries one, which is nearly all of them here. This is - * also the exclusion that makes the gate blind to BORN-FALSE claims, and - * that limit is reported rather than papered over. + * every change that carries one, which is nearly all of them here. ⛔ This + * exclusion is NOT relaxed by objectui#9509: the born-false reading takes + * those same bodies on a different coordinate (an unbound LINE ADDRESS into + * a file this change moves), which is rare where "names a file from its own + * pull request" is near-universal. * 2. Changesets that declare NO bump (empty frontmatter). Their body never * reaches a CHANGELOG, so there is no verbatim publication to protect. 427 * of the 1,384 pending here are these. @@ -479,6 +544,321 @@ export function audit(root, ref = null) { return totals; } +// -- the BORN-FALSE reading (objectui#9509) ----------------------------------- + +/** + * A backticked span that carries a LINE ADDRESS. + * + * Two spellings, and the second is why this is not a one-line regex over the + * whole body. `imported-defaults.ts:246` names its own file. `:246` does not — + * it CONTINUES a file named earlier in the same paragraph, which is how both + * measured instances were actually written, and reading it without that binding + * would turn every port number and every `key: 246` into a citation. + * + * A trailing column (`:281:75`) and a range (`:221-:248`, `ts:221-228`) are both + * kept: the first number is the one that moves, and the one a reader follows. + */ +const LINE_ADDRESS = /^(?[^\s`]*?):(?\d+)(?::\d+)?(?:-:?\d+)?$/; + +/** + * The sha a sentence binds its numbers to, if any. + * + * Hex, 7 to 40, with at least one of `a-f` in it — a bare `12345678` is a + * number, not a tree. `at this head` / `@head` counts too: it names the tree as + * definitely as a sha does, and it is the spelling the landed repairs used for + * the half of a pair that was read at the head. + */ +const SHA_BINDING = /\b(?=[0-9a-f]{7,40}\b)[0-9a-f]*[a-f][0-9a-f]*\b|@\s*head\b|\bat\s+(?:this\s+)?head\b/i; + +/** + * The sentence of `paragraph` containing the character at `index`. + * + * ⚠️ Deliberately NOT the paragraph. The went-false reading quotes a paragraph + * because a paragraph is the unit that ROTS (objectui#8617); a BINDING is the + * opposite kind of thing — it is an author saying which tree THIS number came + * from — and a sha three sentences away binds nothing. Measured against the + * carded instance: objectui#9496's §2 paragraph names a sha, and a + * paragraph-wide window would have exempted the very claim the card is about. + */ +export function sentenceAround(paragraph, index) { + const before = paragraph.slice(0, index); + const start = Math.max(before.lastIndexOf('. '), before.lastIndexOf('。'), before.lastIndexOf('\n')); + const rest = paragraph.slice(index); + const endOffset = rest.search(/\.\s|。|\n/); + const end = endOffset === -1 ? paragraph.length : index + endOffset + 1; + return paragraph.slice(start + 1, end); +} + +/** + * Every line address in one body, each already bound to the file it continues. + * + * Paragraph by paragraph, because the continuation binding is paragraph-scoped: + * a bare `:246` reaches back to the last span in ITS paragraph that named a file, + * and ⛔ never across a blank line into somebody else's subject. + * + * @param {string} source + * @param {(span: string) => string | null} resolve a spelling to one tracked file + * @returns {{ span: string, written: string, line: number, file: string, bound: boolean, + * paragraph: string, sentence: string }[]} + */ +export function lineAddresses(source, resolve) { + const out = []; + for (const paragraph of source.split(/\r?\n\s*\r?\n/)) { + let carried = null; + for (const match of paragraph.matchAll(/`([^`\n]{2,200})`/g)) { + const span = match[1].trim(); + // A plain file name with no number still sets the subject for the bare + // addresses after it: "`imported-defaults.ts` ... at `:223`". + if (NAMED_FILE.test(span) && PATH_SHAPED.test(span) && !span.includes(':')) { + const resolved = resolve(span); + if (resolved) carried = resolved; + continue; + } + const parsed = LINE_ADDRESS.exec(span); + if (!parsed) continue; + const written = parsed.groups.written; + let file = null; + if (written === '') { + file = carried; + } else if (PATH_SHAPED.test(written) && NAMED_FILE.test(written)) { + file = resolve(written); + if (file) carried = file; + } + if (!file) continue; + const sentence = sentenceAround(paragraph, match.index); + out.push({ + span, + written, + line: Number(parsed.groups.line), + file, + bound: SHA_BINDING.test(sentence), + paragraph: paragraph.replace(/\s+/g, ' ').trim(), + sentence: sentence.replace(/\s+/g, ' ').trim(), + }); + } + } + return out; +} + +/** + * `@@ -a,b +c,d @@` hunk headers for one file, base to head. + * + * `-U0` deliberately: context lines would merge neighbouring hunks and make the + * arithmetic below approximate, and this reading exists precisely to be exact + * about which line a number lands on. + */ +export function hunksFor(root, { base, head = null }, file) { + const args = ['diff', '-U0', '--no-color', base]; + if (head) args.push(head); + args.push('--', file); + const out = git(root, args, { allowFailure: true }) ?? ''; + const hunks = []; + for (const match of out.matchAll(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm)) { + hunks.push({ + oldStart: Number(match[1]), + oldCount: match[2] === undefined ? 1 : Number(match[2]), + newCount: match[4] === undefined ? 1 : Number(match[4]), + }); + } + return hunks; +} + +/** + * Where a BASE line ends up at the head, given this change's own hunks. + * + * `null` means the change rewrote or deleted that line outright — the sharpest + * shape of the defect, because the number now points at whatever the diff put + * there instead. + * + * A pure insertion (`oldCount === 0`) is spelled by git as "after old line + * `oldStart`", so it moves lines STRICTLY BELOW it and leaves `oldStart` itself + * alone. Getting that boundary wrong is an off-by-one that would report a + * stable citation as moved, which is the false positive this reading can least + * afford on a report-only channel. + */ +export function mapLine(hunks, line) { + let delta = 0; + for (const hunk of hunks) { + if (hunk.oldCount === 0) { + if (line <= hunk.oldStart) break; + delta += hunk.newCount; + continue; + } + if (line < hunk.oldStart) break; + if (line < hunk.oldStart + hunk.oldCount) return null; + delta += hunk.newCount - hunk.oldCount; + } + return line + delta; +} + +/** + * The born-false verdict for one address, given what this change did to its file. + * + * Pure, and injectable, so the controls below and the real run are judged by one + * function rather than by two that can disagree. + * + * @param {{ file: string, line: number, bound: boolean }} address + * @param {(file: string) => 'added' | 'modified' | null} statusOf + * @param {(file: string) => {oldStart: number, oldCount: number, newCount: number}[]} hunksOf + */ +export function judgeAddress(address, statusOf, hunksOf) { + if (address.bound) return { verdict: 'anchored', movedTo: null }; + const status = statusOf(address.file); + if (status === null) return { verdict: 'untouched', movedTo: null }; + if (status === 'added') return { verdict: 'unanchored', movedTo: null }; + const movedTo = mapLine(hunksOf(address.file), address.line); + if (movedTo === address.line) return { verdict: 'stable', movedTo }; + return { verdict: 'moved', movedTo }; +} + +/** Verdicts that are findings. The other two are the discrimination, not noise. */ +export const BORN_FALSE_VERDICTS = new Set(['moved', 'unanchored']); + +/** + * The firing controls, taken from the artefact this card was measured on. + * + * ⚠️ NOT a round number invented here. objectui#9496 inserts 28 lines at + * `imported-defaults.ts:221` and rewrites base `:223`; the landed pull request + * body and the landed docblock BOTH publish the arithmetic independently — + * `:246`@`b8a006883d` = `:274`@head. So 274 is attested outside this file and + * ⛔ cannot be moved by an implementation that is wrong: a mapper that drifts by + * one fails here rather than reporting a clean branch. + * + * The first two cases are the PAIR, one step apart: the same sentence, the same + * geometry, differing only in whether the number names the tree it was read + * from. The bound one must stay silent and the unbound one must fire — a + * control that only ever fires proves nothing about discrimination. + */ +export const BORN_FALSE_CONTROLS = [ + { + id: 'unbound-address-into-a-line-this-diff-moves', + why: "objectui#9496 §2: `:246` at the base is `:274` at the head, moved by the diff's own 28-line insertion", + body: 'The shape recurs exactly twice in `imported-defaults.ts`: `:223` and `:246`.', + want: (rows) => rows.length === 2 && rows.every((r) => BORN_FALSE_VERDICTS.has(r.verdict)) + && rows[0].movedTo === null && rows[1].movedTo === 274, + }, + { + id: 'the-same-address-bound-to-a-sha-is-silent', + why: 'the durable form both pull requests converged on is ⛔ never reported, or the gate teaches authors to unbind', + body: 'At `b8a006883d` the shape recurs twice in `imported-defaults.ts`: `:223` and `:246`.', + want: (rows) => rows.length === 0, + }, + { + id: 'an-address-this-diff-does-not-move-is-silent', + why: 'a line above every hunk is untouched — without this the reading is an absolute count of citations, not a differential', + body: 'See `imported-defaults.ts:100` for the walker entry point.', + want: (rows) => rows.length === 0, + }, + { + id: 'an-address-into-a-file-this-change-adds-is-unanchored', + why: 'instance 1: a frame in a file the branch itself creates was read from a tree that exists nowhere else, and moved twice', + body: 'The deep-clean control is at `imported-defaults-rest-less-tuple-9088.test.ts:281`.', + want: (rows) => rows.length === 1 && rows[0].verdict === 'unanchored', + }, +]; + +/** + * Runs every control through the real reader, against objectui#9496's geometry. + * + * HERMETIC — the hunks are the ones that pull request actually produced, written + * out here rather than read from the tree. A control that resolved against the + * live history would answer a different question after the next squash, and a + * gate whose controls rot reports "instrument broken" wherever it is not run + * from this checkout. + * + * `judgeFor` is injectable for one reason: a test has to be able to show that a + * judge which LIES fails these controls rather than passing them. A control + * suite that cannot be made to fail is decoration. + */ +export function evaluateBornFalseControls(judgeFor = judgeAddress) { + const MODIFIED = 'packages/types/src/zod/imported-defaults.ts'; + const ADDED = 'packages/types/src/__tests__/imported-defaults-rest-less-tuple-9088.test.ts'; + // `@@ -220,0 +221,28 @@` and `@@ -223 +251 @@`, as measured on 8700d6d93. + const hunks = [ + { oldStart: 220, oldCount: 0, newCount: 28 }, + { oldStart: 223, oldCount: 1, newCount: 1 }, + ]; + const resolve = (span) => + MODIFIED.endsWith('/' + span) ? MODIFIED : ADDED.endsWith('/' + span) ? ADDED : null; + const statusOf = (file) => (file === MODIFIED ? 'modified' : file === ADDED ? 'added' : null); + + return BORN_FALSE_CONTROLS.map((control) => { + let rows = []; + let ok = false; + let detail = ''; + try { + rows = lineAddresses(control.body, resolve) + .map((address) => ({ ...address, ...judgeFor(address, statusOf, () => hunks) })) + .filter((row) => BORN_FALSE_VERDICTS.has(row.verdict)); + ok = control.want(rows) === true; + detail = rows.map((r) => `${r.span} -> ${r.verdict}${r.movedTo ? ` (:${r.movedTo})` : ''}`).join(', ') || '(silent)'; + } catch (error) { + detail = `threw: ${error instanceof Error ? error.message : String(error)}`; + } + return { id: control.id, why: control.why, ok, detail }; + }); +} + +/** + * The born-false findings for this change, over the prose it publishes about + * itself. + * + * @param {string} root + * @param {{ base: string, head?: string | null, prBody?: string | null }} options + * @returns {{ findings: object[], corpus: {origin: string, addresses: number}[], read: number }} + */ +export function bornFalse(root, { base, head = null, prBody = null }) { + const changed = changedFiles(root, { base, head }); + const added = new Set(changedFiles(root, { base, head, filter: 'A' })); + const subject = changed.filter((file) => !file.startsWith(CHANGESET_DIR)); + const ownChangesets = changed.filter((file) => file.startsWith(CHANGESET_DIR)); + + const baseIndex = treeIndex(root, base); + const headIndex = treeIndex(root, head); + // Resolved against the HEAD tree as well as the base: a file this change ADDS + // is not in the base listing at all, and the unanchored half of this reading + // is exactly about those. + const resolve = (span) => resolveNamed(headIndex, span) ?? resolveNamed(baseIndex, span); + const touched = new Set(subject); + const statusOf = (file) => (touched.has(file) ? (added.has(file) ? 'added' : 'modified') : null); + + const hunkCache = new Map(); + const hunksOf = (file) => { + if (!hunkCache.has(file)) hunkCache.set(file, hunksFor(root, { base, head }, file)); + return hunkCache.get(file); + }; + + const corpus = []; + if (typeof prBody === 'string' && prBody.trim() !== '') { + corpus.push({ origin: 'the pull request body', source: prBody }); + } + for (const [path, source] of readBlobs(root, head, ownChangesets)) { + corpus.push({ origin: path, source }); + } + + const findings = []; + const read = []; + for (const entry of corpus) { + const addresses = lineAddresses(entry.source, resolve); + read.push({ origin: entry.origin, addresses: addresses.length }); + for (const address of addresses) { + const judged = judgeAddress(address, statusOf, hunksOf); + if (!BORN_FALSE_VERDICTS.has(judged.verdict)) continue; + findings.push({ + origin: entry.origin, + span: address.span, + file: address.file, + line: address.line, + verdict: judged.verdict, + movedTo: judged.movedTo, + sentence: address.sentence, + }); + } + } + findings.sort((a, b) => a.origin.localeCompare(b.origin) || a.line - b.line); + return { findings, corpus: read, subject: subject.length }; +} + // -- CLI ---------------------------------------------------------------------- if (isEntrypoint(import.meta.url)) { @@ -554,6 +934,52 @@ if (isEntrypoint(import.meta.url)) { process.exit(1); } + // ── the born-false reading (objectui#9509) ──────────────────────────────── + // + // The corpus is the prose THIS change publishes about itself, and the pull + // request body is where two of the three carded instances lived. It is read + // from the event payload the job already receives — ⛔ no new workflow, no new + // permission and no API call: `GITHUB_EVENT_PATH` is a file on the runner, and + // `check-governed-queue-guard.mjs` already reads it the same way. + // + // ⚠️ A `merge_group` build has no `pull_request` payload. That is an ABSENCE + // of corpus, never a clean verdict, and the report below says which. + const prBodyFile = argOf('--pr-body'); + let prBody = null; + let prBodyHow = 'not available on this event'; + if (prBodyFile) { + try { + prBody = readFileSync(resolve(root, prBodyFile), 'utf8'); + prBodyHow = `--pr-body ${prBodyFile}`; + } catch (error) { + console.error(`⚠️ Could not read ${prBodyFile}: ${error.message}`); + prBodyHow = `--pr-body ${prBodyFile} (UNREADABLE — this run read no pull request body)`; + } + } else if (process.env.GITHUB_EVENT_PATH) { + try { + const payload = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')); + if (typeof payload?.pull_request?.body === 'string') { + prBody = payload.pull_request.body; + prBodyHow = 'GITHUB_EVENT_PATH → pull_request.body'; + } + } catch (error) { + prBodyHow = `GITHUB_EVENT_PATH unreadable (${error.message}) — this run read no pull request body`; + } + } + + const bornControls = evaluateBornFalseControls(); + let born; + try { + born = bornFalse(root, { base: base.ref, head, prBody }); + } catch (error) { + console.error( + `❌ ${error.message}\n\n` + + ' The born-false reading lost an input, so it measured nothing. Reported as a failure\n' + + ' rather than as an empty finding set (objectui#4690).', + ); + process.exit(1); + } + // The DELIVERY hand-off (objectui#9140). One measurement, two consumers: the // log below, which a human reads by opening the job, and this file, which // `render-changeset-claims-comment.mjs` turns into the pull request comment. @@ -581,6 +1007,10 @@ if (isEntrypoint(import.meta.url)) { pending: result.pending, considered: result.considered, findings: result.findings, + bornFalse: born.findings, + bornFalseCorpus: born.corpus, + bornFalseControls: bornControls, + prBodyHow, }, null, 2, @@ -597,8 +1027,76 @@ if (isEntrypoint(import.meta.url)) { `pending declaration(s) that publish a body (${result.pending} pending in total).`, ); + // ── the born-false report (objectui#9509) ───────────────────────────────── + // + // Printed FIRST and unconditionally. It is about the prose this change is + // publishing right now, which is the one thing the seat reading this can still + // change for free — and unlike the went-false half, nothing later will ever + // turn it red. + console.log('\n── Born false — claims this change publishes about a tree it replaced ──\n'); + for (const control of bornControls) { + console.log(` ${control.ok ? 'PASS' : 'FAIL'} ${control.id}: ${control.detail}`); + } + const corpusRead = born.corpus.reduce((sum, entry) => sum + entry.addresses, 0); + console.log( + `\n Corpus: ${born.corpus.length} body(ies) this change publishes about itself ` + + `(${prBodyHow}).\n Line addresses read in them: ${corpusRead}.`, + ); + if (born.corpus.length === 0) { + // ⛔ The FLOOR. An empty corpus is an absence of subject, and printing a tick + // for it would report "this change publishes no false claim" on a run that + // read no prose at all — the shape objectstack#4928 named. + console.log( + '\n ⛔ NOT a clean verdict: this run read NO published prose, so it measured NOTHING\n' + + ' of this class. On a pull request the body arrives via GITHUB_EVENT_PATH; on a\n' + + ' merge_group build there is no pull request and no body to read.', + ); + } else if (born.findings.length === 0) { + console.log( + `\n ✅ Every one of those ${corpusRead} address(es) either names the tree it was read\n` + + ' from, or points at a line this change does not move.', + ); + } else { + console.log( + `\n⚠️ ${born.findings.length} address(es) in the prose this change publishes were read from a\n` + + " tree this change itself replaces:\n", + ); + for (const hit of born.findings) { + const where = + hit.verdict === 'unanchored' + ? `${hit.file} is ADDED by this change — that line exists in no tree outside this pull request` + : hit.movedTo === null + ? `this change REWRITES ${hit.file}:${hit.line}` + : `this change moves ${hit.file}:${hit.line} to :${hit.movedTo}`; + console.log(` ${hit.origin} says \`${hit.span}\` -> ${where}`); + console.log(` sentence: ${hit.sentence}`); + } + console.log(` + ⛔ This is NOT a claim that any of those sentences is false, and ⛔ not an instruction to + change the number. Correcting \`:246\` to \`:274\` produces a claim that is true today and + born false again on the next insertion — objectui#9509 states that before anything else. + + BIND THE NUMBER TO THE TREE IT WAS READ FROM. \`\`\`:246\` at \`b8a006883d\`, \`:274\` at this + head\`\` cannot re-stale, because each number names its own tree. Both pull requests that + produced this card converged on that form independently. Or state a RULE instead of a + coordinate — "every file in \`git diff --name-only\` against the merge base" — which is + what objectui#9495 did with a file count that had already staled once between rounds. + + Report-only, exactly like the half above it: a moved address is usually a moved address + and not a lie. The finding is a request to re-read, addressed to the one seat that can + answer it without re-deriving anything.`); + } + if (result.findings.length === 0) { - console.log('✅ No pending changeset names a file this change touches.'); + console.log('\n✅ No pending changeset names a file this change touches.'); + if (bornControls.some((control) => !control.ok)) { + console.error( + `❌ ${bornControls.filter((c) => !c.ok).length} born-false control(s) FAILED — this run is ` + + 'not a reading. ⛔ Not a finding failure: the instrument failed, and a differential ' + + 'reader that reports zero because its differ broke is indistinguishable from clean prose.', + ); + process.exit(1); + } process.exit(0); } @@ -644,8 +1142,10 @@ if (isEntrypoint(import.meta.url)) { intended shape here: one gate asks for the read, the other records the write. ⛔ What this gate does NOT cover, stated so nobody reads it as more: - - A claim that was BORN false — written against the merge base while describing the - head (objectui#8759). A changeset this change adds is excluded by construction. + - A born-false claim carrying no LINE ADDRESS. The section above this one now reads + that class (objectui#9509) over the prose this change publishes about itself, but + only where it spells a coordinate: objectui#9495's "a grep finds that member first" + is an ORDINAL claim, and deciding it means reading what the sentence means. - A claim about anything it does not spell as a file name in backticks: a symbol, a package, an installed dependency's version, a count. Symbol matching was measured and dropped — it asked for 191 paragraphs per commit, which is the release-time @@ -657,5 +1157,13 @@ if (isEntrypoint(import.meta.url)) { usually still true. The finding is a request to read, addressed to the one seat that can answer it cheaply — the one whose diff might have falsified it.`); + if (bornControls.some((control) => !control.ok)) { + console.error( + `❌ ${bornControls.filter((c) => !c.ok).length} born-false control(s) FAILED — this run is ` + + 'not a reading. ⛔ Not a finding failure: the instrument failed, and a differential ' + + 'reader that reports zero because its differ broke is indistinguishable from clean prose.', + ); + process.exit(1); + } process.exit(0); } diff --git a/scripts/render-changeset-claims-comment.mjs b/scripts/render-changeset-claims-comment.mjs index 9e46b3efeb..6cd89e0f7a 100644 --- a/scripts/render-changeset-claims-comment.mjs +++ b/scripts/render-changeset-claims-comment.mjs @@ -119,8 +119,48 @@ function measurement(result) { * @param {string} [options.runUrl] link back to the workflow run that measured it * @returns {string} the comment body, opening with {@link MARKER} */ +/** + * The BORN-FALSE half (objectui#9509), rendered above the went-false half. + * + * Above it deliberately: this half is about the prose the reader is publishing + * RIGHT NOW, which is the one thing they can still change for free, and + * objectui#9509's triage ruling is that nothing later will ever turn it red. + */ +function bornFalseSection(rows) { + const lines = [ + `## ⚠️ ${rows.length} address(es) in this pull request's own prose name a tree it replaced`, + '', + 'Each was read from a tree **this change itself moves**, so a reader who follows it lands ' + + 'somewhere else. ⛔ Nothing here blocks and nothing here says the sentence is false — the ' + + 'question asked is arithmetic: does this diff move the line that number points at?', + '', + ]; + for (const row of rows) { + const what = + row.verdict === 'unanchored' + ? `\`${row.file}\` is **added by this change**, so \`:${row.line}\` exists in no tree outside this pull request` + : row.movedTo === null || row.movedTo === undefined + ? `this change **rewrites** \`${row.file}:${row.line}\`` + : `this change moves \`${row.file}:${row.line}\` to **\`:${row.movedTo}\`**`; + lines.push(`- in ${row.origin === 'the pull request body' ? 'this body' : `\`${row.origin}\``}, \`${row.span}\` — ${what}`); + if (row.sentence) lines.push('', ` > ${spellOutTags(row.sentence)}`); + lines.push(''); + } + lines.push( + '⛔ **The repair is not to correct the number.** Changing `:246` to `:274` is true today and ' + + 'born false again on the next insertion — objectui#9509 states that before anything else. ' + + '**Bind the number to the tree it was read from** (`` `:246` at `b8a006883d`, `:274` at this ' + + 'head ``), which cannot re-stale because each number names its own tree; or state a **rule** ' + + 'instead of a coordinate, the way objectui#9495 replaced a file count with "every file in ' + + '`git diff --name-only` against the merge base".', + '', + ); + return lines; +} + export function renderClaimsComment(result = {}, { runUrl = '' } = {}) { const findings = Array.isArray(result.findings) ? result.findings : []; + const born = Array.isArray(result.bornFalse) ? result.bornFalse : []; const grouped = byChangeset(findings); const trailer = runUrl ? `\n\n${measurement(result)} · [run](${runUrl})\n` : `\n\n${measurement(result)}\n`; @@ -129,21 +169,24 @@ export function renderClaimsComment(result = {}, { runUrl = '' } = {}) { // It has to exist all the same: a pull request that fixed the thing, or whose // diff moved off the named file, would otherwise keep a stale request to // re-read at the top of its thread forever. - if (grouped.size === 0) { + if (grouped.size === 0 && born.length === 0) { return ( `${MARKER}\n\n` + - '## ✅ Nothing pending names a file this change touches\n\n' + - 'An earlier revision of this pull request did. That request to re-read does **not** apply to ' + - 'the current diff.\n\n' + + '## ✅ Nothing to re-read\n\n' + + 'No pending changeset names a file this change touches, and no address in this pull ' + + "request's own prose points at a line this change moves. An earlier revision did. That " + + 'request to re-read does **not** apply to the current diff.\n\n' + 'This comment is updated in place on every re-run rather than posted again, so the thread ' + 'does not grow one per push.' + trailer ); } - const lines = [ - MARKER, - '', + const lines = [MARKER, '']; + if (born.length > 0) lines.push(...bornFalseSection(born)); + // ⛔ Never printed as an empty section: a heading over nothing reads as a + // measurement that came back clean, and this half may simply not have run. + if (grouped.size > 0) lines.push( `## ⚠️ ${grouped.size} pending changeset(s) describe a file this change touches`, '', 'Their bodies publish **verbatim** into the CHANGELOG at the next release, so this is a ' + @@ -155,7 +198,7 @@ export function renderClaimsComment(result = {}, { runUrl = '' } = {}) { 'pending body names a file you touched. "Is this sentence still true?" is the one question ' + 'it will not answer, and the one you are being asked to answer.', '', - ]; + ); let repaired = false; for (const [changeset, hits] of grouped) { @@ -173,19 +216,24 @@ export function renderClaimsComment(result = {}, { runUrl = '' } = {}) { } } + if (grouped.size > 0) { + lines.push( + 'Read the **paragraph**, not the line: both false halves of the objectui#8617 claim sat in ' + + 'one paragraph, and correcting either alone would have left it asserting the same wrong ' + + 'thing.', + '', + 'If a claim did go false, **correct the body**. That is precedented and prose-only, ' + + 'frontmatter untouched; `check-changeset-overwrite.mjs` will report the correction as its ' + + 'own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended ' + + 'shape — one gate asks for the read, the other records the write.', + '', + ); + } lines.push( - 'Read the **paragraph**, not the line: both false halves of the objectui#8617 claim sat in ' + - 'one paragraph, and correcting either alone would have left it asserting the same wrong ' + - 'thing.', - '', - 'If a claim did go false, **correct the body**. That is precedented and prose-only, ' + - 'frontmatter untouched; `check-changeset-overwrite.mjs` will report the correction as its ' + - 'own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended ' + - 'shape — one gate asks for the read, the other records the write.', - '', - 'Not covered, stated so nobody reads this as more: a claim that was born false (a changeset ' + - 'this change adds is excluded by construction), a claim spelled as a symbol or a package ' + - 'rather than a backticked file name, and a file named ambiguously.', + 'Not covered, stated so nobody reads this as more: a born-false claim that spells no line ' + + 'address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member ' + + 'first" — and deciding that means reading what the sentence means), a claim spelled as a ' + + 'symbol or a package rather than a backticked file name, and a file named ambiguously.', ); // ⛔ Only when a span was ACTUALLY rewritten. A standing note about a repair From c9430eb1a8c9b19ba6fff1c76f6644b84b80eed5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 23:03:35 +0000 Subject: [PATCH 2/4] test(gate): pin the born-false reading, its floors and its firing controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arithmetic cases use objectui#9496's own geometry, because that pull request published the figure independently of this tree: its body and the docblock it landed both state `:246`@`b8a006883d` = `:274`@head. A mapper that drifts by one fails there rather than reporting a clean branch. Two controls guard the instrument rather than the prose: a judge that lies fails the control suite instead of passing it, and a corpus that was read but spells no address is reported as neither clean nor a finding — one tick for "nothing to judge" and "everything checked out" teaches the reader to skim it. The gate's own footer inverted rather than moved: it used to name born-false as a class it cannot see, and now names the one shape still out of reach. ⇒ a count of the phrase across this landing reads 1 -> 1 and proves nothing, so the pin asserts the sentence, on a run where that footer actually prints. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- .../__tests__/check-changeset-claims.test.ts | 335 +++++++++++++++++- .../render-changeset-claims-comment.test.ts | 98 ++++- scripts/check-changeset-claims.mjs | 9 + 3 files changed, 432 insertions(+), 10 deletions(-) diff --git a/scripts/__tests__/check-changeset-claims.test.ts b/scripts/__tests__/check-changeset-claims.test.ts index db4c119562..9d114419e6 100644 --- a/scripts/__tests__/check-changeset-claims.test.ts +++ b/scripts/__tests__/check-changeset-claims.test.ts @@ -5,7 +5,19 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { audit, namedFiles, paragraphNaming, resolveNamed, treeIndex } from '../check-changeset-claims.mjs'; +import { + BORN_FALSE_VERDICTS, + audit, + evaluateBornFalseControls, + judgeAddress, + lineAddresses, + mapLine, + namedFiles, + paragraphNaming, + resolveNamed, + sentenceAround, + treeIndex, +} from '../check-changeset-claims.mjs'; /** * objectui#9003 — a pending changeset's prose is judged by nothing. @@ -30,8 +42,10 @@ import { audit, namedFiles, paragraphNaming, resolveNamed, treeIndex } from '../ * file left alone — so a green can never come from the gate looking at * nothing. * 3. **The exclusions are deliberate, not accidents.** A changeset this change - * ADDS is never reported (which is also why the gate is blind to a BORN - * FALSE claim — objectui#8759 — and that limit is pinned as a limit). A + * ADDS is never reported BY THE WENT-FALSE HALF. ⚠️ That is no longer the + * same sentence as "the gate is blind to BORN FALSE": objectui#9509 added a + * second reading with its own corpus and its own coordinate, and section 6 + * below pins it. The exclusion itself is unchanged and still load-bearing. A * changeset declaring no bump is never reported: its body never publishes. * An ambiguously-named file is never reported: a span resolving to many * files names none of them. @@ -339,15 +353,24 @@ describe('a changeset THIS change adds', () => { fixture.commit('fix(alpha): rewrite untouched'); const run = runGate(fixture.root, lastCommitRange(fixture)); - it('is never reported — and that is the BORN-FALSE blind spot, pinned as a limit', () => { - // objectui#8759 was born false: authored against the merge base while - // describing the head. Excluding a change's own changesets is what makes the - // gate readable at all (they name their own files by construction), and it - // is exactly why this gate cannot see that sub-shape. It says so in its own - // output rather than letting a reader assume coverage. + it('is never reported by the WENT-FALSE half — the exclusion that keeps it readable', () => { + // Excluding a change's own changesets is what makes that half readable at + // all: they name their own files by construction, so it would fire on + // nearly every change here. objectui#9509 ⛔ did not relax this — it took + // those same bodies on a different coordinate (section 6). expect(run.output).toContain('No pending changeset names a file this change touches'); expect(run.status).toBe(0); }); + + it('⛔ reports a corpus it read but could not judge as neither clean nor a finding', () => { + // This fixture's own changeset IS in the born-false corpus and spells no + // line address. "Read, but nothing to judge" and "every address checked + // out" are different answers, and one tick for both teaches the reader to + // skim the tick. + expect(run.output).toContain('Corpus: 1 body(ies)'); + expect(run.output).toContain('Read, but nothing to judge'); + expect(run.output).not.toContain('either names the tree it was read'); + }); }); describe('a changeset that declares no bump', () => { @@ -396,6 +419,20 @@ describe('a file the change DELETES', () => { expect(run.output).toContain('THIS CHANGE LEAVES NO SUCH FILE'); expect(run.status).toBe(0); }); + + it('⛔ no longer prints that a born-false claim is outside the gate entirely', () => { + // ⚠️ THE CARRIER INVERTED, so a bare count of the words "BORN false" across + // this landing proves nothing — the phrase survives in both trees. What + // changed is what the sentence SAYS. The footer used to name born-false as a + // whole class the gate cannot see ("a changeset this change adds is excluded + // by construction"); it now names the ONE born-false shape still out of + // reach — an ordinal claim spelling no line address — and the section above + // it reads the rest. This is asserted on a run that HAS went-false findings, + // because that footer prints nowhere else. + expect(run.output).not.toContain('A changeset this change adds is excluded by construction'); + expect(run.output).toContain('A born-false claim carrying no LINE ADDRESS'); + expect(run.output).toContain('Born false — claims this change publishes about a tree it replaced'); + }); }); describe('.changeset/README.md', () => { @@ -623,3 +660,283 @@ describe('--json, the hand-off the pull request comment is rendered from', () => expect(broken.output).toContain('Could not write'); }); }); + +// ── 6. the BORN-FALSE reading (objectui#9509) ──────────────────────────────── + +/** + * objectui#9509: a claim written against the MERGE BASE while describing the + * HEAD, falsified by the diff's OWN insertion. It fails worse than went-false — + * it is false at the moment of publication, and ⛔ no later event will ever turn + * it red. + * + * The geometry every case below uses is objectui#9496's, because that pull + * request published the arithmetic independently of this file: its body and the + * docblock it landed both state `:246`@`b8a006883d` = `:274`@head. ⇒ 274 is + * attested OUTSIDE this test and cannot be quietly re-derived to match a mapper + * that drifts. + */ +describe('mapLine — where this change moves a base line', () => { + // `@@ -220,0 +221,28 @@` then `@@ -223 +251 @@`, measured on 8700d6d93. + const hunks = [ + { oldStart: 220, oldCount: 0, newCount: 28 }, + { oldStart: 223, oldCount: 1, newCount: 1 }, + ]; + + it('reproduces the figure objectui#9496 published: :246 at the base is :274 at the head', () => { + expect(mapLine(hunks, 246)).toBe(274); + }); + + it('reports a rewritten line as gone rather than as some other number', () => { + // Base `:223` IS the line the repair rewrites. Returning 251 for it would be + // the worst possible answer: a number that resolves, to the wrong thing. + expect(mapLine(hunks, 223)).toBeNull(); + }); + + it('leaves the insertion point itself alone — the off-by-one that would fake a finding', () => { + // git spells a pure insertion as "after old line 220", so 220 is untouched + // and 221 is not. An implementation that moved 220 would report stable + // citations as displaced, which is the false positive this reading can + // least afford on a report-only channel. + expect(mapLine(hunks, 220)).toBe(220); + expect(mapLine(hunks, 221)).toBe(249); + }); + + it('leaves a line above every hunk where it is', () => { + expect(mapLine(hunks, 100)).toBe(100); + }); +}); + +describe('lineAddresses — what counts as an address, and what binds it', () => { + const resolve = (span: string): string | null => + span.includes('imported-defaults.ts') ? 'packages/types/src/zod/imported-defaults.ts' : null; + + it('binds a bare `:246` to the file named earlier in ITS paragraph', () => { + // Both carded instances were written this way. Reading `:246` without that + // binding would turn every port number and every `key: 246` into a citation. + const rows = lineAddresses('The shape recurs in `imported-defaults.ts`: `:223` and `:246`.', resolve); + expect(rows.map((r) => r.line)).toEqual([223, 246]); + expect(rows.every((r) => r.file.endsWith('imported-defaults.ts'))).toBe(true); + }); + + it('⛔ never carries that binding across a blank line into somebody else\'s subject', () => { + const rows = lineAddresses('See `imported-defaults.ts` for the walker.\n\nThe frame is at `:246`.', resolve); + expect(rows).toEqual([]); + }); + + it('reads the sha binding per SENTENCE, ⛔ not per paragraph', () => { + // THE load-bearing choice. objectui#9496's §2 paragraph names a sha, and a + // paragraph-wide window would have exempted the very claim the card is + // about. A sha three sentences away binds nothing. + const bound = lineAddresses('At `b8a006883d` the shape recurs in `imported-defaults.ts`: `:246`.', resolve); + expect(bound[0].bound).toBe(true); + + const adrift = lineAddresses( + 'The repair landed at `b8a006883d`. The shape recurs in `imported-defaults.ts`: `:246`.', + resolve, + ); + expect(adrift[0].bound).toBe(false); + }); + + it('accepts `at this head` as a binding — it names a tree as definitely as a sha', () => { + const rows = lineAddresses('At this head `imported-defaults.ts:274` is the tuple arm.', resolve); + expect(rows[0].bound).toBe(true); + }); + + it('⛔ does not read a bare decimal as a sha', () => { + // `12345678` is a number. Accepting it would let any figure in a sentence + // silence every address beside it. + const rows = lineAddresses('Run 12345678 read `imported-defaults.ts:246` as the tuple arm.', resolve); + expect(rows[0].bound).toBe(false); + }); + + it('keeps a column and a range, because the FIRST number is the one that moves', () => { + const rows = lineAddresses( + 'The frame is `imported-defaults.ts:281:75` and the arm is `imported-defaults.ts:221-228`.', + resolve, + ); + expect(rows.map((r) => r.line)).toEqual([281, 221]); + }); + + it('drops a spelling that resolves to no tracked file — it names nothing definite', () => { + expect(lineAddresses('The frame is at `…9088:189:7`.', resolve)).toEqual([]); + }); +}); + +describe('sentenceAround', () => { + it('stops at the sentence boundary rather than running to the paragraph end', () => { + const paragraph = 'First at `abc1234`. Second cites `:246`. Third.'; + expect(sentenceAround(paragraph, paragraph.indexOf('`:246`'))).not.toContain('abc1234'); + }); +}); + +describe('judgeAddress — the arithmetic, never the meaning', () => { + const hunks = [{ oldStart: 220, oldCount: 0, newCount: 28 }]; + const address = { file: 'a.ts', line: 246, bound: false }; + + it('reports a moved line in a file this change MODIFIES', () => { + const verdict = judgeAddress(address, () => 'modified', () => hunks); + expect(verdict).toEqual({ verdict: 'moved', movedTo: 274 }); + expect(BORN_FALSE_VERDICTS.has(verdict.verdict)).toBe(true); + }); + + it('reports any address into a file this change ADDS as unanchored', () => { + // Instance 1's shape: the frame moved TWICE, between revisions of one + // branch. There is no tree outside the pull request in which that number can + // be read, so binding it is the only thing that can make it durable. + const verdict = judgeAddress(address, () => 'added', () => hunks); + expect(verdict.verdict).toBe('unanchored'); + }); + + it('⛔ says nothing about a file this change does not touch', () => { + // That address may well be false, but nothing about THIS diff made it so. + // It is the citation census's population (`check-new-cross-file-line-citations.mjs`) + // and ⛔ not this one's — one reader per population. + expect(judgeAddress(address, () => null, () => hunks).verdict).toBe('untouched'); + }); + + it('⛔ says nothing about an address bound to the tree it was read from', () => { + // The durable form. A gate that reported it would be teaching authors to + // unbind, which is the opposite of what objectui#9509 asks for. + const verdict = judgeAddress({ ...address, bound: true }, () => 'modified', () => hunks); + expect(verdict.verdict).toBe('anchored'); + }); + + it('distinguishes a stable line from a moved one — without this it is an absolute count', () => { + expect(judgeAddress({ ...address, line: 100 }, () => 'modified', () => hunks).verdict).toBe('stable'); + }); +}); + +describe('the born-false controls', () => { + it('all pass against objectui#9496\'s own geometry', () => { + const failures = evaluateBornFalseControls().filter((control) => !control.ok); + expect(failures.map((f) => `${f.id}: ${f.detail}`)).toEqual([]); + }); + + it('CAN fail — a judge that lies is caught rather than passed', () => { + // ⭐ The firing control on the controls. A suite that cannot be made to fail + // is decoration, and a differential reader that reports zero because its + // differ broke is indistinguishable from prose with nothing wrong in it. + const lying = (): { verdict: string; movedTo: null } => ({ verdict: 'stable', movedTo: null }); + const failed = evaluateBornFalseControls(lying).filter((control) => !control.ok); + expect(failed.map((f) => f.id)).toEqual([ + 'unbound-address-into-a-line-this-diff-moves', + 'an-address-into-a-file-this-change-adds-is-unanchored', + ]); + }); + + it('keeps BOTH directions — a control suite that only ever fires proves nothing', () => { + const controls = evaluateBornFalseControls(); + expect(controls.filter((c) => c.detail === '(silent)').length).toBeGreaterThan(0); + expect(controls.filter((c) => c.detail !== '(silent)').length).toBeGreaterThan(0); + }); +}); + +describe('end to end — a pull request body read against its own diff', () => { + const fixture = fixtureRepo('born-false'); + // 20 lines, so a citation into the middle of it has somewhere to be moved to. + fixture.write( + 'packages/alpha/src/walker.ts', + Array.from({ length: 20 }, (_, i) => `export const step${i + 1} = ${i + 1};\n`).join(''), + ); + fixture.commit('feat(alpha): the walker'); + // The change under test INSERTS above the cited line, exactly as objectui#9496 + // did, and adds a brand-new file. + fixture.write( + 'packages/alpha/src/walker.ts', + '// inserted\n// inserted\n// inserted\n' + + Array.from({ length: 20 }, (_, i) => `export const step${i + 1} = ${i + 1};\n`).join(''), + ); + fixture.write('packages/alpha/src/brand-new.ts', 'export const fresh = 1;\n'); + const head = fixture.commit('feat(alpha): insert above the cited line'); + const base = fixture.git('rev-parse', 'HEAD~1'); + + const bodyFile = path.join(fixture.root, 'body.md'); + fs.writeFileSync( + bodyFile, + 'The subject is `walker.ts:10`.\n\n' + + `At \`${base}\` the subject is \`walker.ts:10\`.\n\n` + + 'The new pin is `brand-new.ts:1`.\n\n' + + 'Untouched: `reconciliation.test.ts:1`.\n', + ); + const run = runGate(fixture.root, ['--base', base, '--head', head, '--pr-body', bodyFile]); + + it('reports the unbound address the diff moved, and says where it went', () => { + expect(run.output).toContain('this change moves packages/alpha/src/walker.ts:10 to :13'); + }); + + it('reports an address into a file this change ADDS as having no tree outside the pull request', () => { + expect(run.output).toContain('exists in no tree outside this pull request'); + }); + + it('⛔ stays silent on the same address bound to a sha — the discrimination', () => { + // ⛔ A checker that flags everything is not a checker. Four addresses were + // read; two were reported. Both halves of that split are the measurement. + expect(run.output).toContain('Line addresses read in them: 4'); + expect(run.output).toContain('2 address(es) in the prose this change publishes'); + }); + + it('⛔ stays silent on an address into a file this change does not touch', () => { + expect(run.output).not.toContain('reconciliation.test.ts:1'); + }); + + it('asks for the number to be BOUND, ⛔ never for it to be corrected', () => { + // The card states this before anything else: correcting `:246` to `:274` + // produces a claim that is true today and born false again on the next + // insertion. ⇒ the instruction has to be the durable form, or the gate + // manufactures the next instance. + expect(run.output).toContain('BIND THE NUMBER TO THE TREE IT WAS READ FROM'); + expect(run.output).toContain('not an instruction to'); + }); + + it('is REPORT-ONLY — findings do not fail the run', () => { + expect(run.status).toBe(0); + }); +}); + +describe('the floor under the born-false census', () => { + const fixture = fixtureRepo('born-false-floor'); + fixture.write('packages/alpha/src/untouched.ts', 'export const untouched = 2;\n'); + fixture.commit('chore(alpha): touch a file, publish no prose'); + const run = runGate(fixture.root, lastCommitRange(fixture)); + + it('⛔ never reports an empty corpus as clean', () => { + // A merge_group build has no pull request body, and a change may add no + // changeset. Reading NOTHING and printing a tick would be reporting "this + // change publishes no false claim" on a run that read no prose at all + // (objectstack#4928). + expect(run.output).toContain('Corpus: 0 body(ies)'); + expect(run.output).toContain('NOT a clean verdict'); + expect(run.output).toContain('measured NOTHING'); + }); + + it('⛔ does not print the all-clear line it prints when it DID read prose', () => { + expect(run.output).not.toContain('either names the tree it was read'); + }); +}); + +describe('the corpus is the prose this change publishes about itself', () => { + it('reads the pull request body from the event payload the job already receives', () => { + // ⛔ No new workflow, no new required context, no new permission and no API + // call: `GITHUB_EVENT_PATH` is a file on the runner, and + // `check-governed-queue-guard.mjs` already reads it the same way. + const gate = fs.readFileSync(path.join(repoRoot, GATE), 'utf8'); + expect(gate).toContain('GITHUB_EVENT_PATH'); + expect(gate).toContain('pull_request?.body'); + }); + + it('⛔ does not take the tree at large — that population has a reader already', () => { + // A citation written into an ordinary source file is + // `check-new-cross-file-line-citations.mjs`'s differential population. Two + // readers over one population is how two answers start disagreeing. + expect(fs.existsSync(path.join(repoRoot, 'scripts/check-new-cross-file-line-citations.mjs'))).toBe(true); + const gate = fs.readFileSync(path.join(repoRoot, GATE), 'utf8'); + expect(gate).toContain('check-new-cross-file-line-citations.mjs'); + }); + + it('delivers a born-false-only finding instead of leaving it in the job log', () => { + // objectui#9140 measured the job-log channel at zero answers out of four. + // The comment step decides whether to post from the finding COUNT, so a run + // whose only finding is born-false has to be counted there too. + expect(workflowYaml).toContain('measured.findings.length + (measured.bornFalse ?? []).length'); + }); +}); diff --git a/scripts/__tests__/render-changeset-claims-comment.test.ts b/scripts/__tests__/render-changeset-claims-comment.test.ts index a8e6a88204..7715b10ae8 100644 --- a/scripts/__tests__/render-changeset-claims-comment.test.ts +++ b/scripts/__tests__/render-changeset-claims-comment.test.ts @@ -206,7 +206,12 @@ describe('an empty finding set', () => { // The inversion this pins is objectui#3152's in this gate's costume: // rendering "not measured" or "nothing found" as a finding is how a channel // gets muted — and this channel was just un-muted by ruling. - expect(body).toContain('Nothing pending names a file this change touches'); + // ⚠️ The resolved body now speaks for BOTH halves (objectui#9509): a stale + // request at the top of a thread is as misleading about the born-false half + // as about the went-false one. + expect(body).toContain('Nothing to re-read'); + expect(body).toContain('No pending changeset names a file this change touches'); + expect(body).toContain("no address in this pull request's own prose"); expect(body).not.toContain('pending changeset(s) describe a file'); expect(body).not.toContain('⚠️'); }); @@ -245,3 +250,94 @@ describe('the hand-off from the gate', () => { expect(() => renderFromFile(path.join(os.tmpdir(), 'absent-claims-hand-off.json'), {})).toThrow(); }); }); + +// ── the born-false half (objectui#9509) ────────────────────────────────────── + +describe('the born-false section', () => { + const moved = { + origin: 'the pull request body', + span: ':246', + file: 'packages/types/src/zod/imported-defaults.ts', + line: 246, + verdict: 'moved', + movedTo: 274, + sentence: 'The shape recurs exactly twice: `:223` and `:246`.', + }; + const unanchored = { + origin: '.changeset/9088-rest-less-tuple-identity.md', + span: 'pin.test.ts:281', + file: 'packages/types/src/__tests__/pin.test.ts', + line: 281, + verdict: 'unanchored', + movedTo: null, + sentence: 'The deep-clean control is at `pin.test.ts:281`.', + }; + + it('is rendered even when NO pending changeset names anything', () => { + // The two halves are independent. A born-false-only run must still produce + // a body, or the half nothing later will ever turn red is delivered to the + // job log objectui#9140 measured at zero answers out of four. + const body = renderClaimsComment({ findings: [], bornFalse: [moved] }); + expect(body).toContain("address(es) in this pull request's own prose name a tree it replaced"); + expect(body).not.toContain('Nothing to re-read'); + }); + + it('says where the number went, rather than only that it is wrong', () => { + const body = renderClaimsComment({ findings: [], bornFalse: [moved] }); + expect(body).toContain('to **`:274`**'); + }); + + it('names an added file as having no tree outside the pull request', () => { + const body = renderClaimsComment({ findings: [], bornFalse: [unanchored] }); + expect(body).toContain('added by this change'); + expect(body).toContain('exists in no tree outside this pull request'); + }); + + it('⛔ asks for the number to be BOUND, never for it to be corrected', () => { + // Correcting `:246` to `:274` is true today and born false again on the + // next insertion. A comment that asked for the correction would manufacture + // the next instance of the class it reports. + const body = renderClaimsComment({ findings: [], bornFalse: [moved] }); + expect(body).toContain('The repair is not to correct the number'); + expect(body).toContain('Bind the number to the tree it was read from'); + }); + + it('⛔ prints no went-false heading when that half found nothing', () => { + // A heading over an empty list reads as a measurement that came back clean. + const body = renderClaimsComment({ findings: [], bornFalse: [moved] }); + expect(body).not.toContain('pending changeset(s) describe a file'); + }); + + it('repairs tag-shaped spans in the quoted sentence too', () => { + // GitHub deletes tag-shaped fragments from a stored body, and this half + // quotes prose that was never written for this channel either. + const body = renderClaimsComment({ + findings: [], + bornFalse: [{ ...moved, sentence: 'The frame is in `FieldWidgetProps` at `:246`.' }], + }); + expect(body).toContain('ANGLE-BRACKETS(T)'); + expect(body).not.toContain(''); + }); + + it('renders both halves together, born-false first', () => { + const body = renderClaimsComment({ + findings: [{ changeset: '.changeset/a.md', span: 'x.ts', file: 'packages/alpha/src/x.ts', severity: 'edited', paragraph: 'p' }], + bornFalse: [moved], + }); + expect(body.indexOf("this pull request's own prose")).toBeLessThan( + body.indexOf('pending changeset(s) describe a file'), + ); + }); + + it('opens with the marker whichever halves fired', () => { + // The workflow finds its own earlier comment by the FIRST LINE of the body + // it is about to post. A body that opened with a section heading instead + // would stack one comment per push. + for (const result of [ + { findings: [], bornFalse: [moved] }, + { findings: [], bornFalse: [] }, + ]) { + expect(renderClaimsComment(result).split('\n', 1)[0]).toBe(MARKER); + } + }); +}); diff --git a/scripts/check-changeset-claims.mjs b/scripts/check-changeset-claims.mjs index baea62269a..e5bd4313f2 100644 --- a/scripts/check-changeset-claims.mjs +++ b/scripts/check-changeset-claims.mjs @@ -1051,6 +1051,15 @@ if (isEntrypoint(import.meta.url)) { ' of this class. On a pull request the body arrives via GITHUB_EVENT_PATH; on a\n' + ' merge_group build there is no pull request and no body to read.', ); + } else if (corpusRead === 0) { + // A SECOND floor, one level in. The corpus was read but spells no address at + // all, so there was nothing of this class to judge — which is not the same + // answer as "every address checked out", and printing the same tick for both + // is how a reader learns to read the tick as noise. + console.log( + '\n ⚠️ Read, but nothing to judge: that prose spells no line address this gate can\n' + + ' resolve to one tracked file. ⛔ Not the same answer as a clean one.', + ); } else if (born.findings.length === 0) { console.log( `\n ✅ Every one of those ${corpusRead} address(es) either names the tree it was read\n` + From d61041317ade579de02e620eb3cb1cb351a63468 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 23:04:55 +0000 Subject: [PATCH 3/4] fix(gate): control the insertion-point boundary the first ablation exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured, ⛔ not anticipated. Mutating `line <= hunk.oldStart` to `line <` turned the section-6 pin red while ALL FOUR of the gate's own controls stayed green — so the gate would have printed "instrument fine" while reporting every stable citation at an insertion point as moved. A gate that manufactures findings on a report-only channel is how a channel gets muted. A fifth control reads the anchor line itself. git spells a pure insertion as "after old line N", so N is the last line it does not move. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- scripts/__tests__/check-changeset-claims.test.ts | 14 ++++++++++++++ scripts/check-changeset-claims.mjs | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/scripts/__tests__/check-changeset-claims.test.ts b/scripts/__tests__/check-changeset-claims.test.ts index 9d114419e6..b7f89a3084 100644 --- a/scripts/__tests__/check-changeset-claims.test.ts +++ b/scripts/__tests__/check-changeset-claims.test.ts @@ -940,3 +940,17 @@ describe('the corpus is the prose this change publishes about itself', () => { expect(workflowYaml).toContain('measured.findings.length + (measured.bornFalse ?? []).length'); }); }); + +describe('the boundary control the first ablation of this change exposed', () => { + it('catches the off-by-one that the other four controls all pass', () => { + // ⚠️ MEASURED, not anticipated. The first ablation of this change mutated + // `line <= hunk.oldStart` to `line <`, and the pin in section 6 went red + // while ALL FOUR of the gate's own controls stayed green — so the gate + // would have reported "instrument fine" while silently marking every stable + // citation at an insertion point as moved. A control suite that cannot see + // the mutation its own unit tests can see is not a self-check. + const ids = evaluateBornFalseControls().map((control) => control.id); + expect(ids).toContain('the-insertion-point-itself-does-not-move'); + expect(evaluateBornFalseControls().every((control) => control.ok)).toBe(true); + }); +}); diff --git a/scripts/check-changeset-claims.mjs b/scripts/check-changeset-claims.mjs index e5bd4313f2..c4915154fd 100644 --- a/scripts/check-changeset-claims.mjs +++ b/scripts/check-changeset-claims.mjs @@ -749,6 +749,18 @@ export const BORN_FALSE_CONTROLS = [ body: 'See `imported-defaults.ts:100` for the walker entry point.', want: (rows) => rows.length === 0, }, + { + // ⭐ THE BOUNDARY. git spells a pure insertion as "after old line 220", so + // 220 is the last line the insertion does not move and 221 is the first it + // does. Measured: mutating `line <= hunk.oldStart` to `line <` leaves the + // three controls above it all PASSING while every stable citation at an + // insertion point is silently reported as moved — a gate that manufactures + // findings, which on a report-only channel is how a channel gets muted. + id: 'the-insertion-point-itself-does-not-move', + why: 'a pure insertion moves the lines BELOW it; reporting its own anchor line would fake a finding on every stable citation', + body: 'The walker entry is at `imported-defaults.ts:220`, just above the block.', + want: (rows) => rows.length === 0, + }, { id: 'an-address-into-a-file-this-change-adds-is-unanchored', why: 'instance 1: a frame in a file the branch itself creates was read from a tree that exists nowhere else, and moved twice', From ab5cb40a48d78e4064a890dbc9c0d5e80269647c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 23:32:32 +0000 Subject: [PATCH 4/4] fix(gate): stop the corpus being ambient, and stop a fixture naming a live declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes, four red assertions, three gates. 1. A renderer fixture spelled `.changeset/9088-rest-less-tuple-identity.md`, a declaration the tree actually carries. `scripts/markdown-test-inputs.mjs` offers only documents that EXIST, so the literal became a ledger candidate on the pre-version tree and would have had nothing to resolve to after `changeset:version` deleted it — reddening the release lane days later, in a job no reader of a pull request sees. Renamed to a `fixture-` spelling, which belongs to neither the generated `adjective-animal-verb` namespace nor this repository's issue-number-and-slug one. ⛔ No ledger entry is added, deliberately: with no resolvable document left the scanner stops seeing that file entirely, and an entry for a file the scanner no longer sees is itself a `stale-test` finding. 2. ⭐ `GITHUB_EVENT_PATH` is exported to every process on a runner, and the gate read it whenever `--pr-body` was absent. So in CI the gate under test picked up the real pull request body of the build that happened to be running and counted it as "the prose this change publishes about itself", in a temp repository that has nothing to do with it — off by exactly one body. The empty-corpus floor, whose whole job is to report that a run measured NOTHING, did not hold in the one environment that actually runs it. The env read stays — it is the whole reason this needs no new workflow — but it is now gated on a predicate about the TREE rather than about how the process was launched: the payload names `pull_request.head.sha`, and a tree that cannot resolve that commit is not the tree the event is about. Fails closed and prints why, ⛔ never silently. Pinned in both directions, and the test harness strips the variable for every case rather than for the two that reddened — the others survived it by luck, not by hermeticity. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- .../__tests__/check-changeset-claims.test.ts | 96 ++++++++++++++++++- .../render-changeset-claims-comment.test.ts | 9 +- scripts/check-changeset-claims.mjs | 28 +++++- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/scripts/__tests__/check-changeset-claims.test.ts b/scripts/__tests__/check-changeset-claims.test.ts index b7f89a3084..a413fe965f 100644 --- a/scripts/__tests__/check-changeset-claims.test.ts +++ b/scripts/__tests__/check-changeset-claims.test.ts @@ -172,11 +172,20 @@ interface Run { * anyway, so this is what a reader actually gets. */ function runGate(root: string, args: string[] = [], env: Record = {}): Run { + // ⚠️ EVERY case decides its own corpus. `GITHUB_EVENT_PATH` is exported to every + // process on a runner, and the gate reads it when `--pr-body` is absent — so an + // inherited environment fed the gate under test the REAL pull request body of + // whatever build was running, in a temp repository that has nothing to do with + // it (objectui#9509, patch round 1: measured, off by exactly one body). The + // cases that did not redden survived it by luck, not by hermeticity, so it is + // stripped here for all of them rather than at the two that noticed. A case + // that WANTS the variable sets it back through `env`. + const { GITHUB_EVENT_PATH: _inherited, ...hermetic } = process.env; const run = spawnSync('node', [path.join(repoRoot, GATE), '--root', root, ...args], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], - env: { ...process.env, ...env }, + env: { ...hermetic, ...env }, }); return { status: run.status ?? -1, output: `${run.stdout ?? ''}${run.stderr ?? ''}` }; } @@ -954,3 +963,88 @@ describe('the boundary control the first ablation of this change exposed', () => expect(evaluateBornFalseControls().every((control) => control.ok)).toBe(true); }); }); + +// ── 7. the corpus may not be AMBIENT (objectui#9509, patch round 1) ────────── + +/** + * ⭐ Found by this file's own empty-corpus floor, in CI, ⛔ not by review. + * + * `GITHUB_EVENT_PATH` is exported to every process on a runner. The gate reads + * it when `--pr-body` is absent, so a run against a throwaway fixture repository + * picked up the REAL pull request body of the build that happened to be running + * and counted it as "the prose this change publishes about itself" — off by + * exactly one body. The floor whose whole job is "this run measured NOTHING" + * slid up to the next floor instead. ⇒ the reading built to be unfakeable was + * being fed by the environment. + * + * The repair is a predicate about the TREE rather than about how the process was + * launched: the payload names `pull_request.head.sha`, and a tree that cannot + * resolve that commit is not the tree the event is about. + */ +describe('an event payload from another tree', () => { + const fixture = fixtureRepo('ambient-corpus'); + fixture.write('packages/alpha/src/untouched.ts', 'export const untouched = 3;\n'); + fixture.commit('chore(alpha): touch a file, publish no prose'); + + // A payload shaped exactly like a real one, naming a head this tree cannot + // have. `.json`, ⛔ never `.md`: a markdown literal here would become a + // candidate for the ledger in `scripts/markdown-test-inputs.mjs`. + const foreign = path.join(fixture.root, 'foreign-event.json'); + fs.writeFileSync( + foreign, + JSON.stringify({ + pull_request: { number: 4242, head: { sha: '0'.repeat(40) }, body: 'The frame is at `untouched.ts:1`.' }, + }), + ); + const run = runGate(fixture.root, lastCommitRange(fixture), { GITHUB_EVENT_PATH: foreign }); + + it('is ⛔ ignored, and the run says so rather than counting it', () => { + expect(run.output).toContain('THIS TREE DOES NOT CARRY'); + expect(run.output).not.toContain('The frame is at'); + }); + + it('leaves the empty-corpus floor standing — BOTH halves of it', () => { + // ⚠️ The control the patch round set, reasoned before it was read: a run + // that prints `Corpus: 0` while having silently skipped the section is the + // same lie one level down. Both, ⛔ never either. + expect(run.output).toContain('Corpus: 0 body(ies)'); + expect(run.output).toContain('NOT a clean verdict'); + expect(run.output).toContain('measured NOTHING'); + }); + + it('⛔ does not move the five born-false controls, which are hermetic by construction', () => { + // If ANY of them moved when the environment changed, the hermeticity claim in + // the gate's own docblock would be false — and that, not the test, would be + // the finding. + expect(run.output.match(/^\s+PASS\s/gm)?.length).toBe(5); + expect(run.output).not.toMatch(/^\s+FAIL\s/m); + }); +}); + +describe('an event payload for THIS tree', () => { + const fixture = fixtureRepo('carried-corpus'); + fixture.write('packages/alpha/src/walker.ts', Array.from({ length: 12 }, (_, i) => `export const s${i} = ${i};\n`).join('')); + fixture.commit('feat(alpha): the walker'); + fixture.write( + 'packages/alpha/src/walker.ts', + '// inserted\n// inserted\n' + Array.from({ length: 12 }, (_, i) => `export const s${i} = ${i};\n`).join(''), + ); + const head = fixture.commit('feat(alpha): insert above the cited line'); + const base = fixture.git('rev-parse', 'HEAD~1'); + + const payload = path.join(fixture.root, 'event.json'); + fs.writeFileSync( + payload, + JSON.stringify({ pull_request: { number: 1, head: { sha: head }, body: 'The subject is `walker.ts:6`.' } }), + ); + const run = runGate(fixture.root, ['--base', base, '--head', head], { GITHUB_EVENT_PATH: payload }); + + it('IS read — the mechanism the whole no-new-workflow design rests on still works', () => { + // ⛔ The repair must not throw the mechanism away to silence the tests. The + // pull request body is where two of the three carded instances lived, and + // the event payload is the only way to reach it without a new workflow. + expect(run.output).toContain('carried by this tree'); + expect(run.output).toContain('Corpus: 1 body(ies)'); + expect(run.output).toContain('this change moves packages/alpha/src/walker.ts:6 to :8'); + }); +}); diff --git a/scripts/__tests__/render-changeset-claims-comment.test.ts b/scripts/__tests__/render-changeset-claims-comment.test.ts index 7715b10ae8..6560cb4713 100644 --- a/scripts/__tests__/render-changeset-claims-comment.test.ts +++ b/scripts/__tests__/render-changeset-claims-comment.test.ts @@ -264,7 +264,14 @@ describe('the born-false section', () => { sentence: 'The shape recurs exactly twice: `:223` and `:246`.', }; const unanchored = { - origin: '.changeset/9088-rest-less-tuple-identity.md', + // ⛔ A name no committed declaration can wear. `pnpm changeset` generates + // `adjective-animal-verb` and this repository commits an issue-number-and-slug + // name, so a `fixture-` prefix belongs to neither namespace — and the file does + // not exist, so `scripts/markdown-test-inputs.mjs` never offers it as a ledger + // candidate. Naming a LIVE pending declaration here made `changeset:version` + // delete the thing a ledger entry pointed at, and reddened the release lane days + // later (objectui#9583). + origin: '.changeset/fixture-born-false-origin.md', span: 'pin.test.ts:281', file: 'packages/types/src/__tests__/pin.test.ts', line: 281, diff --git a/scripts/check-changeset-claims.mjs b/scripts/check-changeset-claims.mjs index c4915154fd..fe60a4c583 100644 --- a/scripts/check-changeset-claims.mjs +++ b/scripts/check-changeset-claims.mjs @@ -968,11 +968,33 @@ if (isEntrypoint(import.meta.url)) { prBodyHow = `--pr-body ${prBodyFile} (UNREADABLE — this run read no pull request body)`; } } else if (process.env.GITHUB_EVENT_PATH) { + // ⚠️ GATED, and the gate is the whole point (objectui#9509, patch round 1). + // + // `GITHUB_EVENT_PATH` is exported to EVERY process on a runner, not just the + // workflow step this reading was written for. An ungated read made this gate's + // corpus AMBIENT: run against a throwaway fixture repository in CI, it picked + // up the real pull request body of whatever build happened to be running and + // counted it as "the prose this change publishes about itself" — which in that + // tree it provably was not. Measured, by this gate's own empty-corpus floor + // going soft in CI while passing locally. + // + // The predicate is about the TREE, not about how the process was launched: the + // payload names `pull_request.head.sha`, and if the tree under this run cannot + // resolve that commit then the payload describes some other repository at some + // other head. Fails CLOSED and says so — ⛔ never silently. try { const payload = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')); - if (typeof payload?.pull_request?.body === 'string') { - prBody = payload.pull_request.body; - prBodyHow = 'GITHUB_EVENT_PATH → pull_request.body'; + const body = payload?.pull_request?.body; + const sha = payload?.pull_request?.head?.sha; + const carried = typeof sha === 'string' && git(root, ['cat-file', '-e', `${sha}^{commit}`], { allowFailure: true }) !== null; + if (typeof body === 'string' && carried) { + prBody = body; + prBodyHow = `GITHUB_EVENT_PATH → pull_request.body (head ${String(sha).slice(0, 9)}, carried by this tree)`; + } else if (typeof body === 'string') { + prBodyHow = + `GITHUB_EVENT_PATH names a pull request at ${sha ? String(sha).slice(0, 9) : 'an unstated head'} ` + + 'which THIS TREE DOES NOT CARRY — ⛔ ignored, because a body from another tree is not the prose ' + + 'this change publishes about itself'; } } catch (error) { prBodyHow = `GITHUB_EVENT_PATH unreadable (${error.message}) — this run read no pull request body`;