Skip to content

Commit 1297178

Browse files
claude[bot]claude
andauthored
fix(agents,gates): the merge queue blocks on seven contexts, and a set-stating surface is now a census (#18244)
Fixes #17798 `AGENTS.md`'s merge-queue paragraph named SIX required contexts and called everything else "advisory and rides through". The live ruleset has SEVEN — `Governed Surface Queue Guard`, enrolled 2026-08-27 (#12427) and pinned in `REQUIRED_CONTEXTS` by #15233. The sentence a review seat acts on therefore classified the one gate that refuses a zero-review governed PR as advisory, which is verbatim the incident shape that guard exists to prevent. Nothing reddened on it, and that is the half worth fixing. `mustName` is a FLOOR: it reds when a listed literal goes stale and is blind to one going MISSING. So the registry grew, every surface's list stayed legal, every gate stayed green, and the sentence stayed behind. The same misclassification has now landed twice — 2 to 6 by the #9677 ruling, 6 to 7 here. ## 维护者速读(草稿) **改了什么。** 两处。一、`AGENTS.md` 的合并队列段落从「六个必需上下文」改成七个,把第七个 `Governed Surface Queue Guard` 写进名单(等行数改写,1075 行没动)。二、`scripts/check-required-contexts.mjs` 的 `INSTRUCTION_SURFACES` 增加一个 `statesTheSet` 声明:声明了它的文件,其名单必须与注册表**逐个且等长**地对上。注册表本身(`REQUIRED_CONTEXTS`)一行未动,那是 #15233 的面。 **为什么改。** 这句话不是描述,是审核席翻 ready / 挂 auto-merge / 入队前照着做的操作指令。它把治理面守卫说成 advisory,而那个守卫的职责恰恰是拒掉零审查的治理 PR —— #12427 的事故形态。更关键的是:上一次入列(#15233 加第七行)时,全部门禁是绿的,没有任何东西提示这句话已经过期。同一个漏洞已经发生两次,所以这次不只是手跟一遍数字,而是把「谁陈述了整个集合」变成机器可查的:下一次(第八个)入列时,加注册行的那个 PR 自己会变红。 **风险与代价(含回滚)。** 风险低。新规则只对**显式声明** `statesTheSet: true` 的条目生效,今天是两个文件(`AGENTS.md` 与 pm-dispatch 的 `platform-readings.md`);`review-checklist.md` 被明确归类为**不陈述集合**(它点名的是审核席亲手确认的两个 job,不是集合),保留它原有的两名下限,集合变大不会误伤它。声明也不能被悄悄摘掉换取豁免:名单已覆盖整个集合却没声明的条目同样变红。代价:每次入列多一处必须同 PR 跟进的数组。回滚 = `git revert`,两个文件都是纯文本,没有生成物、没有发布面、没有数据迁移。 **席位意见。** **你要做的。** 确认一件事:第七个上下文 `Governed Surface Queue Guard` 今天确实在 Settings 的必需集合里(卡面 2026-09-12 的实测读数是七个,本 PR 不改 Settings)。其余不需要你操作。本 PR 触及受管面 `AGENTS.md`,按 Prime Directive #14 走人工合并或已批准的队列路径。 ## What changed **1. `AGENTS.md` :505-:510 — six to seven.** The seventh name inserted, "six" to "seven" in all three places, equal-line at the 1075 ceiling (before 1075 / after 1075 / ceiling 1075). Each context literal is kept whole on one line: the scan matches them contiguously, and a wrap that split `TypeScript Type Check` across a line break made the surface red. That is a real trap for the next hand-follow, so it is recorded here rather than only avoided. **2. `INSTRUCTION_SURFACES` gains `statesTheSet`.** An entry that declares it must name the registry EXACTLY — membership *and* count: - omitting a member reds, naming the omitted literal and the count it is short by; - padding past the registry length reds on the count, so a duplicate cannot mask a member lost to a typo; - a full list with the declaration dropped reds ("a list that covers the whole set IS a statement of it"), so the exemption cannot be taken silently; - ablating every declaration reds rather than ticking (#4690). `review-checklist.md` is classified the other way and keeps its two-name floor: it names the two required jobs a seat confirms by hand — its own next line sends the seat to `true-green.md` for the rest — so it never claimed to enumerate the set, and the set growing must not red it. **3. Ten self-test cases, battery floor 31 to 41.** The 6-of-7 omission and its restore-leg ablation; the eighth-row enrolment end to end, plus the hand-off where following the ARRAYS clears the census red and leaves the naming floor demanding the PROSE; the padded duplicate; the undeclared full list; the no-declaration floor; and the two classification pins (which surfaces state the set, by NAME never by count; and the checklist's partial list staying legal). ## Evidence Baseline first, on `origin/main` `b3b43b6` in a clean worktree, BEFORE any edit — the known pit from hold note 5651882793 (PR #17803 reported `--self-test` red on a pre-existing `branches: [main]` filter on `governed-surface-guard.yml`): ``` node scripts/check-required-contexts.mjs --self-test exit 0 159 assertions node scripts/check-required-contexts.mjs exit 0 7 required context name(s) pinned across 3 workflow(s) ``` **The known pit is NOT red on `main` today.** The residual named in the hold note is gone; the work below is measured against a green baseline, not against a standing red. After (`78959ab`): `--self-test` exit 0, 169 assertions; the pin exit 0. **Reverse verification, both legs from the committed implementation, each with its on-disk mutation proved and each restored byte-identical (`git hash-object` vs the HEAD blob, `git diff HEAD` empty):** | leg | mutation (proved on disk) | result | |---|---|---| | A — the registry array rots back | drop `'Governed Surface Queue Guard'` from the `AGENTS.md` entry's `mustName` (grep 2 to 1; `git diff --numstat` 0/1) | **RED**, exit 1: "declares statesTheSet: true, so its mustName must be the required set EXACTLY — it lists 6 name(s) against a registry of 7, missing 'Governed Surface Queue Guard'". Self-test exit 1 too. | | B — the numeral alone rots back | `seven contexts block` to `six contexts block` in `AGENTS.md`, all seven literals still listed (numstat 1/1) | **GREEN, exit 0** — reported as measured, not as expected. The scan pins literals, never the word introducing them. Recorded as a residual in the script header rather than implied covered. | | B2 — the prose drops the literal | delete `` and `Governed Surface Queue Guard` `` from the paragraph (grep 1 to 0; numstat 1/1) | **RED**, exit 1: "AGENTS.md no longer names the required context 'Governed Surface Queue Guard'" | | floor control | battery floor 41 to 42 | **RED**, exit 1, naming the exact count: "registered 41 case(s), below its pinned floor of 42" — so the floor binds at headroom 0 and 41 is the measured count, not a number below it | Leg B is the honest finding of this PR: the census forces every enrolled literal INTO the prose, so a stale numeral now sits next to a complete list rather than a short one. Bounded, not covered; pinning the numeral needs the arbitrary-literal recognition the script header already measured as out of reach. **Derived gate union**, run after the final commit, on `78959ab` (`git rev-parse --short HEAD`), each exit captured by redirect before any pipe: ``` node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 37 command(s) from a 2-path change set (AGENTS.md, scripts/check-required-contexts.mjs) ``` 36 of 37 exit 0. The one not measured: `pnpm check:pm-dispatch-gates` (`scripts/pm/check-dispatch-gates.mjs`) runs past this session's foreground ceiling — it was still printing passing cases at 560s. NOT MEASURED, reason: runtime exceeds the foreground cap; CI owns it. Its subject matter — the `ROOT_FILE_WATCH_HINTS` declaration this file carries — is separately green under `pnpm check:watch-hint-literal` (exit 0) and under this script's own `the dispatch-gates declaration (#9979)` battery (6 assertions). `--ran` reconciliation is reported in the dev report; the beyond-derivation families this card owes because it edits a gate script — `check:required-contexts` and its `--self-test` — are the two green readings above. `git grep` finds no `*.test.*` naming `check-required-contexts.mjs`, so that script has no separate test suite to owe. **Lint, narrowed with the three readings that make a narrowing a measurement:** 1. covered population read from eslint's own config, not guessed: `npx eslint --print-config scripts/check-required-contexts.mjs` reports exactly **2 rules enabled** for this path (`no-restricted-imports`, `comment-swallow/no-code-inside-block-comment`); `eslint.config.mjs` declares no markdown population at all, so `AGENTS.md` is outside the lint verdict in either direction; 2. file count read from `--format json`: 1 file, 0 errors, 0 warnings; 3. invariance for untouched files: this repo runs one `eslint.config.mjs` which **never enables type-aware linting for any file** (no `parserOptions.project`, no typed rules — `eslint.config.mjs` :326-:329, with its own positive-control measurement recorded there), so this diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` scan is CI's run. **`skip-changeset`, measured rather than asserted:** both paths lie outside every package directory, and of the 70 published packages none has a `files[]` entry escaping its own directory (0 entries starting with `../` or `/`). Nothing published moves. ## Acceptance notes Out of scope, noted, not filed: - **`.claude/skills/pm-dispatch/references/platform-readings.md` :385-:386 becomes FALSE when this lands.** It reads 「⭐ 本表的 `mustName` 不要求排他 ⇒ 第七个加注册行不会让本表变红」 and 「⇒ ⛔ 门绿不是本行已对的读数:计数行只能手跟改」. After this PR that entry declares `statesTheSet: true` and the registry growing DOES red it, so a seat reading those two lines would keep hand-following a line the gate now holds. Not fixed here: the file is outside this card's declared REGION claim (`AGENTS.md` :505-:510 plus this script), and the script's own header records `.claude/skills/pm-dispatch/**` as a surface a dev seat may not edit — the reason the checklist half of #9325 was its own card. **Successor: the `domain:skills` seat, in its own lane.** Dedupe words: `platform-readings`, `mustName 不要求排他`, `计数行只能手跟改`, `statesTheSet`, `required contexts 的名单`. - The count WORD in a set-stating surface stays hand-followed (leg B above). Recorded as a residual in the script header, in the paragraph that already records the paraphrase-drift and shortening-rename residuals. No card: it is bounded by the census and closing it needs recognition the header measured as out of reach. - The triage grading comment 5651027386, which this card's acceptance is quoted from, answers **HTTP 404** — it is not on #17798 (the card carries exactly 3 comments) and the direct comment endpoint does not find it. Its content survives verbatim inside hold note 5651882793 and in this dispatch's own text, which is what acceptance items 1, 2 and 4 were read from here. Recorded as NOT MEASURED against the primary source, not as "no flags". Nothing else was touched. `REQUIRED_CONTEXTS` is byte-identical to `origin/main`. --- _Generated by [Claude Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1a02ef1 commit 1297178

2 files changed

Lines changed: 201 additions & 25 deletions

File tree

‎AGENTS.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -502,12 +502,12 @@ Even inside your own worktree, operate defensively:
502502
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:
503503
every poll then misreads "not on `main` yet" as "queued". Always read *two* things:
504504
the queue branch **and** `origin/main`. And **the queue enforces only the required
505-
set** — six contexts block: `Lint & Repo Gates` (all `check:*` gates),
506-
`TypeScript Type Check`, `Test Core`, `Dogfood Regression Gate`, `Build Core` and
507-
`Temporal Conformance (live PG + MySQL)`. A check outside those six is advisory and
508-
rides through, and an advisory red that lands rides `main`'s merge ref into every later
509-
PR until stanched. A required context is matched by check-run name, so a rename
510-
detaches its gate silently — treat those six names as contract.
505+
set** — seven contexts block: `Lint & Repo Gates` (all `check:*` gates),
506+
`TypeScript Type Check`, `Test Core`, `Dogfood Regression Gate`, `Build Core`,
507+
`Temporal Conformance (live PG + MySQL)` and `Governed Surface Queue Guard`. A check outside
508+
those seven is advisory and rides through, and an advisory red that lands rides `main`'s
509+
merge ref into every later PR until stanched. A required context is matched by check-run
510+
name, so a rename detaches its gate silently — treat those seven names as contract.
511511

512512
**Re-arm awareness** — none of these is a reason to avoid the queue; all are reasons to
513513
confirm a PR is still *in* it: a red queue build **ejects** your entry and drops

‎scripts/check-required-contexts.mjs‎

Lines changed: 195 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
226226
'(10) a `carries` string that embeds a step count': 3,
227227
'missing input is a failure, never a pass (#4690)': 6,
228228
'the `on:` key under both YAML schemas': 3,
229-
'instruction surfaces (#9491): the stale-name scan': 31,
229+
'instruction surfaces (#9491): the stale-name scan': 41,
230230
'the dispatch-gates declaration (#9979)': 6,
231231
'the wiring: this gate must actually run on every PR': 28,
232232
'the live mode stays OFF the required path': 3,
@@ -505,12 +505,26 @@ export const REQUIRED_CONTEXTS = [
505505
* that states the required set must contain each required literal (current,
506506
* or ledgered-former while in flight), so replacing a name with garbage or
507507
* deleting the sentence is red even though the garbage itself is never
508-
* recognised. RESIDUALS, recorded rather than implied covered: paraphrase
508+
* recognised. That half is a FLOOR and says nothing about what a surface
509+
* OMITS, which is its own defect shape: enrolling a context grew the registry
510+
* while every surface's list stayed legal, so the prose kept calling the new
511+
* context advisory-and-rides-through with every gate green (#9677 for the
512+
* 2→6 instance, #17798 for the 6→7 one — twice is a mechanism, not an
513+
* oversight). Hence `statesTheSet`: an entry that declares it must name the
514+
* registry EXACTLY, so the NEXT enrolment cannot land without following the
515+
* sentence. RESIDUALS, recorded rather than implied covered: paraphrase
509516
* drift (naming a context loosely, e.g. "Lint & Type Check" in
510517
* docs/launch-readiness.md) is invisible to both halves; and after a
511518
* SHORTENING rename (new name a substring of the old), an old-literal mention
512519
* satisfies the new name's `mustName` by substring — the budget half still
513-
* tracks the old name, so staleness stays bounded.
520+
* tracks the old name, so staleness stays bounded; and the COUNT WORD that
521+
* introduces such a list is prose like any other, so it is hand-followed and
522+
* unpinned — measured, not assumed: reverting AGENTS.md's 「seven contexts
523+
* block」 to 「six」 while all seven literals stay listed runs GREEN here.
524+
* Bounded by the census rather than covered by it: every enrolled literal is
525+
* forced into the prose, so a stale numeral sits next to a complete list, not
526+
* a short one. Pinning the numeral needs the arbitrary-literal recognition
527+
* this scan measured as out of reach above.
514528
*
515529
* ## The scan set, derived 2026-08-18, not assumed
516530
*
@@ -531,32 +545,42 @@ export const REQUIRED_CONTEXTS = [
531545
export const INSTRUCTION_SURFACES = [
532546
{
533547
// The merge-queue rule ("the queue enforces only the required set"),
534-
// naming every blocking context. States the required set ⇒ mustName.
535-
// Widened to all six by the #9677 ruling (2026-08-18): the sentence had
536-
// named two and called the other four advisory-and-rides-through, which
537-
// is the misclassification that puts a PR into the queue to be ejected.
538-
// The full set is pinned here so the corrected sentence cannot rot back
539-
// — a rename in ANY of the six now reddens this gate instead.
548+
// naming every blocking context. States the required set ⇒ statesTheSet.
549+
// Widened 2→6 by the #9677 ruling (2026-08-18): the sentence had named
550+
// two and called the other four advisory-and-rides-through, which is the
551+
// misclassification that puts a PR into the queue to be ejected. Then
552+
// 6→7 by #17798 — the same misclassification, one enrolment later and
553+
// with every gate green, which is what put the exclusivity rule below in
554+
// the judge. The full set is pinned here so the corrected sentence cannot
555+
// rot back — a rename in ANY of the seven now reddens this gate instead.
540556
file: 'AGENTS.md',
557+
statesTheSet: true,
541558
mustName: [
542559
'Lint & Repo Gates',
543560
'TypeScript Type Check',
544561
'Test Core',
545562
'Dogfood Regression Gate',
546563
'Build Core',
547564
'Temporal Conformance (live PG + MySQL)',
565+
'Governed Surface Queue Guard',
548566
],
549567
},
550568
{
551569
// The review seat's gate-clearance step: confirm both jobs' `conclusion`
552-
// before flipping ready / arming / enqueuing. States the required set.
570+
// before flipping ready / arming / enqueuing. It names these two as the
571+
// required FLOOR a reviewer confirms by hand — its own next line sends the
572+
// seat to `true-green.md` for the rest — so it does NOT state the set and
573+
// carries no `statesTheSet`. The two literals stay a floor: a rename in
574+
// either still reddens here, while the set growing does not falsify a
575+
// sentence that never claimed to enumerate it.
553576
file: '.claude/skills/pm-dispatch/references/review-checklist.md',
554577
mustName: ['Lint & Repo Gates', 'TypeScript Type Check'],
555578
},
556579
{
557580
// The seat's readings ledger states the required set it reads before a PR
558-
// may be enqueued, so it states the required set ⇒ mustName, all six.
581+
// may be enqueued ⇒ statesTheSet, the whole registry.
559582
file: '.claude/skills/pm-dispatch/references/platform-readings.md',
583+
statesTheSet: true,
560584
mustName: [
561585
'Lint & Repo Gates',
562586
'TypeScript Type Check',
@@ -566,8 +590,8 @@ export const INSTRUCTION_SURFACES = [
566590
'Temporal Conformance (live PG + MySQL)',
567591
// The seventh, enrolled 2026-08-27 (#12427) and pinned here by #15233.
568592
// The ledger's own count line is hand-followed prose, so it is this
569-
// array — asserted against REQUIRED_CONTEXTS.length in `--self-test` —
570-
// that makes the seat's copy of the required set non-optional.
593+
// array — held equal to the registry by the exclusivity rule in
594+
// judgeInstructionSurfaces — that makes the seat's copy non-optional.
571595
'Governed Surface Queue Guard',
572596
],
573597
},
@@ -590,8 +614,8 @@ export const INSTRUCTION_SURFACES = [
590614
* own source, and "looks like a path" there means "carries a separator". Five
591615
* of the six surfaces have one; `AGENTS.md` does not, because a repo-root FILE
592616
* has no separator to be found by — so an AGENTS.md card derived this gate not
593-
* at all, while that surface is the one carrying `mustName` for all six
594-
* required contexts (widened 2→6 by the #9677 ruling). Editing the merge-queue
617+
* at all, while that surface is the one carrying `mustName` for all seven
618+
* required contexts (widened 2→6 by #9677, 6→7 by #17798). Editing the merge-queue
595619
* paragraph there is precisely how this gate goes red, and it was reachable
596620
* only by judgment.
597621
*
@@ -1148,7 +1172,7 @@ function countOccurrences(text, literal) {
11481172
*
11491173
* @param {{
11501174
* registry: ReadonlyArray< { workflow: string, job: string, context: string } >,
1151-
* surfaces: ReadonlyArray< { file: string, mustName: ReadonlyArray<string> } >,
1175+
* surfaces: ReadonlyArray< { file: string, mustName: ReadonlyArray<string>, statesTheSet?: boolean } >,
11521176
* retired: ReadonlyArray< { name: string, replacedBy: string | null, staleSites?: Record<string, number>, renameInFlight?: boolean } >,
11531177
* files: Map< string, { text?: string, error?: string } >,
11541178
* }} input
@@ -1170,12 +1194,14 @@ export function judgeInstructionSurfaces({ registry, surfaces, retired, files })
11701194
// ── registry-level hygiene: a malformed ledger or scan set tolerates or ──
11711195
// ── bans the wrong thing silently, so each shape is its own named red. ──
11721196
const surfaceFiles = new Set();
1197+
let statesTheSetCount = 0;
11731198
for (const surface of surfaces) {
11741199
if (surfaceFiles.has(surface.file)) {
11751200
problems.push(`the instruction-surface scan set lists '${surface.file}' twice.`);
11761201
}
11771202
surfaceFiles.add(surface.file);
1178-
for (const context of surface.mustName ?? []) {
1203+
const named = surface.mustName ?? [];
1204+
for (const context of named) {
11791205
if (!currentNames.has(context)) {
11801206
problems.push(
11811207
`'${surface.file}' is required to name '${context}', which is not a registered required context — if the context was ` +
@@ -1184,6 +1210,42 @@ export function judgeInstructionSurfaces({ registry, surfaces, retired, files })
11841210
);
11851211
}
11861212
}
1213+
// ── EXCLUSIVITY: `mustName` is a FLOOR, `statesTheSet` makes it a CENSUS. ──
1214+
//
1215+
// The floor catches a name going stale and is blind to one going MISSING,
1216+
// which is how the same defect landed twice: the registry grew, every
1217+
// listed name stayed real, the scan stayed green, and a surface saying
1218+
// "these are all of them" kept calling the new context advisory. A surface
1219+
// that states the set therefore has to name the registry EXACTLY — count
1220+
// and membership, so neither a dropped literal nor a padded duplicate
1221+
// passes — which puts the red on the enrolment PR, where the registry edit
1222+
// is already in hand.
1223+
if (surface.statesTheSet === true) {
1224+
statesTheSetCount += 1;
1225+
const missing = registry.map((entry) => entry.context).filter((context) => !named.includes(context));
1226+
if (missing.length > 0 || named.length !== registry.length) {
1227+
problems.push(
1228+
`'${surface.file}' declares statesTheSet: true, so its mustName must be the required set EXACTLY — it lists ` +
1229+
`${named.length} name(s) against a registry of ${registry.length}` +
1230+
(missing.length > 0 ? `, missing ${missing.map((context) => `'${context}'`).join(', ')}` : '') +
1231+
`. A surface that states the set and omits a member keeps calling that context advisory-and-rides-through while ` +
1232+
`every gate is green — the #9677 defect, repeated. Follow the prose AND this array in the same PR as the ` +
1233+
`registry row; if this file no longer states the set, drop statesTheSet and leave mustName as the floor it names.`,
1234+
);
1235+
}
1236+
} else if (named.length >= registry.length) {
1237+
problems.push(
1238+
`'${surface.file}' lists ${named.length} name(s) against a registry of ${registry.length} without declaring ` +
1239+
`statesTheSet: true — a list that covers the whole set IS a statement of it, and leaving it undeclared exempts ` +
1240+
`this surface from the exclusivity check. Declare statesTheSet: true, or drop the names this file does not state.`,
1241+
);
1242+
}
1243+
}
1244+
if (statesTheSetCount === 0) {
1245+
problems.push(
1246+
`no instruction surface declares statesTheSet: true — the exclusivity half of this scan then verifies nothing while ` +
1247+
`printing a tick (#4690). At least one instruction file states the required set as operative prose; declare it there.`,
1248+
);
11871249
}
11881250
const seenRetired = new Set();
11891251
for (const row of retiredList) {
@@ -2359,13 +2421,127 @@ async function selfTest() {
23592421
'a retired name written fresh into the readings ledger ⇒ red (the standing ban reaches it through the same entry)',
23602422
);
23612423

2424+
// ── exclusivity: the set-stating surfaces are a CENSUS (#17798) ─────────
2425+
//
2426+
// The floor half above reds when a listed name goes stale. It is blind to a
2427+
// name going MISSING, and that blindness is the measured defect: #15233 added
2428+
// the seventh registry row, both set-stating surfaces kept their six-name
2429+
// lists, every gate stayed green, and AGENTS.md went on calling the seventh
2430+
// context advisory-and-rides-through — the #9677 misclassification, one
2431+
// enrolment later. These cases pin the other direction: the count, the
2432+
// declaration that opts a surface into it, and the floor under the whole rule.
2433+
const setStating = INSTRUCTION_SURFACES.filter((s) => s.statesTheSet === true).map((s) => s.file);
2434+
assert(
2435+
setStating.length === 2 && setStating.includes('AGENTS.md') && setStating.includes(LEDGER_SURFACE),
2436+
`the surfaces that state the required set are declared by NAME, never by count — got ${JSON.stringify(setStating)}`,
2437+
);
2438+
assert(
2439+
INSTRUCTION_SURFACES.filter((s) => s.statesTheSet === true).every(
2440+
(s) =>
2441+
s.mustName.length === REQUIRED_CONTEXTS.length && REQUIRED_CONTEXTS.every((e) => s.mustName.includes(e.context)),
2442+
),
2443+
`every set-stating surface names all ${REQUIRED_CONTEXTS.length} required contexts, exactly`,
2444+
);
2445+
// The review checklist is the counter-example that keeps a PARTIAL list
2446+
// legal: it names the two required jobs a seat confirms by hand, not the
2447+
// set, so growing the registry must NOT red it. Pinned, because collapsing
2448+
// it into the census would force it to enumerate a set it never claimed —
2449+
// and dropping its mustName instead would silence the floor it does carry.
2450+
const checklistEntry = INSTRUCTION_SURFACES.find((s) => s.file === CHECKLIST_SURFACE);
2451+
assert(
2452+
checklistEntry?.statesTheSet !== true &&
2453+
checklistEntry.mustName.length > 0 &&
2454+
checklistEntry.mustName.length < REQUIRED_CONTEXTS.length,
2455+
`the review checklist keeps a partial mustName and no statesTheSet — got ${JSON.stringify(checklistEntry)}`,
2456+
);
2457+
// THE PIN: a set-stating surface one name short of the registry ⇒ red,
2458+
// naming the surface and the literal it dropped. The FILES are the ones that
2459+
// ship — AGENTS.md still names all seven — so the floor half sees nothing
2460+
// and this red can only be the census.
2461+
const shortOfTheSet = judgeSurfaces({
2462+
surfaces: INSTRUCTION_SURFACES.map((s) =>
2463+
s.file === 'AGENTS.md' ? { ...s, mustName: s.mustName.filter((c) => c !== 'Governed Surface Queue Guard') } : s,
2464+
),
2465+
});
2466+
assert(
2467+
shortOfTheSet.problems.some(
2468+
(p) =>
2469+
p.includes('AGENTS.md') && p.includes("'Governed Surface Queue Guard'") && p.includes('statesTheSet: true'),
2470+
),
2471+
`a set-stating surface listing ${REQUIRED_CONTEXTS.length - 1} of ${REQUIRED_CONTEXTS.length} ⇒ red, naming the omitted literal`,
2472+
);
2473+
// Single-variable ablation of exactly that: the same call with the name put
2474+
// back is green, so the red above is the omission and not the file's text.
2475+
assert(
2476+
judgeSurfaces().problems.length === 0,
2477+
`restoring the omitted name ⇒ green — the red above is the census, isolated; got ${JSON.stringify(judgeSurfaces().problems)}`,
2478+
);
2479+
// The NEXT enrolment, end to end — the case this whole rule exists for. An
2480+
// eighth registry row with every instruction surface left exactly as it
2481+
// ships: red on EVERY set-stating surface, so the enrolment PR cannot land
2482+
// without following them. Under the floor alone this was green.
2483+
const eighth = { workflow: 'ci.yml', job: 'eighth-gate', context: 'An Eighth Required Gate' };
2484+
const grown = judgeSurfaces({ registry: [...REQUIRED_CONTEXTS, eighth] });
2485+
assert(
2486+
setStating.every((f) =>
2487+
grown.problems.some((p) => p.includes(f) && p.includes("'An Eighth Required Gate'") && p.includes('statesTheSet')),
2488+
),
2489+
'growing the registry without following the set-stating surfaces ⇒ red on every one of them',
2490+
);
2491+
// …and the hand-off: following the ARRAYS clears the census red and leaves
2492+
// the floor demanding the PROSE. Both halves are needed, neither substitutes
2493+
// for the other, and the remedy the author sees moves from one to the next.
2494+
const grownFollowed = judgeSurfaces({
2495+
registry: [...REQUIRED_CONTEXTS, eighth],
2496+
surfaces: INSTRUCTION_SURFACES.map((s) =>
2497+
s.statesTheSet === true ? { ...s, mustName: [...s.mustName, eighth.context] } : s,
2498+
),
2499+
});
2500+
assert(
2501+
!grownFollowed.problems.some((p) => p.includes('statesTheSet')) &&
2502+
setStating.every((f) =>
2503+
grownFollowed.problems.some(
2504+
(p) => p.includes(f) && p.includes("'An Eighth Required Gate'") && p.includes('no longer names'),
2505+
),
2506+
),
2507+
`following the arrays clears the census red and hands off to the naming floor — got ${JSON.stringify(grownFollowed.problems)}`,
2508+
);
2509+
// Exclusivity is a COUNT as well as a coverage: a duplicate pads the list to
2510+
// the registry's length while still covering it, so coverage alone would
2511+
// pass a surface that has quietly lost a member to a typo.
2512+
const padded = judgeSurfaces({
2513+
surfaces: INSTRUCTION_SURFACES.map((s) => (s.file === 'AGENTS.md' ? { ...s, mustName: [...s.mustName, 'Test Core'] } : s)),
2514+
});
2515+
assert(
2516+
padded.problems.some((p) => p.includes('AGENTS.md') && p.includes(`${REQUIRED_CONTEXTS.length + 1} name(s)`)),
2517+
'a set-stating surface padded past the registry length ⇒ red on the count',
2518+
);
2519+
// The declaration cannot be dropped to buy the exemption: a full list with
2520+
// no `statesTheSet` is still a statement of the set, and says so.
2521+
const undeclared = judgeSurfaces({
2522+
surfaces: INSTRUCTION_SURFACES.map((s) => (s.file === 'AGENTS.md' ? { file: s.file, mustName: s.mustName } : s)),
2523+
});
2524+
assert(
2525+
undeclared.problems.some((p) => p.includes('AGENTS.md') && p.includes('without declaring')),
2526+
'a full list with the declaration dropped ⇒ red (the exemption cannot be taken silently)',
2527+
);
2528+
// And the floor under the rule itself: with every declaration ablated the
2529+
// census checks nothing, which must be a red rather than a tick (#4690).
2530+
const noneStating = judgeSurfaces({
2531+
surfaces: INSTRUCTION_SURFACES.map((s) => ({ file: s.file, mustName: s.statesTheSet === true ? [] : s.mustName })),
2532+
});
2533+
assert(
2534+
noneStating.problems.some((p) => p.includes('no instruction surface declares statesTheSet')),
2535+
'ablating every statesTheSet declaration ⇒ red, never a silent tick (#4690)',
2536+
);
2537+
23622538
// ── the dispatch-gates declaration (#9979) ───────────────────────────────
23632539
//
23642540
// Enforcement cannot hold any of these: the declaration is read by another
23652541
// tool entirely, so a wrong or missing entry runs perfectly green here and
23662542
// shows up only as a dev dispatched on an AGENTS.md card with this gate
2367-
// absent from the brief — on the surface that carries `mustName` for all six
2368-
// required contexts.
2543+
// absent from the brief — on the surface that carries `mustName` for all
2544+
// seven required contexts.
23692545
battery('the dispatch-gates declaration (#9979)');
23702546
assert(
23712547
INSTRUCTION_SURFACES.map((s) => s.file)

0 commit comments

Comments
 (0)