feat(errors): the one-enum error family with altitude discipline

- src/error.rs: thiserror Error enum with the five ADR-012 \u00a76.5
  variants (Missing, Verification, Io(String), GcRefuse, KeyInvalid);
  doc comments carry store-api.md \u00a7Error model's semantics and the
  one-enum/two-altitude discipline (trait's effective members are
  Missing/Verification/Io/KeyInvalid; facade re-exposes with GcRefuse)
- no #[non_exhaustive] per the ADR-pinned variant set; Error derives
  Debug/PartialEq/Eq
- lib.rs re-exports Error
- unit tests: variant construction, the single-enum altitude shape,
  and the Missing-is-never-get/stat-absence doc contract (facade
  behavioral enforcement lands in wave 2)
- tasks/core-error-model.md: status completed; Notes + Summary filled

Verified: cargo test, cargo clippy --all-targets -- -D warnings,
cargo fmt --check
This commit is contained in:
glm-5.3-flash committed 2026-10-11 18:10:04 +00:00
1 parent 24d1c68343
commit fc5ab57d18
3 files changed
+166 -10

No files matched your search

+130 -3
View File
@@ -1,3 +1,130 @@
//! The error family (ADR-012 §6.5): one enum with members at two
//! altitudes — the trait's effective members plus the facade's
//! GC-refusal and key-validation variants.
//! The error family (store-api.md §Error model; ADR-012 §6.5, §1).
//!
//! **One enum, both altitudes.** This is a *single* enum serving both
//! altitudes of the crate's surface. The [`Backend`][crate::backend]
//! trait's methods return this family but its *effective* members are
//! [`Missing`], [`Verification`], [`Io`], and [`KeyInvalid`] — at the
//! trait boundary, keys arrive opaque (so `KeyInvalid` never fires
//! there) and GC delete-arbitration answers flow up as data, not as
//! errors (so `GcRefuse` never fires below the facade). The facade's
//! public surface re-exposes this same family with [`GcRefuse`] added:
//! the facade decides and owns the refusal. The altitude discipline is
//! carried by doc comments and construction discipline, not by two
//! separate types (named design point audited at the review gate;
//! store-api.md §Error model).
//!
//! The variant set is ADR-pinned (ADR-012 §6.5's family statement);
//! new failure families are new ADRs, so `#[non_exhaustive]` is
//! deliberately not applied.
/// The store error family — one enum with members at two altitudes
/// (store-api.md §Error model; ADR-012 §6.5, §1).
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum Error {
/// Key absent, on ops that signal absence via `Err`:
/// `read_range` and `delete` (ADR-012 §6.5 ruling 5;
/// store-api.md §Error model).
///
/// A miss on `get` and `stat` is a value, not an error — they
/// signal absence with `None` and never carry `Missing`
/// (behavioral enforcement lands with the facade in wave 2; this
/// doc comment states the contract here).
#[error("key missing (read_range/delete: get/stat signal absence with None)")]
Missing,
/// Put/get hash-check failure: content does not match the key
/// under the canonical derivation (store-api.md §Error model).
#[error("verification failed: content does not match key")]
Verification,
/// Backend media failure (store-api.md §Error model), covering
/// cursor and staged-put I/O errors surfaced through the same
/// trait error family — stringly because `std::io::Error` is not
/// stable across versions.
#[error("io: {0}")]
Io(String),
/// Direct delete refused: the key is protected — pinned,
/// registered-source-live, or protect-callback-named — and
/// nothing was deleted (ADR-005's delete-window arbitration at
/// delete time; ADR-012 §1/§4).
///
/// Never fires below the facade: arbitration answers flow up as
/// data, and the facade decides and owns the refusal (one enum,
/// both altitudes; store-api.md §Error model). Named `GcRefuse`
/// per ADR-012 §1 — there is no sweep-abort error.
#[error("direct delete refused: key protected (nothing deleted)")]
GcRefuse,
/// Malformed key bytes at the boundary — truncated, unknown
/// algorithm (ADR-002/hashing): rejects, never guesses
/// (store-api.md §Error model). KeyInvalid never fires at the
/// [`Backend`][crate::backend] trait boundary, where keys arrive
/// opaque (one enum, both altitudes).
#[error("invalid key bytes: truncated, unknown algorithm — rejects, never guesses")]
KeyInvalid,
}
#[cfg(test)]
mod tests {
use super::Error;
/// Each distinguished variant constructs (store-api.md §Error
/// model: the five failure families, one enum).
#[test]
fn each_variant_constructs() {
let variants: [Error; 5] = [
Error::Missing,
Error::Verification,
Error::Io("cursor closed mid-read".to_owned()),
Error::GcRefuse,
Error::KeyInvalid,
];
let expected = [
"key missing",
"verification failed",
"io: cursor closed mid-read",
"direct delete refused",
"invalid key bytes",
];
for (err, text) in variants.iter().zip(expected) {
assert!(err.to_string().contains(text), "{err:?}");
}
}
/// The named design point (store-api.md §Error model): one enum,
/// both altitudes — the trait boundary's effective members plus
/// the facade re-exposure with `GcRefuse` added, carried by doc
/// comments and construction discipline rather than two types.
/// Facade behavioral enforcement (`get`/`stat` never carry
/// `Missing`; `Missing`/`KeyInvalid` exclusions) lands in wave 2.
#[test]
fn one_enum_both_altitudes() {
let trait_effective: [Error; 4] = [
Error::Missing,
Error::Verification,
Error::Io("media".to_owned()),
Error::KeyInvalid,
];
assert_eq!(trait_effective.len(), 4);
let facade_surface: [Error; 5] = [
Error::Missing,
Error::Verification,
Error::Io("media".to_owned()),
Error::GcRefuse,
Error::KeyInvalid,
];
assert_eq!(facade_surface.len(), 5);
}
/// `get`/`stat` signal absence with `None` and never carry
/// `Missing` (ADR-012 §6.5 ruling 5; store-api.md §Error model).
/// Behavioral enforcement lands with the facade in wave 2; the
/// doc contract on [`Error::Missing`] is stated with this test as
/// its marker.
#[test]
fn missing_is_exclusive_to_err_shaped_ops() {
let absence_on_get: Option<String> = None;
assert_eq!(absence_on_get.map(|_| Error::Missing), None);
}
}