Plan: Prevention-Gate Lints (epic &63 wave 1)
On this page
- Status
- Context
- Scope
- Design
- Shared architecture
- Gate 1 — #867: ban new crate-root arithmetic blankets
- Gate 2 — #866: syn rebuild of the reqwest choke-point lint
- Gate 3 — #869: workspace dependency-inheritance lint + flips
- Gate 4 — #870: partner parse-never-panics coverage lint
- Gate 5 — #891: advisory-ignore audit gate
- Gate 6 — #879: serialize-credentials lint + store-model site baseline
- Steps
- Files Touched
- Verification
- Documentation Updates
Status
| Step | Description | Status |
|---|---|---|
1 |
Tracker preflight: follow-ups #1574/#1575/#1576/#1577 filed + related; |
Done (2026-08-24) — follow-up numbers exist before any commit cites them |
2 |
Gate 1 (#867): crate-root arithmetic-blanket ban with the 8-entry grandfathered ratchet |
Done (2026-08-24) — seed targets=77, baseline 8/8, ghost-blanket negative proof |
3 |
Gate 2 (#866): syn rebuild of the reqwest choke-point lint + (file, count) builder allowlist |
Done (2026-08-24) — files=1055, allowlist 5/5 at exact counts, both classes ghost-proven |
4 |
Gate 3 (#869): workspace dependency-inheritance lint + mechanical flips + exception baseline |
Done (2026-08-24) — members=66, deps=1210, 0 violations, exceptions 4/4, lockfile-identical flips |
5 |
Gate 4 (#870): partner parse-never-panics coverage lint (census / token-stream / reachability) |
Done (2026-08-24) — adapters=11, covered=11; orphaned-registration + gutted-macro negative proofs |
6 |
Gate 5 (#891): advisory-ignore audit gate (parity / per-ID records / review-by expiry) |
Done (2026-08-24) — 5 records reformatted, parity 5/5, live EXPIRED negative proof; CI rules:changes fixed |
7 |
Gate 6 (#879): serialize-credentials lint + store-model per-file site baseline |
Done (2026-08-24) — 17 markers/10 files inventory; baseline 48 files/153 sites; ghost + bless-refusal proofs |
8 |
Close-out: CLAUDE.md &83 bullet, quality-gates.adoc catalog stitches, battery, MR |
Done (2026-08-24) — battery green twice (standalone + pre-push hook); !1475 merged 558f8815; all six issues closed with records |
Epic: &63
Issues: #867, #866, #869, #870, #891, #879 (batched — same subsystem; the batch-small-issues
authorization is cited in the MR)
Branch: feature/prevention-gate-lints
Context
Epic &63 (the 2026-06-28 shift-left roadmap) converts one-time fixes into self-enforcing
floors so a defect class cannot regress. The 2026-08-23 triage found five of these six issues
half-landed — the underlying defects were fixed in earlier MRs — and narrowed each to its
enforcement residue (recorded in per-issue comments). This wave ships the six gates that all
live in xtask/src/cmd/lints.rs + xtask/src/cmd/validate.rs, plus the seed-green fixes each
needs. No runtime behavior changes; the only production-code edits are Cargo.toml inheritance
flips and per-field marker comments.
The design went through an external stop-ship review (40 findings) whose core verdict — several drafted gates could pass while the prohibited condition remained — drove four structural choices now baked in below: syn visitors instead of line scanners (Gate 2), committed baseline files instead of comment carve-outs wherever parsing discards comments (Gates 3, 6b), a per-file site baseline instead of an aggregate budget (Gate 6b), and time-boxed review-by expiry instead of an unsound Cargo.lock liveness check (Gate 5).
Ratified constraints honored:
-
The 8 existing crate-root
arithmetic_side_effectsblankets are ratified decisions (#815 accept-as-is with measured ~195-site evidence; #806 folded into the #630 pedantic sweep; craig-test-lib declared-permanent). Gate 1 grandfathers exactly these 8 and bans NEW ones — it never forces ad-hoc migration of a recorded lint-policy override. -
run_cargo_deny_soft()no longer exists (#1402);cargo deny checkruns in the synced pre-push hook and CI. Gate 5 does not re-implement advisory-DB awareness — it enforces the structural contract cargo-deny cannot (parity, per-ID rationale, review-by expiry). -
Pre-push
cargo xtask validateis the sole functional-correctness gate; every gate lands there. CI trigger hygiene is still fixed where the review caught gaps (Gate 5).
Scope
In scope:
-
Six blocking gates in the xtask lint subsystem, each with fixture tests, validate wiring, a
quality-gates.adocsection, and a CHANGELOG entry. -
The seed-green fixes each gate needs: ~19 Cargo.toml inheritance flips (#869), per-field
// serialize-credential:markers (#879), the deny.toml/audit.toml structured-record reformat (#891). -
Two new committed baseline files:
xtask/dep-inherit-exceptions.toml,xtask/store-model-responses.toml. -
CI
rules:changeshygiene for the advisory jobs (#891).
Out of scope (recorded):
-
Migrating the 8 ratified arithmetic blankets (#630 owns re-scoping); manifest-defined custom target paths (Gate 1 covers conventional crate roots, stated in its doc).
-
The ~150 store-model handler migrations (the site baseline ratchets them down over time).
-
from_valuepartner properties + craig-partner-audit coverage (#1575), the db-error scanner’s external-declaration blind spot (#1576), the craig-web rand 0.9 migration (#1574), the CI partner-property accuracy gap (#1577) — filed follow-ups, never fold-ins. -
rust-version inheritance legislation (59 of 66 members omit it; no defect); promoting single-crate deps (askama etc.) into
[workspace.dependencies]— no violation class. -
reqwest::blockingin xtask (dev tooling; recorded in the lint doc). -
Full type-resolution for the Gate 2/6a scanners — recorded-false-negative tier.
Design
Shared architecture
Every gate is a LintsAction variant (doc comment citing its issue) + pub
run_<x>_blocking() + inline #[cfg(test)] fixture tests, registered via an
enforce_<x>(steps) helper in validate.rs (steps.mark(…) + .context(…), fail-fast).
Gate 1 stays a per-file scan_<x>(path, source) pure function; Gates 2-6 are workspace-level
collect-then-reconcile (the no_transitional_allows + authz-coverage shape).
Cross-cutting disciplines:
-
Universe ≠ findings. Every gate separately reports its scan UNIVERSE (files/crates/ entries enumerated, per root) and its FINDINGS. Zero findings is a desirable state; a zero or implausibly-shrunken universe fails loudly, per root. Each gate’s floor is stated below.
-
Parsers get property tests (testing-discipline mandate): Gate 5’s record parser, Gate 3’s manifest interpretation, Gate 2’s cfg-test predicate detector.
-
Escape hatches are pinned: baseline files are bidirectional (new violation AND stale entry both fail;
--blessshrinks only); comment markers require a non-empty reason and are stale-checked (a marker adjacent to nothing flaggable = violation). -
B8 containment: the new
run_<x>()bodies share ONE report-and-bail helper so the B8 duplicate-blocks budget (locked 204) does not trip; no--write-lockhappens in this MR.
Gate 1 — #867: ban new crate-root arithmetic blankets
Extend run_no_transitional_allows():
-
clippy_group_lintadditionally matchesclippy::arithmetic_side_effects; -
const ARITHMETIC_BLANKET_BASELINE: &[&str]names exactly the 8 ratified files (services/craig-financial/src/lib.rs,services/craig-financial/src/main.rs,xtask/src/main.rs,services/craig-cli/src/lib.rs,services/craig-cli/src/main.rs,crates/craig-test-lib/src/lib.rs,tools/craig-seed/src/lib.rs,tools/craig-seed/src/main.rs), each entry commented with its ratification ref. Blanket present + file not in baseline → violation; baseline file with no blanket → stale entry → violation (shrink-only ratchet); -
suppression-equivalent forms banned: inner
cfg_attr(…)whose predicate is not provably test-gated (testrequired through anyall/anynesting) with a banned-group payload, and innerexpect(…)of the banned groups. Test-gatedcfg_attr(test, allow(…))roots stay exempt; -
glob set widened with brace-free pathspecs (git ls-files performs no brace expansion):
plugins//src/lib.rs,plugins//src/main.rs,crates//src/main.rs, and/src/bin/*.rsvariants for crates/services/tools/plugins/xtask. Coverage claim = conventional production crate roots only.
Universe floor: every glob group nonzero at seed time stays nonzero (plugins: 2). Seeds green with zero source changes.
Gate 2 — #866: syn rebuild of the reqwest choke-point lint
Rewrite run_reqwest_client_new as a syn visitor (line scanners mishandle
#[cfg(all(test, feature = "…"))] modules and hide production items after inline test
modules; substring needles miss ClientBuilder::new() / Client::default() / aliased imports
and false-trigger inside comments and strings):
-
the visitor skips items whose attrs carry a cfg predicate that provably requires
test(recursive throughall/any; proptested); external#[cfg(test)] mod x;declarations are skipped, never scan-terminating; -
call detection is path-based with an import table (grouped imports and
asaliases tracked): paths resolving toreqwest::Client::{new,builder,default}orreqwest::ClientBuilder::{new,default}are flagged. Recorded false-negative: non-usealiasing (type C = reqwest::Client;); -
the allowlist is (file, expected builder-site count) pairs over the 5 production files (
crates/craig-common/src/http.rs,crates/craig-auth/src/oidc_discovery.rs,crates/craig-auth/src/jwks.rs,crates/craig-intake-sdk/src/client.rs,crates/craig-exchange-transport/src/egress.rs), rationale one-liners taken from each file’s own doc comments. Count above baseline = new decision; below = stale = bless down; -
craig-test-libkeeps its path exemption for this gate only (test infrastructure — the production choke-point threat model does not cover it; recorded residue); -
walker = the workspace-member surface (crates, services, tools, plugins, xtask src trees minus
[workspace].exclude); -
xtask’s
reqwest::blockingbuilders out of scope (recorded).
The shipped db-error lint’s line scanner shares the external-declaration blind spot — #1576, separate branch. Universe floor: allowlisted files all present with nonzero counts. Seeds green with zero source changes.
Gate 3 — #869: workspace dependency-inheritance lint + flips
New run_workspace_dep_inheritance():
-
membership authority = the workspace itself: expand
[workspace].membersglobs minus[workspace].exclude(never fixed directory globs); -
manifests parsed with
tomlfor structure; exceptions live in the committedxtask/dep-inherit-exceptions.toml, keyed(manifest path, table, dependency key)with a mandatoryreason— exceptions apply ONLY to check 1; checks 2-3 have no exception channel; -
check 1: a dependency whose resolved package name (
package =rename honored; git deps compared by name; dotted/inline/expanded forms normalized by the parser) exists in[workspace.dependencies]but is declared hardcoded → violation; -
check 2:
[package]version/edition/licenseeach PRESENT and.workspace = true(omission is a violation too);rust-versionout of scope (recorded); -
check 3:
default-features = trueanywhere, or a hardcoded twin of adefault-features = falseworkspace dep dropping thefalse→ violation (zero today).
Exception seed (4 entries): rules-client reqwest (feature subtraction, −multipart);
intake-sdk tokio (leaf-slimming to ["time"]); xtask reqwest (+blocking/−multipart);
craig-web rand = "0.8" (divergence vs workspace 0.9 — retirement = #1574).
Seed-green flips in the same commit: ~12 mechanical workspace = true flips (craig-test-lib
toml; craig-reference strum; craig-web uuid/tempfile; xtask anyhow/clap/serde/
serde_json/proc-macro2/quote/syn/toml); 2 path-alias flips (craig-cli dev-dep
craig-test-lib; craig-seed craig-reference); 4 self-dev-dependency flips
(craig-mq/craig-store/craig-api/craig-crypto { path = ".", … } → { workspace = true,
features = […] }, resolving identically); the craig-matching workspace alias + the
craig-cases flip; xtask [package] fields → .workspace = true.
Proptest invariant: classification stable across equivalent TOML spellings. Universe floor: member count == the workspace’s own count (66).
Gate 4 — #870: partner parse-never-panics coverage lint
New run_parse_prop_coverage(), three layers (string co-occurrence proves nothing; a
tests/** file is not necessarily compiled; keying on "crates that have adapter.rs" lets
deletion remove a crate from enforcement):
-
Census: enumerate ALL
crates/craig-partner-*workspace members. Each must contain an adapter implementation (type Inbound = X;located by syn inside the adapter’simplblock —src/adapter.rsorsrc/adapter/mod.rs) or appear on the named exception list (craig-partner-audit— dispatch crate, no adapter; extension tracked as #1575). Neither → violation. Census shrink below the seed (11 adapters + 1 exception) → violation. -
Property presence: for each inbound type X, some file under the crate’s
tests/must contain aproptest!macro invocation whose TOKEN STREAM contains bothfrom_sliceandX(comments cannot satisfy it; deleting the property body removes the macro → fail). -
Compilation reachability: the containing file must be a top-level
tests/*.rstarget or reachable from one via amod/#[path]chain (caps' nested layout is the seed case).
Escape hatch: // allow-no-parse-prop: <reason> above the type Inbound declaration; the
reason MUST contain a #NNN issue ref. Day-one green: all 11 inbound types covered (#858).
The properties execute in the pre-push full battery; the CI --lib --bins accuracy gap is
#1577.
Gate 5 — #891: advisory-ignore audit gate
New run_advisory_ignore_audit() (cargo-deny/cargo-audit stay the advisory-DB layer; a
Cargo.lock "liveness" check was reviewed and rejected as unsound — a crate name persists in
the lock after upgrading past an advisory):
-
Parity: the two ignore arrays (
deny.toml,.cargo/audit.toml) compared as ORDERED lists, duplicates rejected, IDs matchingRUSTSEC-\d{4}-\d{4,}. -
Per-ID structured record (no shared blocks): each ID’s contiguous preceding comment block in deny.toml must contain
# crate: <name>, non-empty# rationale:,# tracked: NNNORretire: <condition>, and# review-by: YYYY-MM-DD. The quick-xml pair splits into two records; RUSTSEC-2023-0071’s above-the-array prose moves inside the array.# lock-onlystays as human documentation (the gate attaches no semantics). -
Review-by expiry: today >
review-by→ FAIL. Every ignore is time-boxed; staleness is a hard stop. Dates seeded ≤ 180 days out; 0071’s aligned with #764 (which stays open — the gate never judges reachability). -
The record parser is hand-rolled over the raw array region (TOML parsing discards comments) and proptested: totality, well-formed acceptance, missing-marker rejection.
CI hygiene in the same commit: deny.toml + .cargo/audit.toml added to the cargo-audit
job’s rules:changes in .gitlab-ci.yml (the cargo-deny job already watched deny.toml;
.cargo/audit.toml does not affect cargo-deny). Universe floor: both files parse (an empty parity-equal
list is legal).
Gate 6 — #879: serialize-credentials lint + store-model site baseline
(a) run_no_serialize_credentials() — syn-based, sharing Gate 2’s cfg-test-aware visitor,
over the workspace-member walker:
-
flags named fields of
Serialize-deriving structs AND enum variants (derives detected throughcfg_attr) whose field ident ORserde(rename)wire name contains one of:secret,password,token,api_key,auth_config,signing_key,client_secret,private_key,credential,bearer,verifier,nonce; -
pass conditions:
[serde(skip_serializing)]/[serde(skip)]on the field, or a// serialize-credential: <reason>marker on the line DIRECTLY above that field (per-field, never per-struct; association via proc-macro2 span lines). Stale markers = violation; -
scope stated honestly: direct named fields only —
flattenchains, manualimpl Serialize(one exists, a test helper), and non-usealiasing are recorded false-negatives; -
NO whole-crate test-lib exemption:
FileCacheEntry.access_token/FileCache.tokens(#1163 test infra) get per-field markers and stay visible in the inventory.
Day-one seed: the 11 censused structs (financial-contracts ImportWarning / ImportBlocker /
PreviewSweepResponse / ExecuteSweepRequest; intake-contracts SubmitReportRequest; craig-mq
ParkWrapper; craig-security DeadLetterAudit + IssueKeyResponse; craig-cli CachedToken;
craig-exchange PartnerIntentV1; craig-web WebSession) + craig-web PkceState (PKCE verifier
OIDC nonce in a signed cookie) + the two test-lib structs — re-censused at implementation
with the final vocabulary and walker; every new hit gets a marker with an honest reason or a
fix, never a silent skip.
(b) run_store_model_response_baseline() — a committed per-file site baseline
xtask/store-model-responses.toml mapping each services/*/src file to its count of
Json<…store::models::…> occurrences (including PageResponse<…> forms). Any file above its
baselined count (or any new file with count > 0) → violation; below → stale → --bless
rewrites DOWNWARD only. Same-file swaps are the recorded residue; documented as a coarse
textual metric (aliased imports / multiline generics under- or over-count). The baseline IS
the reproducible site definition (~150 sites at census). Zero-count end state is legal
(empty baseline).
Both halves wired in phase_code_security_lints.
Steps
Commit sequence (one branch; each lint commit carries its fixture tests, validate wiring, quality-gates.adoc section, and CHANGELOG entry — independently green):
-
docs(plans): prevention-gate lints wave plan (&63)— this page, nav-linked Active. -
chore(xtask): ban new crate-root arithmetic blankets (#867) -
chore(xtask): syn rebuild of the reqwest choke-point lint (#866) -
chore(xtask): workspace dep-inheritance lint + flips (#869) -
test(xtask): partner parse-prop coverage lint (#870) -
chore(deps): advisory-ignore audit gate (#891) -
chore(xtask): serialize-credentials lint + site baseline (#879) -
Close-out: CLAUDE.md &83-closure bullet, quality-gates.adoc catalog/overview stitches (named edits — the gate inventory and counts must match), plan step statuses updated.
The plan page stayed Active through !1475 (merged 558f8815, 2026-08-24); this archive move + the Plan Completion Audit ride the next main-touching MR per the recorded vehicle rule. Audit result (2026-08-24): plan↔code diff zero — every design deviation discovered during implementation (audit.toml deliberately unchanged; the cargo-audit-only CI edit; B2/B3a/B5 budget fixes; the marker block-stacking rule) was folded back into this page before merge. Post-merge records: closing comment per issue with impl + merge SHAs, epic &63 updated, branch deleted.
Files Touched
| File | Change |
|---|---|
|
Gate 1 extension; Gates 2-6 new lints + fixture tests + shared report helper |
|
|
|
New committed baselines (bidirectional, bless-down-only) |
root |
Inheritance flips + the |
|
Structured ignore records + review-by dates; the cargo-audit job’s |
~10 production files |
Per-field |
|
Gate sections + catalog stitches; per-issue entries; the deferred &83 bullet |
Verification
-
Fixture tests per gate: violation / pass / escape-hatch / empty-or-refless-reason / stale-baseline / universe-floor; Gate 4 additionally: comment-only co-occurrence does NOT pass, orphaned-uncompiled file does NOT pass.
-
Proptests: Gate 5 record parser, Gate 3 spelling-equivalence, Gate 2 cfg-predicate detector.
-
cargo xtask validateend-to-end green with plausible printed universe counts — then the negative proof per gate (synthetic violation → fail → revert; not committed). -
Full battery + reseed + contextless J1-J8 review before push (standing rules).
Documentation Updates
-
docs/modules/ROOT/pages/quality-gates.adoc— one section per gate + catalog stitches -
CHANGELOG.adoc— per-issue entries under== Unreleased -
.claude/CLAUDE.md— the deferred &83-closure bullet (close-out commit) -
docs/modules/ROOT/nav.adoc— this page under Plans › Active (Archive move is post-merge)