From 072880cace14cb1967e42b94e360239f3df7c01b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 18:45:45 +0000 Subject: [PATCH] =?UTF-8?q?conformance:=20gated=20means=20a=20CI=20context?= =?UTF-8?q?=20is=20required=20=E2=80=94=20the=20claim=20alone=20is=20not?= =?UTF-8?q?=20a=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First measured run after #390 (snapshot run 34264517316): gate_ready 37 as expected, but gated 90 of 90 and gate-absent 0 — because required-baseline requires pr-claim / pr-claim on every default branch, so "any required context" is true everywhere by construction and says nothing about CI. The claim is the passkey's rung, the human touch #913 keeps outside the CI gate on purpose. gated now excludes pr-claim / pr-claim. A caller whose only required context is the claim is gate-absent (its CI decides nothing — the fail-open set); arming-lane-absent narrows to repos with a CI gate and no armer. The claim is still listed in required_checks; gate_ready is unchanged. Closes #391 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CAE3i1NCfwwSVYScKWvEdd --- scripts/repo-standard-conformance.mjs | 10 +++++++--- scripts/repo-standard-conformance.test.mjs | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/repo-standard-conformance.mjs b/scripts/repo-standard-conformance.mjs index c029e9a..7c4d9f3 100644 --- a/scripts/repo-standard-conformance.mjs +++ b/scripts/repo-standard-conformance.mjs @@ -323,9 +323,13 @@ export function classifyRepo({ repo, archived = false, defaultBranch = "main", f gaps.push(`rules-unreadable:${rules.reason}`); } else { const req = rules.contexts; - const gated = req.length > 0; const hasTest = req.includes("standard / test"); const hasClaim = req.includes("pr-claim / pr-claim"); + // `gated` means a CI context is required. The claim is NOT one: `required-baseline` + // requires `pr-claim / pr-claim` on every default branch, so counting it made + // `gated` 90 of 90 by construction on the first run (#391). The claim is the + // passkey's rung — the human touch #913 keeps outside the CI gate on purpose. + const gated = req.some((c) => c !== "pr-claim / pr-claim"); row.dark_factory = { required_checks: req, rulesets: rules.rulesets, arming_lane: arming, legacy_arming: legacy, gate_ready: hasTest && hasClaim && arming === "auto-merge.yml" }; // Green gates nothing: a caller whose contexts nothing on the default branch // requires is the fail-open case #913 names — the check runs and decides nothing. @@ -363,14 +367,14 @@ export function summarize(rows) { with_findings: 0, findings: 0, gaps: 0, - // the dark factory: any required check at all · the org arming lane present · both plus the standard required + // the dark factory: a CI context required (the claim alone does not count) · the org arming lane present · both plus the standard required gated: 0, arming_lane: 0, gate_ready: 0, }; for (const r of rows) { if (r.dark_factory) { - if (r.dark_factory.required_checks?.length) t.gated++; + if (r.dark_factory.required_checks?.some((c) => c !== "pr-claim / pr-claim")) t.gated++; if (r.dark_factory.arming_lane) t.arming_lane++; if (r.dark_factory.gate_ready === true) t.gate_ready++; } diff --git a/scripts/repo-standard-conformance.test.mjs b/scripts/repo-standard-conformance.test.mjs index 62dde04..43200ee 100644 --- a/scripts/repo-standard-conformance.test.mjs +++ b/scripts/repo-standard-conformance.test.mjs @@ -401,6 +401,16 @@ test("classifyRepo: dark factory — gated but unarmed is a finding; a caller no const bare = classifyRepo({ repo: "k", files: [], root: [], rules: { read: true, contexts: [], rulesets: [] } }); assert.deepEqual(bare.findings, ["caller-absent"]); + // THE CLAIM ALONE IS NOT A GATE (#391). required-baseline requires pr-claim on every default + // branch, so a caller whose only required context is the claim is still the fail-open case, + // and a bare repo with only the claim is neither gated nor unarmed-while-gated. + const claimOnly = { read: true, contexts: ["pr-claim / pr-claim"], rulesets: [21805316] }; + const claimGated = classifyRepo({ repo: "k", files: [wf(".github/workflows/standard.yml", KEYCARD)], root: RUST_ROOT, run: run("success"), rules: claimOnly }); + assert.deepEqual(claimGated.findings, ["gate-absent"], "the claim is the human touch, not CI — nothing requires the standard"); + assert.deepEqual(claimGated.dark_factory.required_checks, ["pr-claim / pr-claim"], "the claim is still LISTED; it just does not count as a gate"); + const bareClaim = classifyRepo({ repo: "k", files: [], root: [], rules: claimOnly }); + assert.deepEqual(bareClaim.findings, ["caller-absent"], "no caller and no CI gate: not arming-lane-absent — there is no green to wait on"); + // Only the legacy lane: still unarmed — its precondition is false by construction (.github-private#929). const legacy = classifyRepo({ repo: "k", files: [wf(".github/workflows/standard.yml", KEYCARD), wf(".github/workflows/dependabot-auto-merge.yml", "name: dependabot-auto-merge\non: pull_request\n")], root: RUST_ROOT, run: run("success"), rules: RULES_37 }); assert.deepEqual(legacy.findings, ["arming-lane-absent"]); @@ -433,12 +443,14 @@ test("summarize + renderSummary: the dark-factory totals count what was measured classifyRepo({ repo: "ready", files: [wf(".github/workflows/standard.yml", KEYCARD), wf(".github/workflows/auto-merge.yml", AUTO_MERGE)], root: RUST_ROOT, run: run("success"), rules: RULES_37 }), classifyRepo({ repo: "gated", files: [wf(".github/workflows/standard.yml", KEYCARD)], root: RUST_ROOT, run: run("success"), rules: RULES_37 }), classifyRepo({ repo: "unmeasured", files: [wf(".github/workflows/standard.yml", KEYCARD)], root: RUST_ROOT, run: run("success") }), + // Only the org baseline's claim is required: counted in the row, not as gated (#391). + classifyRepo({ repo: "claim-only", files: [], root: [], rules: { read: true, contexts: ["pr-claim / pr-claim"], rulesets: [21805316] } }), ]; const t = summarize(rows); - assert.equal(t.gated, 2); + assert.equal(t.gated, 2, "the claim-only repo is not gated"); assert.equal(t.arming_lane, 1); assert.equal(t.gate_ready, 1); - const snap = buildSnapshot({ now: "2026-09-08T12:00:00Z", rows, denominator: { public_repos: 3, enumerated: 3, verified: true, archived: 0, rows: 3 }, fleet: { unavailable: "x" }, standard: { head_sha: null, selftest: { state: "none" } }, strict: false }); + const snap = buildSnapshot({ now: "2026-09-08T12:00:00Z", rows, denominator: { public_repos: 4, enumerated: 4, verified: true, archived: 0, rows: 4 }, fleet: { unavailable: "x" }, standard: { head_sha: null, selftest: { state: "none" } }, strict: false }); assert.match(renderSummary(snap), /\| gated \/ arming lane \/ dark-factory ready \| 2 \/ 1 \/ 1 \|/); });