Plan: Code Quality Remediation
On this page
|
Status: COMPLETE (2026-04-22). All 6 steps shipped via MRs !130 (Step 1), !131 (Step 2), !132 (Step 3), !133 (Step 4), !134 (Step 5), !135 (Step 6 audit). Issue #180 closed. See Archive for cross-reference. |
|
Plan refreshed 2026-04-21 (MR !129). Each of the 6 steps audited against current
Step bodies rewritten with accurate file:line references. Original plan text preserved under Original (historical) subheadings where scope changed. |
Status
Status column reflects audit at 2026-04-21.
| Step | Description | Status |
|---|---|---|
1 |
Replace |
Done (2026-04-21) — MR !130 — |
2 |
Replace |
Done (2026-04-21) — MR !131 — |
3 |
Replace silent error discards with |
Done (2026-04-21) — MR !132 — 4 subscriber.rs ack/nack sites + 2 update_last_used sites now log on failure via |
4 |
Type AFCARS |
Done (2026-04-22) — MR !133 — added |
5 |
Decompose functions exceeding 40-line limit |
Done (2026-04-22) — MR !134 — all five target functions decomposed and now well under the 40-line ceiling: |
6 |
Full validation pass |
Done (2026-04-22) — MR !135 — all gates green: |
Issues: #180
Related: #179 (bootstrap integration — Phase 1 residual from archived code-quality-review plan)
Branch: chore/code-quality-remediation
Context
Audit of CRAIG codebase against standardized code quality constraints revealed violations that need remediation before rebasing against claude-quickstart template conventions.
Five categories of issues were found: string-wrapped error variants that lose structured context, expect() calls in library code that can panic on production inputs, silent error discards (let _ =) that hide failures in critical paths, serde_json::Value usage where typed structs should exist, and functions exceeding 40 lines that are difficult to test and review.
Scope
In scope (refreshed 2026-04-21):
-
3 error types:
ApiError(full rewrite from tuple variants),StoreError(tuple → structured),IntakeError(named-field → addentitydiscriminator) -
2
reqwest::Client::builder().build().expect(…)sites (jwks.rs:50,intake-sdk/client.rs:59) — propagate viaResult. Sites originally listed as "15 expect()" reduced to 2 after filtering out doc examples, tests, infallible static-JSON serialization, and the OTEL-exporter startup path. -
11
let _ =patterns (4 critical subscriber ack/nack + 2 update_last_used + 1 store cleanup + 2 craig-api best-effort + 6 service-main held-for-lifetime — last 6 need only a comment, not tracing). -
3
serde_json::Valuefields incraig-intake-sdk/src/types.rs, plus 3 builder methods. -
5 functions over 40 lines — extract helpers.
Out of scope:
-
expect()in startup code (shutdown_signal, OTEL-exporter-build intelemetry.rs:86,103) — acceptable with adjacent// Panic: irrecoverable startup failurecomment -
expect()in test-lib,#[cfg(test)]modules, and docstring code blocks — acceptable as test-only / illustrative -
expect()on infallible static-JSON serialization (e.g.serde_json::to_string(&header).expect("…")whereheaderis a 2-key map literal) — the expect message is the assertion; conversion toResultwould add boilerplate without exposing a real failure mode. Rewriting this class of expect is a separate code-style decision, not a correctness fix. -
serde_json::ValueinEventEnvelope.payload— protocol-level generic; wildcard subscribers need the untyped form -
serde_json::Valuein JWS canonicalization (craig-signing) — protocol-level; signing contract shared across language SDKs -
craig-webroute handlers over 40 lines — Askama-heavy, extract less cleanly; tracked separately in route-cleanup work if it materializes
Design
Steps
Step 1: String-wrapped error variants
Files: crates/craig-common/src/error.rs, crates/craig-intake-sdk/src/error.rs, crates/craig-store/src/error.rs
-
ApiError: ReplaceNotFound(String)withNotFound { entity: &'static str, id: String },BadRequest(String)withBadRequest { detail: String, field: Option<&'static str> },Conflict(String)withConflict { entity: &'static str, detail: String },Internal(String)withInternal { detail: String } -
Update
IntoResponseimpl to constructdetailfrom structured fields -
Update all call sites across 8 services (grep for
ApiError::NotFound(, etc.) -
IntakeError: Same pattern — replace String detail with structured fields -
StoreError: Same pattern -
Run
cargo build --workspaceto find all broken call sites -
Fix each call site with the structured constructor
Step 2: expect() → Result propagation
Files (refreshed): crates/craig-auth/src/jwks.rs, crates/craig-intake-sdk/src/client.rs. Originally listed signing.rs/publisher.rs/telemetry.rs — audit reclassified those as acceptable (infallible static-JSON serialization and OTEL-exporter startup).
-
crates/craig-auth/src/jwks.rs:50—reqwest::Client::builder().build().expect("failed to build HTTP client")insideJwksProvider::new(). Change the constructor to returnResult<Self>; propagate the build error with.map_err(|e| anyhow::anyhow!("HTTP client: {e}"))?. Update the 1 call site in craig-api bootstrap. -
crates/craig-intake-sdk/src/client.rs:59— same pattern insideIntakeClient::new(). Same fix. -
Annotate the kept expects with an adjacent
// Reason:comment explaining why they’re acceptable:-
crates/craig-common/src/telemetry.rs:86,103— OTEL-exporter-build in the startup path. If the exporter fails to build, the service can’t observe itself; panicking is equivalent to failing startup. -
crates/craig-mq/src/publisher.rs:30—serde_json::to_vec(&EventEnvelope).expect(…). The envelope contains aserde_json::Valuepayload; unless the payload is produced by a buggyimpl Serialize, this can’t fail. Treat as an assertion. -
crates/craig-intake-sdk/src/signing.rs:75— 2-key map literal serialization. Infallible by construction.
-
-
For
crates/craig-api/src/bootstrap.rsshutdown_signal: same treatment — add// Panic: irrecoverable startup failure. -
For
crates/craig-test-lib/src/client.rsandtoken.rs: add// Test-only: panic on infra failure.
Verify: grep -rn '\.expect(' crates/ --include='*.rs' — every remaining occurrence has an adjacent // Reason: or // Panic: or // Test-only: comment. Pre-push runs the new jwks::JwksProvider::new() error path in at least one integration test.
Step 3: Silent error discards → tracing::warn
Files (refreshed line numbers):
Critical — must log on error:
-
crates/craig-mq/src/subscriber.rs:137,144,153,157—let _ = delivery.ack(…)/let _ = delivery.nack(…). Rewrite asif let Err(e) = delivery.ack(BasicAckOptions::default()).await { tracing::warn!(error = %e, "failed to ack AMQP delivery"); }. Dropping an ack silently means the broker re-delivers on the next consumer-up, causing duplicate processing — exactly the bug the warn is supposed to surface. -
services/craig-intake/src/api/api_key_lookup.rs:60—let _ = store::api_keys::update_last_used(…). Fire-and-forget last-used-timestamp update; addtracing::warn!(error = %e, key_id = %key_id, "failed to update api_key last_used")on error. -
services/craig-intake/src/api/jws.rs:112— same pattern forsigner_keys::update_last_used. -
crates/craig-store/src/store.rs:103— best-effort cleanup on a failure path; still worth a warn so the leak is observable.
Non-critical — add a // Reason: comment, no tracing::warn:
-
crates/craig-api/src/rate_limit.rs:93—let _ = limiter.check_key(&ip)inside a test helper that warms the limiter. Comment:// Reason: test warmup — check() return is unused on purpose. -
crates/craig-api/src/lib.rs:299—let _ = encoder.encode(…)in the/metricshandler’s Prometheus-text encoder. Comment:// Reason: metrics encoder failure is surfaced via empty body; HTTP 200 preserves scrape compatibility. -
crates/craig-api/src/bootstrap.rs:182-187—let _ = &br.db; let _ = &br.auth; …. These hold refs so Drop timing is documented; comment as a block:// Reason: held-for-lifetime; explicit binding documents that BootstrapResult fields are kept alive until main() returns. -
Service
main.rsfiles (craig-cases/main.rs:142,craig-exchange/main.rs:108,craig-placement/main.rs:95, 3 others):let _ = dbbinding pattern — comment:// Reason: held-for-lifetime; DbPool kept in scope so connections stay live for the axum server.
Verify: grep gate in Step 6 — every let _ = ` in `crates/ and services/ has either a // Reason: comment on the line above / same line, OR the discarded expression is a Result being handled via if let Err(e) just below.
Step 4: serde_json::Value → typed structs
Files: crates/craig-intake-sdk/src/types.rs
-
Define
Child,Adult,Narrativestructs with AFCARS-aligned fields -
Replace
children: Value,adults: Value,narrative: Valuewith typed fields -
Add
#[serde(default)]for backward compatibility -
Add comments to
EventEnvelope.payloadandcanonicalize_jsonexplaining why Value is acceptable there
Step 5: Functions over 40 lines → split
Files (refreshed line numbers):
-
idempotency_middleware(88 lines,crates/craig-api/src/idempotency.rs:63-151) → extractcheck_cache(&cache, &cache_key) → Option<Response>,capture_and_cache(parts, body, …) → Response. Middleware body collapses to the POST/key-extraction guards + the two extracted-fn calls. -
router(58 lines,crates/craig-api/src/lib.rs:90) → extractbuild_middleware_stack()that returns the configuredServiceBuilder, andbuild_core_routes()that wires the/healthz+/metrics+/docsendpoints.router()becomes a merge of those two plus the user-suppliedservice_routes. -
health_check(54 lines,crates/craig-api/src/lib.rs:234) → extractcheck_database(&db) → HealthStatusandcheck_rabbitmq(&mq) → HealthStatus.health_checkcomposes the two into the final JSON response. -
create_case(77 lines,services/craig-cases/src/api/cases.rs) → extractvalidate_case_payload(&body) → Result<ValidatedCase, ApiError>andpersist_case(&db, &validated, &claims) → Result<Case, ApiError>. Handler drops to auth check + the two fn calls + event publish + JSON response. -
update_case(64 lines, same file) → extractvalidate_status_transition(current, next) → Result<(), ApiError>andapply_case_update(&db, id, patch) → Result<Case, ApiError>. Transition tests already exist intransitions.rs; the extract preserves them.
Out of scope (even though 40+ lines): services/craig-web/src/routes/* handlers. They’re Askama-heavy — the template struct population is the body and extracting it doesn’t reduce cognitive load proportional to the churn.
Step 6: Full validation
-
cargo fmt --check --all -
cargo clippy --workspace — -D warnings -
cargo nextest run --workspace --profile integration— all tests pass -
cargo xtask e2e— E2E tests pass -
Grep: every
.expect(incrates/is either:-
on an adjacent line below a
// Reason:/// Panic:/// Test-only:comment, OR -
inside a
#[cfg(test)]module, OR -
inside a docstring code block (
/// … .expect(…))Pre-push clippy additionally enforces
clippy::expect_usedon non-test code (to add later — not in this plan’s scope).
-
-
Grep: every
let _ = ` in `crates/andservices/is either:-
on an adjacent line below a
// Reason:comment, OR -
the discarded value is immediately-rematched via
if let Err(e) = …on the next statement.
-
-
Grep: zero
ApiError::NotFound(String::from(orApiError::NotFound("…".to_string())pattern — all constructors go through the structured variant.
Files Touched
| File | Change |
|---|---|
|
Structured error variants, From<sqlx::Error> |
|
Structured error variants |
|
Structured error variants |
|
expect() → Result |
|
expect() → Result |
|
expect() → Result |
|
expect() → Result |
|
let _ = → tracing::warn |
|
expect() → Result |
|
Split into helper functions |
|
Split router() and health_check() |
|
Value → typed AFCARS structs |
|
Split create_case() and update_case() |
All service API modules |
Update ApiError call sites for structured variants |
Verification
-
cargo nextest run --workspace --profile integration— all tests pass -
cargo xtask e2e— E2E tests pass -
cargo xtask validate— full pre-push passes -
Grep:
grep -rn "expect(" crates/ --include="*.rs" | grep -v test-lib | grep -v "shutdown_signal"— zero results -
Grep:
grep -rn "let _ =" crates/ services/ --include="*.rs" | grep -v "// "— zero uncommented discards
Documentation Updates
-
.claude/docs/coding-conventions.md— document structured error pattern, expect() policy -
CHANGELOG.adoc— entry under== Unreleased