diff --git a/src/rust/state_checkpoint/src/lib.rs b/src/rust/state_checkpoint/src/lib.rs index e53b44a9..b5691dff 100644 --- a/src/rust/state_checkpoint/src/lib.rs +++ b/src/rust/state_checkpoint/src/lib.rs @@ -43,6 +43,18 @@ //! No sck_free_string export — sibling crates sharing the Python //! process already export identical free symbols; callers free through //! capability_scoring's cs_free_string. +//! +//! Language-advantage pass (Phase B, battery-pinned): the restore +//! decision is a `RestoreOutcome` enum (Unknown / DryRun / Proceed) +//! classified once and matched exhaustively; the listing sort key is a +//! `SortKey` enum (Dated / Undated — the oracle's missing-`at` default +//! rendered by match); each diff side classifies into a `DiffSide` +//! (Counted / Errored) so the `.get("entries", 0)` fallback is a type; +//! the diff lookup outcome is a `LookupOutcome` enum (Found / Unknown). +//! FFI errors are a typed `CheckpointError` enum behind the Result +//! envelope. The ledger doc and snapshot records stay pass-through +//! Values — the oracle forwards any caller shape verbatim. Battery +//! green throughout. use serde_json::{json, Map, Value}; use std::fmt; @@ -73,9 +85,28 @@ fn snapshots_array(ledger: &Map) -> Result<&Vec, Checkpoin .ok_or_else(|| CheckpointError::Key("snapshots".to_string())) } -/// `s.get("at", "")` — the listing sort key (missing → sorts last). -fn sort_key(s: &Value) -> &str { - s.get("at").and_then(Value::as_str).unwrap_or("") +/// The listing sort key — classified per snapshot so the oracle's +/// `.get("at", "")` default is a type: dated entries rank by their +/// stamp, undated ones rank as "" (hence last under reverse order). +enum SortKey<'a> { + Dated(&'a str), + Undated, +} + +impl SortKey<'_> { + fn classify(s: &Value) -> SortKey<'_> { + match s.get("at").and_then(Value::as_str) { + Some(at) => SortKey::Dated(at), + None => SortKey::Undated, + } + } + + fn rank(&self) -> &str { + match self { + SortKey::Dated(at) => at, + SortKey::Undated => "", + } + } } // ── checkpoint operations (pure functions of the ledger doc) ──────────────── @@ -112,7 +143,7 @@ pub fn snapshot( /// original order, and Rust's stable sort-by-reversed-key matches. pub fn list(snapshots: &[Value]) -> Vec { let mut out = snapshots.to_vec(); - out.sort_by(|a, b| sort_key(b).cmp(sort_key(a))); + out.sort_by(|a, b| SortKey::classify(b).rank().cmp(SortKey::classify(a).rank())); out } @@ -153,6 +184,16 @@ pub fn restore_commit(record: &Value, safety_id: &str) -> Value { json!({"restored": true, "snapshot": record, "safety_snapshot": safety_id}) } +/// The diff lookup outcome — classified once from the by_id map, then +/// rendered by an exhaustive match. +enum LookupOutcome { + Found { + before_path: String, + after_path: String, + }, + Unknown, +} + /// `diff` lookup core: by_id built dict-comprehension style — the LAST /// duplicate id wins. Either side unknown → the oracle's error doc. pub fn diff_lookup(snapshots: &[Value], before_id: &str, after_id: &str) -> Value { @@ -169,16 +210,50 @@ pub fn diff_lookup(snapshots: &[Value], before_id: &str, after_id: &str) -> Valu .and_then(Value::as_str) .map(str::to_string) }; - match (path_of(before_id), path_of(after_id)) { - (Some(bp), Some(ap)) => json!({"before_path": bp, "after_path": ap}), - _ => json!({"error": "unknown snapshot id"}), + let outcome = match (path_of(before_id), path_of(after_id)) { + (Some(bp), Some(ap)) => LookupOutcome::Found { + before_path: bp, + after_path: ap, + }, + _ => LookupOutcome::Unknown, + }; + match outcome { + LookupOutcome::Found { + before_path, + after_path, + } => json!({"before_path": before_path, "after_path": after_path}), + LookupOutcome::Unknown => json!({"error": "unknown snapshot id"}), } } -/// `diff` shape core: per-side buckets pass through (an error side has -/// no `entries` and contributes the oracle's .get("entries", 0) → 0). +/// One diff side — classified before the delta so the oracle's +/// `.get("entries", 0)` fallback is a type, not a silent default (an +/// error bucket has no `entries` and counts as 0). +enum DiffSide { + Counted(i64), + Errored, +} + +impl DiffSide { + fn classify(side: &Value) -> DiffSide { + match side.get("entries").and_then(Value::as_i64) { + Some(n) => DiffSide::Counted(n), + None => DiffSide::Errored, + } + } + + fn entries(self) -> i64 { + match self { + DiffSide::Counted(n) => n, + DiffSide::Errored => 0, + } + } +} + +/// `diff` shape core: per-side buckets pass through, the delta compares +/// classified sides. pub fn diff_shape(before: &Value, after: &Value) -> Value { - let entries = |side: &Value| side.get("entries").and_then(Value::as_i64).unwrap_or(0); + let entries = |side: &Value| DiffSide::classify(side).entries(); json!({ "before": before, "after": after,