Five places where the gating configuration claims a guarantee the tooling does not enforce. Every one was found by running the real gates locally during a full ci-prep on fixes; each is verified against the file, not inferred.
CLAUDE.md is unambiguous about why this class matters: "A gate you can turn off is not a gate", "Advisory is deleted", and the record of both rulesets being found enforcement: disabled with a 212-file PR already merged past them. These findings are the same failure mode one layer down — the gate is on, but it does not check what its own documentation says it checks.
None of these are being fixed silently. Filing first.
1. BLOCKER — _coverage_check_rust gates 9 of 12 workspace members
Makefile:502 states:
Per-crate enforcement: every rust crate is gated independently against its own threshold (floor 95% + monotonic ratchet).
It is not. The loop at Makefile:512 iterates only the keys that already exist in the JSON:
for crate in $(jq -r '.projects | to_entries[] | select(.value.language=="rust") | .key' "$(COVERAGE_THRESHOLDS_FILE)")
|
|
Workspace members (Cargo.toml:6-19) |
12 |
Rust entries in coverage-thresholds.json |
9 |
| Ungated |
osprey-fmt, osprey-project, tree-sitter-osprey |
cargo llvm-cov --workspace measures all twelve into lcov.info. The gate then discards three. osprey-project is the module / name-resolution / purity layer — not a peripheral crate.
The correct implementation already exists 150 lines further down the same file. _coverage_check_c_runtime walks C_SHIPPED_UNITS and fails on any library that is neither gated nor explicitly exempt (Makefile:676-678), with the reason written out:
[c] FAIL: runtime/$n.c ships in a native archive but is neither gated in
coverage-thresholds.json nor in C_COV_EXEMPT -- an ungated library cannot
regress visibly
The Rust gate has no analogue. A crate added tomorrow inherits no floor and nothing reports it. Note also that tree-sitter-osprey is a workspace member outside crates/, so the gate's awk path match /crates/<name>/ could never see it even if a key were added — the completeness check and the path matcher both need fixing.
coverage-thresholds.json:3 compounds this by claiming "FLOOR: every Rust crate is gated at >=95%."
2. HIGH — the Rust coverage gate is lines-only, which this repo already diagnosed as dishonest
Makefile:515 sums LH:/LF: and nothing else. The FN/FNF/FNH function records llvm-cov writes into the same lcov.info are ignored.
The comment above the TypeScript gate (Makefile:770-774) says why that is wrong:
Gates EVERY metric the summary reports, not lines alone. Lines-only was the dishonest half of this gate: branches and functions were measured, printed … and then thrown away, so a module could add an untested else arm … without moving the number the gate reads.
The problem was diagnosed, fixed for the extension (all four metrics, Makefile:783-795), and left in place for the entire Rust compiler.
3. HIGH — .deslop.toml states three different ceilings, one of which is enforced
| Line |
Says |
| 12 |
# Ceiling = 4.4% (2026-08-11), ratcheted down from 5%. |
| 46 |
8.0% remains the AUTHORISED HARD CAP — never raise above it. |
| 51 |
max_duplication_percent = 5 ← the only one that runs |
Today's measured value is 4.5% (3512 / 77401 LOC). If the file enforced what line 12 says, make _deslop and the required Build, Format & Analyse job would be red right now — the reviewed, written-down budget has been exceeded and no gate can say so. The measurement cited at line 13 (4.29% of 68400 LOC) is stale by ~9k LOC.
A ratchet whose own record contradicts itself cannot be audited, and the next person to raise the value can cite line 46 as authorisation.
Two smaller defects in the same file:
- Line 9 says "Local
make ci uses the deslop target". There is no deslop target; it is _deslop (Makefile:250, wired via lint: _deslop _lint). Commit 3f40dfb4 fixed this exact typo at line 14 and missed line 9 — the file's own instructions do not run as written.
- Lines 6-7 say the gate covers "Rust/C#/Python/Dart" and that this scopes it to the Rust workspace. TypeScript and JavaScript are in scope too — the file's own
exclude list proves it (**/*.test.ts at line 61, website/src/assets/vendor/** at line 82 justified as minified JS bundles "dominating the report"). Those exclusions are meaningless unless JS/TS is parsed. This matters for finding 4.
4. HIGH — verify-branch-protection.mjs has a dead field standing in for a check that does not exist
The header states:
Two traps are checked structurally, because both are why the gates were off.
Only one is. The phantom-context and path-filter traps are asserted. The job-level-if: trap is not: hasIf is assigned at line 88 and set at line 95, and read nowhere in the file.
$ grep -n hasIf scripts/verify-branch-protection.mjs
88: current = { key: line.trim().replace(':', ''), name: null, hasIf: false }
95: if (/^\s{4}if:/.test(line)) current.hasIf = true
Related, and arguably worse: the same file says
Every job in the four-stage pipeline is here, because every job blocks. There is no advisory tier: a suite worth running is worth failing the merge, and one marked "not required" has been deleted in all but name.
checkContexts compares EXPECTED_CONTEXTS against the ruleset in both directions, but never asserts that every job defined in ci.yml / ci-windows.yml appears in EXPECTED_CONTEXTS. The data is already gathered (allJobs, line 157) — only the assertion is missing. Adding a job tomorrow creates exactly the advisory tier this comment forbids, and the script stays green. The 14 contexts and both rulesets do reconcile today, but by hand, not by gate.
Also worth noting: make hawk is the dead-code gate and it is Rust-only, so nothing in the pipeline can catch a dead field in a .mjs checker.
5. HIGH — a website-only PR skips the duplication gate, and website source is in the measured set
The Deslop step lives in the build job, which carries if: needs.changes.outputs.code == 'true' with the filter code: - '!website/**'. A skipped required check counts as passing.
Per finding 3, website/src/js/** is measured — only website/scripts/** and website/src/assets/vendor/** are excluded. So a website-only PR can push duplication past the ceiling while the required check reports skipped.
Format, clippy, hawk and the manifest validation lose nothing by skipping on a website-only PR. This one does. Note the precedent already in the tree: test-website deliberately needs: nothing, with the comment "it must not be gated on changes — a job that needs: a failed job is SKIPPED".
Lower severity, same class
- Every corpus floor ships with an environment override.
crates/run_test_corpus.sh:68 GOLDEN_MIN=${OSPREY_GOLDEN_MIN:-142/203}, :72 GPU_MODE_MIN=${OSPREY_GPU_MODE_MIN:-18}, and Makefile:661 C_COV_EXEMPT ?= — each says "Ratchet UP; never lower it to turn a red build green" while providing the switch that does exactly that. CI sets none of them today. The contrast is inside the same script: the WASM skip set deliberately ships no regeneration flag, because "a --update switch would turn every new hole into one keystroke."
cargo install cargo-llvm-cov --locked || true — a swallowed non-zero inside the required Tests: Rust workspace (coverage) job. It fails closed today only because the next step errors without the binary; it is protected by what happens to come after it, not by an assertion.
_coverage_check_c_runtime discards suite output entirely (Makefile:671): { ./$(s) >/dev/null 2>&1 || true; } drops exit status, stdout and stderr. A failure occurring only under --coverage -DOSP_COVERAGE_DUMP is invisible and surfaces as an unexplained coverage number.
- 53 silent
this.skip() sites in vscode-extension/test/suite/test-explorer.test.ts (31) and test-docs.test.ts (22), guarded by if (!compiler). Mocha reports pending as green: no count printed, no manifest compared, nothing fails if all 53 vanish. crates/osprey-cli/src/test_skips.rs:60-64 makes a reason-less skip a hard error: for Osprey tests ([TESTING-SKIP-REASON]), and tests/WASM_UNPORTABLE.txt is diffed exactly — the extension suite has neither. The correct pattern is two functions away: resolveRequiredLldbDap() calls assert.fail instead of skipping.
coverage-thresholds.json documents a rule it also documents breaking. _doc says "each C entry is the WEAKEST measured platform minus ~2 points" and lists websocket_server, websocket_client, http_client, fiber, coverage_runtime, file_runtime, http_shared as having "come off this list" — all seven are pinned at 85, a ~5-point silent regression budget each. The same field then says "RATCHET OWED: … That was deliberately NOT done." The named to-do list (3 libraries) understates the real debt by 8: eleven C entries sit below the stated 90 target.
- Monotonicity is policy, not gate.
CLAUDE.md:38 and coverage-thresholds.json:3 both say thresholds only go up. It has held — no key has ever decreased — but by discipline alone. Branch protection got verify-branch-protection.mjs; this got nothing. A one-screen git-based check would pin it.
Suggested order
- Add the
C_SHIPPED_UNITS-style completeness loop to _coverage_check_rust, and fix the awk path matcher so a non-crates/ member can be gated. Land this red — it will fail until entries exist for osprey-fmt, osprey-project and tree-sitter-osprey.
- Add those three entries at
max(95, measured − 1), per the ratchet rule.
- Extend the Rust gate to functions/branches, mirroring
_coverage_check_vscode_extension.
- Reconcile
.deslop.toml to one ceiling — 4.5% by its own rule — and delete the other two numbers.
- Assert
allJobs ⊆ EXPECTED_CONTEXTS in verify-branch-protection.mjs, and either use hasIf or delete it.
- Move the Deslop step out of
build, or give the changes filter a deslop output that includes website/src/js/**.
Verified green during the same run
So this is not a report of a broken pipeline — the pipeline works, its self-description does not. On fixes: branch protection intact (2 rulesets, 14 required checks), cargo fmt, clippy -D warnings, extension lint, Shipwright manifest, deslop (4.5%/5%), hawk (0 findings), make build, the corpus at 203/203 with 203/203 goldens under all three memory backends (TEST_CORPUS_ARC_LEAKY=0), GPU alt-lowering 18/18, and the Docker web-compiler suite.
Five places where the gating configuration claims a guarantee the tooling does not enforce. Every one was found by running the real gates locally during a full ci-prep on
fixes; each is verified against the file, not inferred.CLAUDE.mdis unambiguous about why this class matters: "A gate you can turn off is not a gate", "Advisory is deleted", and the record of both rulesets being foundenforcement: disabledwith a 212-file PR already merged past them. These findings are the same failure mode one layer down — the gate is on, but it does not check what its own documentation says it checks.None of these are being fixed silently. Filing first.
1. BLOCKER —
_coverage_check_rustgates 9 of 12 workspace membersMakefile:502states:It is not. The loop at
Makefile:512iterates only the keys that already exist in the JSON:Cargo.toml:6-19)coverage-thresholds.jsonosprey-fmt,osprey-project,tree-sitter-ospreycargo llvm-cov --workspacemeasures all twelve intolcov.info. The gate then discards three.osprey-projectis the module / name-resolution / purity layer — not a peripheral crate.The correct implementation already exists 150 lines further down the same file.
_coverage_check_c_runtimewalksC_SHIPPED_UNITSand fails on any library that is neither gated nor explicitly exempt (Makefile:676-678), with the reason written out:The Rust gate has no analogue. A crate added tomorrow inherits no floor and nothing reports it. Note also that
tree-sitter-ospreyis a workspace member outsidecrates/, so the gate'sawkpath match/crates/<name>/could never see it even if a key were added — the completeness check and the path matcher both need fixing.coverage-thresholds.json:3compounds this by claiming"FLOOR: every Rust crate is gated at >=95%."2. HIGH — the Rust coverage gate is lines-only, which this repo already diagnosed as dishonest
Makefile:515sumsLH:/LF:and nothing else. TheFN/FNF/FNHfunction recordsllvm-covwrites into the samelcov.infoare ignored.The comment above the TypeScript gate (
Makefile:770-774) says why that is wrong:The problem was diagnosed, fixed for the extension (all four metrics,
Makefile:783-795), and left in place for the entire Rust compiler.3. HIGH —
.deslop.tomlstates three different ceilings, one of which is enforced# Ceiling = 4.4% (2026-08-11), ratcheted down from 5%.8.0% remains the AUTHORISED HARD CAP — never raise above it.max_duplication_percent = 5← the only one that runsToday's measured value is 4.5% (3512 / 77401 LOC). If the file enforced what line 12 says,
make _deslopand the requiredBuild, Format & Analysejob would be red right now — the reviewed, written-down budget has been exceeded and no gate can say so. The measurement cited at line 13 (4.29% of 68400 LOC) is stale by ~9k LOC.A ratchet whose own record contradicts itself cannot be audited, and the next person to raise the value can cite line 46 as authorisation.
Two smaller defects in the same file:
make ciuses thedesloptarget". There is nodesloptarget; it is_deslop(Makefile:250, wired vialint: _deslop _lint). Commit3f40dfb4fixed this exact typo at line 14 and missed line 9 — the file's own instructions do not run as written.excludelist proves it (**/*.test.tsat line 61,website/src/assets/vendor/**at line 82 justified as minified JS bundles "dominating the report"). Those exclusions are meaningless unless JS/TS is parsed. This matters for finding 4.4. HIGH —
verify-branch-protection.mjshas a dead field standing in for a check that does not existThe header states:
Only one is. The phantom-context and path-filter traps are asserted. The job-level-
if:trap is not:hasIfis assigned at line 88 and set at line 95, and read nowhere in the file.Related, and arguably worse: the same file says
checkContextscomparesEXPECTED_CONTEXTSagainst the ruleset in both directions, but never asserts that every job defined inci.yml/ci-windows.ymlappears inEXPECTED_CONTEXTS. The data is already gathered (allJobs, line 157) — only the assertion is missing. Adding a job tomorrow creates exactly the advisory tier this comment forbids, and the script stays green. The 14 contexts and both rulesets do reconcile today, but by hand, not by gate.Also worth noting:
make hawkis the dead-code gate and it is Rust-only, so nothing in the pipeline can catch a dead field in a.mjschecker.5. HIGH — a website-only PR skips the duplication gate, and website source is in the measured set
The Deslop step lives in the
buildjob, which carriesif: needs.changes.outputs.code == 'true'with the filtercode: - '!website/**'. A skipped required check counts as passing.Per finding 3,
website/src/js/**is measured — onlywebsite/scripts/**andwebsite/src/assets/vendor/**are excluded. So a website-only PR can push duplication past the ceiling while the required check reportsskipped.Format, clippy, hawk and the manifest validation lose nothing by skipping on a website-only PR. This one does. Note the precedent already in the tree:
test-websitedeliberatelyneeds:nothing, with the comment "it must not be gated onchanges— a job thatneeds:a failed job is SKIPPED".Lower severity, same class
crates/run_test_corpus.sh:68GOLDEN_MIN=${OSPREY_GOLDEN_MIN:-142/203},:72GPU_MODE_MIN=${OSPREY_GPU_MODE_MIN:-18}, andMakefile:661C_COV_EXEMPT ?=— each says "Ratchet UP; never lower it to turn a red build green" while providing the switch that does exactly that. CI sets none of them today. The contrast is inside the same script: the WASM skip set deliberately ships no regeneration flag, because "a--updateswitch would turn every new hole into one keystroke."cargo install cargo-llvm-cov --locked || true— a swallowed non-zero inside the requiredTests: Rust workspace (coverage)job. It fails closed today only because the next step errors without the binary; it is protected by what happens to come after it, not by an assertion._coverage_check_c_runtimediscards suite output entirely (Makefile:671):{ ./$(s) >/dev/null 2>&1 || true; }drops exit status, stdout and stderr. A failure occurring only under--coverage -DOSP_COVERAGE_DUMPis invisible and surfaces as an unexplained coverage number.this.skip()sites invscode-extension/test/suite/test-explorer.test.ts(31) andtest-docs.test.ts(22), guarded byif (!compiler). Mocha reports pending as green: no count printed, no manifest compared, nothing fails if all 53 vanish.crates/osprey-cli/src/test_skips.rs:60-64makes a reason-less skip a harderror:for Osprey tests ([TESTING-SKIP-REASON]), andtests/WASM_UNPORTABLE.txtis diffed exactly — the extension suite has neither. The correct pattern is two functions away:resolveRequiredLldbDap()callsassert.failinstead of skipping.coverage-thresholds.jsondocuments a rule it also documents breaking._docsays "each C entry is the WEAKEST measured platform minus ~2 points" and listswebsocket_server,websocket_client,http_client,fiber,coverage_runtime,file_runtime,http_sharedas having "come off this list" — all seven are pinned at 85, a ~5-point silent regression budget each. The same field then says "RATCHET OWED: … That was deliberately NOT done." The named to-do list (3 libraries) understates the real debt by 8: eleven C entries sit below the stated 90 target.CLAUDE.md:38andcoverage-thresholds.json:3both say thresholds only go up. It has held — no key has ever decreased — but by discipline alone. Branch protection gotverify-branch-protection.mjs; this got nothing. A one-screen git-based check would pin it.Suggested order
C_SHIPPED_UNITS-style completeness loop to_coverage_check_rust, and fix theawkpath matcher so a non-crates/member can be gated. Land this red — it will fail until entries exist forosprey-fmt,osprey-projectandtree-sitter-osprey.max(95, measured − 1), per the ratchet rule._coverage_check_vscode_extension..deslop.tomlto one ceiling — 4.5% by its own rule — and delete the other two numbers.allJobs ⊆ EXPECTED_CONTEXTSinverify-branch-protection.mjs, and either usehasIfor delete it.build, or give thechangesfilter adeslopoutput that includeswebsite/src/js/**.Verified green during the same run
So this is not a report of a broken pipeline — the pipeline works, its self-description does not. On
fixes: branch protection intact (2 rulesets, 14 required checks),cargo fmt,clippy -D warnings, extension lint, Shipwright manifest, deslop (4.5%/5%), hawk (0 findings),make build, the corpus at 203/203 with 203/203 goldens under all three memory backends (TEST_CORPUS_ARC_LEAKY=0), GPU alt-lowering 18/18, and the Docker web-compiler suite.