diff --git a/.github/workflows/repo-gates.yml b/.github/workflows/repo-gates.yml index 4bfdf6a35b..082855c9e5 100644 --- a/.github/workflows/repo-gates.yml +++ b/.github/workflows/repo-gates.yml @@ -18,9 +18,11 @@ name: Repo Gates # # ⚠️ What DID move: the check-run each gate used to produce. None of them was # in the merge queue's required set (`node scripts/check-required-check-set.mjs` -# reads the live set; the ruleset names `Governed Surface Queue Guard`, -# `Changeset Declaration` and `Spec Main Shape Gate`, which is why those three -# workflows were NOT folded), so the queue is unchanged. The one in-tree reader +# reads the live set; when this fold landed the ruleset named +# `Governed Surface Queue Guard`, `Changeset Declaration` and +# `Spec Main Shape Gate`, which is why those three workflows were NOT folded -- +# the last has since left the set by objectui#12240's ruling and keeps its own +# workflow for its hourly run on `main`), so the queue is unchanged. The one in-tree reader # of the old names, `scripts/dependabot-merge-gate.mjs`, now waits for the one # context this job produces, `Repo Gates`. # diff --git a/.github/workflows/required-check-set-patrol.yml b/.github/workflows/required-check-set-patrol.yml index 742374fc37..54fce00174 100644 --- a/.github/workflows/required-check-set-patrol.yml +++ b/.github/workflows/required-check-set-patrol.yml @@ -4,7 +4,8 @@ name: Required Check Set Patrol # # ## What it watches for (objectui#9422) # -# `main`'s merge queue requires nine check contexts. That set lives in GitHub +# `main`'s merge queue requires a set of check contexts — which ones is this +# patrol's reading, not a number written here. That set lives in GitHub # ruleset 11776024 — repository SETTINGS. Removing a member from it reds no # gate, fires no alarm, and leaves no trace in any diff a reviewer reads. The # one time this repository was in that state (objectui#3523) three pull requests @@ -144,7 +145,7 @@ jobs: cat "$RUNNER_TEMP/patrol.err" >&2 || true - name: Publish the reading to the run summary - # Always: this IS the delivery. A drift in the eight WATCHED contexts is + # Always: this IS the delivery. A drift in the WATCHED contexts is # reported here and deliberately does not fail the job — see the script's # header for the two tiers and the failure mode that split accepts. if: always() diff --git a/.github/workflows/spec-main-shape-gate.yml b/.github/workflows/spec-main-shape-gate.yml index 9aed98d6d4..ba27aba46c 100644 --- a/.github/workflows/spec-main-shape-gate.yml +++ b/.github/workflows/spec-main-shape-gate.yml @@ -33,20 +33,40 @@ name: Spec Main Shape Gate # required check that never reports does not fail the pull request, it leaves it # pending until the ruleset's 60-minute timeout). # -# ## Why `merge_group` is subscribed although this context is NOT required yet +# ## ADVISORY, not required: the full battery on `main`, a signal on a PR # -# Ordering, and objectui#3523 paid for it: subscribe the trigger FIRST (a pure -# addition that changes nothing about pull requests), and only then may a -# maintainer write the context into the ruleset's required set. Reversed — a -# required context whose workflow does not subscribe the queue — every queued -# pull request burns an hour and fails with nothing red to point at. +# objectui#12240 executes the maintainer's ruling B on objectui#12214's open +# question 1 (ACCEPT 6123370050), the two-speed doctrine of objectstack#22998 +# applied to this gate: the context leaves the merge queue's required set. It +# was the pull request's critical path, and most of its reds are objectstack +# drift rather than the pull request's own change (the measurement is on +# objectui#12214, not copied here). ⛔ NOTHING it verifies changed -- the +# bypass, the anti-false-green marker and the attribution below are as they +# were. What changed is who WAITS for the verdict: # -# ⚠️ This context is REQUIRABLE, and it is not REQUIRED: requiredness is -# repository SETTINGS (GitHub ruleset 11776024), which no file in this tree can -# set. `scripts/dependabot-merge-gate.mjs` classifies it, with the reason, in -# `NOT_A_GATE` until a maintainer enrols it there; moving the name to -# `REQUIRED_CONTEXTS` in that file is the in-tree half of the same action. -# ⛔ Nothing here claims the check is required. +# - `schedule`, hourly on `main`, is its place in the full battery. +# ⭐ A red on that run is a MAIN-RED for the `domain:devx` lane at objectui, +# under objectstack#22998's rule 3: it is that lane's first work, the +# lane's next pull request carries the fix or the pin bump, and no pull +# request of that lane arms the queue while the red stands. +# - `pull_request` posts the same verdict as an advisory check. A human reads +# it; nothing waits for it. +# - `merge_group` stays subscribed, deliberately. The ruled order is merge +# first, ruleset edit second, and until that edit the queue still requires +# this name: a workflow that stopped reporting on `merge_group` would stall +# every queue build at the ruleset's 60-minute timeout with nothing red to +# point at (objectui#3523 -- subscribe first, enrol second, and the mirror +# image on the way out). It also keeps the context REQUIRABLE, so +# re-enrolling it stays a settings edit. +# +# ⚠️ Requiredness is repository SETTINGS (GitHub ruleset 11776024), which no +# file in this tree can set, so "advisory" is not a mechanism in this file. +# ⛔ Never add `continue-on-error` to make it look so: it would hide the verdict +# on the run and change nothing about who waits for it. The in-tree halves are +# `scripts/dependabot-merge-gate.mjs`, which classifies the name in +# `NOT_A_GATE` again (a Dependabot bump does not wait for it either), and +# `scripts/check-required-check-set.mjs`, which no longer watches it. Whether +# the ruleset still lists it is that script's reading, ⛔ not this comment's. # # ## The fetch shape: sparse, blobless, at a sha # diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index ab242239a6..c5637e2997 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -29,7 +29,7 @@ one has its own section below. | `changeset-presence.yml` | Changeset Declaration | PR to `main`, `develop` — **no path filter**; merge-queue builds | **Yes** — when a released package's `src/` changed and no changeset was added | | `repo-gates.yml` | Repo Gates | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — fifteen gates as steps of one job ([#12214](https://github.com/objectstack-ai/objectui/issues/12214)); the job is red when any step is, and `scripts/dependabot-merge-gate.mjs` declares the context in `REQUIRED_CONTEXTS` | | `line-citation-gate.yml` | Line Citation Gate | PR to `main`, `develop` — **no path filter**; manual | No — **report-only** while it beds in; it exits 0 whatever it finds, and exits 1 only when one of its own synthetic controls fails. It declares no `merge_group` trigger, so it cannot be a required context in its current state | -| `spec-main-shape-gate.yml` | Spec Main Shape Gate | PR to `main`, `develop` — **no path filter**; merge-queue builds; manual; hourly `schedule` on `main` | **Yes** — a shape break is a real red naming the objectui file and the objectstack commit ([#9860](https://github.com/objectstack-ai/objectui/issues/9860)). The maintainer enrolled the context under [#9969](https://github.com/objectstack-ai/objectui/issues/9969)'s ruling A, and `scripts/dependabot-merge-gate.mjs` declares it in `REQUIRED_CONTEXTS`, so a Dependabot bump waits for it too | +| `spec-main-shape-gate.yml` | Spec Main Shape Gate | PR to `main`, `develop` — **no path filter**; merge-queue builds; manual; hourly `schedule` on `main` | No — **advisory** since [#12240](https://github.com/objectstack-ai/objectui/issues/12240): the maintainer's ruling took the context out of the required set, and `scripts/dependabot-merge-gate.mjs` classifies it `NOT_A_GATE`. Its verdict is unchanged — a shape break is a real red naming the objectui file and the objectstack commit ([#9860](https://github.com/objectstack-ai/objectui/issues/9860)) — and a red on the hourly `schedule` run on `main` is a main-red for the `domain:devx` lane | | `governed-surface-guard.yml` | Governed Surface Queue Guard | PR to `main`, `develop` (incl. `ready_for_review`) — **no path filter**; merge-queue builds | **Yes on a queue build only** — a governed-surface diff with no authorized approval record (on any commit) is refused there, and so is any merge group whose queued pull requests still carry `needs:contract-review`; on the pull request itself it is deliberately green and prints an early warning | | `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget | | `lockfile-integrity.yml` | Lockfile Integrity Check | PR to `main`, `develop` touching `pnpm-lock.yaml` or the gate's own two files; manual | No — **deliberately not a blocking context** ([#8326](https://github.com/objectstack-ai/objectui/issues/8326)); it names the packages and the Dependabot merge gate classifies it `NOT_A_GATE` | @@ -572,7 +572,10 @@ job, and nothing was weakened in the fold: **What moved:** the check runs. None of the fifteen was a required context of the merge queue — `scripts/check-required-check-set.mjs` reads the live set — which is also why `changeset-presence.yml` (**Changeset Declaration**), `spec-main-shape-gate.yml` and `governed-surface-guard.yml` were not -folded: their contexts are required, and a step inside a job is not a check run. The one in-tree +folded: their contexts were required when the fold landed, and a step inside a job is not a check run +(the spec gate's has since left the set, by +[#12240](https://github.com/objectstack-ai/objectui/issues/12240), and keeps its own workflow for its +hourly run on `main`). The one in-tree reader of the old names, `REQUIRED_CONTEXTS` in `scripts/dependabot-merge-gate.mjs`, now waits for the one context this job produces. @@ -2160,19 +2163,37 @@ to match in it. `scripts/__tests__/spec-main-shape-gate.test.ts` pins the bypass flag, the unfiltered trigger, the queue subscription and both attribution carriers, each with the firing control that proves the assertion is not satisfied by something else. -**How it became a required check.** Requiredness is a GitHub ruleset — repository settings — -which `scripts/check-required-check-set.mjs` can read and nothing in this tree can write. The -workflow was built to be requirable (unfiltered on `pull_request`, subscribed to `merge_group`, no -`continue-on-error`), so the maintainer could enrol the context on its own without stalling the -queue. It waited for the day-one break to clear — its first run was red on one objectui file over -an objectstack commit — and was enrolled under +**How it became a required check, and how it left the set.** Requiredness is a GitHub ruleset — +repository settings — which `scripts/check-required-check-set.mjs` can read and nothing in this tree +can write. The workflow was built to be requirable (unfiltered on `pull_request`, subscribed to +`merge_group`, no `continue-on-error`), so the maintainer could enrol the context on its own without +stalling the queue. It waited for the day-one break to clear — its first run was red on one objectui +file over an objectstack commit — and was enrolled under [#9969](https://github.com/objectstack-ai/objectui/issues/9969)'s ruling A. The in-tree half of that action came second: the name moved from `NOT_A_GATE` to `REQUIRED_CONTEXTS` in -`scripts/dependabot-merge-gate.mjs`, where the two conditions and their order are written down. That -gate reads check runs and never the ruleset, so the order could not let a red through either way; -enrolling first only meant no Dependabot bump waited on a check the queue did not. Whether the -context is *still* enrolled is not answered here: `scripts/check-required-check-set.mjs` watches it, -and reports its loss in the patrol's run summary without failing. +`scripts/dependabot-merge-gate.mjs`. + +[#12240](https://github.com/objectstack-ai/objectui/issues/12240) reversed the enrolment on the +maintainer's ruling B, the two-speed doctrine of +[objectstack#22998](https://github.com/objectstack-ai/objectstack/issues/22998) applied to this gate: +it was the pull request's critical path, and most of its reds are objectstack drift rather than the +pull request's own change. **Nothing it verifies changed**, and it still runs on every event above; +what changed is who waits for its verdict. + +- On `main`'s hourly `schedule` run it is part of the full battery, and **a red there is a main-red + for the `domain:devx` lane**: the lane's next pull request carries the fix or the pin bump, and no + pull request of that lane arms the queue while the red stands. +- On a pull request it posts the same verdict as an advisory check. Advisory is not a mechanism in + the workflow — requiredness is the ruleset's — so it carries no `continue-on-error`, which would + hide the verdict and change nothing about who waits for it. +- The name moved back to `NOT_A_GATE` in `scripts/dependabot-merge-gate.mjs`, so a Dependabot bump + does not wait for it either, and `scripts/check-required-check-set.mjs` no longer watches it. +- `merge_group` stays subscribed. The maintainer removes the context from the ruleset in the same + sitting as that pull request's merge, and until that edit the queue still requires the name: a + workflow that stopped reporting on queue builds would stall each of them at the ruleset's + 60-minute timeout. The subscription also keeps the context requirable. + +Whether the context is enrolled today is not answered here; the patrol's reading answers it. ## Release Workflows @@ -3018,13 +3039,15 @@ alone and why. ⛔ Do not add another: point at the gate, do not copy its answer **Two tiers, because a job rename must not manufacture a red.** `PINNED_CONTEXTS` is `Type Check` alone — the leg the recorded incident actually failed on — and its absence fails the job. -`WATCHED_CONTEXTS` is the other eight; their absence is reported in the run summary and fails -nothing. Pinning a name is pinning a spelling, and a check that goes red on healthy work is how a -repository learns to ignore red ([#6596](https://github.com/objectstack-ai/objectui/issues/6596)). -The shard names are the riskiest of the nine to pin: the 4-way matrix is itself a shape this -repository has already changed once (`71be244d52`, 2026-07-17). ⚠️ The accepted cost is stated -plainly — a silent removal of any of the eight is *observable*, not *enforced*. Promoting one is a -one-line move between the two lists. +`WATCHED_CONTEXTS` is the rest of the declared set; their absence is reported in the run summary and +fails nothing, and so is a required context neither list declares. Pinning a name is pinning a +spelling, and a check that goes red on healthy work is how a repository learns to ignore red +([#6596](https://github.com/objectstack-ai/objectui/issues/6596)). The test-shard names were the +riskiest to pin: the 4-way matrix was a shape this repository had already changed once +(`71be244d52`, 2026-07-17), and [#9499](https://github.com/objectstack-ai/objectui/issues/9499) +replaced them with one aggregator context. ⚠️ The accepted cost is stated plainly — a silent removal +of any watched name is *observable*, not *enforced*. Promoting one is a one-line move between the +two lists. Which names the two lists hold is the script's declaration, not this page's. **The rule TYPES are pinned as well**, and that is not the same kind of claim: `Type Check` being in a `required_status_checks` rule means nothing if the `merge_queue` rule is gone. A rule type is not a diff --git a/scripts/__tests__/check-required-check-set.test.ts b/scripts/__tests__/check-required-check-set.test.ts index 5632176b1f..a52b21b8c3 100644 --- a/scripts/__tests__/check-required-check-set.test.ts +++ b/scripts/__tests__/check-required-check-set.test.ts @@ -22,25 +22,30 @@ const GATE = 'scripts/check-required-check-set.mjs'; const WORKFLOW = '.github/workflows/required-check-set-patrol.yml'; const FIXTURES = 'scripts/__tests__/fixtures/required-check-set'; /** - * The set objectui#9499's ruling installs: the four `Test (shard N/4)` contexts - * replaced by the one aggregator context `Test`. It WAS the control fixture - * until objectui#9969 declared one more context; it is kept because it is now - * exactly the declared set with `Spec Main Shape Gate` lost, which is the case - * that card asked the patrol to report. + * The verbatim live answer of 2026-10-12, taken the day objectui#12240 was + * executed and before the maintainer's ruleset edit. That card's window is + * this reading: between its pull request merging and the maintainer removing + * `Spec Main Shape Gate` from the ruleset, the live set is exactly this one and + * the declaration is `RULING_FIXTURE`. A case below asserts the evaluator calls + * that `drifted` — reported, exit 0, never red. */ -const RULING_9499_FIXTURE = `${FIXTURES}/ruling-9499.json`; +const LIVE_2026_10_12_FIXTURE = `${FIXTURES}/live-2026-10-12.json`; /** - * objectui#9499's set plus `Spec Main Shape Gate`, which objectui#9969's ruling - * A enrolled and this patrol now watches. The CONTROL fixture: byte-identical - * to the one above except for that one appended entry. + * The set objectui#12240's ruling installs — the maintainer's ruling B on + * objectui#12214's open question 1 (ACCEPT 6123370050): `Spec Main Shape Gate` + * leaves the required set, and `Governed Surface Queue Guard`, enrolled under + * objectui#6596 and never declared here, is. The CONTROL fixture: the dated + * reading above with exactly that one entry removed, every other rule + * byte-identical. It replaced objectui#9969's control, which watched the gate + * that card enrolled. */ -const RULING_FIXTURE = `${FIXTURES}/ruling-9969.json`; +const RULING_FIXTURE = `${FIXTURES}/ruling-12240.json`; /** * The verbatim live answer of 2026-09-14, kept because it is a real reading and - * because the window objectui#9499 opens is worth pinning: between this pull - * request merging and the maintainer adding `Test` to the ruleset, the live set - * is exactly this one and the declaration is `RULING_FIXTURE`. The case below - * asserts the evaluator calls that `drifted` — reported, exit 0, never red. + * because objectui#9499's window was worth pinning: between that pull request + * merging and the maintainer adding `Test` to the ruleset, the live set was + * exactly this one. The case below asserts the evaluator still calls it + * `drifted` — reported, exit 0, never red — against today's declaration. */ const LIVE_2026_09_14_FIXTURE = `${FIXTURES}/live-2026-09-14.json`; const LIVE_FIXTURE = RULING_FIXTURE; @@ -209,10 +214,10 @@ describe('check-required-check-set — the committed fixture is the live shape', expect(reading.verdict).toBe('drifted'); expect(exitCodeFor(reading.verdict)).toBe(EXIT_OK); - // `Spec Main Shape Gate` is missing from this dated answer too: it was not - // enrolled until objectui#9969, and the declaration it is read against now - // watches it. The fixture is a verbatim reading and stays as it was. - expect(reading.missingWatched).toEqual(['Test', 'Spec Main Shape Gate']); + // `Governed Surface Queue Guard` is missing from this dated answer too: it + // was not enrolled until objectui#6596 and not declared here until + // objectui#12240. The fixture is a verbatim reading and stays as it was. + expect(reading.missingWatched).toEqual(['Test', 'Governed Surface Queue Guard']); expect(reading.unexpected).toEqual([ 'Test (shard 1/4)', 'Test (shard 2/4)', @@ -225,33 +230,52 @@ describe('check-required-check-set — the committed fixture is the live shape', }); /** - * objectui#9969: the reason `Spec Main Shape Gate` is watched at all. The - * pre-#9969 declared set is the current one with that context lost, so it is - * the case to read. Losing it must be NAMED (the patrol's summary says it is - * no longer required) and must stay exit 0: watched, not pinned, so no new - * red leg is added by declaring it. + * objectui#12240's window, pinned at its middle step — objectui#9499's shape + * run the other way. The ruling REMOVES a context: the pull request that + * declares the new set merges, and the maintainer edits the ruleset in the + * same sitting. Between the two the live answer is the dated reading of + * 2026-10-12 and the declaration is the ruling's. The patrol must NAME the + * leftover context and stay at exit 0: a ruled removal must not manufacture + * a red on healthy work, which is objectui#9422's two tiers doing their job. */ - it('reads the declared set with `Spec Main Shape Gate` lost as DRIFTED, naming it', () => { + it('reads the live answer of the day objectui#12240 was executed as DRIFTED, naming `Spec Main Shape Gate`', () => { const contexts = (rules: Rule[]): string[] => rules.flatMap((r) => r.type === 'required_status_checks' ? (r.parameters?.required_status_checks ?? []).map((c) => String(c.context)) : [], ); - // The two ruling fixtures differ by exactly that one context and nothing - // else, so the drift below cannot be caused by some second difference. - expect(contexts(readFixture(RULING_9499_FIXTURE))).toEqual( - contexts(readFixture(RULING_FIXTURE)).filter((c) => c !== 'Spec Main Shape Gate'), + // The two fixtures differ by exactly that one context and nothing else, so + // the drift below cannot be caused by some second difference. + expect(contexts(readFixture(RULING_FIXTURE))).toEqual( + contexts(readFixture(LIVE_2026_10_12_FIXTURE)).filter((c) => c !== 'Spec Main Shape Gate'), ); const others = (rules: Rule[]) => JSON.stringify(rules.filter((r) => r.type !== 'required_status_checks')); - expect(others(readFixture(RULING_9499_FIXTURE))).toBe(others(readFixture(RULING_FIXTURE))); + expect(others(readFixture(RULING_FIXTURE))).toBe(others(readFixture(LIVE_2026_10_12_FIXTURE))); - const r = runGate(['--fixture', RULING_9499_FIXTURE]); + const reading = evaluate({ + rules: readFixture(LIVE_2026_10_12_FIXTURE), + branch: 'main', + repository: 'objectstack-ai/objectui', + }); + expect(reading.verdict).toBe('drifted'); + expect(reading.unexpected).toEqual(['Spec Main Shape Gate']); + expect(reading.missingWatched).toEqual([]); + expect(reading.missingPinned).toEqual([]); + + const r = runGate(['--fixture', LIVE_2026_10_12_FIXTURE]); expect(r.status).toBe(EXIT_OK); - expect(r.stdout).toContain('No longer required: `Spec Main Shape Gate`.'); + expect(r.stdout).toContain('Required but not declared here: `Spec Main Shape Gate`.'); expect(r.stdout).not.toContain('✅'); expect(r.stderr).toContain('DRIFTED'); }); + + it('the context objectui#12240 removed is neither pinned nor watched', () => { + const declared = [...PINNED_CONTEXTS, ...WATCHED_CONTEXTS]; + expect(declared).not.toContain('Spec Main Shape Gate'); + // Control: the same membership test finds the name that ruling declared. + expect(declared).toContain('Governed Surface Queue Guard'); + }); }); describe('check-required-check-set — the declaration cannot drift from the repository', () => { diff --git a/scripts/__tests__/ci-cd-pipeline-doc.test.ts b/scripts/__tests__/ci-cd-pipeline-doc.test.ts index 5ec787e515..d19fb00ebf 100644 --- a/scripts/__tests__/ci-cd-pipeline-doc.test.ts +++ b/scripts/__tests__/ci-cd-pipeline-doc.test.ts @@ -3567,15 +3567,17 @@ function inheritedDeclarations(): Map { const SWEEP_DECLARED_NON_RUN_COMMANDS = new Map([ [ 'spec-main-shape-gate.yml: scripts/dependabot-merge-gate.mjs', - 'Named as the registry whose `REQUIRED_CONTEXTS` declares this context blocking — the ' + - 'in-tree half of the enrolment, taken second, after the ruleset. A declaration this ' + - 'section points at, not a step this workflow runs (objectui#9860, objectui#9969).', + 'Named as the registry that classifies this context: `REQUIRED_CONTEXTS` from ' + + 'objectui#9969 (the in-tree half of the enrolment), `NOT_A_GATE` since objectui#12240 ' + + '(the in-tree half of its removal from the required set). A declaration this section ' + + 'points at, not a step this workflow runs (objectui#9860, objectui#9969, objectui#12240).', ], [ 'spec-main-shape-gate.yml: scripts/check-required-check-set.mjs', 'Named as the one thing in this tree that can READ the ruleset where requiredness actually ' + - 'lives, and as the patrol that watches this context\'s enrolment. A different ' + - "workflow's instrument, cited to keep the claim falsifiable (objectui#9860, objectui#9969).", + 'lives — the instrument that says whether this context is still enrolled, which the ' + + "section deliberately does not answer. A different workflow's instrument, cited to keep " + + 'the claim falsifiable (objectui#9860, objectui#9969, objectui#12240).', ], [ 'changeset-guard.yml: scripts/check-changeset-presence.mjs', diff --git a/scripts/__tests__/dependabot-merge-gate.test.ts b/scripts/__tests__/dependabot-merge-gate.test.ts index 39ad76a0fc..ee3e8a3171 100644 --- a/scripts/__tests__/dependabot-merge-gate.test.ts +++ b/scripts/__tests__/dependabot-merge-gate.test.ts @@ -393,41 +393,49 @@ describe('the path-filtered blocking checks (optional bucket)', () => { }); /** - * objectui#9969, ruling A. `Spec Main Shape Gate` moved from `NOT_A_GATE` to - * `REQUIRED_CONTEXTS` once the maintainer's enrolment was read back. The - * generic cases above already say what "required" means here; this block pins - * the MOVE, so an edit that parks the name back in `NOT_A_GATE` (or drops it - * from the list) fails by name rather than quietly letting a shape break ride a - * Dependabot bump. The control comes first: the same snapshot reads green while - * the check passes, so each refusal below is about this one name. + * objectui#12240 — the maintainer's ruling B on objectui#12214's open question + * 1 (ACCEPT 6123370050). `Spec Main Shape Gate` leaves the required set and is + * advisory on a pull request, so it moved back from `REQUIRED_CONTEXTS` (where + * objectui#9969's ruling A had put it) to `NOT_A_GATE`. This block pins the + * MOVE by name, in both directions: an edit that parks the name back in + * `REQUIRED_CONTEXTS` without a ruling fails here, and so does one that drops + * it from every bucket. The control comes first: the same red on a context + * that IS required still refuses the head, so each green below is about this + * one name and not about a snapshot that stopped being read. */ -describe('Spec Main Shape Gate is waited for (objectui#9969)', () => { +describe('Spec Main Shape Gate is advisory, not waited for (objectui#12240)', () => { const NAME = 'Spec Main Shape Gate'; - - it('is declared required, and in neither other bucket', () => { - expect(REQUIRED_CONTEXTS).toContain(NAME); - expect(Object.keys(NOT_A_GATE)).not.toContain(NAME); + const withSpecGate = (conclusion: string | null, status = 'completed') => [ + ...allGreenSnapshot(), + { id: 95_325_290_000, name: NAME, status, conclusion }, + ]; + + it('is declared NOT_A_GATE, with a reason, and in neither other bucket', () => { + expect(Object.keys(NOT_A_GATE)).toContain(NAME); + expect(NOT_A_GATE[NAME as keyof typeof NOT_A_GATE]).toMatch(/objectui#12240/); + expect(REQUIRED_CONTEXTS).not.toContain(NAME); expect(Object.keys(OPTIONAL_CONTEXTS)).not.toContain(NAME); }); - it('CONTROL: the all-green snapshot carries it as success and reads green', () => { - expect(allGreenSnapshot().find((run) => run.name === NAME)?.conclusion).toBe('success'); - expect(evaluateGate({ checkRuns: allGreenSnapshot() }).verdict).toBe('green'); + it('CONTROL: the same red on a REQUIRED context still refuses the head', () => { + const runs = withSpecGate('success').map((run) => + run.name === 'Type Check' ? { ...run, conclusion: 'failure' } : run, + ); + expect(evaluateGate({ checkRuns: runs }).verdict).toBe('red'); }); - it('refuses a head on which it reported failure', () => { - const runs = allGreenSnapshot().map((run) => (run.name === NAME ? { ...run, conclusion: 'failure' } : run)); - const result = evaluateGate({ checkRuns: runs }); + it('does not refuse a head on which it reported failure', () => { + const result = evaluateGate({ checkRuns: withSpecGate('failure') }); - expect(result.verdict).toBe('red'); - expect(result.failing).toEqual([`${NAME} (failure)`]); + expect(result.verdict).toBe('green'); + expect(result.failing).toEqual([]); }); - it('holds a head on which it has not reported at pending, never green', () => { - const result = evaluateGate({ checkRuns: allGreenSnapshot().filter((run) => run.name !== NAME) }); - - expect(result.verdict).toBe('pending'); - expect(result.missing).toEqual([NAME]); + it('does not hold a head on which it is still running, or has not reported', () => { + expect(evaluateGate({ checkRuns: withSpecGate(null, 'in_progress') }).verdict).toBe('green'); + const absent = evaluateGate({ checkRuns: allGreenSnapshot().filter((run) => run.name !== NAME) }); + expect(absent.verdict).toBe('green'); + expect(absent.missing).toEqual([]); }); }); diff --git a/scripts/__tests__/fixtures/required-check-set/ruling-9499.json b/scripts/__tests__/fixtures/required-check-set/live-2026-10-12.json similarity index 91% rename from scripts/__tests__/fixtures/required-check-set/ruling-9499.json rename to scripts/__tests__/fixtures/required-check-set/live-2026-10-12.json index 75ae5756df..9d860eaacd 100644 --- a/scripts/__tests__/fixtures/required-check-set/ruling-9499.json +++ b/scripts/__tests__/fixtures/required-check-set/live-2026-10-12.json @@ -68,16 +68,24 @@ "context": "Build & E2E", "integration_id": 15368 }, + { + "context": "Build Docs", + "integration_id": 15368 + }, + { + "context": "Changeset Declaration", + "integration_id": 15368 + }, { "context": "Test", "integration_id": 15368 }, { - "context": "Build Docs", + "context": "Spec Main Shape Gate", "integration_id": 15368 }, { - "context": "Changeset Declaration", + "context": "Governed Surface Queue Guard", "integration_id": 15368 } ] diff --git a/scripts/__tests__/fixtures/required-check-set/ruling-9969.json b/scripts/__tests__/fixtures/required-check-set/ruling-12240.json similarity index 97% rename from scripts/__tests__/fixtures/required-check-set/ruling-9969.json rename to scripts/__tests__/fixtures/required-check-set/ruling-12240.json index 6475bdd40f..bb069776fa 100644 --- a/scripts/__tests__/fixtures/required-check-set/ruling-9969.json +++ b/scripts/__tests__/fixtures/required-check-set/ruling-12240.json @@ -69,19 +69,19 @@ "integration_id": 15368 }, { - "context": "Test", + "context": "Build Docs", "integration_id": 15368 }, { - "context": "Build Docs", + "context": "Changeset Declaration", "integration_id": 15368 }, { - "context": "Changeset Declaration", + "context": "Test", "integration_id": 15368 }, { - "context": "Spec Main Shape Gate", + "context": "Governed Surface Queue Guard", "integration_id": 15368 } ] diff --git a/scripts/__tests__/fixtures/required-check-set/type-check-removed.json b/scripts/__tests__/fixtures/required-check-set/type-check-removed.json index dcca45212f..5e611855e5 100644 --- a/scripts/__tests__/fixtures/required-check-set/type-check-removed.json +++ b/scripts/__tests__/fixtures/required-check-set/type-check-removed.json @@ -65,19 +65,19 @@ "integration_id": 15368 }, { - "context": "Test", + "context": "Build Docs", "integration_id": 15368 }, { - "context": "Build Docs", + "context": "Changeset Declaration", "integration_id": 15368 }, { - "context": "Changeset Declaration", + "context": "Test", "integration_id": 15368 }, { - "context": "Spec Main Shape Gate", + "context": "Governed Surface Queue Guard", "integration_id": 15368 } ] diff --git a/scripts/check-required-check-set.mjs b/scripts/check-required-check-set.mjs index a9078b64a3..7637385368 100644 --- a/scripts/check-required-check-set.mjs +++ b/scripts/check-required-check-set.mjs @@ -16,9 +16,9 @@ * * ## The gap (objectui#9422, out of the residual of objectui#3535) * - * `main` sits behind an enforced merge queue whose required set currently holds - * nine contexts. That set lives in GitHub ruleset 11776024 -- repository - * SETTINGS. Removing a member from it reds no gate, fires no alarm, and leaves + * `main` sits behind an enforced merge queue whose required set lives in GitHub + * ruleset 11776024 -- repository SETTINGS (what it holds is this script's + * reading, not a number written here). Removing a member from it reds no gate, fires no alarm, and leaves * no trace in a diff anybody reviews. * * What that costs is not hypothetical. On 2026-08-07, with the required set @@ -72,7 +72,7 @@ * * Measured 2026-09-14 on ruleset 11776024, which answered with all five rule * types (`deletion`, `merge_queue`, `non_fast_forward`, `pull_request`, - * `required_status_checks`) and the nine contexts in `WATCHED_CONTEXTS` below. + * `required_status_checks`) and the nine contexts it required that day. * * ⚠️ The reading above was taken through an agent egress proxy that injects * credentials, so it does NOT establish what scope the endpoint needs: the same @@ -112,10 +112,10 @@ * only, and it is an argument FOR watching: the `Type Check` leg is untested in * practice, so its silent removal is the removal least likely to be noticed. * - * ⇒ the failure mode ACCEPTED by this split: a silent removal of any of the - * eight watched contexts is REPORTED in the run summary and does not fail - * anything. Eight ninths of the set is made observable, not enforced. Promoting - * one is a one-line move from `WATCHED_CONTEXTS` to `PINNED_CONTEXTS` and a + * ⇒ the failure mode ACCEPTED by this split: a silent removal of any watched + * context is REPORTED in the run summary and does not fail anything. Every + * member but the pinned one is made observable, not enforced. Promoting one is + * a one-line move from `WATCHED_CONTEXTS` to `PINNED_CONTEXTS` and a * maintainer's call, not a tidy-up. * * ## Why the rule types are pinned too, and why that is not a widening @@ -156,7 +156,7 @@ export const EXIT_BREACHED = 3; /** * The contexts whose absence FAILS this gate. See the header for why this is - * one name and not nine. + * one name and not the whole set. */ export const PINNED_CONTEXTS = Object.freeze(['Type Check']); @@ -184,13 +184,20 @@ export const PINNED_CONTEXTS = Object.freeze(['Type Check']); * assumed. ⛔ Re-take the live reading after the click; do not edit this list * to agree with whatever the endpoint says. * - * `Spec Main Shape Gate` joined under objectui#9969's ruling A, which is a - * ruling and not an endpoint reading: the maintainer enrolled the context, and - * `scripts/dependabot-merge-gate.mjs` moved it into `REQUIRED_CONTEXTS` in the - * same change that added it here. Before that change the patrol printed the - * name as "required but not declared", and losing it from the ruleset would - * only have shortened that list, naming nothing. WATCHED, not pinned: - * promoting it is the maintainer's call this header describes, ⛔ not a tidy-up. + * ⭐ objectui#12240 is the same kind of change, run the other way: its ruling + * names the set this list and `PINNED_CONTEXTS` now declare, the pull request + * that writes it here merges, and the maintainer edits the ruleset in the same + * sitting. Between those two acts the live answer still carries + * `Spec Main Shape Gate`, and the patrol reports it as required but not + * declared -- `drifted`, exit 0, never red. The test holds that window too, on + * a dated reading taken the day the ruling was executed. + * + * `Governed Surface Queue Guard` is declared here by objectui#12240, whose + * ruling names it. The maintainer enrolled it under objectui#6596 and nothing + * added it to this list, so until this change every reading printed it as + * required but not declared (measured on the live endpoint 2026-10-12). WATCHED, + * not pinned, like every name below: promoting one is the maintainer's call + * this header describes, ⛔ not a tidy-up. */ export const WATCHED_CONTEXTS = Object.freeze([ 'Lint', @@ -198,7 +205,17 @@ export const WATCHED_CONTEXTS = Object.freeze([ 'Test', 'Build Docs', 'Changeset Declaration', - 'Spec Main Shape Gate', + // ⛔ TOMBSTONE -- `Spec Main Shape Gate`, watched here from objectui#9969 + // (ruling A, which enrolled it) to objectui#12240, which removed it: the + // maintainer's ruling B on objectui#12214's open question 1 (ACCEPT + // 6123370050) takes the context out of the required set -- advisory on pull + // requests, hourly on `main` in the full battery, nothing it verifies + // changed (`.github/workflows/spec-main-shape-gate.yml`'s header). ⛔ Do not + // re-add it to agree with whatever the endpoint says: the ruleset edit is + // the maintainer's act, and this list names the set the ruling installs. + // Re-enrolling it is a ruling, and its in-tree half is moving the name back + // into `REQUIRED_CONTEXTS` in `scripts/dependabot-merge-gate.mjs`. + 'Governed Surface Queue Guard', ]); /** diff --git a/scripts/dependabot-merge-gate.mjs b/scripts/dependabot-merge-gate.mjs index d7ed53137d..21f065a24e 100644 --- a/scripts/dependabot-merge-gate.mjs +++ b/scripts/dependabot-merge-gate.mjs @@ -142,7 +142,6 @@ import { isEntrypoint } from './invoked-as.mjs'; * repo-gates.yml Repo Gates * changeset-presence.yml Changeset Declaration * governed-surface-guard.yml Governed Surface Queue Guard - * spec-main-shape-gate.yml Spec Main Shape Gate * * `Repo Gates` is ONE context for fifteen checks (objectui#12214): the * one-check workflows that each produced a context of their own -- the @@ -201,28 +200,10 @@ export const REQUIRED_CONTEXTS = Object.freeze([ 'Repo Gates', 'Changeset Declaration', 'Governed Surface Queue Guard', - // objectui#9969, ruling A — the residual of objectui#9860's ruling C. This - // name sat in `NOT_A_GATE` until two facts that were somebody else's to - // establish had both been established, and it moved only after both were: - // (1) the maintainer enrolled the context in ruleset 11776024, read back - // on 2026-09-27 and recorded on objectui#9969. ⛔ A dated reading, not - // a live one: `scripts/check-required-check-set.mjs` re-takes it, and - // no decision in this file turns on what the ruleset says; - // (2) the day-one break cleared: the check read `success` on this - // repository's `main` on 2026-09-27, and the job itself re-derives that - // verdict on every queue build. On 2026-09-18 its first run was RED on - // one objectui file for a spec change made in objectstack — enrolled in - // that state, it would have turned every objectui pull request red over - // an objectstack commit, which is why it waited. - // ⭐ The two halves were ordered, not simultaneous: ruleset first, this line - // second. This gate reads check runs and never the ruleset, so with this name - // here and the ruleset silent it would still wait for the check on a - // Dependabot head and refuse it red or absent — stricter than the queue, never - // looser. Enrolling first means no Dependabot bump ever waited on a check the - // queue did not. What a red here says, and why it may name an objectstack - // commit rather than anything the bump changed, is `spec-main-shape-gate.yml`'s - // header. - 'Spec Main Shape Gate', + // ⛔ `Spec Main Shape Gate` is NOT here since objectui#12240: it sat here from + // objectui#9969 (ruling A, the in-tree half of enrolling it) and moved to + // `NOT_A_GATE` below when the maintainer's ruling B took it out of the + // required set. The reason is written there. ]); /** @@ -306,6 +287,8 @@ export const NOT_A_GATE = Object.freeze({ 'Test (coverage shard 2/4)': COVERAGE_SHARD_NOT_A_GATE, 'Test (coverage shard 3/4)': COVERAGE_SHARD_NOT_A_GATE, 'Test (coverage shard 4/4)': COVERAGE_SHARD_NOT_A_GATE, + 'Spec Main Shape Gate': + "spec-main-shape-gate.yml is ADVISORY on a pull request by ruling (objectui#12240: the maintainer's ruling B on objectui#12214's open question 1, ACCEPT 6123370050). It compiles this repository against `@objectstack/spec` built from objectstack `main`, so most of its reds name an objectstack commit rather than anything the pull request changed (measured on objectui#12214, not copied here), and its place in the two-speed doctrine (objectstack#22998) is the hourly `schedule` run on `main`, where a red is a main-red for the `domain:devx` lane. It still reports on every pull request and its verdict is real -- nothing it verifies changed and it carries no `continue-on-error` -- so a red here is ignored because the lane is advisory, never because the check cannot fail. It sat in `REQUIRED_CONTEXTS` from objectui#9969 (ruling A) to objectui#12240, the in-tree half of the maintainer's enrolment; ⛔ moving it back is the in-tree half of a re-enrolment, a ruling and not a tidy-up, and it puts the workflow back inside the `merge_group` floor `merge-queue-reporting.test.ts` derives from that list.", 'Live E2E (informational)': 'live-e2e.yml is declared INFORMATIONAL and NON-REQUIRED in its own header, and ci-cd-pipeline.md says in as many words not to add it to required checks. It carries no `continue-on-error` since objectui#8084 — that flag left this check run red anyway and only inverted the workflow-run conclusion — so a red here is a real red, ignored because the lane is advisory and never because the check could not fail.', label: