Plan: Code-Quality Gating
On this page
ADR-030 is Accepted (2026-06-09). This plan is the implementation-of-record for epic &62. Strictness was signed off on 2026-06-09: exceed-craig (deny the pedantic-noise sub-lints + nursery escape-hatches craig allow-lists; keep only the structural exceptions) with a tiered M1–M6 rollout — see Design — strictness calibration (DECIDED 2026-06-09). Every Status cell is Not started.
Status
| MR | Description | Status |
|---|---|---|
Phase A — stand up the gate at status quo (before &56/&58 feature work) |
||
M1 (foundation) |
Root |
Done (2026-06-09) — 54 |
M2 (panic class) |
Enable |
Done (2026-06-09) — all 8 enabled at |
M3 (index / overflow / IO) |
Enable |
Done (2026-06-09) — split by surface size. The slice/index/overflow class ( |
M4 (complexity + hygiene) |
Enable |
Done (2026-06-09) — |
M5 (nursery) |
Enable the |
Done (2026-06-09) — |
M6 (ratchet) |
Port craig’s |
Done (2026-06-09) — |
Phase B — burndown (ratchet down; rides during/after &56/&58, "finish the ratchet") |
||
B1 (oversized files) |
Split |
Not started |
B2 (authz-gate DRY + security) |
Hoist the inlined |
Not started |
B3 (error model + boilerplate) |
Replace the blanket |
Not started |
B4 (expect/budget sweep) |
Burn down the grandfathered |
Not started |
B5 (property testing — stretch) |
Expand |
Not started |
Design — strictness calibration (DECIDED 2026-06-09)
Signed off 2026-06-09: exceed-craig, tiered. Every lint craig denies, canopy denies; plus the noise/nursery lints craig allow-listed for transition are denied here.
| Dial | Decision |
|---|---|
Function-size ceiling |
|
Nursery escape-hatches |
Denied. Do not inherit craig’s 5-lint nursery allow-list ( |
Pedantic noise sub-lints |
Denied. |
|
Denied (every private item documented). The single largest expected grandfather surface; M4 |
Structural allow-lists (KEPT) |
|
Rollout |
Tiered M1→M6; each lint family lands as its own reviewable, independently-grandfathered MR. |
|
Out of scope for now (beyond the agreed "exceed craig" scope); revisit post-M6 via the ADR-030 §4 promotion pattern if wanted. |
Design — the exact posture (ported from craig, verbatim)
The [workspace.lints] table and clippy.toml are ported from craig (Cargo.toml lines 218–553 / clippy.toml). Denied families: panic class (unwrap_used/expect_used/panic/todo/unimplemented/unreachable/unwrap_in_result/dbg_macro), index/slice (indexing_slicing/string_slice), overflow (arithmetic_side_effects), IO (print_stdout/print_stderr), groups (pedantic/cargo/nursery at deny priority −1), complexity (cognitive_complexity/too_many_lines), hygiene (wildcard_enum_match_arm/partial_pub_fields/allow_attributes_without_reason/let_underscore_must_use/ignored_unit_patterns/missing_docs_in_private_items); rust-level unused_must_use = deny, unsafe_code = deny. Priority-1 allow-list (each with a rationale comment per ADR-030 §4) — under the exceed-craig calibration this is minimal: only the structural cargo_common_metadata (unpublished workspace) and multiple_crate_versions (tracked in the ratchet instead), plus any individual lint that M-tier measurement shows has >10 genuinely-low-value emissions. The pedantic-noise sub-lints and nursery escape-hatches craig allow-lists are denied, not inherited. clippy.toml: too-many-lines-threshold = 40, allow-unwrap-in-tests = true, allow-expect-in-tests = true.
Design — the grandfather mechanism
Turning a lint family on does not require fixing every violation first. Each existing violation is either fixed inline (cheap cases) or annotated [expect(clippy::…, reason = "grandfathered M<n>; see &62 / plans/code-quality-gating §B4")]. [expect] (not [allow]) is used so the annotation self-removes: when the underlying violation is later fixed, [expect] itself fires unfulfilled_lint_expectations, forcing the now-dead annotation to be deleted. The burndown (Phase B) is therefore self-policing — you cannot fix a violation and leave its grandfather marker behind.
Design — the ratchet counters (port)
Port craig’s 8 counters, recalibrated to canopy paths: route-module LOC (>500, services//src/api/.rs), function LOC (>100, syn-parsed, test-fns skipped), untyped serde_json::Value (src=0 / tests=budget), #[allow] count, .unwrap_or_default() (>budget, // SILENT-OK: skip-marker), duplicate dep versions (cargo tree -d), and untyped test-client methods. Enforcement is lock-authoritative ceiling() = if locked > 0 { locked } else { threshold }; --fail-on-regression bails if actual > ceiling(); --write-lock lowers the floor after a cleanup (justified in the MR). M6 seeds the .lock at canopy’s current counts.
Design — test carve-out
unwrap/expect in tests are permitted by the clippy.toml toggles (no per-crate header). The remaining panic/index/print/overflow lints need a [cfg(test)]-scoped allow because [workspace.lints] cannot express cfg(test). Canopy centralizes this as far as the tooling allows (a single shared header include! or the minimal per-root ![cfg_attr(test, allow(…))] block) to avoid craig’s ~31×-repeated header.
Notes
-
Phase A is &62 workstreams 1+2; Phase B is workstreams 3+4 (the user’s "finish the ratchet that wasn’t addressed"). Phase A lands before the first &56/&58 implementation MR so all feature code is born to the gate.
-
Issues are cut from this plan per ADR-013 (one per M*/B* row) once the calibration is signed off.
-
B2’s fail-open-default retirement is the one Phase-B item with a security dimension — pull forward into Phase A if treated as a security fix rather than hygiene.