The defect
scripts/ci/check_pr_branch_filters.py — the check behind Gate Topology — printed:
merge-critical workflows checked: 15
workflow files present: 49
explicitly not merge-critical: 4
CLEAN: no merge-critical workflow filters pull_request by branch.
It printed the two list sizes beside the file count and never subtracted them.
15 + 4 = 19 against 49 present, so 30 files were read by nothing — and the last
line still said CLEAN.
Two of the thirty carried the defect it exists to detect
corpus-ratchet.yml pull_request.branches = ['master']
withdrawn-live-gate.yml pull_request.branches = ['master']
Read directly from the files, not inferred. A branches: filter on pull_request means the
gate does not run at all when a pull request targets any other base: on a stacked PR
gh pr checks prints a green list with that gate simply absent from it. That is the exact
hazard gate-topology.yml's own header describes, "observed in this repository on
2026-08-15 on three gates at once" — and one of the two is corpus-ratchet, the
expected-failure ledger.
The check was not wrong about the 15 it read. It was wrong about what its clean line meant.
The repair
Three parts, and only the first is about the two files:
-
Remove the branches: filter from pull_request in both. A paths: filter selects by
what changed and stays; branches: selects by where the change is headed.
-
Print the third bucket. The summary now states all three counts and their sum against
the file count, on one line, so a reader can see the arithmetic close:
merge-critical workflows checked: 18
explicitly not merge-critical: 4
in NEITHER list, never read: 27
workflow files present: 49
18 + 4 + 27 = 49 (must equal 49)
The final line no longer over-promises: "CLEAN: no merge-critical workflow filters
pull_request by branch, and 27 file(s) remain unread at a ceiling of 27."
-
A ceiling, not a refusal. MAX_UNCLASSIFIED = 27, moving down only. Twenty-seven files
cannot be classified in the commit that discovers them, and a gate that is red on the day it
lands teaches everyone to ignore red. What the ceiling buys is that the next workflow
added cannot land unread — classify it and the ceiling holds.
The same read is also run over the unclassified files and reported, not failed: whether
one of them ought to block a merge is a human call, but whether anybody looked is not.
That count is 0 today and is printed as a zero rather than omitted.
Controls
Each new failure direction was seen failing on purpose:
| mutation |
result |
| add a 28th unclassified workflow |
UNCLASSIFIED ROSE 27 -> 28, exit 1 |
| put one name in both lists |
IN BOTH LISTS (1): seal-coverage.yml, exit 1 |
restore branches: [master] on corpus-ratchet.yml |
BRANCH-FILTERED MERGE-CRITICAL WORKFLOWS (1), exit 1 |
| all restored |
exit 0 |
The third is the historical control: it is how the two offenders read before this change, and
the check reached exit 1 for them the moment they were classified — the classification is what
was missing, not the rule.
Also in this change: tri harness scratch --gate is wired
#2955 landed the command, its five-leg control and four repairs, and said the workflow was held
back only because the gate stayed red until #2949 removed the fifth carrier. Both have landed:
on master the command reports none and --gate exits 0, so the workflow goes in now.
harness-scratch.yml carries no paths: filter and a push: branches: [master] trigger,
deliberately, and its header says why — the second reason is new and cost a measurement:
emit-bitexact-gate.yml is pull_request with a paths: filter and no push:, so it has
never run on master, and when a change made it fail there was no baseline to compare against.
Historical control for that gate too: against the tree as it stood before #2955, --gate exits
1 and names all five carriers; on master today it exits 0 with none.
The remaining 27
A work list, not a verdict. Nothing here claims they should be merge-critical — only that
nothing has read them:
agent-runner-docker bootstrap-tests brain-seal-refresh
build-vivado-image cli-tri conflict-markers
conformance-integrity-gate coq-proofs deploy-api
exhaustive-nightly gate-topology gf-wide-conformance
gf16-conformance l1-traceability lean-proofs
loop-tools-gate orphan-modules pack-index-consistency-gate
release rings-rust sandbox-docker
sbom scorecard sign-release
untrusted-input-gate vivado-synth zenodo-publish
l1-traceability.yml is the one worth deciding first: it enforces law L1 and reports a context
that appears on every pull request, and this check has never read its trigger block.
Related: #2919 is the other half of this — the gap between what the tree calls merge-critical
and what the ruleset actually requires. This issue is about a list that does not cover its own
directory; #2919 is about a list that does not match repository settings. Neither subsumes the
other.
Refs #2954
Refs #2919
The defect
scripts/ci/check_pr_branch_filters.py— the check behind Gate Topology — printed:It printed the two list sizes beside the file count and never subtracted them.
15 + 4 = 19against49present, so 30 files were read by nothing — and the lastline still said CLEAN.
Two of the thirty carried the defect it exists to detect
Read directly from the files, not inferred. A
branches:filter onpull_requestmeans thegate does not run at all when a pull request targets any other base: on a stacked PR
gh pr checksprints a green list with that gate simply absent from it. That is the exacthazard
gate-topology.yml's own header describes, "observed in this repository on2026-08-15 on three gates at once" — and one of the two is
corpus-ratchet, theexpected-failure ledger.
The check was not wrong about the 15 it read. It was wrong about what its clean line meant.
The repair
Three parts, and only the first is about the two files:
Remove the
branches:filter frompull_requestin both. Apaths:filter selects bywhat changed and stays;
branches:selects by where the change is headed.Print the third bucket. The summary now states all three counts and their sum against
the file count, on one line, so a reader can see the arithmetic close:
The final line no longer over-promises: "CLEAN: no merge-critical workflow filters
pull_request by branch, and 27 file(s) remain unread at a ceiling of 27."
A ceiling, not a refusal.
MAX_UNCLASSIFIED = 27, moving down only. Twenty-seven filescannot be classified in the commit that discovers them, and a gate that is red on the day it
lands teaches everyone to ignore red. What the ceiling buys is that the next workflow
added cannot land unread — classify it and the ceiling holds.
The same read is also run over the unclassified files and reported, not failed: whether
one of them ought to block a merge is a human call, but whether anybody looked is not.
That count is
0today and is printed as a zero rather than omitted.Controls
Each new failure direction was seen failing on purpose:
UNCLASSIFIED ROSE 27 -> 28, exit 1IN BOTH LISTS (1): seal-coverage.yml, exit 1branches: [master]oncorpus-ratchet.ymlBRANCH-FILTERED MERGE-CRITICAL WORKFLOWS (1), exit 1The third is the historical control: it is how the two offenders read before this change, and
the check reached exit 1 for them the moment they were classified — the classification is what
was missing, not the rule.
Also in this change:
tri harness scratch --gateis wired#2955 landed the command, its five-leg control and four repairs, and said the workflow was held
back only because the gate stayed red until #2949 removed the fifth carrier. Both have landed:
on master the command reports
noneand--gateexits 0, so the workflow goes in now.harness-scratch.ymlcarries nopaths:filter and apush: branches: [master]trigger,deliberately, and its header says why — the second reason is new and cost a measurement:
emit-bitexact-gate.ymlispull_requestwith apaths:filter and nopush:, so it hasnever run on master, and when a change made it fail there was no baseline to compare against.
Historical control for that gate too: against the tree as it stood before #2955,
--gateexits1 and names all five carriers; on master today it exits 0 with
none.The remaining 27
A work list, not a verdict. Nothing here claims they should be merge-critical — only that
nothing has read them:
l1-traceability.ymlis the one worth deciding first: it enforces law L1 and reports a contextthat appears on every pull request, and this check has never read its trigger block.
Related: #2919 is the other half of this — the gap between what the tree calls merge-critical
and what the ruleset actually requires. This issue is about a list that does not cover its own
directory; #2919 is about a list that does not match repository settings. Neither subsumes the
other.
Refs #2954
Refs #2919