From ec095fb990b4c673002990cd93b4caa105e81015 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 19:12:45 +0000 Subject: [PATCH] docs(check-cross-repo-closer-outcome): repair two evidence citations, and stop hand-counting the battery (#9917) Both items are evidence citations inside comments. No behaviour, no assertion, no scenario and no mutation changes. 1. L9's fixture and `makeDoubles`' `issues.get` default cited objectui#4478 as a closed ISSUE answering `state_reason: null`. That number is a pull request, i.e. exactly the object L13 exists to refuse, so the file offered one observation as evidence for two opposite scenarios. Both sites now model the null from the endpoint's contract and cite nothing; L9 records why a sampled specimen is the wrong kind of evidence for this fixture in particular, and leaves L13's measured pull request (#9143) as the only cited object. 2. lint.yml's step comment hand-counted "Ten scenarios" and "seven ways" against a measured eighteen and fifteen. Both counts are replaced by the mutation CLASSES, the shape the objectui port chose; the totals now live only where they are computed -- the check's own OK line and `--self-test`'s. The same stale enumeration in this script's own header is repaired the same way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- .github/workflows/lint.yml | 33 +++++++--- scripts/check-cross-repo-closer-outcome.mjs | 73 +++++++++++++++------ 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bb0c37ed2f..022f50a8e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1081,9 +1081,12 @@ jobs: # — one card per silent exit — and each fix lands as more unexercised # code. This step is the exercise: the shipped script is extracted from # the YAML with a real parser (never retyped) and run under doubles the - # way actions/github-script runs it, as one AsyncFunction body. Ten + # way actions/github-script runs it, as one AsyncFunction body. Its # scenarios pin the target parse and the outcome of EVERY exit — which of # setFailed / warning / job summary fires, and which API calls were made. + # How many of them there are is deliberately NOT written here (#9917): + # the check names the total on its own OK line, and `--list` prints the + # whole table. This comment used to say "ten" against a real eighteen. # # Assertion 0 is the compile, and it is not theoretical: this job failed # twice on 2026-08-02 with `SyntaxError: Identifier 'octokit' has already @@ -1091,14 +1094,26 @@ jobs: # workflow whose red nothing else in CI can see. # # Its --self-test runs first and is the half that stops the battery - # rotting into decoration: it mutates the shipped script seven ways — - # downgrade the verdict to a warning, stop collecting failed keys, break - # out of the loop instead of isolating, drop the same-repo skip, narrow - # the keyword set, drop the already-closed skip, downgrade the notice - # path's verdict — and requires the battery to go RED for each, naming the - # scenario that catches it. A mutation whose anchor no longer exists is a - # failure too, so a rewrite of the workflow cannot leave the mutations - # silently matching nothing. + # rotting into decoration: it mutates the shipped script along every + # CLASS the contract is made of — a verdict downgraded (red becomes a + # warning, or an info line nothing annotates), the loop's bookkeeping + # deleted (which keys failed, which half was lost, which target was + # refused), isolation abandoned mid-loop, the target parse narrowed (the + # keyword set, the optional colon, the same-repo skip), a guard removed + # (already-closed, triage, pull-request), idempotency eroded (the + # backlink marker dropped, or a blind post where a skip belongs) — and + # requires the battery to go RED for each, naming the scenario that + # catches it. A mutation whose anchor no longer exists is a failure too, + # so a rewrite of the workflow cannot leave the mutations silently + # matching nothing. + # + # Classes rather than a count, and rather than the enumeration that used + # to sit here, because BOTH hand-maintained numbers in this comment had + # gone stale unnoticed — measured 2026-08-20 from the check's own output, + # eighteen scenarios and fifteen mutations against a comment claiming ten + # and seven (#9917). A stale enumeration reads exactly as authoritative + # as a fresh one, and this comment is what the next reader checks the + # harness against. Prose classes survive an M16; a count does not. # # Invoked as `node` rather than through a `pnpm check:*` alias: that alias # belongs in root package.json, declared territory of the @changesets/cli diff --git a/scripts/check-cross-repo-closer-outcome.mjs b/scripts/check-cross-repo-closer-outcome.mjs index 2429d31740..826d61b3f1 100644 --- a/scripts/check-cross-repo-closer-outcome.mjs +++ b/scripts/check-cross-repo-closer-outcome.mjs @@ -89,15 +89,26 @@ // // A battery of assertions over a script that is already correct is green on day // one and green forever, including the day someone deletes the thing it -// guards. `--self-test` mutates the extracted source -- drop the `setFailed`, -// stop collecting failed keys, `break` out of the loop instead of isolating, -// disable the same-repo skip, narrow the keyword set, collapse the -// already-closed branch, strip the backlink marker, drop the triage guard, -// post blind when the comment listing is refused, stop recording which half -// was lost -- and requires the battery to go RED for each, naming the scenario -// it expects. Each mutation also asserts its own anchor was PRESENT before -// substituting: a mutation that silently matched nothing would leave the -// battery green and read exactly like a passing self-test. +// guards. `--self-test` mutates the extracted source and requires the battery +// to go RED for each mutation, naming the scenario it expects. +// +// What the mutations cover is written here as CLASSES, and their number is not +// written here at all (#9917). The list this paragraph used to carry named ten +// of them and was never updated again; the same habit in lint.yml's step +// comment was counting seven when there were fifteen. A prose class survives +// the next mutation being added, a hand-count does not, and `--self-test` +// prints its own total on every run. The classes: a verdict DOWNGRADED (red +// becomes a warning, or an info line nothing annotates), the loop's +// BOOKKEEPING deleted (which keys failed, which half was lost, which target +// was refused -- each one lets a run report green about work it did not do), +// ISOLATION abandoned mid-loop, the TARGET PARSE narrowed (the keyword set, +// the optional colon, the same-repo skip), a GUARD removed (already-closed, +// triage, pull-request), and IDEMPOTENCY eroded (the backlink marker dropped, +// or a blind post where a skip belongs). +// +// Each mutation also asserts its own anchor was PRESENT before substituting: a +// mutation that silently matched nothing would leave the battery green and +// read exactly like a passing self-test. // // One scenario (L11) is driven TWICE, the second run's world built out of the // first run's calls. Re-run idempotency is a property of the PAIR, and a @@ -200,9 +211,14 @@ function makeDoubles({ body, token, issues = {}, prCommentError = null, summaryE calls.get.push(`${owner}/${repo}#${n}`); const t = target(owner, repo, n); if (t.getError) throw t.getError; - // `state_reason` is nullable on a real closed issue -- objectui#4478 - // answers `null` from this very endpoint -- so the default models that - // rather than inventing a value the API does not promise. + // `state_reason` is nullable BY CONTRACT on this endpoint -- the API + // states a reason when it has one and promises nothing otherwise -- so + // the default models `null` rather than inventing a value the API does + // not promise. Modelled, not sampled, and that distinction is load + // bearing here: the citation that stood on this line named a foreign + // number as a closed issue answering `null` and it was a pull request + // (#9917). L9 carries the reason a sampled specimen is the wrong kind of + // evidence for THIS default in particular. // // `pull_request` is how the same endpoint says the number is a PULL // REQUEST, and it is modelled as ABSENCE rather than as `undefined` @@ -667,9 +683,25 @@ export const SCENARIOS = [ scenario: () => ({ body: MIXED_BODY, token: 'pat', - // Measured, not invented: objectstack-ai/objectui#4478 is closed and - // answers `state_reason: null` from `issues.get`. A fix that keys on the - // reason has to say what null means, and this pins the answer. + // MODELLED FROM THE CONTRACT, deliberately not sampled. `issues.get` + // declares `state_reason` nullable and promises no reason on a closed + // issue, so a fix that keys on the reason has to say what null means, and + // this pins the answer -- the shipped script's own reading of it, "no + // objection recorded", is stated on the already-closed branch of + // .github/workflows/cross-repo-issue-closer.yml, which is the subject + // this scenario drives. + // + // A SAMPLED citation stood here and was withdrawn (#9917): it named + // objectui#4478 as a closed issue answering `null`, and that number is a + // PULL REQUEST. Read that as the trap it is rather than as one bad + // lookup -- the objects that most dependably answer `state: 'closed'` + // with `state_reason: null` ARE pull requests, so hunting a specimen for + // this fixture walks straight into L13's subject, and the file then + // offers ONE observation as evidence for two opposite scenarios. L9 and + // L13 are kept on separate footings on purpose: L9 models the contract + // and cites nothing, L13 cites a measured pull request + // (objectstack-ai/objectstack#9143). Do not "improve" this by finding a + // real issue to name. issues: { [CLOSED_TARGET]: { state: 'closed', stateReason: null, comments: [] } }, }), check: (r, t) => [ @@ -845,10 +877,13 @@ export const SCENARIOS = [ token: 'pat', // A merged pull request answers `state: 'closed'` with // `state_reason: null` from the issues endpoint -- measured on - // objectstack-ai/objectstack#9143. That is the very fixture L9 uses to - // prove a closed ISSUE gets its backlink, which makes this scenario the - // ORDERING test: a `pull_request` guard placed after the state branch - // would sail past it and comment on somebody else's pull request. + // objectstack-ai/objectstack#9143. That is the very fixture SHAPE L9 uses + // to prove a closed ISSUE gets its backlink -- the shape, not the + // observation: L9 models its null from the contract and cites no object, + // precisely so that this measured pull request stays evidence for THIS + // scenario only (#9917). The shared shape is what makes this the ORDERING + // test: a `pull_request` guard placed after the state branch would sail + // past it and comment on somebody else's pull request. issues: { [PR_TARGET]: { state: 'closed',