Skip to content

feat(gates): create branch gates where none exist, and harden three policies - #1038

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/branch-gates-source-type-discriminator
Sep 23, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
fix/branch-gates-source-type-discriminator

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

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-shaped
changes. Each policy item is named separately so a reviewer can object to one without
rejecting the rest.

1. host profile — config/rulesets/gates.json

A new estate profile for repos shipping a desktop/GUI host workflow:

"host": { "applies_to": "repos that ship a desktop/GUI host workflow",
          "detect_workflows": ["host.yml"], "gate_workflows": ["host.yml"] }

It keys on detect_workflows, matching the proof profile's convention rather than the
file-glob detect: convention used by rust/zig/ada. Proven live, not assumed — see
arm 2 below, where build-and-test appears in the derived set and is absent without it.

2. --no-integration-bypass — applier flag

Owner ruling (selection UI): "harden the body for paint-type first, canon later." The flag
strips Integration actors from the bypass list of a gates-only ruleset while retaining
RepositoryRole:5
.

⚠ This diverges from the committed canon body config/rulesets/gates-only.json, and the
divergence 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.json sitting
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 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_review lives on the
org-level EstateBranching ruleset, whose own bypass list carries nine actors. The ruleset
this applier creates carries only required_status_checks. Bypass binds a ruleset, not a
rule, 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_workflows

The justification is PR-reachability, which neither the context derivation nor
--require-green checks. Measured on metadatastician/paint-type today:

  • codeql.yml's trigger on main is verbatim on: / workflow_dispatch: — dispatch-only,
    since the push trigger was retired (org config sets allow_advanced: false, so the advanced
    workflow can never run on a PR).
  • Its last five runs on main are nonetheless all event: push, all conclusion: failure —
    stale artefacts of the retired trigger.
  • The applier selects the newest (35782158972), whose sole job is analyze (actions, none).
  • ⇒ Without this entry the applier writes a required context that no pull request can ever
    emit
    , permanently blocking every future merge on that repository.

🚨 derive_failed cannot catch this — it fires on an unreadable run, not an unreachable
workflow. Greenness and readability are not PR-reachability.

🚨 --require-green N is not a substitute control. It DROPS non-green contexts; it does
not refuse. Arm 1g below dropped analyze (actions, none) only because it happens to be
failing — a coincidence, not a control.
Had that dispatch-only workflow been manually
dispatched green, --require-green would have admitted a permanently-unsatisfiable required
check.

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)" on main, 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 bolted on here as a fourth policy item.


Live evidence — the four-arm matched pair

Identical invocation against metadatastician/paint-type, read-only, only gates.json
differing between arms
:

arm gates.json --require-green state gate_files contexts discriminator
1 origin/main — WOULD-CREATE 5 19 analyze (actions, none) present; build-and-test ABSENT
2 this branch — WOULD-CREATE 5 19 build-and-test present; no codeql context
1g origin/main 3 WOULD-CREATE 5 17 not_green=[analyze (actions, none), Lean4 proofs (TP-2, INV-2)]
2g this branch 3 WOULD-CREATE 5 18 not_green=[Lean4 proofs (TP-2, INV-2)]

Arm 1 contains no build-and-test. Without the host profile, 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 host profile is not a nicety; it is what
makes the required list describe the product.

Arm 2g's 18 contexts: Agda proof (INV-3), build-and-test, the twelve
Governance Check / … contexts, Hypatia Neurosymbolic Analysis, and
scan / {gitleaks,rust-secrets,shell-secrets}. Three are excluded by
never_required_contexts (Allowlist Preflight, Code quality + docs,
Live Actions policy (credentialed advisory)), and Lean4 proofs (TP-2, INV-2) is dropped by
--require-green 3 because it is red in each of its last three runs — the applier correctly
refusing 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 ^FAIL lines.

The gh shim gains a POST branch — it records to POSTS.log, copies the body to
LAST_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_DRIFT and
a bypass-drift seam exercise both DRIFT arms.

Eleven new cases cover the create path: ORG-INHERITED and NORULESET (each carrying the
--create-gates cure pointer), WOULD-CREATE with no POST, CREATED with the POST body
asserted, --no-integration-bypass retaining RepositoryRole:5, flag misuse without
--create-gates (usage die, no POST), the empty-bypass refusal, a typed-context body refused
by the canon-shape guard, two-rulesets picked_by=shape and AMBIGUOUS, and the zero-context
UNGATED vacuous-gate control.

Three new mutants, G/H/I (A–F were taken), each killing its own case, each with the committed
cmp -s + bash -n guards so a sed that stopped matching cannot silently "pass" and an
invalid mutant cannot redden every case for the wrong reason:

  • G — shape discriminator removed → must fall to AMBIGUOUS
  • H — canon-shape guard neutered → must create from a typed-context body
  • I — empty-bypass refusal neutered → must POST a zero-bypass branch ruleset, reproducing the
    2026-09-11 outage shape

CI coverage needs no wiring: scripts/run-shell-test-suite.sh:9 discovers tests with
find scripts/tests -maxdepth 1 -name '*.sh' -type f, so the new cases run as part of the
existing suite.


Verification after merge

Running the applier with --apply --create-gates --no-integration-bypass --require-green 3
against paint-type must be verified by re-GET, never by rc=0: [.rules[].type] == ["required_status_checks"], 18 contexts with build-and-test among them, and
bypass_actors holding RepositoryRole:5 and zero Integration actors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YSq3UodR3CjsuAK5yoTzHF

…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
@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 310c7c8e-6330-42ea-a788-8be0570fe442

📥 Commits

Reviewing files that changed from the base of the PR and between 779d8a9 and c2005e1.

📒 Files selected for processing (4)
  • config/README.adoc
  • config/rulesets/gates.json
  • scripts/apply-branch-gates.sh
  • scripts/tests/branch-gates-apply-test.sh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit 65de939 into main Sep 23, 2026
45 checks passed
@hyperpolymath
hyperpolymath deleted the fix/branch-gates-source-type-discriminator branch September 23, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant