--- id: w1-error-model name: "Wave 1: the public error model" status: completed depends_on: [w1-manifest] scope: single risk: low impact: project level: implementation --- ## Description Land the crate's one public error enum (core.md §Error model; this is the contract every later task and every consumer maps against): - `pub enum Error` in `src/error.rs`, via thiserror (AGENTS.md convention 2), covering the engine-owned operations core.md names: registration, template parsing, and rendering. A malformed template (registration-time parse failure) and a failed render are data: typed variants wrapping the substrate's `handlebars::TemplateError` and `handlebars::RenderError`. No panics, no `unwrap`/`expect` outside tests. - No catch-alls: every variant names the operation that failed, and every engine-owned failure path maps to exactly one variant. No lossy flattening: the underlying substrate error stays reachable (source chain via thiserror `#[from]`/`#[source]`), for consumers that need detail (alkforge maps render failure to an error page). - **No context-build arms in this scope**: no engine-owned template set ships in v0.1.0, and consumer context builders own their errors (ADR-008). The arm lands additively with the first engine-owned set. Recorded interpretation, flagged for the wave-1 gate. - `src/lib.rs` re-exports the enum per convention 10; doc comments on the public surface (no inline `//` comments per convention 1). ## Acceptance Criteria - [ ] `pub enum Error` with registration/parse and render variants wrapping `TemplateError`/`RenderError`; the substrate error stays reachable through the source chain - [ ] no catch-all variant; registration, parse, and render failures each map to exactly one variant - [ ] no `unwrap`/`expect` in `src/`; no panics - [ ] re-exported from `src/lib.rs`; doc comments on public items - [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` pass ## References - docs/architecture/core.md §Error model - docs/architecture/decisions/002-handlebars-substrate-strict-mode.md (decision 3: typed fallible registration) - docs/architecture/decisions/008-template-set-ownership.md (consumer builders own their errors) - docs/plans/implementation.md (the context-build-arms interpretation) ## Notes > Design: two variants total. Registration and template parsing are > one variant because the substrate's `register_template_string` > parses at registration time and returns `Result<(), TemplateError>`; > there is no substrate operation that can fail as "registration but > not parse". `Registration` names the operation and its doc comment > states that parse failures surface under it, so the core.md > three-operation list (registration, parsing, rendering) is fully > covered. No `#[non_exhaustive]`: the plan records the enum's > evolution policy (the context-build arm lands additively pre-1.0, > and the freeze inventory carries the policy post-publish), and > `#[non_exhaustive]` would tax every consumer match arm for a > guarantee the plan does not call for. Revisit at publish review if > the freeze inventory wants it. ## Summary > Agent fills this on completion. Landed 2026-11-06. `src/error.rs` holds the one public enum (core.md §Error model), `src/lib.rs` re-exports `alkgen::Error` (convention 10): - `Error::Registration(#[from] handlebars::TemplateError)`: template and partial registration failure, including the registration-time parse failure. Doc comment points to the substrate error carrying reason, template name, and position. - `Error::Render(#[from] handlebars::RenderError)`: render failure, e.g. a missing context variable under strict mode. Doc comment points to the substrate error carrying the originating template. The no-catch-all and one-variant-per-failure criteria hold by construction (only the two substrate error types exist on the two engine-owned paths). No context-build arm in this scope, per the recorded interpretation, flagged for the wave-1 gate. Findings for later tasks: - The substrate's `TemplateError` derives `std::error::Error` with its reason as a plain field, so `TemplateError::source()` is `None`; the reason is reachable through the `reason()` accessor instead. The crate's source chain still satisfies the no-lossy-flattening rule: `Error::source()` returns the substrate error itself (tests pin this via downcast), whose `Display` embeds the reason text. - `RenderError::source()` returns the `RenderErrorReason`; the substrate's strict-mode missing-variable reason is `MissingVariable`, with its detail in the reason's `Display`. - `RenderError::new` is deprecated since substrate 5.0; tests use `RenderError::strict_error` for constructing substrate errors. Tests (unit, in `src/error.rs`): malformed template maps to `Registration` with the substrate error, name ("bad"), and reason reachable through the source chain; strict-mode missing variable maps to `Render` with the reason reachable; substrate detail (the unclosed `beta`) survives on the render error; `Display` for both variants starts with the failing operation's name. Verification: `cargo test` (6 passed), `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, `cargo doc --no-deps`, and `cargo build --target wasm32-unknown-unknown --release` all green. No `unwrap`/`expect` outside tests, no panics in lib code, no em dashes.