Plan M: Quality Lint Followthrough
On this page
- Status
- Context
- Threat model
- Cross-cutting invariants
- Step DAG
- Steps
- Step 1 — Plan filing
- Step 2 — Tier 1a — foundational crates
- Step 3 — Tier 1b — contracts + reference + shared
- Step 4 — Tier 1c — 8 backend services
- Step 5 — Tier 1d — leaf binaries + per-partner crates
- Step 6 — Tier 1e — regression gate
- Step 7 — Tier 2 — panic-surface tightening
- Step 8 — Tier 2 — overflow-surface tightening
- Step 9 — Tier 2 — IO-surface tightening
- Step 10 — Tier 2 — struct hygiene
- Step 11 — Tier 3 — rustc-level gaps
- Step 12 — Tier 4 — nursery probe
- Step 13 — Plan completion audit + archive
- Risks
- Successor plans
- References
Status
| Step | Description | Status |
|---|---|---|
1 |
Plan filing — body lands as a docs-only MR alongside epic + per-tier-step issues. |
Done (2026-06-04) — this MR. Epic &37 + issues #493–#504 filed. |
2 |
Tier 1a — lift the transitional |
Done (2026-06-04) — #493 closed. 6 crates lifted across MRs !536-!541. |
3 |
Tier 1b — lift the transitional |
Done (2026-06-04) — #494 closed. 10 crates lifted across MRs !542-!551. |
4 |
Tier 1c — lift the transitional |
Done (2026-06-04) — #495 closed. 8 services lifted across MRs !552-!559. |
5 |
Tier 1d — lift the transitional |
Done (2026-06-04) — #496 closed. 14 leaf binaries + partner stubs lifted across MRs !560-!567. |
6 |
Tier 1e — regression gate. New xtask sub-lint |
Done (2026-06-05) — #497 closed via !568. Scanner uses |
7 |
Tier 2 — panic-surface tightening. Add to |
Done (2026-06-05) — #498 closed via !570. Actual production-code surface: 28 sites across 8 crates/services (the ~928 estimate counted test-code + tooling-crate surfaces that the cfg_attr exemption / crate-level allows handle). Workspace clippy clean. 2414/2414 tests pass. |
8 |
Tier 2 — overflow-surface tightening. Add |
Done (2026-06-05) — #499 closed via !576. ~40 production-code fixes across 16 crates/services (saturating_mul on pagination offsets, checked_add_signed on chrono dates, checked_add on Instant deadlines, saturating_add on counters); 4 tool-crate-level allows (cli + seed + xtask + mock-server) + craig-financial service-level allow (domain-specific Decimal currency math) + craig-test-lib permanent allow. |
9 |
Tier 2 — IO-surface tightening. Add |
Done (2026-06-05) — 500 closed via !572. Surface: 1 production site (craig-common/telemetry.rs Drop-fallback eprintln, per-site |
10 |
Tier 2 — struct hygiene. Add |
Done (2026-06-05) — 501 closed via !574. Surface: 4 structs across 2 crates. 1 strip-pub (ConsumerBackpressureInjector), 1 add-pub (I18n::messages), 2 per-struct |
11 |
Tier 3 — rustc-level gaps. Add to |
Done (2026-06-05) — 502 closed via !579. Shipped 2 of 3 lints ( |
12 |
Tier 4 — nursery probe. Add |
Done (2026-06-05) — #503 closed via !581. Probe surface: 130+ emissions across 16 unique nursery lints. Swept in same MR (<10 emissions + clear value): |
13 |
Plan completion audit + archive. Per |
Done (2026-06-05) — this MR closes 504. Audit subagent verified all 12 prior steps |
Epic: &37 (epic: Quality Lint Followthrough (Plan M))
Issues: #493 (Step 2 — Tier 1a foundational crates) · #494 (Step 3 — Tier 1b contracts + shared) · #495 (Step 4 — Tier 1c backend services) · #496 (Step 5 — Tier 1d leaf binaries + per-partner) · #497 (Step 6 — Tier 1e xtask regression gate) · #498 (Step 7 — Tier 2 panic-surface) · #499 (Step 8 — Tier 2 overflow-surface) · #500 (Step 9 — Tier 2 IO-surface) · #501 (Step 10 — Tier 2 struct hygiene) · #502 (Step 11 — Tier 3 rustc-level) · #503 (Step 12 — Tier 4 nursery probe) · #504 (Step 13 — plan completion audit)
Branch prefix: refactor/plan-m-
*Milestone: TBD
Context
Plan H (Idiomatic Rust + Clippy Strictness — archived 2026-05-21 at plans/archive/idiomatic-rust-clippy.adoc) staged a workspace-wide clippy lint deny set across 16 lints + the full pedantic + cargo lint groups. Step 9 Phase A’s release note read:
16 clippy lints
denyworkspace-wide;cargo clippy --workspace --all-targets — -D warningsclean.
That statement was true on the merge-commit’s HEAD — but only because Plan H Step 2 introduced a transitional ![allow(clippy::pedantic, clippy::cargo, clippy::missing_docs_in_private_items, clippy::too_many_lines, clippy::cognitive_complexity, clippy::ignored_unit_patterns, reason = "scheduled for Plan H Steps 3-7 sweeps")] block at every production source root. Steps 3-7 finished, the workspace-level lint levels were promoted to deny per Step 9 Phase B, but the per-crate transitional ![allow] blocks were never removed.
A 2026-06-04 audit confirmed 35 production source roots (every services//src/{lib,main}.rs + most crates//src/lib.rs) still carry the block. The workspace-level pedantic = "deny" and cargo = "deny" are paper tigers inside every production crate.
Probe (2026-06-04): lift the transitional allow on crates/craig-common/src/lib.rs alone, run cargo clippy -p craig-common --all-targets — -D warnings:
error: could not compile `craig-common` (lib) due to 105 previous errors error: could not compile `craig-common` (lib test) due to 62 previous errors
105 production-code clippy emissions on a single foundational crate. The same shape applies to 34 other source roots.
Beyond closing this gap, several panic-risk and overflow-risk clippy lints (indexing_slicing, arithmetic_side_effects, string_slice, unwrap_in_result) remain unconfigured — they’re not denied today and don’t fire even with the transitional allow removed. They’re the next-tier of strictness this plan layers on top, but only AFTER the transitional allows are gone (the existing pedantic/cargo cleanup must come first; otherwise new lints get fixed before old ones).
Threat model
What this plan prevents in the long term:
-
Silent introduction of panicking code paths. Every
arr[i]in production is a potential OOB panic; today the workspace doesn’t catch them. Plan M Step 7 makes the compiler refuse compilation. -
Silent overflow. Plain
+/-/on integer types silently wraps in release. CRAIG processes counts, deadlines, monetary cents — all overflow surfaces. Plan M Step 8 forces explicitchecked_/wrapping_*/saturating_*per-site decision. -
Re-introduction of transitional-allow patterns. The exact same "we’ll sweep this later" pattern that Plan H Step 2 created is currently silenced — and could be re-introduced any time a future plan files a similar transitional carve-out. Plan M Step 6 makes that mechanically impossible via a new xtask lint at
[4h/14]. -
Drift between
pedantic/cargogroup denies and the actual per-crate behavior. Today the workspace-level lint config and per-crate#![allow]blocks are out-of-sync. Plan M Tier 1 makes the workspace-level config the source of truth. -
Library crates writing to stdout/stderr instead of using
tracing. Plan M Step 9 enforces structured logging in every non-binary crate. -
Unused crate dependencies accumulating in
Cargo.toml. Plan M Step 11’sunused_crate_dependencies = "deny"catches these at compile time, complementingcargo machete(catches them atvalidate [9b/14]post-build).
Cross-cutting invariants
These hold from Step 2 onward and are subagent-verified per MR:
-
No
![allow(clippy::pedantic)]or![allow(clippy::cargo)]at any production crate root. Per-fn#[allow(clippy::<specific_lint>, reason = "<justification>")]is acceptable when the §Style carve-out applies (e.g.clippy::too_many_lineson a documented important-stay function). The blanket group-level allow is the failure mode this plan eliminates. -
Test code stays exempt. Inside
#[cfg(test)] mod testsblocks andtests/*.rsfiles, panicking calls (unwrap,expect,panic) are the assertion mechanism. Thecfg_attr(test, allow(…))on each lib root that scopes these to test compilation stays — this plan does NOT remove that scoping. -
Each per-fn
#[allow(…)]MUST carry areason = "…"string. Reviewer subagent rejects bare allows. Theallow_attributes_without_reason = "deny"workspace lint enforces this mechanically; this plan keeps it on. -
No
unused_must_use = "warn"downgrades inside production code. The Tier 3 rustc lint addition isdeny, notwarn— and staysdenyin every per-step MR. -
The xtask regression-gate at Step 6 lands BEFORE Tier 2 / Tier 3 lint additions. Otherwise a future agent could introduce a new transitional-allow pattern under the cover of Tier 2 sweep work; the gate prevents that ordering risk.
Step DAG
Sequence the steps in this exact order:
1 (plan filing — docs-only)
↓
2 (Tier 1a — foundational crates)
↓
3 (Tier 1b — contracts + reference + shared)
↓
4 (Tier 1c — 8 backend services)
↓
5 (Tier 1d — leaf binaries + per-partner crates)
↓
6 (Tier 1e — xtask regression gate, blocks Tier 2 entry)
↓
├─→ 7 (Tier 2 — indexing/string/unwrap-in-Result)
├─→ 8 (Tier 2 — arithmetic-overflow)
├─→ 9 (Tier 2 — print-stdout/stderr)
└─→ 10 (Tier 2 — partial-pub-fields)
↓
11 (Tier 3 — rustc-level: unused_must_use + unused_crate_dependencies + unsafe_code)
↓
12 (Tier 4 — nursery probe; outcome-dependent)
↓
13 (plan completion audit + archive)
Tier 2 sub-steps (7-10) MAY run in parallel after Step 6 lands. Step 8 (arithmetic) is the highest-risk sub-step and benefits from landing AFTER Steps 7/9/10 so reviewers aren’t fatigued.
Steps
Step 1 — Plan filing
This MR. Body lands at docs/modules/ROOT/pages/plans/quality-lint-followthrough.adoc. nav.adoc § Plans § Active gains a Plan M row. CHANGELOG.adoc § Unreleased describes the docs-only filing. No code changes.
Epic + per-step issues filed via the same MR (12 issues, one per step 2-13).
Step 2 — Tier 1a — foundational crates
For each of craig-common, craig-auth, craig-crypto, craig-db, craig-mq, craig-store:
-
Branch
refactor/plan-m-tier1a-<crate>. -
Delete the
#![allow(clippy::pedantic, clippy::cargo, clippy::missing_docs_in_private_items, clippy::too_many_lines, clippy::cognitive_complexity, clippy::ignored_unit_patterns)]block from the lib root. -
Run
cargo clippy -p <crate> --all-targets — -D warnings. -
For every emission: either fix the underlying code (
as→From::from,&v[..]→&v[..n], etc.) OR add a per-fn#[allow(clippy::<specific_lint>, reason = "<justification>")]if the §Style carve-out applies. Subagent verifies each allow. -
Commit + push + MR.
One MR per crate so review surface is bounded. 6 MRs total for Step 2.
Step 3 — Tier 1b — contracts + reference + shared
Same shape as Step 2 for: craig-cases-contracts, craig-exchange-contracts, craig-reference, craig-matching, craig-signing, craig-intake-sdk, craig-test-lib, craig-rules-client, craig-authz, craig-api. 10 MRs total for Step 3.
Step 4 — Tier 1c — 8 backend services
Same shape for: craig-rules, craig-cases, craig-placement, craig-exchange, craig-financial, craig-reporting, craig-security, craig-intake. 8 MRs total for Step 4.
Each service crate is larger than a shared crate — expect more per-fn #[allow(…)] decisions per MR. The reviewer subagent’s job is bigger here: every allow needs §Style justification.
Step 5 — Tier 1d — leaf binaries + per-partner crates
Same shape for: craig-cli, craig-web, craig-mock-server, craig-seed, xtask, craig-partner-caps, craig-partner-empi, craig-partner-ies, craig-partner-ions, craig-partner-smile, craig-partner-tcm, craig-partner-wic. 12 MRs total for Step 5.
Skip craig-partner-cprs and craig-partner-doe-slds — these are Plan L Step 3 batch 4 paused work (see memory project_plan_l_step3_batch4_paused_2026_05_26). When Plan L resumes those crates, the Plan L MRs MUST land them without the transitional allow already in place.
Step 6 — Tier 1e — regression gate
Single MR. Adds a new xtask sub-command cargo xtask lints no-transitional-allows that:
-
Walks every
.rswhose path matchesservices//src/{lib,main}.rsorcrates/*/src/lib.rs. -
Rejects any file containing a crate-root attribute matching
![allow(…)]that includesclippy::pedanticORclippy::cargo(the lint groups) as direct entries. Per-fn[allow(clippy::pedantic)]is allowed (different surface). -
Exits 1 with file:line citations if any violations are found.
Wired into cargo xtask validate as new blocking step [4h/14] between [4g/14] (route-role-coverage) and [5/14] (SPDX). After this lands, Tier 2 entry is unblocked.
Step 7 — Tier 2 — panic-surface tightening
Single MR (or 2 MRs if indexing_slicing sweep is large). Adds to Cargo.toml:
[workspace.lints.clippy]
indexing_slicing = "deny"
string_slice = "deny"
unwrap_in_result = "deny"
Plus cfg_attr(test, allow(indexing_slicing, string_slice, unwrap_in_result)) on each lib root (33 lib/main files). Sweep call sites:
-
arr[i]→arr.get(i).ok_or(<typed error>)?(production) orarr.get(i).expect("invariant")if loop-invariant proves bounds -
&s[a..b]→s.get(a..b).ok_or(<typed error>)? -
x.unwrap()inside a Result-returning fn →x?if applicable,x.ok_or(<typed error>)?if Option
~928 indexing sites identified by grep -rcE '[[a-z_]\]|\[[0-9]\]' --include='.rs' services//src crates/*/src. May warrant splitting Step 7 into Step 7a (indexing) + Step 7b (string_slice + unwrap_in_result).
Step 8 — Tier 2 — overflow-surface tightening
Adds arithmetic_side_effects = "deny" to workspace lints. Sweep each + / - / * / / in production to:
-
checked_*+ error propagation if overflow indicates a bug -
saturating_*if clamp-at-bound is acceptable -
wrapping_*if wrap is semantically correct (rare)
Test code exempt via cfg_attr(test, allow(arithmetic_side_effects)).
Most invasive Tier 2 lint. Sub-step into per-domain batches if call-site count is high: count first, then split.
Step 9 — Tier 2 — IO-surface tightening
Adds print_stdout = "deny" + print_stderr = "deny" to workspace lints. Sweep:
-
Library + service code: convert each
println!/eprintln!/print!/eprint!totracing::info!/tracing::warn!etc. -
CLI (
craig-cli) and developer tooling (xtask): per-crate#![allow(clippy::print_stdout, clippy::print_stderr, reason = "CLI by definition writes structured output to stdout/stderr")]. -
Web BFF (
craig-web): no exemption — all output goes viatracing+ Axum response bodies.
Step 10 — Tier 2 — struct hygiene
Adds partial_pub_fields = "deny" to workspace lints. Sweep struct definitions:
-
If MOST fields are
pub, make ALL fieldspub(struct is a passive carrier — convert to public-by-default). -
If MOST fields are private, REMOVE
pubfrom outliers and add an accessor method. -
Per-struct call decision; reviewer subagent confirms the pattern matches the struct’s role.
Small surface (most CRAIG structs are already homogeneous); high payoff (forces intentional encapsulation decisions).
Step 11 — Tier 3 — rustc-level gaps
Adds to Cargo.toml [workspace.lints.rust]:
[workspace.lints.rust]
unused_must_use = "deny"
unused_crate_dependencies = "deny"
unsafe_code = "forbid"
Plus audit: every existing #![forbid(unsafe_code)] at a bin/lib root becomes redundant once unsafe_code = "forbid" is workspace-level. Remove the per-crate forbid attributes in the same MR.
unused_crate_dependencies = "deny" complements cargo machete at validate [9b/14] — the lint fires at compile time per-target, machete fires at validate post-build. Both catch the same drift; the compile-time signal is faster feedback.
Step 12 — Tier 4 — nursery probe
Single exploratory MR. Adds nursery = { level = "deny", priority = -1 } workspace-wide. Builds. Reads the resulting clippy emissions. Triage:
-
Lints with <10 emissions and clear value: stay denied + sweep in the same MR or a follow-up.
-
Lints with >10 emissions but case-by-case judgment: allow-list at priority 1 with documented reason.
-
Lints that fire only on test code:
cfg_attr(test, allow(…)). -
Lints with no clear value (known-noisy nursery entries): allow-list at priority 1.
Outcome-dependent: this MR may end up landing as-is, may need a follow-up sweep MR, or may revert if nursery is too noisy. Either way, the triage is documented in the MR body and a new ADR records the decision pattern for future nursery promotions.
Step 13 — Plan completion audit + archive
Per delivery-protocol.md § Plan completion audit:
-
Spawn
plan-completion-auditsubagent. Verify all 11 prior steps complete via MR list (glab mr list --search "Plan M" --merged). -
nav.adocActive → Archived (move Plan M xref). -
plans/archive/quality-lint-followthrough.adoc—git mvbody into archive directory. -
archive.adoc— new row. -
.claude/CLAUDE.md§ Phase Status — final stats row for Plan M (lints added, MRs shipped, allow blocks removed, sites fixed). -
Plan G Step 6 / #462 — re-evaluate unblock status now that the workspace-level pedantic/cargo denies are actually in force. If unblocked, file Plan G Step 6 issue resumption; if still blocked, record blocker explanation.
Risks
-
Surprise from
nurserygroup probe. Some nursery lints have very high noise-to-value ratios. The Step 12 probe is structured to handle this — but if the post-probe state is "100+ noisy lints, no clear winners," the MR may revert. Risk is contained to one MR. -
arithmetic_side_effectssweep at Step 8 may surface latent bugs. Convertinga + btoa.checked_add(b)?exposes overflow paths that current code silently hits. The fix is to handle them — but doing so during a sweep MR is awkward if the fix changes API surface. Mitigation: surface per-site to issue tracker; defer non-trivial fixes to follow-up MRs that get linked from Step 8’s MR. -
Plan L Step 3 batch 4 paused work (
crates/craig-partner-cprs/+craig-partner-doe-slds/) — Plan M Step 5 explicitly skips these. When Plan L resumes those crates, the Plan L MRs MUST NOT re-introduce the transitional allow block (regression-gated by Plan M Step 6’s xtask lint, but documenting here so the Plan L resumer sees it). -
MR review fatigue across the 36-MR Tier 1 sweep. Per-crate MRs are intentionally small to bound review surface, but the sheer count is real. Mitigation: batch review (e.g. Tier 1a’s 6 MRs land in a single review session); reviewer subagent does first-pass on each MR’s allow-list decisions.
-
Plan H Step 9 "0 warnings" claim re-evaluation. Plan M Step 4 (services sweep) will likely surface that many services emit dozens of pedantic warnings each. The release-note narrative changes: "Plan H staged 16 deny lints; Plan M actually enforced them across all crates."
Successor plans
After Plan M closes, the next likely code-quality plans:
-
Plan N — Test-coverage-axis sweep. Promote
_happy/_sad/_evilsuffixes onto the 587 currently-untagged tests undertests/api/*.rs, lifting the coverage matrix from #310’s 0.8% baseline into double-digits. Mechanical sweep, large surface. -
Plan O — Mock-server adapter de-orphan. Wire one test per shipped per-partner adapter against the mock-server. Builds on Plan L F-063 (deferred).
-
Plan P — Nursery promotion sweep (if Step 12 surfaces winners). Land the nursery lints that survived Step 12 triage with proper sweeps.
References
-
Plan H archived body:
plans/archive/idiomatic-rust-clippy.adoc— describes the Step 2 transitional allows + Steps 3-7 burn-down + Step 9 deny promotion. -
Cargo.toml [workspace.lints.clippy]— current 16-lint deny set + 5 allow-listed escape hatches. -
clippy.toml— function-size threshold (40 lines) + test-code unwrap/expect allow. -
xtask/src/cmd/validate.rs— current 20-step pre-push gate. -
xtask/src/cmd/lints.rs— pattern for the Step 6 regression gate (mirrorno-silent-skips/dto-length/secrets-yaml/route-role-coverage). -
ADR-030 — Plan Lifecycle and Status Vocabulary.
-
delivery-protocol.md— plan completion audit protocol.