Plan: Code Quality Remediation
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Replace String-wrapped error variants with structured types (ApiError, SigningError, StoreError, RenderError) |
Done (2026-04-05) — (ApiError::Internal structured in |
2 |
Replace |
Done (2026-04-05) — (MR !32) |
3 |
Replace silent error discards with |
Done (2026-04-05) — (MR !32) |
4 |
Type Typst context |
Done (2026-04-05) — (all remaining |
5 |
Decompose |
Done (2026-04-05) — (extracted |
Epic: &45
Issues: TBD
Branch: refactor/structured-error-types (step 1), future branches for steps 4-5
Labels: type::chore, priority::high, program::infrastructure, service::shared-crates
Context
A codebase audit against standardized code quality constraints revealed 5 categories of violations.
Two have been shipped (MR !32): expect() in library code replaced with Result propagation, and silent error discards fixed with tracing::warn or explanatory comments.
The remaining highest-priority item is String-wrapped error variants.
Four error enums use String fields where structured types would preserve error chains and eliminate the format!("{e}") / .to_string() pattern at ~163 call sites.
Scope
In scope (step 1):
-
ApiError::Internal(String)→Internal { context, #[source] source }withBox<dyn Error> -
SigningError— 4 String variants →#[source] Box<dyn Error>wrappers -
StoreError::Config(String)→Config(#[source] Box<dyn Error>);NotFound(String)→NotFound { path } -
RenderError—ManifestError(String)→ManifestParse([from] toml::de::Error);CompilationFailed/ContextError→[source] Box<dyn Error> -
All call sites across services and crates
Out of scope (by design):
-
ApiError::NotFound(String)— user-facing RFC 9457 detail text, not a wrapped error -
ApiError::BadRequest(String)— validation/regulatory messages -
ApiError::Conflict(String)— state transition messages -
StoreError::DisallowedContentType(String),InvalidFilename(String)— value strings, not wrapped errors -
SigningError::InvalidJws(String)— hand-written validation messages (3 sites) -
SigningError::NoKeyForProgram(String)— program name identifier
In scope (steps 4-5, future branches):
-
Type
canopy-typstcontextserde_json::Valuefields -
Document protocol-level
Valueexceptions (event envelope, rules engine I/O) -
Decompose
idempotency_middleware(75 lines → 3 helpers)
Design
See plan file at .claude/plans/cryptic-gathering-quokka.md for full type definitions and migration patterns.
Key design decisions:
-
ApiError::Internal takes
{ context: &'static str, #[source] source: Box<dyn Error> }withinternal()helper constructor -
SigningError variants use
#[source] Box<dyn Error>(not concrete types, since p256 has many error types) -
StoreError::Config wraps
Box<dyn Error>(wraps io::Error or object_store builder errors) -
RenderError::ManifestError splits into
ManifestParse([from] toml::de::Error)+ existingIo([from] std::io::Error)
Steps
Step 1: Structured error types
Files: crates/canopy-common/src/error.rs, crates/canopy-signing/src/error.rs, crates/canopy-store/src/error.rs, crates/canopy-typst/src/error.rs, all service API and domain files with ApiError::Internal call sites
See detailed migration plan in .claude/plans/cryptic-gathering-quokka.md.
Step 4: Type Typst context Value fields
Files: crates/canopy-typst/src/context.rs, crates/canopy-typst/src/engine.rs, crates/canopy-mq/src/envelope.rs, crates/canopy-rules-client/src/lib.rs
-
program_data: serde_json::Value→ typed per-program context enum -
Add code comments on protocol-level
Valueexceptions (event envelope, rules engine I/O)
Files Touched
| File | Change |
|---|---|
|
Internal → named-field variant with Box source; add internal() constructor |
|
4 String variants → #[source] Box wrappers |
|
Config → Box source; NotFound → named field |
|
ManifestError → ManifestParse(#[from]); CompilationFailed/ContextError → Box source |
|
.map_err(to_string) → ? operator |
All service API/domain files |
ApiError::Internal(format!(…)) → ApiError::internal("ctx", e) |
Verification
-
cargo nextest run --workspace --lib --bins— unit tests pass -
cargo xtask dev start --shared-db— devstack healthy -
cargo nextest run --workspace --test '*' --profile integration— integration tests pass -
cargo clippy --workspace --all-targets — -D warnings— zero warnings -
cargo xtask validate— full pre-push validation
Acceptance Criteria
-
Zero
expect()orunwrap()in library code (crates/) — done -
Zero
let _ =discarding Result without a code comment ortracing::warn— done -
Zero
format!("{e}")or.to_string()wrapping a source error into a String variant -
Zero
serde_json::Valuein application-level data types (step 4) -
No function exceeds 40 lines without documented exception (step 5)
Documentation Updates
-
.claude/docs/coding-conventions.md— update error handling examples -
CHANGELOG.adoc— entry under== Unreleased