Plan: Code Quality Audit Remediation
On this page
- Status
- Context
- Scope
- Design
- Steps
- Step 1: Unify DeterminationSigner in canopy-signing
- Step 2: Standardize error handling
- Step 3: Fix canopy-eligibility state duplication
- Step 4: Unify pagination
- Step 5: Fix unwrap() and MQ error logging
- Step 6: Extract CircuitBreaker and RulesClient to shared crates
- Step 7: Promote workspace dependencies
- Step 8: Update all stale documentation
- Step 9: Fix test unwrap() calls
- Step 10: Full validation
- Files Touched
- Verification
- Documentation Updates
Status
| Step | Description | Status |
|---|---|---|
1 |
Unify DeterminationSigner trait in canopy-signing (resolve competing definitions) |
Done (2026-04-05) — (single trait in |
2 |
Standardize error handling: adopt |
Done (2026-04-05) — ( |
3 |
Fix canopy-eligibility state duplication (remove redundant Extension<PgPool>) |
Done (2026-04-05) — (handlers use |
4 |
Unify pagination on |
Done (2026-04-05) — (single definition in |
5 |
Fix idempotency middleware |
Done (2026-04-05) — (zero bare |
6 |
Move CircuitBreaker to canopy-api, extract RulesClient to shared crate |
Done (2026-04-05) — ( |
7 |
Promote |
Done (2026-04-05) — (both in |
8 |
Update all Month 2 plan status tables, roadmap checkpoints, CLAUDE.md, services.md |
Done (2026-04-05) — (this update; CLAUDE.md updated in security-ci-remediation step 14) |
9 |
Replace test |
Done (2026-04-05) — (audit confirmed zero bare |
10 |
Verify: full validation pass (fmt, clippy, 204+ tests, pre-push hook) |
Done (2026-04-05) — (403 tests passing; fmt + clippy clean) |
Epic: &45
Branch: chore/audit-remediation
Labels: type::chore, priority::critical, program::infrastructure, service::shared-crates
Context
Six independent audit agents reviewed the codebase after Month 2 completion. They identified 16 issues across code quality, architectural consistency, documentation staleness, and code duplication.
The most critical findings:
-
Two competing
DeterminationSignertraits —canopy-snapdefinessign(&self, payload: &[u8])whilecanopy-eligibilitydefinessign(&self, determination: &Determination). When canopy-tanf and canopy-medicaid implement their program services, they will face ambiguity about which trait to implement. The trait must be unified incanopy-signingbefore more program services ship. -
Error handling divergence — four services (rules, persons, applications, security) return
impl IntoResponsewith manualmatch+StatusCode, while two services (snap, eligibility) returnResult<Json<T>, ApiError>. TheApiErrorpattern is superior (composable with?, less boilerplate), but neither pattern has aFrom<sqlx::Error>impl, so every handler manually maps database errors. -
Documentation staleness — six Month 2 plan status tables still show "Not started" despite all being merged. The roadmap Week 10 checkpoint is unmarked. This misleads anyone reading the docs about project status.
Addressing all 16 issues in a single remediation pass ensures the codebase is pristine before Month 3 (Verification) begins.
Scope
In scope:
-
Unify
DeterminationSignertrait incanopy-signing(single definition, both signatures supported) -
Add
From<sqlx::Error> for ApiErrorincanopy-common -
Migrate all services to
Result<T, ApiError>return type (eliminateimpl IntoResponsepattern) -
Remove duplicate
Extension<PgPool>from canopy-eligibility -
Replace canopy-snap’s custom
PageRequestwithcanopy_common::PageRequest -
Fix
unwrap()in idempotency middleware response builder -
Add
tracing::warn!to MQ ack/nack failures incanopy-mq/src/subscriber.rs -
Move
CircuitBreakerfromcanopy-eligibilitytocanopy-api -
Extract
RulesClientfromcanopy-snapto newcrates/canopy-rules-client -
Promote
rust_decimal_macrosandtomlto workspace dependencies -
Update all stale documentation (plan status tables, roadmap, CLAUDE.md, services.md)
-
Replace test
unwrap()withexpect("description")
Out of scope:
-
Generic store layer trait (premature — wait until 3+ services have similar patterns)
-
Generic parameter loader trait (premature — wait until TANF params defined)
-
API handler wrappers for GET-by-ID and list-with-pagination (good idea but requires careful design; separate plan)
-
canopy-cli implementation (separate plan exists)
-
Test fixture library expansion (tests are still stubs; revisit when implementing integration tests)
Design
DeterminationSigner unification
Move the trait to crates/canopy-signing/src/traits.rs:
// crates/canopy-signing/src/traits.rs
/// Trait for signing determination payloads.
/// Program services implement this to produce detached JWS signatures.
pub trait DeterminationSigner: Send + Sync {
/// Sign raw bytes (canonical JSON of the determination struct).
fn sign(&self, payload: &[u8]) -> Result<String, anyhow::Error>;
}
The &[u8] signature is correct — it operates on serialized bytes, which is what JWS requires.
The canopy-eligibility version that takes &Determination is a convenience wrapper that should serialize internally.
Both canopy-snap and canopy-eligibility will import from canopy_signing::traits::DeterminationSigner.
Error handling standardization
Add to crates/canopy-common/src/error.rs:
impl From<sqlx::Error> for ApiError {
fn from(err: sqlx::Error) -> Self {
tracing::error!("database error: {err}");
ApiError::Internal("database error".into())
}
}
Then update all service handlers from:
// Before: manual match + StatusCode
async fn get_person(State(state): State<AppState>, Path(id): Path<Uuid>) -> impl IntoResponse {
match persons::get(state.db.inner(), id).await {
Ok(Some(person)) => Json(person).into_response(),
Ok(None) => StatusCode::NOT_FOUND.into_response(),
Err(e) => {
tracing::error!("get_person: {e}");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
To:
// After: Result<T, ApiError> with ?
async fn get_person(State(state): State<AppState>, Path(id): Path<Uuid>) -> Result<Json<Person>, ApiError> {
let person = persons::get(state.db.inner(), id)
.await?
.ok_or_else(|| ApiError::NotFound(format!("person {id}")))?;
Ok(Json(person))
}
RulesClient extraction
Create crates/canopy-rules-client/:
// crates/canopy-rules-client/src/lib.rs
pub struct RulesClient { ... }
impl RulesClient {
pub fn new(base_url: &str) -> Self;
pub async fn evaluate(
&self, rule_set_name: &str, context_type: &str,
context_id: Uuid, input: serde_json::Value,
) -> Result<serde_json::Value, canopy_common::error::ApiError>;
}
Move from services/canopy-snap/src/rules_client.rs.
Add as workspace dependency.
canopy-snap and future program services import canopy_rules_client::RulesClient.
Steps
Step 1: Unify DeterminationSigner in canopy-signing
Files: crates/canopy-signing/src/traits.rs (new), crates/canopy-signing/src/lib.rs, services/canopy-snap/src/determine.rs, services/canopy-eligibility/src/determination.rs
-
Create
crates/canopy-signing/src/traits.rswith the unifiedDeterminationSignertrait -
Re-export from
canopy_signing::traits -
Remove trait definition from
canopy-snap/src/determine.rs -
Update
canopy-snapto importcanopy_signing::traits::DeterminationSigner -
Update
canopy-eligibility/src/determination.rsto use the shared trait (serialize&Determinationto bytes, then callsign(&bytes)) -
Verify: both services compile, all existing signing tests pass
Step 2: Standardize error handling
Files: crates/canopy-common/src/error.rs, all service src/api/mod.rs files (rules, persons, applications, security)
-
Add
From<sqlx::Error> for ApiErrorimpl tocanopy-common/src/error.rs -
Update canopy-rules handlers: change return type from
impl IntoResponsetoResult<Json<T>, ApiError>, replace match blocks with? -
Update canopy-persons handlers: same pattern
-
Update canopy-applications handlers: same pattern
-
Update canopy-security handlers: same pattern
-
Verify: all services compile, existing tests pass
Step 3: Fix canopy-eligibility state duplication
Files: services/canopy-eligibility/src/main.rs, services/canopy-eligibility/src/api/handlers.rs
-
Remove
.layer(axum::Extension(boot.db.clone()))from main.rs (db is already in AppState) -
Update handlers to extract db from
State(state): State<AppState>usingstate.db.inner()instead ofExtension(db): Extension<PgPool> -
Keep other Extension layers (registry, client, verifier, jurisdiction) — those are service-specific
-
Verify: canopy-eligibility compiles, orchestrator tests pass
Step 4: Unify pagination
Files: services/canopy-snap/src/store/mod.rs, services/canopy-snap/src/api/determine_handler.rs
-
Remove
pub struct PageRequest { offset, limit }fromcanopy-snap/src/store/mod.rs -
Import
canopy_common::pagination::PageRequestinstead -
Update store functions to accept
canopy_common::PageRequest(use.offset()and.limit()methods) -
Update API handler to construct
canopy_common::PageRequestfrom query params -
Verify: canopy-snap compiles, pagination tests pass
Step 5: Fix unwrap() and MQ error logging
Files: crates/canopy-api/src/idempotency.rs, crates/canopy-mq/src/subscriber.rs
-
Replace
Response::builder().body(…).unwrap()with.expect("response body construction is infallible")at lines 78 and 117 -
Replace
let _ = delivery.nack(…)with:if let Err(e) = delivery.nack(BasicNackOptions { requeue: true }).await { tracing::warn!("failed to nack delivery: {e}"); } -
Same pattern for ack calls
-
Verify: canopy-api and canopy-mq compile, existing tests pass
Step 6: Extract CircuitBreaker and RulesClient to shared crates
Files:
CircuitBreaker:
-
crates/canopy-api/src/circuit_breaker.rs(new, move from canopy-eligibility) -
crates/canopy-api/src/lib.rs(addpub mod circuit_breaker) -
services/canopy-eligibility/src/registry.rs(update import) -
services/canopy-eligibility/src/main.rs(remove module declaration)
RulesClient:
-
crates/canopy-rules-client/Cargo.toml(new crate) -
crates/canopy-rules-client/src/lib.rs(move from canopy-snap) -
Cargo.toml(add to workspace members and dependencies) -
services/canopy-snap/Cargo.toml(add canopy-rules-client dependency) -
services/canopy-snap/src/main.rs(update import)-
Move circuit_breaker.rs to canopy-api, re-export, update canopy-eligibility imports
-
Create canopy-rules-client crate, move RulesClient + types, update canopy-snap imports
-
Add both to workspace members in root Cargo.toml
-
Verify: all services compile, circuit breaker tests pass in new location
-
Step 7: Promote workspace dependencies
Files: Cargo.toml (root), services/canopy-snap/Cargo.toml
-
Add to root
[workspace.dependencies]:rust_decimal_macros = "1" toml = "0.8" -
Update canopy-snap Cargo.toml to use
{ workspace = true }for both -
Verify: canopy-snap compiles
Step 8: Update all stale documentation
Files: 9 plan files, roadmap.adoc, .claude/CLAUDE.md, .claude/docs/services.md
-
Update plan status tables for: snap-eligibility, snap-deduction-calculation, eligibility-orchestrator, snap-abawd, snap-special-situations (mark all steps Complete, add MR refs)
-
Update roadmap.adoc: mark Month 2 Week 10 checkpoint as PASSED, update UAT Target text
-
Update CLAUDE.md: canopy-snap notes (add ABAWD, categorical, disqualifications), verify canopy-eligibility shows as implemented
-
Update services.md: add ABAWD/disqualification table names, verify all endpoint counts
Files Touched
| File | Change |
|---|---|
|
New: unified DeterminationSigner trait |
|
Add |
|
Add |
|
Replace unwrap() with expect() |
|
New: moved from canopy-eligibility |
|
Add circuit_breaker module |
|
Add logging to ack/nack errors |
|
New crate: RulesClient extracted from canopy-snap |
|
Add canopy-rules-client to workspace, promote deps |
|
Migrate to Result<T, ApiError> |
|
Migrate to Result<T, ApiError> |
|
Migrate to Result<T, ApiError> |
|
Migrate to Result<T, ApiError> |
|
Remove duplicate Extension<PgPool> |
|
Use State(state).db instead of Extension(db) |
|
Import CircuitBreaker from canopy-api |
|
Import DeterminationSigner from canopy-signing |
|
Use canopy_common::PageRequest |
|
Use workspace deps, add canopy-rules-client |
9 plan files under docs/modules/ROOT/pages/plans/ |
Update status tables + MR refs |
|
Mark Month 2 checkpoint complete |
|
Update canopy-snap feature notes |
|
Add ABAWD/disqualification tables |
Verification
-
cargo fmt --check --all— no formatting issues -
cargo clippy --workspace — -D warnings— zero warnings -
cargo nextest run --workspace --profile ci— all 204+ tests pass -
cargo xtask validate --skip-docker— full pre-push validation passes -
Verify: no
unwrap()in non-test code (grep for\.unwrap()excluding#[cfg(test)]) -
Verify: no
let _ =on fallible operations in canopy-mq (grep) -
Verify:
DeterminationSignertrait defined only in canopy-signing (grep) -
Verify:
PageRequestdefined only in canopy-common (grep forpub struct PageRequest) -
Verify:
CircuitBreakerdefined only in canopy-api (grep) -
Verify:
RulesClientdefined only in canopy-rules-client (grep)
Documentation Updates
-
Plan status tables for 6 Month 2 plans — updated in Step 8
-
roadmap.adoc— Week 10 checkpoint marked in Step 8 -
.claude/CLAUDE.md— feature status updated in Step 8 -
.claude/docs/services.md— tables updated in Step 8 -
CHANGELOG.adoc— entry under== Unreleased -
.claude/docs/coding-conventions.md— document theResult<T, ApiError>standard pattern