Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions .github/workflows/lint.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1081,24 +1081,39 @@ 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
# been declared`, i.e. a script that never ran at all, on a post-merge
# 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
Expand Down
73 changes: 54 additions & 19 deletions scripts/check-cross-repo-closer-outcome.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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`
Expand DownExpand Up@@ -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) => [
Expand DownExpand Up@@ -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',
Expand Down
Loading