|
105 | 105 | * `required_status_checks` contexts and `strict_required_status_checks_policy: |
106 | 106 | * false`. |
107 | 107 | * |
| 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 | + * |
108 | 122 | * So the two lists are machine-comparable, and `--verify-required-set` below |
109 | 123 | * compares them in BOTH directions. What that mode is NOT is a merge-blocking |
110 | 124 | * gate, for a structural reason rather than a squeamish one: the settings half |
|
122 | 136 | * rulings on #5617. Adding a row here does not make a context required, and |
123 | 137 | * only a maintainer can change the settings. What #9642 changed is that a |
124 | 138 | * 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. |
130 | 145 | * |
131 | 146 | * ## Why `if:` is deliberately NOT asserted |
132 | 147 | * |
133 | 148 | * #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 |
136 | 152 | * `temporal-conformance` each carry `if: ${{ !cancelled() && needs.filter…}}` |
137 | 153 | * BY DESIGN — THE FILTER CONTRACT (#4928). A job-level `if:` that skips still |
138 | 154 | * publishes a check run (conclusion `skipped`, which branch protection counts |
@@ -197,6 +213,60 @@ import { fileURLToPath } from 'node:url'; |
197 | 213 | * the registry-follows half of the header's two-step; the Settings half is |
198 | 214 | * the maintainer attestation above. |
199 | 215 | * |
| 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 | + * |
200 | 270 | * ⛔ `carries` names the gate FAMILY and never a step count — assertion 10 |
201 | 271 | * enforces that, because prose here reads as measurement while being asserted |
202 | 272 | * by nothing. Both counts this registry used to carry were wrong on the day |
@@ -257,28 +327,18 @@ export const REQUIRED_CONTEXTS = [ |
257 | 327 | workflow: 'ci.yml', |
258 | 328 | job: 'build-core', |
259 | 329 | 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', |
261 | 333 | carries: 'the only compile-regression gate in the repo', |
262 | 334 | }, |
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 | | - }, |
277 | 335 | { |
278 | 336 | workflow: 'ci.yml', |
279 | 337 | job: 'temporal-conformance', |
280 | 338 | 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', |
282 | 342 | carries: 'the live-server datetime conformance axis (#3912/#3942)', |
283 | 343 | }, |
284 | 344 | ]; |
@@ -1036,7 +1096,8 @@ export function renderRequiredSetReport(verdict) { |
1036 | 1096 | } |
1037 | 1097 | lines.push( |
1038 | 1098 | ' 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).', |
1040 | 1101 | ); |
1041 | 1102 | } |
1042 | 1103 |
|
@@ -1272,18 +1333,24 @@ async function selfTest() { |
1272 | 1333 | ); |
1273 | 1334 |
|
1274 | 1335 | // ── (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 | + ); |
1276 | 1342 | 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)')), |
1278 | 1344 | 'a required context whose job id is gone ⇒ red', |
1279 | 1345 | ); |
1280 | 1346 |
|
1281 | 1347 | // ── (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'), |
1284 | 1351 | ); |
1285 | 1352 | 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')), |
1287 | 1354 | 'a matrix on a required-context job ⇒ red (its bare name would never report)', |
1288 | 1355 | ); |
1289 | 1356 |
|
@@ -1816,11 +1883,13 @@ async function selfTest() { |
1816 | 1883 | assert(snapshot.strict === false, 'strict_required_status_checks_policy is read, not assumed'); |
1817 | 1884 | assert(snapshot.enforcing.length === 1 && !snapshot.noEnforcingRuleset, 'one active ruleset covers the default branch'); |
1818 | 1885 | 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. |
1824 | 1893 | const liveSet = new Set(snapshot.live); |
1825 | 1894 | assert( |
1826 | 1895 | snapshot.advisory.length === REQUIRED_CONTEXTS.filter((e) => !liveSet.has(e.context)).length && |
|
0 commit comments