Skip to content

Commit b3d26a3

Browse files
committed
fix(ci): drop the Build Docs / Console Pin Gate required-context rows the live ruleset never carried (#9533)
The 2026-08-18 ruling on #9533 reconciles the registry to Settings: both rows go, with a dated tombstone. Re-verified from this seat before touching the file — GET /rulesets/12119582 answers 200 and carries exactly six contexts, neither of these among them. The history behind the rows is not what the ruling's wording assumed, so the tombstone records it: the 2026-08-09 closing ruling APPROVED the four-name second batch in the future tense ("The maintainer applies this in Settings"), and nothing ever attested it applied — while the two 2026-08-07 rows say "applied to the settings the same day". The one contemporaneous transcription of the ruleset (the 2026-08-10 screenshot quoted on #7022) shows only Build Core of that batch. So this is most likely a half-applied approval rather than a removal. It cannot be proven: ruleset history is 403 to a seat (re-measured here and recorded in the header, since the current-state read #9642 unlocked does not extend to how the set got that way). The tombstone is prose, not a RETIRED_CONTEXT_NAMES row, against the #9523 precedent the ruling cited — measured first: neither name is dead (both jobs still publish these check-runs), and a ledger row reds the instruction-surface scan on docs/releases-maintenance.md, which names Console Pin Gate twice in correct current prose, with a diagnostic that would be false. Also: two self-test fixtures re-pointed off the dropped jobs (a fixture mutating an unregistered job asserts nothing while reading unchanged); ci.yml's console-pin preamble no longer claims a required-check contract it does not have; Build Core / Temporal Conformance `authorized` lines now say what is attested applied versus merely approved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
1 parent 11b779e commit b3d26a3

2 files changed

Lines changed: 114 additions & 38 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,10 +1346,17 @@ jobs:
13461346
# under `packages/**`, and watching it here would rebuild the console on a large
13471347
# fraction of every PR. release.yml stays the only check for that direction.
13481348
#
1349-
# The NAME is the required-check contract — the same trap the dogfood shards
1350-
# note above: renaming it silently drops the gate wherever branch protection
1351-
# lists it, with every PR still green. Pinned by `check:required-contexts`
1352-
# since #6865.
1349+
# ⚠️ This gate is ADVISORY, and its name is no longer pinned. It was enrolled
1350+
# in `check:required-contexts` in #6865 on the strength of the 2026-08-09
1351+
# ruling approving it for the required set; the live ruleset read of
1352+
# 2026-08-18 (#9533) found it had never landed there, and the row was dropped
1353+
# rather than left claiming otherwise — see that file's TOMBSTONE for the full
1354+
# provenance. So a red here does NOT block a merge, and renaming this job no
1355+
# longer turns anything red. The name is still load-bearing PROSE in four
1356+
# places that tell it apart from `Console Pin Freshness`
1357+
# (docs/releases-maintenance.md, packages/console/README.md,
1358+
# scripts/check-objectui-pin-fresh.mjs, .github/workflows/objectui-pin-freshness.yml);
1359+
# renaming it silently falsifies all four.
13531360
console-pin:
13541361
name: Console Pin Gate
13551362
needs: filter

‎scripts/check-required-contexts.mjs‎

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@
105105
* `required_status_checks` contexts and `strict_required_status_checks_policy:
106106
* false`.
107107
*
108+
* ⚠️ What is readable is the set as it is NOW, never how it got there. Both
109+
* history routes are shut to a seat, re-measured 2026-08-18 (#9533) with the
110+
* price GitHub names in each response:
111+
*
112+
* GET .../rulesets/12119582/history 403 administration=write
113+
* GET .../rulesets/rule-suites 403 administration=read
114+
*
115+
* So "when did this context leave the set" and "was it ever in it" are NOT
116+
* agent-answerable, and no amount of re-reading the live endpoint turns into
117+
* an answer. A row's provenance therefore has to be carried in writing, here,
118+
* by the PR that adds it — which is why `authorized` distinguishes a ruling
119+
* that APPROVED a context from an application that was ATTESTED APPLIED. That
120+
* distinction is not pedantry: #9533 turned on it (see the tombstone below).
121+
*
108122
* So the two lists are machine-comparable, and `--verify-required-set` below
109123
* compares them in BOTH directions. What that mode is NOT is a merge-blocking
110124
* gate, for a structural reason rather than a squeamish one: the settings half
@@ -122,17 +136,19 @@
122136
* rulings on #5617. Adding a row here does not make a context required, and
123137
* only a maintainer can change the settings. What #9642 changed is that a
124138
* disagreement is now MEASURED instead of invisible — and the first run found
125-
* one: `Build Docs` and `Console Pin Gate` are registered here and are NOT in
126-
* the live required set, i.e. both gate families are advisory today with no
127-
* signal anywhere (#5617's unsignalled half, verbatim). Whether the fix is to
128-
* drop the rows or to restore the contexts is #9533's maintainer decision; this
129-
* file reports it and decides nothing.
139+
* one: `Build Docs` and `Console Pin Gate` were registered here and were NOT
140+
* in the live required set. That first finding is closed (#9533, 2026-08-18
141+
* ruling — the two rows dropped; the tombstone below carries the provenance),
142+
* so the two lists agree in both directions today and the sweep prints its
143+
* ✅ line. The mechanism stays for the next disagreement; it reports and
144+
* decides nothing.
130145
*
131146
* ## Why `if:` is deliberately NOT asserted
132147
*
133148
* #6865's own body proposed asserting the enrolled jobs carry no `if:`. That is
134-
* right for lint.yml's two and WRONG for four of the six the maintainer added
135-
* on 2026-08-09: `build-core`, `build-docs`, `console-pin` and
149+
* right for lint.yml's two and WRONG for the four jobs the 2026-08-09 ruling
150+
* approved (approved — how much of that batch reached the settings is #9533's
151+
* finding below): `build-core`, `build-docs`, `console-pin` and
136152
* `temporal-conformance` each carry `if: ${{ !cancelled() && needs.filter…}}`
137153
* BY DESIGN — THE FILTER CONTRACT (#4928). A job-level `if:` that skips still
138154
* publishes a check run (conclusion `skipped`, which branch protection counts
@@ -197,6 +213,60 @@ import { fileURLToPath } from 'node:url';
197213
* the registry-follows half of the header's two-step; the Settings half is
198214
* the maintainer attestation above.
199215
*
216+
* ⛔ TOMBSTONE — `Build Docs` (ci.yml:build-docs) and `Console Pin Gate`
217+
* (ci.yml:console-pin), dropped 2026-08-18 by the #9533 ruling. Recorded at
218+
* length because the shape here is NOT the one the ruling's wording assumed,
219+
* and a future reader who re-derives it from git will otherwise reach the
220+
* same wrong conclusion twice:
221+
*
222+
* • What the rows claimed. Both carried `authorized: '#5617 closing ruling
223+
* 2026-08-09, second batch'` — and that ruling reads «Second batch
224+
* approved: Build Core, Build Docs, Console Pin Gate, Temporal Conformance
225+
* (live PG + MySQL) join the required set … The maintainer applies this in
226+
* Settings alongside confirming item 2.» An APPROVAL, in the future tense,
227+
* of a Settings action. Contrast the two 2026-08-07 rows, whose
228+
* `authorized` says "applied to the settings the same day": those attest
229+
* an accomplished state. Nothing anywhere ever attested this batch applied.
230+
* • What the settings actually took. The one contemporaneous transcription
231+
* of the ruleset — the maintainer's 2026-08-10 screenshot, quoted on
232+
* #7022 — lists `ADR maintainer approval` "alongside TypeScript Type
233+
* Check / ESLint / Test Core / Dogfood Regression Gate / Build Core".
234+
* Of the four-name batch, only `Build Core` is there. `Temporal
235+
* Conformance` arrived later; these two never appear in any reading.
236+
* • So this is most likely a HALF-APPLIED approval, not a removal — the
237+
* #6865 two-step's other failure mode, and one no one could see while the
238+
* required set was believed unreadable. It cannot be proven: ruleset
239+
* history is 403 to a seat (see the header). Stated as the likelihood it
240+
* is, because the ruling's own veto clause ("if the removal was in fact
241+
* accidental, restore the contexts in Settings instead") is aimed at a
242+
* removal, and the maintainer re-reading this should know the choice on
243+
* the table is really "apply the standing 2026-08-09 approval, or let it
244+
* lapse". The ruling let it lapse; that is what these rows record.
245+
*
246+
* ⛔ And why this tombstone is PROSE and not a RETIRED_CONTEXT_NAMES row,
247+
* against the #9523 precedent the ruling cited. That ledger bans a name from
248+
* the instruction surfaces because the name is DEAD — no check-run reports it
249+
* any more. Neither of these names is dead: both jobs still exist, still carry
250+
* these `name:` literals, and still publish these check-runs on every PR that
251+
* trips their filter. They lost REQUIRED status, which is a different fact.
252+
* Ledgering them anyway was measured before it was rejected (2026-08-18):
253+
* `docs/releases-maintenance.md` names `Console Pin Gate` 2× in correct,
254+
* current prose that exists to keep it apart from `Console Pin Freshness`, so
255+
* the row reds the scan there and prints the diagnostic "a seat following this
256+
* text looks for a check-run that no longer reports" — false, about prose that
257+
* is right. Budgeting around it would only arm the trap for the next author
258+
* who legitimately names the live job.
259+
*
260+
* ⚠️ The cost of the drop, stated rather than discovered later: these two
261+
* `name:` literals are now pinned by NOTHING, while five places still refer to
262+
* the jobs by name (`docs/releases-maintenance.md`, `packages/console/README.md`,
263+
* `scripts/check-objectui-pin-fresh.mjs`, `.github/workflows/objectui-pin-freshness.yml`
264+
* and lint.yml's cross-reference). Renaming either job no longer detaches a
265+
* required gate — that is the whole point — but it does silently falsify that
266+
* prose. Filed as its own card rather than solved here, since a pin for
267+
* "contract job names that are not required contexts" is a new mechanism and
268+
* this card is ledger hygiene.
269+
*
200270
* ⛔ `carries` names the gate FAMILY and never a step count — assertion 10
201271
* enforces that, because prose here reads as measurement while being asserted
202272
* by nothing. Both counts this registry used to carry were wrong on the day
@@ -257,28 +327,18 @@ export const REQUIRED_CONTEXTS = [
257327
workflow: 'ci.yml',
258328
job: 'build-core',
259329
context: 'Build Core',
260-
authorized: '#5617 closing ruling 2026-08-09, second batch — "the highest-value addition"',
330+
authorized:
331+
'#5617 closing ruling 2026-08-09, second batch — "the highest-value addition"; ' +
332+
'the only member of that batch the 2026-08-10 ruleset screenshot (#7022) shows applied, and live in the 2026-08-18 read',
261333
carries: 'the only compile-regression gate in the repo',
262334
},
263-
{
264-
workflow: 'ci.yml',
265-
job: 'build-docs',
266-
context: 'Build Docs',
267-
authorized: '#5617 closing ruling 2026-08-09, second batch',
268-
carries: 'the docs-site build',
269-
},
270-
{
271-
workflow: 'ci.yml',
272-
job: 'console-pin',
273-
context: 'Console Pin Gate',
274-
authorized: '#5617 closing ruling 2026-08-09, second batch',
275-
carries: 'the pinned-console build reconciliation',
276-
},
277335
{
278336
workflow: 'ci.yml',
279337
job: 'temporal-conformance',
280338
context: 'Temporal Conformance (live PG + MySQL)',
281-
authorized: '#5617 closing ruling 2026-08-09, second batch',
339+
authorized:
340+
'#5617 closing ruling 2026-08-09, second batch; absent from the 2026-08-10 screenshot and present in the ' +
341+
'2026-08-18 read, so it was applied somewhere between the two — no attestation names the day',
282342
carries: 'the live-server datetime conformance axis (#3912/#3942)',
283343
},
284344
];
@@ -1036,7 +1096,8 @@ export function renderRequiredSetReport(verdict) {
10361096
}
10371097
lines.push(
10381098
' Resolving this is a MAINTAINER decision (drop the row, or restore the context in Settings → Rulesets);',
1039-
' this script decides nothing. The open card for the current pair is #9533.',
1099+
' this script decides nothing. File a card and follow the #6865 two-step; #9533 is the worked precedent',
1100+
' (it dropped a pair, and its tombstone in REQUIRED_CONTEXTS records what a row does and does not attest).',
10401101
);
10411102
}
10421103

@@ -1272,18 +1333,24 @@ async function selfTest() {
12721333
);
12731334

12741335
// ── (2) the job disappearing entirely ─────────────────────────────────────
1275-
const droppedJob = fixture('drop the console-pin job', 'ci.yml', (s) => s.replace('\n console-pin:\n', '\n console-pin-disabled:\n'));
1336+
// Re-pointed from `console-pin` to `temporal-conformance` when #9533 dropped
1337+
// the console-pin row: a fixture must mutate a job the registry still
1338+
// REGISTERS, or it asserts nothing while reading exactly as before.
1339+
const droppedJob = fixture('drop the temporal-conformance job', 'ci.yml', (s) =>
1340+
s.replace('\n temporal-conformance:\n', '\n temporal-conformance-disabled:\n'),
1341+
);
12761342
assert(
1277-
droppedJob.problems.some((p) => p.includes("job 'console-pin' no longer exists") && p.includes('Console Pin Gate')),
1343+
droppedJob.problems.some((p) => p.includes("job 'temporal-conformance' no longer exists") && p.includes('Temporal Conformance (live PG + MySQL)')),
12781344
'a required context whose job id is gone ⇒ red',
12791345
);
12801346

12811347
// ── (4) growing a matrix on a registered job ──────────────────────────────
1282-
const matrixed = fixture('matrix on build-docs', 'ci.yml', (s) =>
1283-
s.replace(' build-docs:\n name: Build Docs\n', ' build-docs:\n name: Build Docs\n strategy:\n matrix:\n shard: [1, 2]\n'),
1348+
// Re-pointed from `build-docs` for the same reason as (2) above.
1349+
const matrixed = fixture('matrix on build-core', 'ci.yml', (s) =>
1350+
s.replace(' build-core:\n name: Build Core\n', ' build-core:\n name: Build Core\n strategy:\n matrix:\n shard: [1, 2]\n'),
12841351
);
12851352
assert(
1286-
matrixed.problems.some((p) => p.includes("job 'build-docs'") && p.includes('strategy.matrix')),
1353+
matrixed.problems.some((p) => p.includes("job 'build-core'") && p.includes('strategy.matrix')),
12871354
'a matrix on a required-context job ⇒ red (its bare name would never report)',
12881355
);
12891356

@@ -1816,11 +1883,13 @@ async function selfTest() {
18161883
assert(snapshot.strict === false, 'strict_required_status_checks_policy is read, not assumed');
18171884
assert(snapshot.enforcing.length === 1 && !snapshot.noEnforcingRuleset, 'one active ruleset covers the default branch');
18181885
assert(snapshot.unpinned.length === 0, `every context in the 2026-08-18 reading has a registry row — got ${JSON.stringify(snapshot.unpinned)}`);
1819-
// Direction A is LIVE against the checked-in registry: `Build Docs` and
1820-
// `Console Pin Gate` are registered and not required (#9533's open
1821-
// decision). This asserts the SHAPE — that the difference is reported and
1822-
// reported as advisory — not the membership, which is the maintainer's to
1823-
// change; hardcoding the pair would red this self-test the day they resolve it.
1886+
// Direction A against the checked-in registry. It was LIVE when this was
1887+
// written (`Build Docs` / `Console Pin Gate`, #9533) and is EMPTY since
1888+
// that card dropped the two rows — which is exactly why the assertion is
1889+
// written on the SHAPE (the difference is reported, and reported as
1890+
// advisory) and not on the membership: hardcoding the pair would have
1891+
// reddened this self-test on the very PR that resolved it. It survived
1892+
// that resolution unchanged; leave it derived.
18241893
const liveSet = new Set(snapshot.live);
18251894
assert(
18261895
snapshot.advisory.length === REQUIRED_CONTEXTS.filter((e) => !liveSet.has(e.context)).length &&

0 commit comments

Comments
 (0)