Quality Gates & Enforcement (CRAIG)
On this page
- DTO field-length validation (#486)
- Lint Policy (workspace
[lints]table)- Plan M Step 6 regression gate (#497 + #867) —
cargo xtask lints no-transitional-allows - Plan M tier-lift
#![allow]vocabulary cfg_attr(test, allow(X))+ crate-wide#![allow(X)]collision (Plan M lesson 2026-06-05)- Estimating surface for new workspace lint promotions
- Plan N Phase B pay-down sweep methodology (2026-06-06)
- Plan N (Quality-Gate Enforcement) — quality-budget marker comments (2026-06-06)
- Validate step numbering and lint carve-outs
- Plan P (Nursery Promotion Sweep) — workspace state additions (2026-06-06)
- Resuming pre-Plan-M crate authoring (Plan L Step 3 batch 4 lesson, 2026-06-06)
- Plan M Step 6 regression gate (#497 + #867) —
- Quality-budget enforcement gate (Plan N Step 9, #522)
- Function cohesion enforcement gate (Plan R Step 4, #543)
- Axis-coverage enforcement gate (Plan N Step 8, #523)
- SQL data-consistency invariant gate (#860)
- Field-encryption seed verification gate (epic &66 C6, ADR-048 §D6)
- Context-tier hygiene gates (#615)
- Tracing PII-field gate (#896)
- Serialize-credentials + store-model gates (#879)
- Advisory-ignore audit gate (#891)
- Partner parse-prop coverage gate (#870)
- Workspace dependency-inheritance gate (#869)
- Reqwest construction choke-point gate (#771 + #866)
- Route role coverage (#490)
- MQ topology choke-point gate (#1198)
- sops-policy gate (#1382, ADR-064)
- release-artifact fault-feature gate (#1502, ADR-067 §D4)
- Program-verification gate (C24, #1519, ADR-067 §D9)
- Known Agent Biases
These are CRAIG’s project-specific, mechanically-enforced quality gates — run by cargo xtask validate, cargo xtask quality-budgets, and cargo xtask lints. They cover the project’s DTO-validation rules, workspace lint policy, the nine quality budgets, the function-cohesion gate, context-tier hygiene, and route-role coverage. The universal Rust style rules (function size, newtype pattern, typed errors, no-panic, no-Value, etc.) live in the coding-conventions standard, with the terse agent digest in .claude/rules/coding-conventions.md.
DTO field-length validation (#486)
Every String / Option<String> / Vec<String> field on a deserialize-side DTO (request body, query string) carries a [garde(length(max = <CONST>))] cap plus a mirrored [schema(max_length = <literal>)] (or #[param(max_length = <literal>)] on IntoParams structs). The 13 canonical cap constants live in craig-validation:
| Constant | Cap | Category |
|---|---|---|
|
100 |
Person / org / court / admin-unit names |
|
32 |
E.164 + separators + extension |
|
64 |
UUIDs, slugs, hex SHA-256 digests |
|
1024 |
S3 / Garage object keys (spec ceiling) |
|
64 |
Ruleset-validated codes, semver+build-metadata |
|
32 |
String-typed status / sort fields (Plan D F-022 candidates) |
|
500 |
Query searches, single-line annotations |
|
2000 |
Bounded single-concept descriptions |
|
1000 |
Justifications, override reasons |
|
4000 |
Plaintext multi-paragraph narrative |
|
8000 |
Encrypted-at-rest narrative; absorbs base64 + AEAD overhead |
|
255 |
Encrypted-at-rest person-name PII envelope |
|
256 |
Encrypted-at-rest envelope; digit-shape via separate validator |
utoipa literal requirement. utoipa 5.4’s [schema(max_length = …)] and [param(max_length = …)] macros reject const-ident expressions — they require integer literals. The garde side stays canonical ([garde(length(max = NAME_MAX))]); the utoipa side mirrors the literal ([schema(max_length = 100)]). This dup is intentional and bounded; a future garde/utoipa cap-sync xtask lint will guard the pair.
Special-cases. Use the named constant unless concrete evidence (DB column ceiling, RFC reference, real-world data) demands a different cap. When deviating, use a literal and add a // #486 special-case: <reason> comment immediately above the attribute pair. CreateCourtOrderRequest.court_name / UpdateCourtOrderRequest.court_name use literal 200 (compound jurisdictional names) — established precedent.
Cousin-field uniformity. Fields with the same semantic role across cousin DTOs (admin_unit on Create/Update for cases / referrals / reports) MUST share one cap — NAME_MAX for admin_unit cousin fields. The MR 5 workflow caught one cousin asymmetry mid-sweep (CreateReportRequest.admin_unit was a survey miss); cousin reviews are part of every per-file sweep.
Handler wiring. Every handler accepting a Validate-derived DTO calls payload.validate()? (or query.validate()?) immediately after the Json/Query extractor and BEFORE the authz check + parent-resource existence checks. The From<garde::Report> for ApiError impl in craig-common::validation converts validation failures into RFC 9457 ApiError::Validation (HTTP 422) with a per-field errors extension. Note: garde 0.22’s Validate::validate(&self) takes NO context arg under Context = () — use body.validate()?, not body.validate(&())?.
Scope as of MR 1-6 (2026-05-29): all 72 String fields in craig-cases-contracts are length-capped. craig-exchange-contracts surveyed in MR 6 contains zero unbounded String DTOs (the crate’s surface is the ExchangeAdapter trait + ExchangeAdapterKind enum; no request/response DTOs). Inline service api/ DTOs (210 fields across 4 stateful services) + partner-* crates (147 fields) + craig-intake-sdk (52 fields) are tracked as a separate follow-up — they fall outside #486’s craig-*-contracts scope and warrant their own audit ticket.
Partner-crate scope (#1461, 2026-08-15): the cargo xtask lints dto-length walk derives its partner-crate list from a crates/craig-partner-* directory glob — every partner crate present AND every future one is lint-covered automatically (the earlier hardcoded list silently omitted four crates: cprs / stars / doe-slds, whose 41 unbounded fields were annotated when the glob closed the gap, and audit, which carries no DTO structs). A unit test pins the glob against the live crates/ listing, and an unreadable crates/ fails the lint loudly rather than passing it vacuously.
Lint Policy (workspace [lints] table)
Cargo.toml’s `[workspace.lints.clippy] table turns the §Style + §Errors rules above into mechanically-enforced compiler errors. Member crates inherit via [lints] workspace = true. Today’s set (post-Plan-M Tier 1 + Tier 2, 2026-06-05):
-
Pedantic + cargo + complexity (
deny):pedantic,cargo,cognitive_complexity,too_many_lines(threshold 40 viaclippy.toml) -
Panicking calls forbidden (
deny, Plan H F-030 sweep):unwrap_used,expect_used,panic,todo,unimplemented,unreachable,dbg_macro -
Wildcard match arms forbidden (
deny, post-Plan-H):wildcard_enum_match_arm— forces explicit match exhaustiveness -
Panic-surface (Plan M Step 7 / #498) (
deny):indexing_slicing,string_slice,unwrap_in_result -
Overflow-surface (Plan M Step 8 / #499) (
deny):arithmetic_side_effects— every+/-///on integer/Decimal/Duration types must usechecked_/saturating_*/wrapping_* -
IO-surface (Plan M Step 9 / #500) (
deny):print_stdout,print_stderr— library + service code routes throughtracing::* -
Struct hygiene (Plan M Step 10 / #501) (
deny):partial_pub_fields— either ALL fieldspubor NONE -
Docs required (
deny, Plan H Step 3 burn-down):missing_docs_in_private_items -
Silent-discard forbidden (
deny, Plan H F-051 sweep):let_underscore_must_use,ignored_unit_patterns -
Reasoned-allow required (
deny):allow_attributes_without_reason— every#[allow]must carryreason = "…" -
Priority=1 allowed (style-orthogonal):
module_name_repetitions,must_use_candidate,missing_errors_doc,missing_panics_doc,cargo_common_metadata,multiple_crate_versions(dep-graph artifact)
All deny at workspace level. Test-only suppression for the panicking-call + silent-discard + Plan M Step 7/8/9 lints is scoped via #![cfg_attr(test, allow(…))] on each lib/main root, leaving production code deny-enforced. xtask validate runs clippy with -D warnings for defense-in-depth.
Plan M Step 6 regression gate (#497 + #867) — cargo xtask lints no-transitional-allows
Wired at the cargo xtask validate no-transitional-allows gate. Bans clippy::pedantic and clippy::cargo (the lint groups) inside any crate-root ![allow(…)] at the conventional production crate roots: lib/main/src/bin/.rs under crates/, services/, tools/, plugins/*, plus xtask/src/main.rs (each pathspec a separate literal entry — git ls-files performs no brace expansion; manifest-defined custom target paths are recorded out of scope). git ls-files-scoped (untracked paused work transparent to gate). Per-fn [allow(clippy::pedantic)] is unaffected — different surface. Implementation: xtask/src/cmd/lints.rs::run_no_transitional_allows + no_transitional_allows_tests module.
#867 extensions (epic &63):
-
Arithmetic blankets: a crate-root
#![allow(clippy::arithmetic_side_effects)]masks every overflow site at once (J7’s default is per-sitechecked_*+?). NEW blankets are banned; the eight ratified blankets (financial ×2 — #499/#806→#630; xtask — #815 accept-as-is; cli ×2; test-lib — declared-permanent; seed ×2) are grandfathered inARITHMETIC_BLANKET_BASELINE, which is bidirectional: removing a blanket without deleting its entry fails as STALE, so the baseline only shrinks. Re-scoping the eight belongs to #630, never to ad-hoc edits. -
Suppression-equivalent channels: crate-root
![expect(…)]of a banned group, and![cfg_attr(PRED, allow|expect(…))]whose predicate is not PROVABLY test-gated (testrequired through anyall/anynesting —any(test, feature = "x")andnot(test)both count as production-reachable), are banned identically. -
Universe floor: each pathspec is enumerated separately; a glob that matched >0 files at seed time matching 0 fails loudly (
TRANSITIONAL_ALLOWS_NONEMPTY_GLOBS) — the scan surface can never collapse silently.
Plan M tier-lift #![allow] vocabulary
When a new workspace lint promotion fires across many sites, the established Plan M-era resolution patterns are:
-
Per-site
#[expect(clippy::<lint>, reason = "<invariant proof>")]— the default for shared/production crates when the lint marks a provably-safe operation. Reason cites the loop invariant, math bound, or shape guarantee. Example:craig-store/validation.rs&body[..size.min(256)]citessize = body.len(). -
Per-fn
#[allow(clippy::<lint>, reason = "§Style §Important-Function …")]— when a §Important-Function carve-out explains the wide-or-wrapping nature. Example:craig-auth/service_token.rs::currentfor fused fast-path/write-lock/refresh/grace-window. -
Crate-level
#![allow(clippy::<lint>, reason = "<crate role>")]— for tool crates whose role legitimizes the entire lint surface. Established vocabulary:-
BFF / web (craig-web):
format_push_string,if_not_else,indexing_slicingfor HTML/URL builders + serde_jsonIndex<&str>reads (Null-fallback safe). -
CLI orchestrator (craig-cli, xtask):
too_many_lines,cognitive_complexity,unnecessary_wraps,print_stdout,print_stderr,arithmetic_side_effectsfor CLI dispatch + stdout-as-UI + bounded report-gen math. -
xtask CLI long-tail:
needless_pass_by_value(clap Args),manual_let_else,single_match_else(error-path context),indexing_slicing,string_slice(cargo-metadata traversal). -
Tool crates (mock-server, craig-seed):
indexing_slicing,string_slice,arithmetic_side_effects,print_stdout,print_stderr— fixed-shape mock-data / datagen invariants, not in CRAIG’s production request path. -
Domain-specific service (craig-financial):
arithmetic_side_effectsfor Decimal currency math + chrono date math over payment periods (Decimal::MAX ~10^28 vs realistic foster-care amounts; safety story is formula audit + integration tests, not +/- suppression). -
Test-support permanent (craig-test-lib): panicking-call lints +
indexing_slicing,string_slice,unwrap_in_result,arithmetic_side_effects,return_self_not_must_use,unused_async,module_inception— assertion-mechanism crate; permanent per Plan H Step 4 §scope. -
Macro contracts: Askama
filter_fn(inline_always,unused_self,unnecessary_wraps); utoipa OpenApi derive (needless_for_each). -
Federal-spec wire shapes: HHS/OMB Statistical Policy Directive No. 15 multi-race →
struct_excessive_boolson EMPI race flags. -
PATCH-payload contracts:
option_optionon update DTOs (placement + security + financial). -
Helper-layer interface (craig-seed/sql.rs):
ref_option,trivially_copy_pass_by_refonsql_opt_*family.
-
cfg_attr(test, allow(X)) + crate-wide #![allow(X)] collision (Plan M lesson 2026-06-05)
When a tool crate adds crate-level ![allow(clippy::X)] AND retains ![cfg_attr(test, allow(clippy::X))] from a previous step, clippy emits duplicated_attribute warning in test mode (the crate-level allow already covers the test compile). Fix: remove the redundant lint from the cfg_attr block, leaving only test-only-needed lints there. Pattern surfaced in 4 crates during Step 7 sweep (mock-server, cli bin+lib, web bin, xtask) and again in Step 8 (financial bin+lib, xtask).
Estimating surface for new workspace lint promotions
Plan M’s experience: grep-based surface estimates consistently overcount. Plan body §Step 7 estimated ~928 indexing sites from raw grep; actual workspace-clippy emission after cfg_attr(test, allow(X)) test exemption was 119 production-target sites, collapsing further to ~28 after crate-level allows on tool crates (cli, xtask, seed, mock-server). Future tier estimates should grep-and-discount-by-crate-target: count only production-shaped crates first, then subtract tool-crate-allowable patterns.
Plan N Phase B pay-down sweep methodology (2026-06-06)
Phase D blocks on Phase B clear. All 7 OVER budgets must reach historical lock (B1≤8, B2≤49, B3a≤236, B3b≤31, B4≤142, B5≤175, B7≤194) before Step 9 flips the enforcement gate. Gate is report-only during Phase B; pay-down MRs land without the gate firing.
Per-MR cadence:
-
Run
cargo xtask quality-budgets --reportfor current actual. Note the delta to lock. -
Dispatch one Explore subagent to enumerate sites with file:line citations + classify each as (a) TYPE to existing pub DTO, (b) STRUCTURAL-VALUE marker, (c) PARTNER-EDGE-UNTYPED marker, (d) rewrite to drop the annotation (only B3a/B3b — see below).
-
Apply changes by batch. For marker insertion across many files, use the Python script pattern below.
-
Re-run
cargo xtask quality-budgets --reportto confirm the budget reaches lock. -
CHANGELOG entry notes the delta + the per-category breakdown.
Marker placement gotcha (Plan N Step 4 lesson): the comment-skip counter looks back exactly one non-empty line. If a serde_json::Value site has an existing doc comment immediately above it, the marker MUST go BETWEEN the doc comment and the Value line — NOT above the doc comment. Otherwise prev_nonempty is the doc comment, not the marker, and the skip doesn’t fire.
// Per-application discovery URL (sandbox-validated against Authentik 2026.2.2)
// STRUCTURAL-VALUE: OIDC provider response — polymorphic per RFC 8414/7517
let disc: serde_json::Value = client.get(...)...;
Reader-readable: the doc comment explains intent, the marker is the lint exemption.
Python script pattern for batch marker insertion (B3b sweep, !600). The script inserts the marker IMMEDIATELY above each Value line, regardless of what’s above. Skips use serde_json::Value; import statements (those are module-level, not call sites).
import re, pathlib
files_and_markers = {
"path/to/test.rs": "OIDC provider response — polymorphic per RFC 8414/7517",
# ...
}
VALUE_RE = re.compile(r"serde_json\s*::\s*Value")
MARKER_RE = re.compile(r"//\s*STRUCTURAL-VALUE:")
USE_RE = re.compile(r"^\s*use\s+serde_json::Value")
for fpath, reason in files_and_markers.items():
p = pathlib.Path(fpath)
lines = p.read_text().splitlines(keepends=True)
out = []
for line in lines:
if VALUE_RE.search(line) and not USE_RE.match(line) and not MARKER_RE.search(line):
indent = re.match(r"^(\s*)", line).group(1)
out.append(f"{indent}// STRUCTURAL-VALUE: {reason}\n")
out.append(line)
p.write_text("".join(out))
Always cargo fmt --all after script-driven edits — the inserter doesn’t preserve all rustfmt subtleties.
Marker reason vocabulary (cite ONE of these architectural categories):
-
OIDC — "OIDC provider response — polymorphic per RFC 8414/7517"
-
JDM/zen-engine — "JDM ruleset content — polymorphic per zen-engine spec"
-
Postgres EXPLAIN — "Postgres EXPLAIN plan — polymorphic by design"
-
RabbitMQ envelopes — "RabbitMQ event envelope — polymorphic by design"
-
Cloudflare Turnstile / external partner shapes — "Turnstile sidecar response — external partner shape" / "external partner — Plan L F-064 typed-schema deferred"
-
Encryption envelopes — "encryption envelope
{\"v\": \"<ct>\"}— Plan B F-001 shape" -
CLI plumbing — "CLI input — operator-provided arbitrary JSON"
B3a/B3b TRIVIAL rewrites (Plan N Step 4 lesson — typed_dto_pilot.rs). For let _: serde_json::Value = serde_json::to_value(&x).unwrap(); round-trip assertions, rewrite to serde_json::to_value(&x).expect("<DTO> serializes to JSON");. Drops the binding, drops the annotation, adds explicit error context. The annotation was load-bearing for the round-trip — .expect() preserves intent without triggering the counter.
B7 TYPE strategy (Plan N Step 3 lesson — type to pub-re-exported DTOs). Test-client methods returning Result<ApiResponse<serde_json::Value>> should be typed against an EXISTING pub DTO from a *-contracts crate or craig-intake-sdk. Don’t introduce new contract-crate types as part of the sweep — that’s its own design work. If no pub DTO exists, defer.
Phase B remaining order (after Steps 3–4):
| Step | Budget | Current | Lock | Delta | Strategy |
|---|---|---|---|---|---|
5 (#518) |
B5 unwrap_or_default |
222 |
175 |
47 |
|
6 (#519) |
B4 #[allow] |
332 |
142 |
190 |
2–3 MRs by crate batch; audit each per-item allow for staleness vs. legit |
7 (#520) |
B3a Value src |
452 |
236 |
216 |
3–4 MRs by service batch; |
(B1) |
route LOC |
11 |
8 |
3 |
Incidental — decompose 3 specific route modules during another step’s sweep |
(B2) |
function LOC |
56 |
49 |
7 |
Incidental — decompose 7 specific functions during another step’s sweep |
Plan N (Quality-Gate Enforcement) — quality-budget marker comments (2026-06-06)
Plan N Step 2 (#515) introduced three comment markers that the cargo xtask quality-budgets counters honor as documented exemptions. The marker MUST sit on the immediately-preceding non-empty source line (one-line lookback; empty lines are tolerated). Marker above an enclosing attribute is NOT honored — the marker goes immediately above the call/use site:
// PARTNER-EDGE-UNTYPED: #441
let _: serde_json::Value = ...; // skipped from B3a
#[allow(unused)]
// PARTNER-EDGE-UNTYPED: #441
let _: serde_json::Value = ...; // NOT skipped — marker is above the attribute, not the use
// SILENT-OK: <reason> (B5 unwrap_or_default counter). Use when empty-on-failure is a diagnostic fallback and the empty case is acceptable. Canonical example: extracting an error body for a BadStatus error where the status code already carries the failure signal — an unreadable body shouldn’t mask the diagnosis. Established in crates/craig-partner-*/src/adapter.rs body-extraction sites.
// PARTNER-EDGE-UNTYPED: <ticket> (B3a/B3b serde_json::Value counter). Use for serde_json::Value in partner-edge code awaiting Plan L F-064 typed-schema migration. The ticket reference (#441 for Plan L) makes the exemption time-bound and trackable.
// STRUCTURAL-VALUE: <reason> (B3a/B3b serde_json::Value counter). Use for serde_json::Value that’s structurally untyped by design and cannot be replaced with a typed DTO. Canonical sites: zen-engine rule inputs/outputs (JDM inputs are polymorphic per the rule engine’s contract), authz attributes, encryption envelopes ({"v": "<ct>"}), CLI plumbing where the user provides arbitrary JSON.
-- floor-exempt: <reason> (SQL, the #1307 / ADR-063 D3 destructive-migration floor lint — the one BLOCKING class inside validate-migration-constraints; the FK / NUMERIC-CHECK / session-SET trio stays advisory). A destructive statement (DROP TABLE / DROP COLUMN / ALTER TABLE … DROP <col>) in a migration newer than the schema_compat_floor baseline must bump UPDATE schema_compat_floor SET min_required_version = <own version>; in the same file — that is what makes verify-only boot refuse a pre-drop binary. Waive ONLY when no released binary reads the dropped structure (born-and-dropped within one release train — bumping there would foreclose rollback for nothing). DROP CONSTRAINT / DROP DEFAULT are out of scope by decision: removing them widens acceptance; no old binary’s reads break.
Plan N Phase B sweep cadence: each pay-down MR runs cargo xtask quality-budgets --report and notes the delta in CHANGELOG. The gate flips to --fail-on-regression at Plan N Step 9 (the validate quality-budgets gate + CI ci-tests) only after all 7 OVER budgets clear (B1/B2/B3a/B3b/B4/B5/B7 ≤ historical lock).
Validate step numbering and lint carve-outs
cargo xtask validate auto-numbers its steps [N] at runtime via a StepCounter (xtask/src/cmd/validate.rs) — there is no fixed denominator, because the step set varies with --skip-devstack / --skip-docker (any denominator would be wrong for some invocation); the phase roadmap printed at the top of the run gives the coarse overview instead. Refer to a gate by what it does, never by a step number — the numbers renumber whenever a step is added, removed, or skipped. New workspace clippy lints fire via the existing clippy step and need no new validate step; only behavior-distinct gates (custom subcommands like no-transitional-allows, route-role-coverage, quality-budgets) are their own steps.
Static regex expect() exception: Regex::new(r"…").expect("static regex") is the one acceptable expect_used carve-out. Failure indicates a programmer typo in the pattern, not a runtime concern. Use #[allow(clippy::expect_used, reason = "static regex; failure is a programmer bug")] at the call site. Prefer LazyLock<Regex> for hot paths.
When a rule conflicts with planned work, follow the "When you can’t comply" protocol in the coding-conventions standard — alert BEFORE writing the violating code; justify the carve-out with a subagent-verified #[allow(…, reason = "…")].
Plan P (Nursery Promotion Sweep) — workspace state additions (2026-06-06)
Plan P (plans/archive/nursery-promotion-sweep.adoc) closed by flipping 12 nursery lints from allow → deny + ADR-031 documents the triage pattern. The 4 workspace allows + 2 crate-level allows that remain are architectural carve-outs, not deferred work:
-
missing_const_for_fnallow — workspace grep confirmed ZERO callers use these fns inconstcontext. Const-promotion is cosmetic; adds maintenance friction without enabling any real const-context use. Decision is permanent, NOT pending future revisit. -
option_if_let_elseallow — 3 per-fn allows on tools/craig-mock-server/{doe,empi,ies}.rs handler match arms (match wins readability for multi-lineResponse-building bodies) + 1 nursery false-positive oncraig-common::pagination::PageResponse<T>where the lint fires on theVec<T>type-parameter span. Struct- and field-level#[allow]attributes do NOT suppress the lint (the diagnostic span resolves inside a derive-macro expansion). Permanent. -
redundant_pub_crateallow — Plan D F-023 (#470) migrated 1,065 items topub(crate)as a deliberate intra-crate API marker. The lint’spubsuggestion would defeat the visibility-marker intent (defense against accidental cross-crate API promotion during refactors). Permanent. -
future_not_sendallow — 140+ emissions across 5 architectural site classes, ALL intentionally!Sendby design:-
services/craig-cli/src/clients/**(33) —&Transportacross awaits; CLI is single-shot one-binary. -
crates/craig-test-lib/src/harness.rs(98) —&selfmethods yielding borrow futures; tests run single-threaded. -
crates/craig-test-lib/src/client.rs(4) — same pattern. -
crates/craig-authz/src/eval_thread.rs(1) — JDM evaluator is!Sendper zen-engine + Rhai engine constraints (ADR-024 § Architectural Constraints dedicates a!Sendeval thread). -
services/craig-web/src/routes/mod.rs(3) — BFF route helpers hold&AppStateacross awaits;AppStatealready wraps shared state inArc.Refactoring 140 sites to
Arc<RwLock<T>>would lose borrow-check guarantees for negative perf value. Permanent.
-
-
too_long_first_doc_paragraphcrate-level allows in craig-auth + craig-api — clippy nursery emits this lint with no source span when the trigger is inside a derive-macro expansion (thiserror::Error / utoipa::OpenApi / utoipa::ToSchema / sqlx). All authored doc paragraphs were swept; the macro-expansion sites cannot be locally suppressed via attribute on the item. Crate-level allow scopes the impact.
Macro-expansion lint false-positive pattern (Plan P lesson) — when a nursery lint fires with no source span (or a span pointing at a type-parameter or struct field that has no expression), it’s emitting from inside a derive-macro expansion. Local [allow] on the offending item OR field WILL NOT suppress it. Two escape hatches: (1) crate-level ![allow] block at the lib root (scopes impact to one crate), (2) workspace allow with documented rationale (when the false-positive is widespread). Plan P encountered this on option_if_let_else (PageResponse field — workspace allow retained) and too_long_first_doc_paragraph (craig-auth + craig-api — crate-level allow).
Resuming pre-Plan-M crate authoring (Plan L Step 3 batch 4 lesson, 2026-06-06)
When resuming feature work authored before Plan M closed (i.e., the branch still carries the Plan H Step 2 transitional 6-lint #![allow(clippy::pedantic | clippy::cargo | …)] block at the lib root), the Plan M Step 6 no-transitional-allows regression gate (xtask validate) will reject the push. Fix the lib root before pushing:
-
Strip
#![forbid(unsafe_code)]if present (workspaceunsafe_code = "deny"from Plan M Step 11 covers it). -
Lift the Plan H Step 2 transitional allow block (the 6-lint
#![allow(clippy::pedantic, clippy::cargo, clippy::missing_docs_in_private_items, clippy::too_many_lines, clippy::cognitive_complexity, clippy::ignored_unit_patterns)]). -
Add the Plan M Tier 1d
cfg_attr(test, allow(…))vocabulary blocks: Plan M Step 7 (indexing_slicing+string_slice+unwrap_in_result), Step 8 (arithmetic_side_effects), Step 9 (print_stdout+print_stderr). Reference the existing Plan M Tier 1d lift MRs (!536–!567) for the exact reason-string vocabulary.
Production code from pre-Plan-M crates typically passes the post-lift workspace clippy check without further changes (Plan M’s clean-up sweeps were per-emission-driven; new crates authored to the Plan H discipline rarely had problematic patterns). Integration test files at tests/*.rs keep the old test-allow vocabulary because the no-transitional-allows gate only checks lib + main roots.
Quality-budget enforcement gate (Plan N Step 9, #522)
Nine code-quality budgets (B1–B8, with B3 split into B3a/B3b) are tracked by cargo xtask quality-budgets, locked at xtask/quality-budgets.lock, and enforced as a blocking gate at the xtask validate quality-budgets step (pre-push battery) + .gitlab-ci.yml ci-tests job. A budget regression — actual > ceiling, where ceiling is the lock when set (else the threshold; see § Semantic note) — blocks merge.
B8 — duplicate-block budget (copy-paste DRY class, 889). Counts near-duplicate impl/fn bodies in production /src/: each non-test body ≥ 40 normalized tokens is reduced to a structural signature (identifiers + literals erased; control-flow keywords, punctuation, and nesting kept), and the metric is the redundant-copy count — for each group of ≥ 2 identical signatures, group_size − 1. Coarse by design: it caps the *size of the copy-paste class, it does not localize it. Seeded at the current count and shrink-only — a B8 regression is fixed by a real refactor (dedup into a shared helper), never** an autonomous lock-raise. It creates downward pressure on the partner-adapter / CLI-wrapper / registry families the 2026-06-28 prevention-gap analysis flagged. Test modules ([cfg(test)]) and tests/ dirs are excluded so volatile test boilerplate can’t flap a merge-blocking gate.
B6 — redundant dep installs is host-independent (#991, 2026-08-04). The count is parsed from Cargo.lock — total entries minus distinct names (Σ per-name N−1) — NOT from cargo tree, whose default --target filter is the host triple and made the count OS-dependent (a Windows dev box counted the windows- stack a Linux host never saw, tripping the blocking gate on a clean main; ADR-007 names Windows a supported dev platform). The metric definition is therefore *all-targets: the lockfile’s resolved set for every platform, identical text on every host. The re-seed moved the lock 65→59 — lower, because the old tree-output scrape double-counted repeated (name, version) lines across tree sections, so the switch is also a correctness fix for the stated N−1 semantic. A fixture-lockfile unit test pins the parse (b6_counts_redundant_versions_from_a_fixed_lockfile), so host sensitivity cannot silently return.
|
B4 88→89 + B5 10→11 lock-raise (2026-06-18, template resync). The +1 on each came from adopting the claude-quickstart engine source byte-verbatim: |
When the gate fires
The validate step prints the budget table, then bails with the lock-raise instructions. The OVER row identifies the offending budget. Typical responses:
-
Fix the regression (default). Drop the new violation back below the locked floor. For most budgets the answer is mechanical: extract a helper to bring B2 back under 100 LOC, replace
serde_json::Valuewith a typed DTO for B3a, swapunwrap_or_default()for explicit error handling for B5, etc. Marker comments (// SILENT-OK,// PARTNER-EDGE-UNTYPED,// STRUCTURAL-VALUE) documented in thextask/src/cmd/quality_budgets.rsmodule doc exist for legitimate exemptions per budget (the marker must sit on the immediately-preceding non-empty line of the call site).
|
B3a scope is path-based, not attribute-based. B3a counts the literal |
-
Raise the lock (only with rationale). When the regression is intentional and approved — e.g. shipping a deliberately wide one-screen orchestrator that fits the §Style "decompose unless decomposition makes the codebase worse" carve-out — ratchet the lock UP via:
cargo xtask quality-budgets --write-lockThe MR description MUST document (a) which budget changed, (b) why the regression is structurally correct, (c) why the §Style carve-out applies. Without that rationale, reviewers reject the lock-raise; the gate is a tripwire, not a rubber stamp.
Lock-DOWN ratchets
--write-lock always writes current actuals, so it ratchets DOWN automatically when budgets clear. Strict-no-grandfather is the workflow norm: ship the cleanup in a dedicated MR, then run --write-lock to tighten the floor. Lock files commit alongside the cleanup. See Plan Q Step 10 (!620 / 7e701d73) for an example.
Semantic note
BudgetReport::ceiling() at xtask/src/cmd/quality_budgets.rs:90 uses the lock as authoritative whenever set: if locked > 0 { locked } else { threshold }. The pre-Plan-N max(threshold, locked) formula silently masked B1 (8→11) + B2 (49→56) because their thresholds (500/100) shadowed their locks. Plan N Step 2 (!604 / #515) was the fix.
Function cohesion enforcement gate (Plan R Step 4, #543)
cargo xtask lints fn-name-and is enforced as a blocking gate at xtask validate step fn-name-and (pre-push battery) + .gitlab-ci.yml ci-tests job. The lint scans every production fn name in crates/, services/, tools/, xtask/ (excluding tests/ directories and #[cfg(test)] modules, plus target/) for and outside the operand-pattern allowlist (and_or / _and_one / _and_two / _and_three / _and_n / _and_back / _and_friends). Drift in either direction blocks merge: new violations not on the opt-out list, OR stale opt-out entries whose fn has been renamed/decomposed/removed.
This gate is a proxy, not a cohesion proof. It catches only the and naming tell — it cannot catch a single-noun-but-multi-purpose function, e.g. a process_record that secretly validates, persists, AND emits an event. A green gate does not establish that a function does one thing; that remains a human-review concern (the J6 cohesion question in the pre-commit protocol), with the workspace clippy::cognitive_complexity = "deny" lint as a partial multi-branch backstop.
When the gate fires
The lint prints the offending entries + remediation instructions. Two paths forward:
-
Rename or decompose (default). The "no and" rule says a function should do one thing; the
andin the name admits two responsibilities. Typical fixes:-
Split into two single-purpose helpers; the caller composes (e.g.
parse_and_validate_X→parse_X+validate_X). -
Rename to a single-verb concept that captures the shared abstraction (e.g.
log_and_generic_500→emit_internal_500). -
For 2-tx orchestrators, rename to a single noun-phrase that captures the orchestration ("pipeline", "cascade", "round-trip").
-
-
Add to opt-out with rationale. Plan R only when decomposition makes the code WORSE — Plan Q’s §Style "decompose unless decomposition makes the codebase worse" doctrine applies. Append the
path::fn_nametoxtask/fn-name-and-opt-out.txtPRECEDED by a# rationale:comment line citing the specific §Style carve-out:# rationale: §Style §Important-Function — fuses (...) so the (...) control flow is reviewable on one screen; splitting fragments (...). crates/foo/src/bar.rs::do_X_and_Y
The MR description must reference the carve-out. Without the inline rationale + MR justification, reviewers reject the opt-out addition. Re-bless with
cargo xtask lints fn-name-and --blessAFTER hand-editing — the bless preserves your# rationale:lines.
Stale opt-out (the inverse failure)
When a fn on the opt-out list is renamed, decomposed, or deleted, the entry becomes stale. The lint fails on this too — it’s the same monotonic-shrinkage discipline as the axis-coverage gate (see § Axis-coverage enforcement gate). Remove the obsolete entry from the opt-out file (or re-bless if a sweep cleared multiple entries at once).
Existing rationale categories (Plan R Step 3 baseline)
The 11 surviving baseline entries document the three legitimate carve-out categories:
-
§Style §Important-Function — fns with existing
#[expect(clippy::cognitive_complexity)]rationales that defend a single converge point (uniform-401 boundary, response-buffering ownership thread, two-phase compensation tx). -
Lint-internal naming — helpers OWNED by a lint whose name reflects what the lint does; renaming hides the lint’s domain.
-
Bootstrap with one-screen narrative arc (Plan Q §Style carve-out) — phase orchestrators that run sequential gates in execution order; the
andlists the gates in run-order; splitting fragments a phase boundary.
New entries that don’t fit one of these three categories require a NEW rationale category articulated in the MR description.
Axis-coverage enforcement gate (Plan N Step 8, #523)
cargo xtask axis-coverage is enforced as a blocking gate at xtask validate step axis-coverage (pre-push battery) + .gitlab-ci.yml ci-tests job — one of the blocking code-quality gates alongside quality-budgets, fn-name-and, and the boundary lints (route-role-coverage, no-transitional-allows, mq-topology, …; each has its own section on this page). It guarantees every integration test declares which behavioral axis it exercises, so the test corpus can’t silently accrete happy-path-only cases.
Every [test] / [tokio::test] in an integration file must be tagged with a // @axis: <X> comment on one of the lines immediately above the test attribute (or end its fn name in _happy / _sad / _evil), where <X> is exactly one of six tokens:
-
happy— the nominal success path. -
sad— an expected-failure / rejected-input path. -
evil— an adversarial / malicious-input path. -
conc— a concurrency / race path. -
replay— an idempotency / at-least-once / exactly-once replay path. -
fault— an injected-fault / recovery path.
chaos is not a valid token. A test under one of the legacy directory conventions (tests/{concurrency,recovery,fault,security}/) inherits its axis from the directory and needs no inline tag.
Scope: integration tests only
The lint scans .rs files whose path contains /tests/ (or starts with tests/) — the integration-test tree. It does not scan src/ [cfg(test)] unit-test modules, so a // @axis: comment inside a src/ inline test module is decorative and enforces nothing. (This is the mirror of the fn-name-and gate, which excludes /tests/ dirs and [cfg(test)] modules — the two gates cover disjoint test surfaces, so neither one covers unit tests in src/, and neither one lets a and-named integration test slip past cohesion review. Do not assume fn-name-and lints test-function names; it does not.)
Monotonic-shrinkage opt-out
Genuinely-uncategorizable untagged tests live on the opt-out file xtask/axis-coverage-opt-out.txt (one <relative_path>::<test_fn_name> per line). The list ratchets DOWN: the gate fails on drift in both directions — a new untagged test that isn’t on the list, OR a stale entry whose test has since been tagged, renamed, or removed. Bless the current untagged set with cargo xtask axis-coverage --bless after a tagging sweep; hand-adding an entry (with justification in the MR) is the exception, not the default. Default response to a firing gate is to tag the cited test, not to grow the opt-out.
SQL data-consistency invariant gate (#860)
cargo xtask invariants runs the zero-rows SQL catalog under
crates/craig-test-lib/sql/invariants/<service>/.sql (each file is a SELECT that MUST return zero rows;
non-zero rows are a violation) against the seeded + test-exercised devstack DBs. Since #860 it is *wired into
the pre-push battery at the xtask validate invariants step (invariants::run_gate), immediately after the
integration suite has exercised the DBs. CI is force-merged, so this blocking gate lives in pre-push, not CI.
Curated blocking subset. Only invariant NAMES in BLOCKING_INVARIANTS (in xtask/src/cmd/invariants.rs)
fail the gate; every other catalogued invariant is report-only (printed, non-failing). The blocking set holds
only invariants that are green by construction against a fresh devstack:
-
ffp_amount_stale— GENERATED-column-enforced (#861). -
occupancy_mismatch,occupancy_over_capacity— trigger- + CHECK-enforced (#761/#780). -
day_count_gross_consistent— write-time consistency the seed + handlers satisfy (#760/#773).
Report-only invariants include grace-window/timing checks (retried_never_reterminal,
stuck_pending_transactions) and invariants tracking still-open fixes (validated_with_errors → #782,
null_rule_output → #785) — a known-open defect must not red-gate the battery. Promote a report-only invariant
into BLOCKING_INVARIANTS only once its fix has landed and it is green against a fresh devstack. Preview the
gate outcome manually with cargo xtask invariants --gate; a full non-gated report is cargo xtask invariants.
The financial↔placement cross-DB "void every payment on an ended placement" invariant (#776) is not
expressible in this single-DB sweeper and is covered by integration tests instead.
Field-encryption seed verification gate (epic &66 C6, ADR-048 §D6)
cargo xtask verify-seed --expect {keyless,keyed} verifies the devstack seed’s field-encryption state
against the mode the caller asserts (never auto-detected — an auto gate would pass a silent key-mount
failure vacuously). It re-checks the craig-seed one-shot completed (a state-aware poll, not the old
exit-code-only read that misclassified a still-running container; when the exited container has been
pruned — routine docker container prune housekeeping — the durable _seed_marker sql-phase row the
seed transaction committed stands in as the completion evidence, #1420) and asserts, on the raw craig_cases
DB, that every covered column and crypto_key_lineage.kcv matches the expected mode. It is wired into
the pre-push battery at the xtask validate devstack phase (--expect keyed, before the mutating
integration suite) and, non-blocking, into the .gitlab-ci.yml pentest job (allow_failure,
main/schedule). Both call sites assert --expect keyed since C7 (#987) activated devstack encryption;
the CI side receives the key from the repo-committed encrypted store, decrypted by
devstack/ci/write-field-key.sh with the masked+protected CRAIG_CI_AGE_KEY identity in the pinned
devtools container (ADR-048 §D5 as amended by ADR-064 U7, #1385 — an unset variable is a hard error,
never a keyless CI devstack). Run manually against a running devstack with
cargo xtask verify-seed --expect keyed
(add CRAIG_CASES__DATABASE_URL=… to skip port reconciliation).
Context-tier hygiene gates (#615)
Agent context lives in tiers, each pointing DOWN to the next when it doesn’t answer a question:
-
memory (
~/.claude/projects/<project>/memory/) — ONLY machine/user-specific facts for agents on this box (preferences, decisions, shell/path/devstack quirks) + a SHORT active-work pickup pointer. -
.claude/rules/+docs/modules/standards/— the operating rules: terse, always-on agent digests (.claude/rules/, synced from the claude-quickstart template) backed by the canonical full-prose standards (thestandardsmodule, also synced). Drift-gated bycargo xtask check-docs. -
Antora (
docs/modules/ROOT/) — all project knowledge (architecture, ADRs, plans, service/data-model/crate catalogs) + this project’s convention complements (this page, Testing Reference (CRAIG)). The canonical home; one fact, one home.
The failure this guards against (the #613 right-sizing): a doc re-grows into a catalog that duplicates Antora and drifts stale. The gates target the structure that lets facts go stale, not the facts themselves (undecidable):
-
doc-pointer integrity (
cargo xtask lints doc-pointer-integrity, blocking). Every markdown link AND every repo-rooted path pointer (e.g.docs/modules/ROOT/pages/services.adoc,.claude/rules/foo.md) in.claude/CLAUDE.mdmust resolve to a live file. CLAUDE.md is the agent index — a pointer map into Antora +.claude/— so this catches pointer-rot (a pointer at a moved/archived page). Scope is CLAUDE.md only; glob/template/module/URL tokens + non-rooted bare filenames are illustrative and skipped, and prose docs (this file, Testing Reference (CRAIG), …) are not scanned. -
cargo xtask audit-memory(advisory, NOT a gate). Memory is machine-local + outside the repo, so it can’t be CI-gated. This on-demand report flags an oversizedMEMORY.md, dated session-snapshot files (delete after the work lands — history lives in git/Antora), orphans, and the largest files.--strictexits non-zero on a breach for an optional local hook.
The doc-pointer-integrity gate keeps CLAUDE.md’s pointers valid so the canonical detail stays in Antora (near the source it describes) where it can’t rot in a second place. (Before the 2026-06 template resync this tier also held .claude/docs/ thin-index docs guarded by a B8 agent-doc-index size budget; that layer was retired when conventions moved to .claude/rules/ + the standards module and the project docs moved into Antora ROOT.) See the delivery-protocol standard § Context Hygiene for the upstream policy.
Tracing PII-field gate (#896)
Wired at the cargo xtask validate tracing-pii-fields step (blocking, phase_code_security_lints). A single tracing field interpolating a child’s name/DOB/address/SSN/phone/email is a reportable PII incident for a CCWIS. The 2026-08-24 three-agent sweep (cases 26 sites / financial+placement 61 / intake 38 + fleet census 556) found ONE real finding — intake’s redact_upstream_error logging the full raw upstream body at WARN (SHINES/cases 4xx bodies can echo submitted SSNs/names) — plus two contingent raw-value echoes in the NCANDS export warns; all fixed with shape-only logging (length + SHA-256 prefix). This gate keeps the fleet clean:
-
Surface: every
trace!/debug!/info!/warn!/error!/event!AND*_span!invocation (bare,tracing::- orlog::-prefixed) over the workspace-member src trees, PLUS#[instrument(fields(…))]on free fns, impl methods, and trait methods. Field forms covered:ident = …, bare-ident captures,%/?shorthand (leaf-matched), dotted explicit names (person.name = …), and string-literal names. Syn token-stream parsing, never line-based — ~46% of fleet macro sites are multi-line. Recorded out of scope: message-position interpolation (format text is free prose; the field-ident surface is the machine-checkable one). -
Rule: FIELD IDENTS (and
%/?shorthand captures, matched by their final path segment) against the exact denylistname | first_name | last_name | dob | date_of_birth | address | ssn | phone | email | narrative | rationale | body. Value bindings are deliberately out of scope —rule_set = %nameis the CORRECT pattern (the census proved every%namebinding sits under a safe ident). -
Escape:
// pii-field-ok: <reason>on the line above the macro (one seed: reporting’s S2S problem-details body log, whose PII-safety rationale predates the gate); empty reasons and stale markers fail. -
Universe floor: 558 sites at seed (the lint’s own walker — spans + instrument included); below 450 the macro detection is presumed broken and the gate fails loudly.
Implementation: xtask/src/cmd/lints.rs::run_tracing_pii_fields + tracing_pii_tests.
Serialize-credentials + store-model gates (#879)
Two gates wired at cargo xtask validate (blocking, phase_code_security_lints):
Serialize-credentials (cargo xtask lints serialize-credentials) — a heuristic tripwire over the wire-name surface, and its doc says so. Named fields of Serialize-deriving structs AND enum variants (derives seen through cfg_attr) whose ident or serde(rename) wire name contains secret | password | token | api_key | auth_config | signing_key | client_secret | private_key | credential | bearer | verifier | nonce must either #[serde(skip_serializing)] or carry a per-field // serialize-credential: <reason> marker on the line DIRECTLY above (per-field, never per-struct; stale markers fail; empty reasons fail). The 17 seed markers across 10 files are the auditable inventory of every place credential-shaped material intentionally serializes (sweep tokens, idempotency occurrence tokens, the CLI/test-lib token caches, the encrypted WebSession cookie, PkceState’s PKCE verifier + OIDC nonce, the one-time `IssueKeyResponse.api_key, the hash-only PartnerIntentV1.auth_config). Test-only items are excluded by cfg predicate; craig-test-lib is deliberately NOT path-exempt here. Recorded false-negatives: flatten chains, manual impl Serialize, non-use aliasing.
Store-model response baseline (cargo xtask lints store-model-baseline) — a COARSE TEXTUAL per-file ratchet over handlers returning store models directly: xtask/store-model-responses.toml maps each services/*/src file to its count of lines containing both Json< and store::models:: (48 files / 153 sites at seed). Any file above its count — or any new file above zero — fails and is NEVER blessable; below is stale and --bless rewrites DOWNWARD only. The claim is exactly "no per-file growth of the textual pattern": aliased imports and multiline generics under-/over-count, same-file swaps are the recorded residue, and the empty [counts] table is the desired end state.
Advisory-ignore audit gate (#891)
Wired at the cargo xtask validate advisory-ignore-audit step (blocking, phase_doc_lints; pure file parse, no subprocess). cargo-deny/cargo-audit (pre-push hook + CI) remain 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). The gate enforces the structural contract they cannot:
-
Ordered parity: the
deny.tomland.cargo/audit.tomlignore arrays compared as ordered lists (the recorded byte-for-byte convention), duplicates rejected,RUSTSEC-YYYY-NNNNshape checked. -
Per-ID structured records (deny.toml only — audit.toml’s header defers rationale there): each ID’s contiguous preceding comment block must carry
# crate:, a non-empty# rationale:,# tracked: NNNORretire: <condition>, and# review-by: YYYY-MM-DD. No shared blocks — a run of IDs after one block leaves the later IDs marker-less, which fails (paired advisories like the quick-xml pair each carry their own record).# lock-onlyis human documentation of why cargo-deny warns advisory-not-detected (rkyv/#1376); the gate attaches no semantics. -
Review-by expiry: today past the date is a HARD failure — every ignore is time-boxed, so staleness is a stop rather than a judgment nobody re-runs. RUSTSEC-2023-0071’s date rides with #764 (the open reachability re-audit; the gate never judges reachability).
The record parser is hand-rolled over the raw array region (TOML parsing discards comments) and proptested: totality over line soup, generated well-formed records accepted, any dropped marker rejected. CI hygiene shipped with the gate: the cargo-audit job’s rules:changes now watches deny.toml + .cargo/audit.toml, so a config-only MR re-runs the advisory scan. Implementation: xtask/src/cmd/lints.rs::run_advisory_ignore_audit + advisory_ignore_audit_tests.
Partner parse-prop coverage gate (#870)
Wired at the cargo xtask validate parse-prop-coverage step (blocking, phase_code_security_lints). Three layers, each closing a gaming vector the wave-1 review proved against weaker designs:
-
Census — every
crates/craig-partner-*workspace member must expose an adaptertype Inbound = X;(located by syn inside the impl;src/adapter.rsorsrc/adapter/mod.rs) or hold a named entry inPARSE_PROP_NON_ADAPTER_EXCEPTIONS(today: craig-partner-audit, thefrom_valuedispatch crate — no adapter file; its decode surface is censused by the #1575 layer below). Deleting an adapter cannot silently deschedule a crate; the 11-adapter seed floor catches census collapse; a listed crate that gains an adapter goes stale. -
Property presence — some test file must contain a
proptest!invocation whose TOKEN STREAM carries bothfrom_sliceandX. Tokens, not text: a doc comment naming both can never satisfy it, and deleting the property body removes the macro. (Presence-tier honesty: this proves a property exercises the decode, not its assertion semantics — the authz-coverage tier.) -
Compilation reachability — the file must be a top-level
tests/*.rstarget or reachable via amod x;/#[path]chain (caps' nestedtests/properties/layout is the seed case). Orphaned files Cargo never compiles do not count.
The #1575 second census layer covers the erased AUDIT-decode seam the adapter census misses: every partner crate’s src/lib.rs must expose a PUBLIC audit_payload_from_value free fn (dispatch crate: PartnerAuditEvent::decode_jsonb), each needing a compiled proptest! whose tokens call the fn BY NAME — the call sites are type-inferred (serde_json::from_value, no turbofish), so the signature-anchored fn name is the only reliable token. Seed floor 12 (11 per-partner fns + decode_jsonb); same reachability rule; the totality properties live in each crate’s tests/parse_never_panics.rs (arb-JSON documents; ssa-solq’s pre-existing property is string-shaped — same totality tier, same token).
Escape hatch: // allow-no-parse-prop: <reason> on the line above the anchoring declaration (the type Inbound, or the audit-decode fn signature) — the reason MUST cite a #NNN issue. The properties execute in the pre-push full battery — the sole functional-correctness gate; CI’s ci-tests job runs the --lib --bins subset, which never compiles tests/ targets, and its prose says so (#1577 corrected the claim; the selection deliberately stays). Implementation: xtask/src/cmd/lints.rs::run_parse_prop_coverage + parse_prop_coverage_tests.
Workspace dependency-inheritance gate (#869)
Wired at the cargo xtask validate workspace-dep-inheritance step (blocking, phase_doc_lints — pure TOML, no compile). Membership authority is the workspace itself: [workspace].members minus .exclude, expanded from the root manifest (never fixed directory lists), with a 66-member seed floor. Three checks over dependencies/dev-dependencies/build-dependencies and every [target.*] variant:
-
A dependency whose resolved package name (
package =rename honored; git deps compared by name; dotted/inline/expanded spellings normalized by the parser — proptested) exists in[workspace.dependencies]must inherit via{ workspace = true }(additive features stay). Exceptionable ONLY through the committedxtask/dep-inherit-exceptions.toml(mandatory reasons; bidirectional — a stale entry fails, so e.g. the craig-webrand 0.8entry retires itself when #1574 lands). -
[package]version/edition/licensemust each be PRESENT and.workspace = true— omission is a violation, not a pass.rust-versionis recorded out of scope (59/66 members omit it). -
default-features = trueanywhere, and a hardcoded twin of adefault-features = falseworkspace dep that drops thefalse, fail with NO exception channel.
Seed state: 66 members, ~1,210 dependency declarations, 0 violations, 3 exceptions (rules-client/xtask reqwest feature-subtraction, intake-sdk tokio leaf-slimming; the craig-web rand-divergence entry retired with the #1574 migration). Implementation: xtask/src/cmd/lints.rs::run_workspace_dep_inheritance + workspace_dep_inheritance_tests.
Reqwest construction choke-point gate (#771 + #866)
Wired at the cargo xtask validate reqwest-client-new step (blocking). #866 (epic &63) rebuilt the #771 line scanner as a syn visitor over the workspace-member source surface (membership from [workspace].members minus .exclude — tools/ and plugins/ included):
-
Ad-hoc clients banned outright:
reqwest::Client::new()/Client::default()(full-path,use-imported,as-aliased, or glob-imported) — hold the service-widecraig_common::build_shared_clientclient (ADR-014) instead. -
The builder path is machine-held:
Client::builder()/ClientBuilder::new()/ClientBuilder::default()reconcile againstREQWEST_BUILDER_ALLOWLIST— (file, expected count) pairs over the five sanctioned bespoke builders (shared builder itself, auth discovery/JWKS bootstrap, intake-sdk standalone transport, exchange-transport egress). A count above the entry is a fresh decision (new rationale + const bump); below or zero is a STALE entry that must bless down. A bare file list would be a wildcard for piggybacking; the count is the ratchet. -
Test exclusion is by cfg PREDICATE, not line position: items under
[cfg(all(test, feature = "…"))]are exempt;[cfg(any(test, feature = "…"))]is production-reachable and scanned; an early#[cfg(test)] mod x;declaration no longer truncates the scan (the retired scanner left ~1,900 production lines of intake’smain.rsunscanned). The predicate detector is proptested against an oracle over generatedall/any/notnesting. -
Recorded residues:
crates/craig-test-lib/stays path-exempt (test infrastructure; outside the production choke-point threat model);reqwest::blockingin xtask is out of scope (dev tooling); non-usetype aliasing (type C = reqwest::Client;) is the recorded false-negative — syn has no type information. The shipped db-error lint’s line scanner shared the old truncation blind spot until #1576 fixed it (externalmod tests;declarations no longer truncate; only inline trailing test modules stop that scan).
Implementation: xtask/src/cmd/lints.rs::{scan_reqwest_constructions, reconcile_builder_allowlist} + the reqwest_construction_tests module.
Route role coverage (#490)
Every .route(…) inside the protected_routes block in services/craig-web/src/main.rs MUST carry one of:
-
Explicit role gate —
.layer(axum::middleware::from_fn(middleware::require_*))inside the route’s handler expression. Use this when the route needs a higher tier thancaseworker+(e.g.require_admin_or_supervisorfor security/audit, NCANDS approve+transmit;require_admin_onlyfor partner administration). -
Default-tier carve-out — a
// caseworker+comment anywhere within the.route(…)span. Marks the route as intentionally inheriting the caseworker+ default that allprotected_routesget via the outerrequire_authlayer. Use this for read-only views, list pages, and write paths that any worker can use.
Without one of these markers, cargo xtask lints route-role-coverage (wired into xtask validate) bails the build. A new route added under protected_routes without an explicit gate or carve-out is a violation — the lint exists so the caseworker+ default cannot silently absorb a higher-privilege handler.
The carve-out marker may live on the .route( opening line OR on any line inside the call (rustfmt sometimes wraps the comment inside the parens — both placements are equivalent for the lint). For multi-line routes the canonical placement is on a line immediately below the .route( opener.
Background: this lint is the build-time guarantee that #476’s per-route role gates can’t be silently bypassed by a future contributor. See ADR-027 for the underlying role model.
MQ topology choke-point gate (#1198)
cargo xtask lints mq-topology is a blocking xtask validate step (epic &75 C4,
ADR-003 §Amendment #1198). Raw AMQP queue/exchange
declaration tokens are allowed only in crates/craig-mq/src/, crates/craig-test-lib/src/, or
test paths (tests/ at the repo root or any nested /tests/ directory) — a declaration
anywhere else would bypass the queue_args choke-point that pins every CRAIG queue
x-queue-type=classic, silently falling to the broker/vhost default queue type.
Honest scope: this is a crate-boundary convention gate. It does not prove in-boundary
declarations carry the right arguments — that guarantee is queue_args + its full-table
equality unit pins inside craig-mq. The scan is uniform-by-token (comments and string literals
count — the sibling lints' bluntness trade), enumerates tracked *.rs only, and its two
needles exist in xtask source only in split-literal form: an own-source unit test turns red if
a contiguous token ever lands in lints.rs. Recorded residuals: declarations made through the
RabbitMQ management HTTP API and non-Rust code are out of scope.
Remedy when it fires: declare queues through craig_mq::Subscriber (subscribe/
subscribe_exclusive/subscribe_dlq); exchanges are operator-owned topology (#1202) and are
never declared from service code; test-support declares belong in craig-test-lib
(declare_core_exchanges) or a tests/ path.
sops-policy gate (#1382, ADR-064)
Blocking xtask validate step (and in CI via ci-tests, whose path rules include .sops.yaml
+ secrets/** so a store-only rotation MR still runs it): the repo-committed encrypted secrets
store must stay structurally sound. Enforced: strict .sops.yaml grammar (exactly
creation_rules with compiling path_regex + canonical-bech32 age recipients, plus a
parser-differential tripwire — the lint’s YAML view and the store tooling’s own recipient
parser must agree or the gate fails closed); encrypted-only ciphertext (every leaf
ENC[AES256_GCM,…], diagnostics name keys only — never values); .sops.yaml↔ciphertext
recipient parity; a split-form age secret-key needle over both surfaces; and NO non-YAML
artifacts under secrets/ (the walk is filesystem-side, so untracked decrypt residue fails).
Armed from day one — a missing store is itself a violation (partial-revert guard).
Remedy when it fires: recipient changes go through cargo xtask secrets add-recipient /
remove-recipient, store edits through cargo xtask secrets edit; never hand-edit either
surface. Run cargo xtask lints sops-policy for the full list, cargo xtask secrets check
for the non-disclosing store verification.
release-artifact fault-feature gate (#1502, ADR-067 §D4)
Blocking xtask validate step (cargo xtask release-artifact-gate, folded into the pre-push
battery): the contested-environment fault-injection seams
(craig-mq/fault-injection, craig-crypto/fault-injection,
craig-store/test-util) add hook fields to real production structs but MUST stay out of every
release artifact. They ship default-off and activate ONLY through dev-dependencies (a crate’s own
integration tests self-dev-dep with the feature; cross-crate consumers dev-dep craig-test-lib
with its fault-injection forwarding feature). The gate walks cargo tree -e normal,build
(normal + build deps only — dev-deps excluded) over the Dockerfile’s release -p service set
(parsed from the cargo build line, the single source of truth) AND xtask, and fails if any
forbidden (crate, feature) pair is active. Honest claim
(ADR-067 §D4): "absent from release
artifacts and every normal-dependency graph, machine-enforced" — NOT "impossible" (the seams
compile under --all-features, by design).
Remedy when it fires: a normal (non-dev) dependency enabled a host crate’s fault feature — make
that edge a dev-dependency, or drop the feature from it. Run cargo xtask release-artifact-gate
for the offending (crate, feature) pairs.
Program-verification gate (C24, #1519, ADR-067 §D9)
Two blocking xtask validate steps that BRACKET the integration battery, plus the standalone
cargo xtask program-gate:
-
before nextest —
program_gate::resetpurges every executed-fault record (everytest-results/faultdirectory, discovered by walking the checkout) and writes the run stamptest-results/fault/.gate-stamp; -
after nextest —
program_gate::verifychecks that every record postdates the stamp, that no record file was unreadable (the aggregation counts read/parse failures), that every armed fault fired, that the run executed faults AT ALL, that every recording class fired, and that the program’s two self-tests still exist in the nextest inventory; then retainstest-results/fault/fault-report.jsonand consumes the stamp (single-use — only on success, and never from the standalone command).
The fault layer is REQUIRED for the same battery (fault-preflight --required), so a run cannot
go green with the fault sidecar absent. validate --skip-devstack never reaches either step (its
substitute battery is --lib --bins) and is the one sanctioned fault-optional mode.
Remedy when it fires:
-
"no run stamp" — this verification already passed once (the stamp is consumed on success), or the reset/verify pairing broke mid-run. Either way the accounting cannot be attributed to a run again; re-run the battery.
-
"record(s) predate this run’s reset" — two batteries' evidence is mixed, or the record filesystem’s clock lags this host’s. Delete the
test-results/faultdirectories and re-run. -
"record file(s) could not be read or parsed" — a test was killed mid-write. The missing record could be the armed-but-unfired fault the gate exists to catch; re-run rather than deleting it.
-
"armed but NEVER fired" — a real finding: the test armed an injection its codepath never reached, so it passed for the wrong reason. Fix the leg, not the gate.
-
"ZERO executed faults" — the fault-gated legs did not run (
--run-ignored allmissing, or a filter excluded them). -
"class(es) … reported ZERO this run" — a recording class lost its legs or its recorder.
-
"the gate’s self-test(s) are gone" — a rename updates
SELF_TESTSin the same diff; a delete retires a guarantee and needs saying out loud.
Honest limits, recorded in the module doc and the runbook rather than implied away: the per-class floor is enforced only for the classes that emit records today (#1546), the armed⇒fired check is only as sharp as the recorders that observe firing (#1547), and the self-test check pins EXISTENCE, not behavior.
Known Agent Biases
AI agents trend toward recommending older, heavily-documented tools over newer, better alternatives. When making architectural recommendations, always perform web research for current state of the art (see the delivery-protocol standard). Known stale defaults to watch for:
-
OpenSSL over rustls (rustls is mandated — see
deny.toml) -
Selenium/Cypress over Playwright (Playwright is mandated for E2E)
-
reqwestwithopenssl-sysoverreqwestwithrustls-tlsfeature -
chronooverjiffortime(evaluate current state before choosing) -
Heavyweight ORMs over lightweight query builders
-
Class-based or inheritance-heavy patterns over composition and traits
-
Older serialization formats over modern alternatives
-
Assuming library APIs from training data instead of reading current docs — methods get renamed, removed, or change signatures between major versions. Always verify against the actual version in Cargo.toml (see the
delivery-protocolstandard § Library usage) -
Using deprecated config file formats (e.g., cargo-deny v1 format when v2 is current)
-
Jumping to workarounds instead of diagnosing root causes — when something breaks, agents tend to try alternative approaches rather than reading the source to understand WHY it broke (see the
delivery-protocolstandard § Debugging) -
Defending wrong mental models against contradicting evidence — rationalizing instead of re-examining
-
Manufacturing borderline findings to make an audit step "feel productive" — an audit is a measurement, not a mandate. When the discovery honestly surfaces zero action items, the audit documentation itself (clean baseline + evidence the discipline holds) IS the deliverable; forcing a false positive ratchets discipline down
-
Leaving stale placeholders in a plan/close-out Status table — the author knows each step’s real MR + SHA but stays in working memory while the "this MR"/"TBD" cells accumulate. Dispatch a fresh Explore subagent that reads the FILED plan body cold at every archive/close-out MR; it reliably catches the gaps the primary agent’s working memory papers over
-
Charging into a
/loopor resumed prompt without verifying the work is still open — sessions accumulate stale queued prompts that may reference already-shipped work. Run a quick 2–3 command state check (git log for the touched file + nav entry + issue-tracker view) BEFORE doing the work; discovering the task is already done is a valid, correct outcome -
Trusting an audit subagent’s verdict TAG over its own body evidence — the tag and the evidence are generated in different passes and can disagree (a FAIL tag over a body that clearly demonstrates PASS). The body evidence is ground truth; re-read the cited
file:linebefore reacting, and don’t reflexively block on a tag mismatch -
Blindly implementing an aged backlog item from its original framing — old issues are frequently already resolved by later work, or rest on a premise the implemented design has since contradicted. Verify each against current
mainFIRST (grep/read the cited files, run the relevant test); closing-as-verified-resolved with a criterion-by-criterion comment is a valid deliverable, and a since-contradicted premise is a domain decision, not a blind implement -
Building a work master-list from all-state (or unverified) issue inventory — query OPEN-only state (not
--all), and before recommending any item sample its body for external-CR / other-team / gateway / stakeholder-filed signals; a top-ranked recommendation drawn from a closed or externally-owned issue is worse than none -
Recommending a new tool, dep, script, or pattern without first checking whether the project ALREADY has infrastructure for it — the reflex defaults to the lowest common denominator ("add an ad-hoc bash script", "pull in a new crate") instead of the established CRAIG pattern, and quietly builds parallel machinery beside an existing tool. Before proposing anything new, grep
cargo xtask --help, scantools/andtests/, checkCargo.lock/cargo tree, and skim the plans archive for prior work on the same need. If the existing infrastructure is genuinely wrong-shaped for the new use, say so explicitly with reasoning — never silently pretend it doesn’t exist
This list is a living document. When you catch an outdated recommendation, add it here.