ADR-031: Nursery Lint Triage and Promotion Pattern
On this page
Context
CRAIG’s clippy lint policy has hardened progressively across Plan H (16 stable lints denied) and Plan M Tier 1/2/3 (7 more stable lints denied; 2 rustc lints denied). Plan M Step 12 (#503) promoted the full clippy::nursery group at deny workspace-wide as an exploratory measurement.
The nursery group is qualitatively different from stable clippy lint groups:
-
Stability is not guaranteed. Each nursery lint may be renamed, deleted, or have its behavior tightened in any clippy release. A workspace that depends on a specific lint’s emission set will see unrelated lints flip in/out as clippy ships.
-
Surface is uncapped. Stable groups (
correctness,style,complexity,perf,pedantic,cargo) have a documented total lint count and rough emission cap. Nursery is a moving target — promoted lints leave for stable groups, retired lints leave forrestrictionor get removed. -
False-positive rate is higher. The nursery threshold for inclusion is "could be useful sometimes" rather than "is useful in the typical case." Many nursery lints fire on patterns that are correct-by-construction in CRAIG’s idioms (single-threaded operator tooling, axum handler match-arms, async runtime guarantees).
The Plan M Step 12 probe measured CRAIG’s nursery surface as 130+ emissions across 16 unique nursery lints:
| Lint | Emissions | Decision |
|---|---|---|
|
23 |
Allow (low value for internal helpers) |
|
4 |
Allow (match wins readability for multi-line branches) |
|
7 (swept) + 6 (allowed) |
Swept in mock-server; long-tail allowed |
|
1 (swept) + 5 (allowed) |
Swept in exchange-contracts; craig-auth allowed |
|
13 (swept) + 45 (allowed) |
Foundational crates swept; long-tail allowed |
|
1 (swept) + 1 (allowed) |
Swept in craig-seed; second site allowed |
|
1 (swept) |
Swept in craig-common pagination |
|
1 (swept) + 2 (allowed) |
Swept in craig-common telemetry; auth allowed |
|
21 |
Allow (>10 same-MR threshold; defer to Plan P) |
|
32 |
Allow (stylistic; no caller needs Eq) |
|
33 |
Allow (case-by-case |
|
1 |
Allow (craig-cli module-visibility audit out of scope) |
|
4 |
Allow |
|
1 |
Allow |
|
1 |
Allow |
|
1 |
Allow |
|
1 |
Allow |
|
1 |
Allow |
Decision
1. Promotion criteria for new lints
When a new clippy lint group or individual lint is considered for workspace-wide promotion, apply the <10 same-MR threshold rule from Plan M Step 12:
-
<10 emissions AND clear value: Stay denied + sweep in the same MR (per-site fixes inline).
-
>10 emissions AND clear value: Allow-list at workspace priority 1 with documented rationale; file a successor-plan sweep issue.
-
Any count AND case-by-case judgment: Allow-list at workspace priority 1 with documented rationale.
-
Any count AND no clear value (known-noisy nursery entries): Allow-list at workspace priority 1.
The "clear value" judgment is captured in the workspace Cargo.toml comment for each allow-listed lint. A lint without clear value can still be denied workspace-wide if it has <10 emissions, but the burn-down is then exploratory rather than enforced as a regression gate.
2. Allow-list comment requirement
Every lint = { level = "allow", priority = 1 } entry in [workspace.lints.clippy] MUST carry a comment block above the entry that documents:
-
The surface measurement — how many emissions surfaced during the probe (
N emissions across M crates). -
The decision rationale — why allow is the right call (low value, case-by-case, idiomatic conflict, scope-creep risk).
-
The successor plan pointer — when the sweep is deferred, name the plan that will revisit it.
This keeps the workspace lint config self-explanatory to future maintainers — they can read the Cargo.toml and understand WHY each escape hatch exists without diving into git blame.
3. Nursery group default
The clippy::nursery group MUST remain at deny workspace-wide (with documented individual allow-listed sub-lints). Promoting the group itself to allow would mute future emissions of NEW nursery lints (added in clippy updates) without an opportunity to triage them. The deny-with-individual-allows form gives operators the choice on each new emission.
4. Successor plan handoff
When a nursery probe defers sweeps to a successor plan, the deferred work goes to a named successor plan with explicit sweep scope — NOT to a "potential improvements" section, NOT to a vague follow-up issue without owner or scope.
Plan M Step 12’s deferred sweeps go to Plan P (Nursery Promotion Sweep) per plans/quality-lint-followthrough.adoc § Successor plans. Plan P will land each deferred lint as its own bounded sweep MR (e.g. Step 2 = use_self sweep across the 21 impl ApiError arms; Step 3 = too_long_first_doc_paragraph long-tail sweep across the workspace; etc.).
5. Decision-pattern reusability
This ADR records the decision pattern, not a one-time decision. Future nursery probes (e.g. clippy 1.96+ adds new nursery lints, or CRAIG promotes clippy::restriction sub-lints) MUST follow the same triage rule + allow-list comment requirement. The pattern is anchored in the workspace lint config’s audit-readability, not in any specific lint’s status.
Consequences
Positive
-
Future maintainers can read
[workspace.lints.clippy]and understand every escape hatch without git-blame archaeology. -
New nursery lints added by clippy updates surface as build errors at first encounter, forcing explicit triage rather than silent acceptance.
-
Plan P’s scope is bounded and concrete (per-lint sweep MRs), not "improve lint coverage."
Negative
-
The Cargo.toml lint block grows verbose. ~150 lines of comments + allow entries by the end of Plan M Step 12. Trade-off accepted: audit-readability over terseness.
-
Every nursery promotion probe is bounded work (probe + triage + allow-list with comments) before any sweeping happens. Adds friction; intentional.
Alternatives considered
-
Allow the whole
clippy::nurserygroup. Would suppress all current emissions in one line. Rejected: would mute future nursery lints from clippy updates, defeating the regression-gate intent. -
Sweep everything in a single Step 12 MR. Rejected: cascading allow-lists unblocking downstream crates surfaced 130+ emissions across 16 lints. A single sweep MR would exceed bounded-review scope (the Plan M default per-MR review-fatigue mitigation).
-
Skip the nursery probe entirely. Rejected: per Plan M plan body §Step 12 the probe IS the deliverable, even when the outcome is "land with allow-lists and defer sweeps." Documentation of the surface is the value.
Related decisions
-
ADR-030 Plan Lifecycle and Status Vocabulary — establishes the deny-by-default discipline for plan-tracking artifacts that this ADR mirrors for lint-policy artifacts.
-
Plan H Step 9 (archived) — established the
cfg_attr(test, allow(…))+#[expect(reason = "…")]per-site allow vocabulary that Plan M Step 12 extends to workspace-levelpriority = 1allows. -
Plan M Step 11 ADR-equivalent (in CHANGELOG) — established the
forbid→denydowngrade pattern for rustc-level lints that need local override capability (xtask unsafe blocks).