feat(gates): create branch gates where none exist, and harden three policies - #1038
Merged
Merged
Conversation
…olicies
The branch-gates applier could report that a repo was ungated but never cure
it. Adds `--create-gates`, the `host` profile, `--no-integration-bypass`, and
retires `codeql.yml` from the required set. 49 new assertions across 11 cases and 3 mutants.
Three policy-shaped changes, each named so a reviewer can object to one
without rejecting the rest:
1. `host` profile in config/rulesets/gates.json
A new estate profile for repos shipping a desktop/GUI host workflow,
keyed on `detect_workflows: ["host.yml"]` (the `proof` profile's
convention, not the file-glob convention used by rust/zig/ada).
Measured live: without it, paint-type's required list contains NO
`build-and-test` — the product gate that builds the binary and proves it
draws to a canvas is not required at all.
2. `--no-integration-bypass` on the applier
Strips Integration actors from the gates-only bypass body, retaining
`RepositoryRole:5`. Owner ruling: harden the body for paint-type first,
canon later. This DIVERGES from the committed canon body
(config/rulesets/gates-only.json); the divergence is deliberate,
tracked in config/README.adoc, and reported in the applier's own detail
line as `bypass=no_integrations` so it can never be silent.
A flag, not a second body file: a rival `gates-only-strict.json` beside
`gates-only.json` would recreate exactly the `Immutable-Tags.json` vs
`immutable-tags.json` two-rival-files trap that caused the 2026-09-11
zero-bypass outage across 372 repositories.
The applier REFUSES rather than warns if stripping would empty the
bypass list — a zero-bypass ruleset is that outage's shape, not a strict
gate. Mutant I proves the refusal is load-bearing.
3. `codeql.yml` added to `never_required_workflows`
PR-REACHABILITY, which neither the context derivation nor
`--require-green` checks. Measured on paint-type today:
`codeql.yml`'s trigger is `workflow_dispatch:` only (PR 6a retired the
push trigger because org config sets `allow_advanced: false`), yet its
last five runs on main are all stale `event: push` failures. The applier
selects the newest and derives the context `analyze (actions, none)` —
a required check NO pull request can ever emit, which would permanently
block every future merge.
`derive_failed` cannot catch this: it fires on an unreadable run, not an
unreachable workflow. `--require-green` cannot either — it DROPS
non-green contexts rather than refusing, so it masks this one only by
the coincidence that it is currently failing. Had the dispatch-only
workflow been manually dispatched green, `--require-green` would have
admitted a permanently-unsatisfiable check. `never_required_contexts` is
the wrong instrument: it already carries "CodeQL (default setup)", the
green default-setup check-run that separately satisfies EstateBranching's
`code_scanning` rule. The workflow and the context are different objects.
SCOPE, stated because it exceeds the measurement:
`never_required_workflows` is consulted for EVERY repo the applier runs
against, so this entry is estate-wide while the evidence above is
paint-type's. That is deliberate and consistent rather than a widening.
`never_required_contexts` ALREADY carries "CodeQL (default setup)", so
the estate has already ruled that the default-setup CodeQL check-run is
never a required status check; this entry completes that policy for the
advanced workflow, leaving CodeQL enforced estate-wide by
EstateBranching's `code_scanning` RULE rather than by any required
status context. The honest cost: a repo whose `codeql.yml` IS
pull_request-reachable loses that context from its required list until a
reachability check lands in the applier. That check is filed as an issue
with acceptance criteria rather than added as a fourth policy item here.
Tests: scripts/tests/branch-gates-apply-test.sh 549 -> 924 lines,
passed=46 -> passed=95, failed=0. The `gh` shim gains a POST branch that
records to POSTS.log, writes the posted body back as the re-GET fixture,
and honours GH_POST_DRIFT so both drift arms are exercised.
Three new mutants, each killing its own case (A-F were taken):
G shape discriminator removed -> falls to AMBIGUOUS; without it
the estate is permanently ungatable
H canon-shape guard neutered -> CREATEs from a hand-typed context
body (contexts must be DERIVED)
I empty-bypass refusal neutered -> POSTs a zero-bypass ruleset,
reproducing the 2026-09-11 outage
Each mutant is guarded by `cmp -s` (the sed must actually change the file)
and `bash -n` (an invalid mutant cannot red every case for the wrong reason).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSq3UodR3CjsuAK5yoTzHF
Contributor
|
Warning Review limit reachedNext included review available in 15 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
hyperpolymath
deleted the
fix/branch-gates-source-type-discriminator
branch
September 23, 2026 09:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What this changes
The branch-gates applier could report that a repository was ungated, but never cure it — every
path ended in a diagnosis. This adds the create path (
--create-gates) and three policy-shapedchanges. Each policy item is named separately so a reviewer can object to one without
rejecting the rest.
1.
hostprofile —config/rulesets/gates.jsonA new estate profile for repos shipping a desktop/GUI host workflow:
It keys on
detect_workflows, matching theproofprofile's convention rather than thefile-glob
detect:convention used byrust/zig/ada. Proven live, not assumed — seearm 2 below, where
build-and-testappears in the derived set and is absent without it.2.
--no-integration-bypass— applier flagOwner ruling (selection UI): "harden the body for paint-type first, canon later." The flag
strips
Integrationactors from the bypass list of a gates-only ruleset while retainingRepositoryRole:5.⚠ This diverges from the committed canon body
config/rulesets/gates-only.json, and thedivergence is deliberate and tracked in #1032 so a later canon-conformance sweep does not
silently revert it.
Why a flag and not a second committed body file. A rival
gates-only-strict.jsonsittingbeside
gates-only.jsonwould recreate exactly theImmutable-Tags.jsonvsimmutable-tags.jsontwo-rival-files trap that caused the 2026-09-11 zero-bypass outage across372 repositories — the wrong body deployed while the right one sat next to it. A flag keeps
one canon file and makes the divergence explicit, auditable and reversible.
The safety argument, re-measured against live data.
require_code_owner_reviewlives on theorg-level
EstateBranchingruleset, whose own bypass list carries nine actors. The rulesetthis applier creates carries only
required_status_checks. Bypass binds a ruleset, not arule, so stripping Integrations from the gates-only ruleset touches only the checks rule and
cannot deadlock merges. The applier refuses rather than warns if stripping would leave
the bypass list empty — a zero-bypass branch ruleset is the shape of the 2026-09-11 outage, not
a strict gate. Mutant I proves that refusal is load-bearing.
3.
codeql.yml→never_required_workflowsThe justification is PR-reachability, which neither the context derivation nor
--require-greenchecks. Measured onmetadatastician/paint-typetoday:codeql.yml's trigger onmainis verbatimon:/workflow_dispatch:— dispatch-only,since the push trigger was retired (org config sets
allow_advanced: false, so the advancedworkflow can never run on a PR).
mainare nonetheless allevent: push, allconclusion: failure—stale artefacts of the retired trigger.
analyze (actions, none).emit, permanently blocking every future merge on that repository.
🚨
derive_failedcannot catch this — it fires on an unreadable run, not an unreachableworkflow. Greenness and readability are not PR-reachability.
🚨
--require-green Nis not a substitute control. It DROPS non-green contexts; it doesnot refuse. Arm 1g below dropped
analyze (actions, none)only because it happens to befailing — a coincidence, not a control. Had that dispatch-only workflow been manually
dispatched green,
--require-greenwould have admitted a permanently-unsatisfiable requiredcheck.
SCOPE, stated because it exceeds the measurement.
never_required_workflowsis consultedfor every repo the applier runs against, so this entry is estate-wide while the evidence
above is paint-type's. That is deliberate and consistent rather than a widening:
never_required_contextsalready carries"CodeQL (default setup)"onmain, so the estatehas already ruled that the default-setup CodeQL check-run is never a required status check. This
entry completes that policy for the advanced workflow, leaving CodeQL enforced estate-wide by
EstateBranching's
code_scanningrule rather than by any required status context.The honest cost: a repo whose
codeql.ymlispull_request-reachable loses that contextfrom its required list until a reachability check lands in the applier. That check is filed as
an issue with acceptance criteria rather than bolted on here as a fourth policy item.
Live evidence — the four-arm matched pair
Identical invocation against
metadatastician/paint-type, read-only, onlygates.jsondiffering between arms:
gates.json--require-greenorigin/mainanalyze (actions, none)present;build-and-testABSENTbuild-and-testpresent; no codeql contextorigin/mainnot_green=[analyze (actions, none), Lean4 proofs (TP-2, INV-2)]not_green=[Lean4 proofs (TP-2, INV-2)]Arm 1 contains no
build-and-test. Without thehostprofile, paint-type's product gate —the only check that builds the binary and proves it draws to a canvas — is not required at
all, while a context no PR can emit is. The
hostprofile is not a nicety; it is whatmakes the required list describe the product.
Arm 2g's 18 contexts:
Agda proof (INV-3),build-and-test, the twelveGovernance Check / …contexts,Hypatia Neurosymbolic Analysis, andscan / {gitleaks,rust-secrets,shell-secrets}. Three are excluded bynever_required_contexts(Allowlist Preflight,Code quality + docs,Live Actions policy (credentialed advisory)), andLean4 proofs (TP-2, INV-2)is dropped by--require-green 3because it is red in each of its last three runs — the applier correctlyrefusing to require a failing check. That is a paint-type defect, filed separately with the
acceptance criterion that the context be restored once the proof is green.
Tests
scripts/tests/branch-gates-apply-test.sh: 549 → 924 lines, passed=46 → passed=95,failed=0, rc=0, zero
^FAILlines.The
ghshim gains a POST branch — it records toPOSTS.log, copies the body toLAST_POST.json, and writes the posted body back as the fixture for the applier's re-GET,so the create path is asserted end-to-end rather than at the call boundary.
GH_POST_DRIFTanda bypass-drift seam exercise both DRIFT arms.
Eleven new cases cover the create path:
ORG-INHERITEDandNORULESET(each carrying the--create-gatescure pointer),WOULD-CREATEwith no POST,CREATEDwith the POST bodyasserted,
--no-integration-bypassretainingRepositoryRole:5, flag misuse without--create-gates(usagedie, no POST), the empty-bypass refusal, a typed-context body refusedby the canon-shape guard, two-rulesets
picked_by=shapeandAMBIGUOUS, and the zero-contextUNGATEDvacuous-gate control.Three new mutants, G/H/I (A–F were taken), each killing its own case, each with the committed
cmp -s+bash -nguards so asedthat stopped matching cannot silently "pass" and aninvalid mutant cannot redden every case for the wrong reason:
AMBIGUOUS2026-09-11 outage shape
CI coverage needs no wiring:
scripts/run-shell-test-suite.sh:9discovers tests withfind scripts/tests -maxdepth 1 -name '*.sh' -type f, so the new cases run as part of theexisting suite.
Verification after merge
Running the applier with
--apply --create-gates --no-integration-bypass --require-green 3against paint-type must be verified by re-GET, never by rc=0:
[.rules[].type] == ["required_status_checks"], 18 contexts withbuild-and-testamong them, andbypass_actorsholdingRepositoryRole:5and zeroIntegrationactors.🤖 Generated with Claude Code
https://claude.ai/code/session_01YSq3UodR3CjsuAK5yoTzHF