use crate::sqlite_store::DecisionRow;
#[derive(Debug, Clone, serde::Serialize)]
pub struct DecisionView {
pub event_id: String,
pub branch: String,
pub ts: Option<String>,
pub key: String,
pub value: String,
pub reason: String,
pub domain: String,
pub status: String,
pub authority: String,
pub reversibility: String,
pub affected_paths: Vec<String>,
pub tags: Vec<String>,
pub propagation: String,
pub supersedes_id: Option<String>,
pub review_after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub village_id: Option<String>,
}
pub fn to_view(row: &DecisionRow) -> DecisionView {
let affected_paths: Vec<String> = serde_json::from_str(&row.affected_paths).unwrap_or_default();
let tags: Vec<String> = serde_json::from_str(&row.tags).unwrap_or_default();
DecisionView {
event_id: row.event_id.clone(),
branch: row.branch.clone(),
ts: row.ts.clone(),
key: row.key.clone(),
value: row.value.clone(),
reason: row.reason.clone(),
domain: row.domain.clone(),
status: row.status.clone(),
authority: row.authority.clone(),
reversibility: row.reversibility.clone(),
affected_paths,
tags,
propagation: row.scope.clone(),
supersedes_id: row.supersedes_id.clone(),
review_after: row.review_after.clone(),
village_id: row.village_id.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_default_row() -> DecisionRow {
DecisionRow {
event_id: "evt_test".to_string(),
key: "db.engine".to_string(),
value: "sqlite".to_string(),
reason: "embedded".to_string(),
domain: "db".to_string(),
branch: "main".to_string(),
supersedes_id: None,
is_active: true,
ts: Some("2026-03-20T00:00:00Z".to_string()),
scope: "local".to_string(),
source_project_id: None,
source_event_id: None,
status: "active".to_string(),
authority: "human".to_string(),
affected_paths: "[]".to_string(),
tags: "[]".to_string(),
review_after: None,
reversibility: "medium".to_string(),
village_id: None,
}
}
fn make_row_with_paths(affected_paths: &str, tags: &str) -> DecisionRow {
let mut row = make_default_row();
row.affected_paths = affected_paths.to_string();
row.tags = tags.to_string();
row
}
#[test]
fn to_view_parses_json_arrays() {
let row = make_row_with_paths(
r#"["crates/edda-ledger/**", "crates/edda-core/**"]"#,
r#"["architecture", "storage"]"#,
);
let view = to_view(&row);
assert_eq!(
view.affected_paths,
vec!["crates/edda-ledger/**", "crates/edda-core/**"]
);
assert_eq!(view.tags, vec!["architecture", "storage"]);
}
#[test]
fn to_view_defaults_empty_on_empty_array() {
let row = make_row_with_paths("[]", "[]");
let view = to_view(&row);
assert!(view.affected_paths.is_empty());
assert!(view.tags.is_empty());
}
#[test]
fn to_view_defaults_empty_on_invalid_json() {
let row = make_row_with_paths("not json", "{bad}");
let view = to_view(&row);
assert!(view.affected_paths.is_empty());
assert!(view.tags.is_empty());
}
#[test]
fn to_view_renames_scope_to_propagation() {
let mut row = make_default_row();
row.scope = "shared".to_string();
let view = to_view(&row);
assert_eq!(view.propagation, "shared");
}
#[test]
fn decision_view_has_no_is_active() {
let view = to_view(&make_default_row());
let _ = view.status; }
#[test]
fn to_view_preserves_review_after() {
let mut row = make_default_row();
row.review_after = Some("2026-06-01".to_string());
let view = to_view(&row);
assert_eq!(view.review_after, Some("2026-06-01".to_string()));
}
}