Plan N: Quality-Gate Enforcement
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Plan filing — body lands as a docs-only MR alongside epic &40 + step issues #514–#525. |
Done (2026-06-06) — !597 / cb1cbcd6 |
2 |
Phase A: foundations — |
Done (2026-06-06) — !598 / 37395b3c. B1 (8→11) + B2 (49→56) now report OVER (was masked). |
3 |
Phase B step 1: B7 untyped test-client (197 → 194; 3 sites) — easiest sweep first. Type the last 3 |
Done (2026-06-06) — !599 / a5ca406a. Three |
4 |
Phase B step 2: B3b serde_json::Value in tests (63 → 31; 32 sites) — type 32 test-body Value sites across |
Done (2026-06-06) — !600 / ef862010. 28 STRUCTURAL-VALUE markers (OIDC × 11, JDM × 4, EXPLAIN plans × 6, RabbitMQ envelopes × 4, Turnstile × 3) + 3 |
5 |
Phase B step 3: B5 unwrap_or_default (219 → 175; 44 sites) — per-site triage. Diagnostic fallback (extracting an error body where empty-on-failure is acceptable) → add |
Done (2026-06-06) — !602 / |
6 |
Phase B step 4: B4 #[allow] attributes (332 → 142; 2–3 MRs) — after Step 2’s cfg_attr-test counter recognition, the actual B4 count is likely lower than 332 (~250–280 estimate). Remaining ~110–140 per-item allows audited via Plan M Tier 1 cadence. By-crate batches: (a) foundation crates ( |
Done (2026-06-06) — !603 / |
7 |
Phase B step 5: B3a serde_json::Value in src (447 → 236; 3–4 MRs) — largest sweep. By-service batches, smallest-first: (a) |
Done (2026-06-06) — !604 / |
8 |
Phase C: coverage tagging convention + 587-test sweep (2 MRs) — Step 8a: extend |
Done (2026-06-07) — Step 8a !625 / |
9 |
Phase D: flip enforcement gate ON — PRECONDITION: Steps 3–7 all Done; |
Done (2026-06-07) — !622 / |
10 |
Phase E: CI parity — |
Done (2026-06-07) — !623 / |
11 |
Phase F step 1: hooks-exec-check CI job — new |
Done (2026-06-07) — !624 / |
12 |
Phase F step 2: plan completion audit + archive — standard close-out per |
Done (2026-06-07) — this MR. Plan-completion-audit subagent verified all 11 prior steps Done with concrete MR + sha citations (caught 3 stale |
Epic: &40 (https://gitlab.com/groups/gadhs/application/ccwis/-/epics/40)
Issues: #514–#525 (one per step)
Branch prefix: docs/plan-n- for filing/archive, refactor/plan-n-step<N>- for foundation + pay-down, feat/plan-n-step<N>- for new functionality, test/plan-n-step8b- for the 587-test sweep
Milestone: TBD
Context
External review of CRAIG’s quality gates (2026-06-06) found that the quality-budget + risk-coverage matrix systems are dashboards, not controls — they report drift but never block. Concretely:
-
xtask/quality-budgets.toml:6self-declaresreport-onlyand "NOT wired into pre-push or CI today." -
5 budgets OVER historical lock: B3a 236→447, B3b 31→63, B4 142→332, B5 175→219, B7 194→197.
-
2 budgets silently masked by
actual > r.threshold.max(r.locked)atxtask/src/cmd/quality_budgets.rs:149— B1 (8→11) + B2 (49→56) regressed without firing because their thresholds (500/100) shadow their locks (8/49). -
Coverage matrix
cargo xtask coverage-matrix --emit-scorecardreports 11/1314 cells covered + 587 untagged API tests; root cause is the suffix_happy/_sad/_evilnaming convention being adopted after most tests were written, with no back-tag sweep. -
.gitlab-ci.ymlci-testsjob (line 122) runs only fmt + clippy +nextest --lib --bins. The 6 custom xtask lints (validate steps 4c–4h),cargo machete(9b), workspace nextest, and quality-budgets all live in pre-push only. Lost exec bit or web-edit → ship-without-validation surface.
Operating principle: the gate that lets known-failing code ship is the wrong gate. Pre-push runs everything runnable locally; CI runs everything CI can run; the two stay in lockstep via a single source of truth (cargo xtask validate).
User constraint (2026-06-06): strict no-grandfather. Don’t reset locks to acknowledge current debt. Keep historical baselines, pay down the OVER budgets in dedicated sweep MRs (Steps 3–7), then flip the enforcement gate (Step 9). Phase D blocks until Phase B clears.
Architectural decision on coverage tagging: comment-annotation tags (// @axis: happy) parsed by raw line-scan beside syn-discovered fn spans. Naming-as-encoding is fragile across renames; proc-macros are over-engineered for this need. Comments carry intent at the test site, survive refactors, encode both axis AND workflow, and a lint enforces them on new tests.
Threat Model
Plan N’s threat model is "silent quality drift." Three patterns make the drift invisible until a partner integration or a security review surfaces it:
-
Report-only dashboards normalize drift. A counter that ratchets up without firing the gate teaches every contributor that "the number went up but nothing happened, so it must be fine." Over time the dashboard’s signal value approaches zero. Plan N converts the dashboard to a control.
-
Masked semantic bugs in the gate itself. B1 + B2 regressed silently because the regression-check formula
actual > max(threshold, locked)shadows the lock when threshold > lock. Plan N Step 2 fixes the semantics so the lock is authoritative when set. -
Pre-push / CI divergence. The pre-push hook is the "sole functional-correctness gate" per
.githooks/pre-push:26, but a contributor pushing from a web edit OR a hook with a lost exec bit can ship code that pre-push would have rejected. Plan N Step 10 mirrors the no-devstack subset of validate into CI; Step 11 adds a programmatic hook-exec-bit guard.
Cross-cutting Invariants
-
Lock is authoritative when set. After Step 2:
cargo xtask quality-budgets --reportand--fail-on-regressionboth useceiling = if locked > 0 { locked } else { threshold }. A budget without a lock falls back to the threshold (for new budgets); a budget with a lock ignores the threshold. -
Marker comments are documented + counter-honored. Three markers —
// SILENT-OK:(B5),// PARTNER-EDGE-UNTYPED:(B3a/B3b),// STRUCTURAL-VALUE:(B3a/B3b) — are defined inquality-budgets.tomlheader +coding-conventions.md. The counter skips a match site when the immediately-preceding non-empty source line contains the marker. The marker is documentation; the lint is enforcement. -
Strict no-grandfather on the OVER budgets. Phase B pays down each OVER budget back to its historical lock. No
--write-lockruns during Phase B (counter actuals must reach the existing lock; we don’t move the goalposts). -
Pre-push ↔ CI parity via
cargo xtask validate. After Step 10: pre-push runscargo xtask validate; CI runscargo xtask validate --skip-devstack --skip-docker. The set of checks is identical except[12/14](devstack integration tests). A parity smoke check (unit test on a hardcoded step fingerprint) catches drift. -
Coverage tagging is enforced going forward. After Step 8: any new test in
tests/api/**lacking a// @axis:annotation fails the[4j/14]lint. Thextask/axis-coverage-opt-out.txtopt-out file grandfathers existing 587 tests during the Step 8b sweep and shrinks to empty when the sweep completes. -
Plan N MRs do not introduce new lint emissions in unrelated lints. Pre-push battery covers this; a sweep MR that touches
services/craig-exchange/src/adapters/to add 60// PARTNER-EDGE-UNTYPED:markers must not introduce newwildcard_enum_match_armorunreachable_pubviolations elsewhere.
Step DAG
1 (plan filing)
↓
2 (Phase A foundations: semantic fix + comment-skip + counter improvements)
↓
┌─────────────── Phase B (Steps 3–7) ───────────────┐
↓ ↓
3 (B7) → 4 (B3b) → 5 (B5) → 6 (B4 ×2-3) → 7 (B3a ×3-4)
↓ ↓
└────────────────────── (B clear) ──────────────────┘
↓
9 (Phase D: flip enforcement ON)
┌── Parallel to B ──┐
8a (Phase C matcher + lint)
↓
8b (Phase C 587-test sweep)
┌── Parallel to B ──┐
10 (Phase E: --skip-devstack + CI parity)
↓
11 (Phase F: hooks-exec-check)
↓ (all converge)
12 (Phase F: audit + archive)
Phases C + E + F can interleave with B. Phase D blocks on B clear.
Risks
-
Phase B sweeps surface counter-bug edge cases. Step 2’s comment-skip implementation may mis-handle multi-line attribute spans, doc-comments, or cfg_attr-test nesting. Mitigation: Step 2 ships 7 unit tests including the masking-bug regression case; any edge case surfaced during Phase B becomes a 1-day Step 2 follow-up MR rather than blocking the sweep.
-
Step 8b 587-test heuristic mis-classifies tests. Default heuristic is
returns_X → sad,evil* → evil, elsehappy. Mitigation: human review of the script-generated diff before pushing each batch; ambiguous cases manually annotated; opt-out file allows partial-completion landings. -
Step 10 CI duration regression breaks the 2xlarge runner. Adding workspace nextest + machete + deny + 6 custom lints to CI grows duration from ~2 min to ~4–7 min. The recurring 2xlarge disk-OOM (
known-issues.md) gets worse with longer runs. Mitigation: force-merge runbook available; if persistent, splitci-testsinto matrix jobs (fmt+clippy in one, nextest in another, lints in a third). -
Phase B duration drags beyond 5 days, blocking Phase D indefinitely. Per-step Explore subagent audits + per-MR demo of count delta keep momentum visible. Mitigation: if a step exceeds 1.5× estimate, file a follow-up plan and
Deferred (…)that step + open Phase D early with the remaining budgets still OVER (the gate fires on regression vs current actuals; OVER budgets keep the gate green at lock-baseline + the dashboard surfaces the gap). -
cargo macheteinstall in CI hits crates.io rate limits. Mitigation: pre-warm viacargo install --offlineif cached; fall back toapt/apkpackage if available; cache the binary via GitLab CI artifacts between runs. -
Step 2 B6 cfg_attr-test counter improvement double-counts or under-counts. The current regex
r"#\[\s*allow\s*\("matches both standalone and cfg_attr-wrapped allows; teaching the counter to recognizecfg_attr(test, allow(…))as 1 site instead of 1 per inner allow needs careful regex sequencing. Mitigation: Step 2 unit testtest_b6_cfg_attr_test_allow_counts_as_oneasserts the expected reduction explicitly.