From 3d698de84126e16944ff9eef9ef4e27cbbdf0504 Mon Sep 17 00:00:00 2001 From: fagemx Date: Fri, 27 Mar 2026 13:51:52 +0800 Subject: [PATCH] feat(edda-ledger): add error context enrichment across ledger, serve, and CLI (GH-377) Add .context() / .with_context() to ~80 bare `?` propagation sites across three layers for better error diagnostics: - Layer 1 (ledger.rs): All 45+ Ledger public methods now include method name and key parameters in error context - Layer 2 (serve lib.rs): All 33 handler open_ledger() calls now include the HTTP route in error context - Layer 3 (CLI): cmd_draft, cmd_bridge, cmd_pair Ledger::open() calls now include command name in error context Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 1 - crates/edda-cli/src/cmd_bridge.rs | 6 +- crates/edda-cli/src/cmd_draft.rs | 16 +-- crates/edda-cli/src/cmd_pair.rs | 11 +- crates/edda-ledger/src/ledger.rs | 176 ++++++++++++++++++++++-------- crates/edda-serve/src/lib.rs | 75 +++++++------ 6 files changed, 193 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96f863a..cc85aa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -895,7 +895,6 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror", "time", "ulid", ] diff --git a/crates/edda-cli/src/cmd_bridge.rs b/crates/edda-cli/src/cmd_bridge.rs index f2e7f5b..442fac6 100644 --- a/crates/edda-cli/src/cmd_bridge.rs +++ b/crates/edda-cli/src/cmd_bridge.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use clap::Subcommand; use std::io::Read; use std::path::Path; @@ -537,7 +538,7 @@ pub fn decide( edda_bridge_claude::peers::write_binding(&project_id, &session_id, &label, key, value); // 2. Write to workspace ledger (permanent) - let ledger = edda_ledger::Ledger::open(repo_root)?; + let ledger = edda_ledger::Ledger::open(repo_root).context("cmd_bridge: opening ledger")?; let _lock = edda_ledger::lock::WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; let parent_hash = ledger.last_event_hash()?; @@ -1013,7 +1014,8 @@ fn write_accepted_to_ledger( repo_root: &Path, decisions: &[edda_bridge_claude::bg_extract::ExtractedDecision], ) -> anyhow::Result<()> { - let ledger = edda_ledger::Ledger::open(repo_root)?; + let ledger = edda_ledger::Ledger::open(repo_root) + .context("cmd_bridge::write_accepted_to_ledger: opening ledger")?; let _lock = edda_ledger::lock::WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; diff --git a/crates/edda-cli/src/cmd_draft.rs b/crates/edda-cli/src/cmd_draft.rs index cba3b37..6b3b0a0 100644 --- a/crates/edda-cli/src/cmd_draft.rs +++ b/crates/edda-cli/src/cmd_draft.rs @@ -589,7 +589,7 @@ pub struct ProposeParams<'a> { } pub fn propose(p: ProposeParams<'_>) -> anyhow::Result<()> { - let ledger = Ledger::open(p.repo_root)?; + let ledger = Ledger::open(p.repo_root).context("cmd_draft::propose: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -756,7 +756,7 @@ pub fn propose(p: ProposeParams<'_>) -> anyhow::Result<()> { } pub fn show(repo_root: &Path, id: &str) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::show: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let draft = read_draft(&ledger, id)?; @@ -765,7 +765,7 @@ pub fn show(repo_root: &Path, id: &str) -> anyhow::Result<()> { } pub fn list(repo_root: &Path, json: bool) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::list: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let dir = &ledger.paths.drafts_dir; @@ -857,7 +857,7 @@ pub fn inbox( role: Option<&str>, json: bool, ) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::inbox: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let dir = &ledger.paths.drafts_dir; @@ -952,7 +952,7 @@ pub fn approve( note: &str, stage_id: Option<&str>, ) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::approve: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let mut draft = read_draft(&ledger, id)?; @@ -1121,7 +1121,7 @@ pub fn reject( note: &str, stage_id: Option<&str>, ) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::reject: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let mut draft = read_draft(&ledger, id)?; @@ -1250,7 +1250,7 @@ pub fn reject( } pub fn apply(repo_root: &Path, id: &str, dry_run: bool, delete_after: bool) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::apply: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let mut draft = read_draft(&ledger, id)?; @@ -1375,7 +1375,7 @@ pub fn apply(repo_root: &Path, id: &str, dry_run: bool, delete_after: bool) -> a } pub fn delete(repo_root: &Path, id: &str) -> anyhow::Result<()> { - let ledger = Ledger::open(repo_root)?; + let ledger = Ledger::open(repo_root).context("cmd_draft::delete: opening ledger")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let path = draft_path(&ledger, id); diff --git a/crates/edda-cli/src/cmd_pair.rs b/crates/edda-cli/src/cmd_pair.rs index a9a5b33..c8dc17f 100644 --- a/crates/edda-cli/src/cmd_pair.rs +++ b/crates/edda-cli/src/cmd_pair.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use clap::Subcommand; use edda_ledger::device_token::{generate_device_token, hash_token}; use std::path::Path; @@ -35,7 +36,7 @@ fn execute_new(repo_root: &Path, name: &str) -> anyhow::Result<()> { anyhow::bail!("device name cannot be empty"); } - let ledger = edda_ledger::Ledger::open(repo_root)?; + let ledger = edda_ledger::Ledger::open(repo_root).context("cmd_pair::new: opening ledger")?; // Check for duplicate name let existing = ledger.list_device_tokens()?; @@ -108,7 +109,7 @@ fn execute_new(repo_root: &Path, name: &str) -> anyhow::Result<()> { } fn execute_list(repo_root: &Path) -> anyhow::Result<()> { - let ledger = edda_ledger::Ledger::open(repo_root)?; + let ledger = edda_ledger::Ledger::open(repo_root).context("cmd_pair::list: opening ledger")?; let tokens = ledger.list_device_tokens()?; if tokens.is_empty() { @@ -142,7 +143,8 @@ fn execute_list(repo_root: &Path) -> anyhow::Result<()> { } fn execute_revoke(repo_root: &Path, name: &str) -> anyhow::Result<()> { - let ledger = edda_ledger::Ledger::open(repo_root)?; + let ledger = + edda_ledger::Ledger::open(repo_root).context("cmd_pair::revoke: opening ledger")?; // Check the token exists before writing the ledger event let existing = ledger.list_device_tokens()?; @@ -187,7 +189,8 @@ fn execute_revoke(repo_root: &Path, name: &str) -> anyhow::Result<()> { } fn execute_revoke_all(repo_root: &Path) -> anyhow::Result<()> { - let ledger = edda_ledger::Ledger::open(repo_root)?; + let ledger = + edda_ledger::Ledger::open(repo_root).context("cmd_pair::revoke_all: opening ledger")?; let event_id = format!("evt_{}", ulid::Ulid::new()); let branch = ledger.head_branch()?; diff --git a/crates/edda-ledger/src/ledger.rs b/crates/edda-ledger/src/ledger.rs index 20cb169..b2d33f5 100644 --- a/crates/edda-ledger/src/ledger.rs +++ b/crates/edda-ledger/src/ledger.rs @@ -1,6 +1,7 @@ use crate::paths::EddaPaths; use crate::sqlite_store::{BundleRow, DecisionRow, SqliteStore}; use crate::view::{self, DecisionView}; +use anyhow::Context; use edda_core::Event; use std::path::Path; @@ -67,49 +68,63 @@ impl Ledger { /// Read the current HEAD branch name. pub fn head_branch(&self) -> anyhow::Result { - self.sqlite.head_branch() + self.sqlite.head_branch().context("Ledger::head_branch") } /// Write the HEAD branch name. pub fn set_head_branch(&self, name: &str) -> anyhow::Result<()> { - self.sqlite.set_head_branch(name) + self.sqlite + .set_head_branch(name) + .context("Ledger::set_head_branch") } // ── Events ────────────────────────────────────────────────────── /// Append an event to the ledger. Append-only (CONTRACT LEDGER-02). pub fn append_event(&self, event: &Event) -> anyhow::Result<()> { - self.sqlite.append_event(event) + self.sqlite + .append_event(event) + .with_context(|| format!("Ledger::append_event({})", event.event_id)) } /// Append an event idempotently. Returns `true` if inserted, `false` if duplicate. pub fn append_event_idempotent(&self, event: &Event) -> anyhow::Result { - self.sqlite.append_event_idempotent(event) + self.sqlite + .append_event_idempotent(event) + .with_context(|| format!("Ledger::append_event_idempotent({})", event.event_id)) } /// Get the hash of the last event, or `None` if the ledger is empty. pub fn last_event_hash(&self) -> anyhow::Result> { - self.sqlite.last_event_hash() + self.sqlite + .last_event_hash() + .context("Ledger::last_event_hash") } /// Read all events in the ledger. pub fn iter_events(&self) -> anyhow::Result> { - self.sqlite.iter_events() + self.sqlite.iter_events().context("Ledger::iter_events") } /// Get a single event by event_id. pub fn get_event(&self, event_id: &str) -> anyhow::Result> { - self.sqlite.get_event(event_id) + self.sqlite + .get_event(event_id) + .with_context(|| format!("Ledger::get_event({event_id})")) } /// Get all events of a given type, filtered at the SQL level. pub fn iter_events_by_type(&self, event_type: &str) -> anyhow::Result> { - self.sqlite.iter_events_by_type(event_type) + self.sqlite + .iter_events_by_type(event_type) + .with_context(|| format!("Ledger::iter_events_by_type({event_type})")) } /// Get all events for a specific branch, filtered at the SQL level. pub fn iter_branch_events(&self, branch: &str) -> anyhow::Result> { - self.sqlite.iter_branch_events(branch) + self.sqlite + .iter_branch_events(branch) + .with_context(|| format!("Ledger::iter_branch_events({branch})")) } /// Get events filtered by branch with optional type/keyword/date/limit, @@ -125,6 +140,7 @@ impl Ledger { ) -> anyhow::Result> { self.sqlite .iter_events_filtered(branch, event_type, keyword, after, before, limit) + .with_context(|| format!("Ledger::iter_events_filtered(branch={branch})")) } /// Find commit events related to a query by evidence chain or keyword match. @@ -137,6 +153,7 @@ impl Ledger { ) -> anyhow::Result> { self.sqlite .find_related_commits(branch, keyword, decision_event_ids, limit) + .context("Ledger::find_related_commits") } /// Find note events matching a keyword, excluding decisions and session digests. @@ -146,7 +163,9 @@ impl Ledger { keyword: &str, limit: usize, ) -> anyhow::Result> { - self.sqlite.find_related_notes(branch, keyword, limit) + self.sqlite + .find_related_notes(branch, keyword, limit) + .context("Ledger::find_related_notes") } /// Get all events with rowid strictly greater than `after_rowid`. @@ -154,24 +173,30 @@ impl Ledger { /// Returns `(rowid, Event)` pairs ordered by rowid, useful for cursor-based /// polling (e.g. SSE streaming). pub fn events_after_rowid(&self, after_rowid: i64) -> anyhow::Result> { - self.sqlite.events_after_rowid(after_rowid) + self.sqlite + .events_after_rowid(after_rowid) + .context("Ledger::events_after_rowid") } /// Look up the rowid for a given `event_id`. pub fn rowid_for_event_id(&self, event_id: &str) -> anyhow::Result> { - self.sqlite.rowid_for_event_id(event_id) + self.sqlite + .rowid_for_event_id(event_id) + .with_context(|| format!("Ledger::rowid_for_event_id({event_id})")) } // ── Branches JSON ─────────────────────────────────────────────── /// Read branches.json content. pub fn branches_json(&self) -> anyhow::Result { - self.sqlite.branches_json() + self.sqlite.branches_json().context("Ledger::branches_json") } /// Write branches.json content. pub fn set_branches_json(&self, value: &serde_json::Value) -> anyhow::Result<()> { - self.sqlite.set_branches_json(value) + self.sqlite + .set_branches_json(value) + .context("Ledger::set_branches_json") } // ── Decisions ─────────────────────────────────────────────────── @@ -187,6 +212,7 @@ impl Ledger { ) -> anyhow::Result> { self.sqlite .active_decisions(domain, key_pattern, after, before, None) + .with_context(|| format!("Ledger::active_decisions(domain={domain:?})")) } /// Query active decisions with limit for hot path optimization. @@ -200,6 +226,7 @@ impl Ledger { ) -> anyhow::Result> { self.sqlite .active_decisions(domain, key_pattern, after, before, Some(limit)) + .with_context(|| format!("Ledger::active_decisions_limited(domain={domain:?})")) } /// All decisions for a key (active + superseded), ordered by time. @@ -210,7 +237,9 @@ impl Ledger { after: Option<&str>, before: Option<&str>, ) -> anyhow::Result> { - self.sqlite.decision_timeline(key, after, before) + self.sqlite + .decision_timeline(key, after, before) + .with_context(|| format!("Ledger::decision_timeline(key={key})")) } /// All decisions for a domain (active + superseded), ordered by time. @@ -221,12 +250,14 @@ impl Ledger { after: Option<&str>, before: Option<&str>, ) -> anyhow::Result> { - self.sqlite.domain_timeline(domain, after, before) + self.sqlite + .domain_timeline(domain, after, before) + .with_context(|| format!("Ledger::domain_timeline(domain={domain})")) } /// Distinct domain values from active decisions. pub fn list_domains(&self) -> anyhow::Result> { - self.sqlite.list_domains() + self.sqlite.list_domains().context("Ledger::list_domains") } /// Compute aggregate statistics for a village's decisions. @@ -236,7 +267,9 @@ impl Ledger { after: Option<&str>, before: Option<&str>, ) -> anyhow::Result { - self.sqlite.village_stats(village_id, after, before) + self.sqlite + .village_stats(village_id, after, before) + .with_context(|| format!("Ledger::village_stats({village_id})")) } /// Detect recurring patterns in a village's decision history. @@ -248,6 +281,7 @@ impl Ledger { ) -> anyhow::Result> { self.sqlite .detect_village_patterns(village_id, after, min_occurrences) + .with_context(|| format!("Ledger::detect_village_patterns({village_id})")) } /// Find the active decision for a specific key on a branch. @@ -256,7 +290,9 @@ impl Ledger { branch: &str, key: &str, ) -> anyhow::Result> { - self.sqlite.find_active_decision(branch, key) + self.sqlite + .find_active_decision(branch, key) + .with_context(|| format!("Ledger::find_active_decision(branch={branch}, key={key})")) } /// Return active decisions that have non-empty `affected_paths`. @@ -266,7 +302,10 @@ impl Ledger { branch: Option<&str>, limit: Option, ) -> anyhow::Result> { - let rows = self.sqlite.active_decisions_with_paths(branch, limit)?; + let rows = self + .sqlite + .active_decisions_with_paths(branch, limit) + .context("Ledger::query_active_with_paths")?; Ok(rows.iter().map(view::to_view).collect()) } @@ -318,7 +357,9 @@ impl Ledger { /// Query active decisions with shared or global scope. pub fn shared_decisions(&self) -> anyhow::Result> { - self.sqlite.shared_decisions() + self.sqlite + .shared_decisions() + .context("Ledger::shared_decisions") } /// Check if a decision has already been imported from a source project. @@ -329,6 +370,7 @@ impl Ledger { ) -> anyhow::Result { self.sqlite .is_already_imported(source_project_id, source_event_id) + .context("Ledger::is_already_imported") } /// Insert an imported decision from another project. @@ -336,7 +378,9 @@ impl Ledger { &self, params: crate::sqlite_store::ImportParams<'_>, ) -> anyhow::Result<()> { - self.sqlite.insert_imported_decision(params) + self.sqlite + .insert_imported_decision(params) + .context("Ledger::insert_imported_decision") } // ── Decision Dependencies ──────────────────────────────────────── @@ -351,16 +395,21 @@ impl Ledger { ) -> anyhow::Result<()> { self.sqlite .insert_dep(source_key, target_key, dep_type, created_event) + .with_context(|| format!("Ledger::insert_dep({source_key} -> {target_key})")) } /// What does `key` depend on? pub fn deps_of(&self, key: &str) -> anyhow::Result> { - self.sqlite.deps_of(key) + self.sqlite + .deps_of(key) + .with_context(|| format!("Ledger::deps_of({key})")) } /// Who depends on `key`? pub fn dependents_of(&self, key: &str) -> anyhow::Result> { - self.sqlite.dependents_of(key) + self.sqlite + .dependents_of(key) + .with_context(|| format!("Ledger::dependents_of({key})")) } /// Who depends on `key`, joined with active decisions only. @@ -368,7 +417,9 @@ impl Ledger { &self, key: &str, ) -> anyhow::Result> { - self.sqlite.active_dependents_of(key) + self.sqlite + .active_dependents_of(key) + .with_context(|| format!("Ledger::active_dependents_of({key})")) } // ── Decision Outcomes ───────────────────────────────────────────── @@ -378,7 +429,9 @@ impl Ledger { &self, decision_event_id: &str, ) -> anyhow::Result> { - self.sqlite.decision_outcomes(decision_event_id) + self.sqlite + .decision_outcomes(decision_event_id) + .with_context(|| format!("Ledger::decision_outcomes({decision_event_id})")) } /// Get all execution events linked to a decision via `based_on` provenance. @@ -386,7 +439,9 @@ impl Ledger { &self, decision_event_id: &str, ) -> anyhow::Result> { - self.sqlite.executions_for_decision(decision_event_id) + self.sqlite + .executions_for_decision(decision_event_id) + .with_context(|| format!("Ledger::executions_for_decision({decision_event_id})")) } /// Transitive dependents of `key` via BFS, up to `max_depth` hops. @@ -396,14 +451,18 @@ impl Ledger { key: &str, max_depth: usize, ) -> anyhow::Result> { - self.sqlite.transitive_dependents_of(key, max_depth) + self.sqlite + .transitive_dependents_of(key, max_depth) + .with_context(|| format!("Ledger::transitive_dependents_of({key})")) } // ── Causal Chain ───────────────────────────────────────────────── /// Look up a single decision by event_id. pub fn get_decision_by_event_id(&self, event_id: &str) -> anyhow::Result> { - self.sqlite.get_decision_by_event_id(event_id) + self.sqlite + .get_decision_by_event_id(event_id) + .with_context(|| format!("Ledger::get_decision_by_event_id({event_id})")) } /// Traverse the causal chain from a root decision via unified BFS. @@ -412,7 +471,9 @@ impl Ledger { event_id: &str, max_depth: usize, ) -> anyhow::Result)>> { - self.sqlite.causal_chain(event_id, max_depth) + self.sqlite + .causal_chain(event_id, max_depth) + .with_context(|| format!("Ledger::causal_chain({event_id})")) } // ── Task Briefs ────────────────────────────────────────────────── @@ -422,7 +483,9 @@ impl Ledger { &self, task_id: &str, ) -> anyhow::Result> { - self.sqlite.get_task_brief(task_id) + self.sqlite + .get_task_brief(task_id) + .with_context(|| format!("Ledger::get_task_brief({task_id})")) } /// List task briefs, optionally filtered by status and/or intent. @@ -431,19 +494,25 @@ impl Ledger { status: Option<&str>, intent: Option<&str>, ) -> anyhow::Result> { - self.sqlite.list_task_briefs(status, intent) + self.sqlite + .list_task_briefs(status, intent) + .context("Ledger::list_task_briefs") } // ── Review Bundles ─────────────────────────────────────────────── /// Get a review bundle by bundle_id. pub fn get_bundle(&self, bundle_id: &str) -> anyhow::Result> { - self.sqlite.get_bundle(bundle_id) + self.sqlite + .get_bundle(bundle_id) + .with_context(|| format!("Ledger::get_bundle({bundle_id})")) } /// List review bundles, optionally filtered by status. pub fn list_bundles(&self, status: Option<&str>) -> anyhow::Result> { - self.sqlite.list_bundles(status) + self.sqlite + .list_bundles(status) + .context("Ledger::list_bundles") } // ── Device Tokens ─────────────────────────────────────────────── @@ -453,7 +522,9 @@ impl Ledger { &self, row: &crate::sqlite_store::DeviceTokenRow, ) -> anyhow::Result<()> { - self.sqlite.insert_device_token(row) + self.sqlite + .insert_device_token(row) + .context("Ledger::insert_device_token") } /// Validate a device token by its SHA-256 hash. Returns the row if active. @@ -461,12 +532,16 @@ impl Ledger { &self, token_hash: &str, ) -> anyhow::Result> { - self.sqlite.validate_device_token(token_hash) + self.sqlite + .validate_device_token(token_hash) + .context("Ledger::validate_device_token") } /// List all device tokens (active and revoked). pub fn list_device_tokens(&self) -> anyhow::Result> { - self.sqlite.list_device_tokens() + self.sqlite + .list_device_tokens() + .context("Ledger::list_device_tokens") } /// Revoke a device token by device name. Returns true if revoked. @@ -477,11 +552,14 @@ impl Ledger { ) -> anyhow::Result { self.sqlite .revoke_device_token(device_name, revoke_event_id) + .with_context(|| format!("Ledger::revoke_device_token({device_name})")) } /// Revoke all active device tokens. Returns count of revoked tokens. pub fn revoke_all_device_tokens(&self, revoke_event_id: &str) -> anyhow::Result { - self.sqlite.revoke_all_device_tokens(revoke_event_id) + self.sqlite + .revoke_all_device_tokens(revoke_event_id) + .context("Ledger::revoke_all_device_tokens") } // ── Decide Snapshots ──────────────────────────────────────────── @@ -491,7 +569,9 @@ impl Ledger { &self, row: &crate::sqlite_store::DecideSnapshotRow, ) -> anyhow::Result<()> { - self.sqlite.insert_snapshot(row) + self.sqlite + .insert_snapshot(row) + .context("Ledger::insert_snapshot") } /// Query snapshots with optional filtering by village_id and engine_version. @@ -503,6 +583,7 @@ impl Ledger { ) -> anyhow::Result> { self.sqlite .query_snapshots(village_id, engine_version, limit) + .context("Ledger::query_snapshots") } /// Find all snapshots for a given context_hash. @@ -510,7 +591,9 @@ impl Ledger { &self, context_hash: &str, ) -> anyhow::Result> { - self.sqlite.snapshots_by_context_hash(context_hash) + self.sqlite + .snapshots_by_context_hash(context_hash) + .with_context(|| format!("Ledger::snapshots_by_context_hash({context_hash})")) } // ── Suggestions ────────────────────────────────────────────────── @@ -520,7 +603,9 @@ impl Ledger { &self, row: &crate::sqlite_store::SuggestionRow, ) -> anyhow::Result<()> { - self.sqlite.insert_suggestion(row) + self.sqlite + .insert_suggestion(row) + .context("Ledger::insert_suggestion") } /// List suggestions filtered by status. @@ -528,7 +613,9 @@ impl Ledger { &self, status: &str, ) -> anyhow::Result> { - self.sqlite.list_suggestions_by_status(status) + self.sqlite + .list_suggestions_by_status(status) + .with_context(|| format!("Ledger::list_suggestions_by_status({status})")) } /// Get a single suggestion by id. @@ -536,7 +623,9 @@ impl Ledger { &self, id: &str, ) -> anyhow::Result> { - self.sqlite.get_suggestion(id) + self.sqlite + .get_suggestion(id) + .with_context(|| format!("Ledger::get_suggestion({id})")) } /// Update a suggestion's status and reviewed_at timestamp. @@ -549,6 +638,7 @@ impl Ledger { ) -> anyhow::Result { self.sqlite .update_suggestion_status(id, status, reviewed_at) + .with_context(|| format!("Ledger::update_suggestion_status({id})")) } } diff --git a/crates/edda-serve/src/lib.rs b/crates/edda-serve/src/lib.rs index 5ff0fb2..6591449 100644 --- a/crates/edda-serve/src/lib.rs +++ b/crates/edda-serve/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use std::collections::HashMap; use std::convert::Infallible; use std::net::SocketAddr; @@ -383,7 +384,7 @@ async fn auth_middleware( }; let token_hash = hash_token(raw_token); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("auth_middleware")?; let device = ledger.validate_device_token(&token_hash)?; match device { @@ -507,7 +508,7 @@ async fn complete_pairing( let event_id = format!("evt_{}", ulid::Ulid::new()); // Write device_pair event to ledger - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /pair")?; let branch = ledger.head_branch()?; let payload = serde_json::json!({ @@ -564,7 +565,7 @@ struct DeviceInfo { async fn list_paired_devices( State(state): State>, ) -> Result>, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/pair/list")?; let tokens = ledger.list_device_tokens()?; let devices: Vec = tokens @@ -596,7 +597,7 @@ async fn revoke_device( ) -> Result, AppError> { let Json(req) = body.map_err(|e| AppError::Validation(e.to_string()))?; - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/pair/revoke")?; // Check the token exists *before* writing the ledger event let existing = ledger.list_device_tokens()?; @@ -654,7 +655,7 @@ async fn revoke_all_devices( State(state): State>, ) -> Result, AppError> { let event_id = format!("evt_{}", ulid::Ulid::new()); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/pair/revoke-all")?; let branch = ledger.head_branch()?; let now = time::OffsetDateTime::now_utc(); @@ -715,7 +716,7 @@ struct LastCommit { } async fn get_status(State(state): State>) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/status")?; let head = ledger.head_branch()?; let snap = rebuild_branch(&ledger, &head)?; @@ -748,7 +749,7 @@ async fn get_context( State(state): State>, Query(params): Query, ) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/context")?; let head = ledger.head_branch()?; let depth = params.depth.unwrap_or(5); let text = render_context(&ledger, &head, DeriveOptions { depth })?; @@ -792,7 +793,7 @@ async fn get_decisions( validate_iso8601(before).map_err(AppError::Validation)?; } - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/decisions")?; let q = params .q .as_deref() @@ -877,7 +878,7 @@ async fn post_decisions_batch( )); } - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/decisions/batch")?; let mut results = Vec::with_capacity(body.queries.len()); for (i, sub) in body.queries.iter().enumerate() { @@ -946,7 +947,9 @@ async fn get_decision_outcomes( State(state): State>, AxumPath(event_id): AxumPath, ) -> Result { - let ledger = state.open_ledger()?; + let ledger = state + .open_ledger() + .context("GET /api/decisions/:id/outcomes")?; let outcomes = ledger.decision_outcomes(&event_id)?; match outcomes { @@ -1001,7 +1004,9 @@ async fn get_decision_chain( Query(params): Query, ) -> Result, AppError> { let depth = params.depth.unwrap_or(3).min(10); - let ledger = state.open_ledger()?; + let ledger = state + .open_ledger() + .context("GET /api/decisions/:id/chain")?; let (root, chain) = ledger .causal_chain(&event_id, depth)? @@ -1075,7 +1080,7 @@ async fn get_log( State(state): State>, Query(params): Query, ) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/log")?; let head = ledger.head_branch()?; let limit = params.limit.unwrap_or(50); @@ -1186,7 +1191,7 @@ struct MinimalStage { } async fn get_drafts(State(state): State>) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/drafts")?; let drafts_dir = &ledger.paths.drafts_dir; if !drafts_dir.exists() { @@ -1384,7 +1389,7 @@ async fn handle_draft_action( action: &str, body: &ApproveRequest, ) -> Result { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/drafts/:id/action")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; // Read the draft @@ -1641,7 +1646,7 @@ async fn post_note( ) -> Result { let Json(body) = body.map_err(|e| AppError::Validation(e.body_text()))?; - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/note")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -1689,7 +1694,7 @@ async fn post_decide( let key = key.trim(); let value = value.trim(); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/decide")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -1811,7 +1816,7 @@ async fn post_karvi_event( "decision_ref": body.decision_ref, }); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/events/karvi")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; let parent_hash = ledger.last_event_hash()?; @@ -1910,7 +1915,7 @@ async fn post_telemetry( "metadata": body.metadata, }); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/telemetry")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; let parent_hash = ledger.last_event_hash()?; @@ -1961,7 +1966,7 @@ async fn get_telemetry( State(state): State>, Query(q): Query, ) -> Result { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/telemetry")?; let branch = ledger.head_branch()?; let limit = q.limit.unwrap_or(100); @@ -2012,7 +2017,7 @@ async fn get_telemetry_stats( State(state): State>, Query(q): Query, ) -> Result { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/telemetry/stats")?; let branch = ledger.head_branch()?; let days = q.days.unwrap_or(7); @@ -2204,7 +2209,7 @@ async fn post_snapshot( )); } - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/snapshot")?; let _lock = WorkspaceLock::acquire(&ledger.paths)?; let branch = ledger.head_branch()?; @@ -2306,7 +2311,7 @@ async fn get_snapshots( State(state): State>, Query(query): Query, ) -> Result { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/snapshots")?; let rows = ledger.query_snapshots( query.village_id.as_deref(), query.engine_version.as_deref(), @@ -2328,7 +2333,7 @@ async fn get_snapshots_by_hash( State(state): State>, AxumPath(context_hash): AxumPath, ) -> Result { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/snapshots/:hash")?; let rows = ledger.snapshots_by_context_hash(&context_hash)?; if rows.is_empty() { @@ -2368,7 +2373,7 @@ async fn get_village_stats( validate_iso8601(before).map_err(AppError::Validation)?; } - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/villages/:id/stats")?; let stats = ledger.village_stats( &village_id, params.after.as_deref(), @@ -2409,7 +2414,7 @@ async fn get_patterns( .format(&time::format_description::well_known::Rfc3339) .unwrap_or_default(); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/patterns")?; let patterns = ledger.detect_village_patterns(village_id, &after_str, min_occurrences)?; let total = patterns.len(); @@ -2793,7 +2798,7 @@ struct ActorsListResponse { async fn get_actors( State(state): State>, ) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/actors")?; let cfg = policy::load_actors_from_dir(&ledger.paths.edda_dir)?; let actors = cfg .actors @@ -2816,7 +2821,7 @@ async fn get_actor( State(state): State>, AxumPath(name): AxumPath, ) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/actors/:name")?; let cfg = policy::load_actors_from_dir(&ledger.paths.edda_dir)?; match cfg.actors.get(&name) { Some(def) => Ok(Json(ActorResponse { @@ -2847,7 +2852,7 @@ async fn get_quality_metrics( after: params.after, before: params.before, }; - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/metrics/quality")?; let events = ledger.iter_events_by_type("execution_event")?; let report = model_quality_from_events(&events, &range); Ok(Json(report)) @@ -2876,7 +2881,9 @@ async fn get_controls_suggestions( after: params.after, before: params.before, }; - let ledger = state.open_ledger()?; + let ledger = state + .open_ledger() + .context("GET /api/controls/suggestions")?; let events = ledger.iter_events_by_type("execution_event")?; let report = model_quality_from_events(&events, &range); @@ -3361,7 +3368,7 @@ async fn post_approval_check( // Build ReviewBundle from request or from ledger let bundle = if let Some(bundle_id) = &body.bundle_id { - let ledger = Ledger::open(&state.repo_root)?; + let ledger = Ledger::open(&state.repo_root).context("POST /api/approval/check")?; let Some(row) = ledger.get_bundle(bundle_id)? else { return Err(AppError::NotFound(format!( "Bundle '{}' not found", @@ -3511,7 +3518,7 @@ async fn post_sync( dry_run: false, }); - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("POST /api/sync")?; let sources = if let Some(name) = &body.from { sources_from_name(name) @@ -3565,7 +3572,7 @@ async fn get_briefs( State(state): State>, Query(params): Query, ) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/briefs")?; let briefs = ledger.list_task_briefs(params.status.as_deref(), params.intent.as_deref())?; let items: Vec = briefs @@ -3600,7 +3607,7 @@ async fn get_brief( State(state): State>, AxumPath(task_id): AxumPath, ) -> Result, AppError> { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/briefs/:task_id")?; let brief = ledger .get_task_brief(&task_id)? .ok_or_else(|| AppError::NotFound(format!("task brief not found: {task_id}")))?; @@ -4009,7 +4016,7 @@ async fn get_event_stream( // Resolve the initial cursor (rowid) from `since` event_id. let mut cursor: i64 = if let Some(ref event_id) = since { - let ledger = state.open_ledger()?; + let ledger = state.open_ledger().context("GET /api/events/stream")?; ledger.rowid_for_event_id(event_id)?.unwrap_or(0) } else { 0