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 for restriction or 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

missing_const_for_fn

23

Allow (low value for internal helpers)

option_if_let_else

4

Allow (match wins readability for multi-line branches)

or_fun_call

7 (swept) + 6 (allowed)

Swept in mock-server; long-tail allowed

redundant_clone

1 (swept) + 5 (allowed)

Swept in exchange-contracts; craig-auth allowed

too_long_first_doc_paragraph

13 (swept) + 45 (allowed)

Foundational crates swept; long-tail allowed

collection_is_never_read

1 (swept) + 1 (allowed)

Swept in craig-seed; second site allowed

trait_duplication_in_bounds

1 (swept)

Swept in craig-common pagination

significant_drop_tightening

1 (swept) + 2 (allowed)

Swept in craig-common telemetry; auth allowed

use_self

21

Allow (>10 same-MR threshold; defer to Plan P)

derive_partial_eq_without_eq

32

Allow (stylistic; no caller needs Eq)

future_not_send

33

Allow (case-by-case !Send analysis needed)

redundant_pub_crate

1

Allow (craig-cli module-visibility audit out of scope)

needless_collect

4

Allow

suspicious_operation_groupings

1

Allow

unused_peekable

1

Allow

suboptimal_flops

1

Allow

unnecessary_lazy_evaluations

1

Allow

single_option_map

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:

  1. The surface measurement — how many emissions surfaced during the probe (N emissions across M crates).

  2. The decision rationale — why allow is the right call (low value, case-by-case, idiomatic conflict, scope-creep risk).

  3. 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

  1. Allow the whole clippy::nursery group. Would suppress all current emissions in one line. Rejected: would mute future nursery lints from clippy updates, defeating the regression-gate intent.

  2. 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).

  3. 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.

  • 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-level priority = 1 allows.

  • Plan M Step 11 ADR-equivalent (in CHANGELOG) — established the forbiddeny downgrade pattern for rustc-level lints that need local override capability (xtask unsafe blocks).

Edit this page · latest