Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/edda-aggregate/src/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,11 @@ mod tests {
value: "sqlite".to_string(),
reason: Some("embedded".to_string()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let event =
edda_core::event::new_decision_event("main", None, "system", &decision).unwrap();
Expand Down
5 changes: 5 additions & 0 deletions crates/edda-aggregate/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ mod tests {
value: value.to_string(),
reason: reason.map(|r| r.to_string()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
}
}

Expand Down
5 changes: 5 additions & 0 deletions crates/edda-bridge-claude/src/digest/orchestrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ pub(super) fn harvest_inferred_decisions(
value: pkg.to_string(),
reason: Some(reason.clone()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let mut event =
edda_core::event::new_decision_event(branch, chain_hash.as_deref(), "system", &dp)
Expand Down
5 changes: 5 additions & 0 deletions crates/edda-bridge-claude/src/digest/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,11 @@ fn collect_session_ledger_extras_basic() {
value: "jwt".to_string(),
reason: Some("stateless".to_string()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let evt = edda_core::event::new_decision_event(&branch, None, "system", &dp).unwrap();
let decision_ts = evt.ts.clone();
Expand Down
10 changes: 10 additions & 0 deletions crates/edda-cli/src/cmd_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,11 @@ pub fn decide(
value: value.to_string(),
reason: reason.map(|r| r.to_string()),
scope,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let mut event =
edda_core::event::new_decision_event(&branch, parent_hash.as_deref(), actor, &dp)?;
Expand Down Expand Up @@ -991,6 +996,11 @@ fn write_accepted_to_ledger(
value: d.value.clone(),
reason: d.reason.clone(),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};

let actor = match d.kind {
Expand Down
29 changes: 29 additions & 0 deletions crates/edda-core/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,35 @@ pub fn extract_decision(payload: &Value) -> Option<DecisionPayload> {
.get("scope")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok());
// V10 fields — all Option, gracefully default to None if missing
let authority = d
.get("authority")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let affected_paths: Option<Vec<String>> = d
.get("affected_paths")
.and_then(|v| serde_json::from_value(v.clone()).ok());
let tags: Option<Vec<String>> = d
.get("tags")
.and_then(|v| serde_json::from_value(v.clone()).ok());
let review_after = d
.get("review_after")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let reversibility = d
.get("reversibility")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
return Some(DecisionPayload {
key,
value,
reason,
scope,
authority,
affected_paths,
tags,
review_after,
reversibility,
});
}
// Text fallback: "key: value — reason"
Expand All @@ -57,6 +81,11 @@ pub fn extract_decision(payload: &Value) -> Option<DecisionPayload> {
value,
reason,
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
})
}

Expand Down
15 changes: 15 additions & 0 deletions crates/edda-core/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,11 @@ mod tests {
value: "sqlite".to_string(),
reason: Some("embedded, zero-config".to_string()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let event = new_decision_event("main", None, "system", &dp).unwrap();
assert_eq!(event.event_type, "note");
Expand All @@ -802,6 +807,11 @@ mod tests {
value: "JWT".to_string(),
reason: None,
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let event = new_decision_event("main", None, "system", &dp).unwrap();
assert_eq!(event.payload["decision"]["key"], "auth.method");
Expand All @@ -817,6 +827,11 @@ mod tests {
value: "REST".to_string(),
reason: Some("compatibility".to_string()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let event = new_decision_event("main", None, "system", &dp).unwrap();
let extracted = crate::decision::extract_decision(&event.payload).unwrap();
Expand Down
72 changes: 72 additions & 0 deletions crates/edda-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ pub struct DecisionPayload {
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<DecisionScope>,
/// Decision authority: "human", "agent", "system". Default: "human".
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authority: Option<String>,
/// Glob patterns for guarded file paths. Default: [].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub affected_paths: Option<Vec<String>>,
/// Categorization tags. Default: [].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
/// ISO-8601 date for scheduled re-evaluation. Default: None.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub review_after: Option<String>,
/// Reversibility level: "easy", "medium", "hard". Default: "medium".
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reversibility: Option<String>,
}

/// Status of a task brief.
Expand Down Expand Up @@ -460,6 +475,11 @@ mod tests {
value: "sqlite".to_string(),
reason: Some("embedded, zero-config".to_string()),
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let json = serde_json::to_string(&dp).expect("serialize");
let decoded: DecisionPayload = serde_json::from_str(&json).expect("deserialize");
Expand All @@ -471,6 +491,11 @@ mod tests {
value: "JWT".to_string(),
reason: None,
scope: None,
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let json2 = serde_json::to_string(&dp_no_reason).expect("serialize");
assert!(!json2.contains("reason"), "None reason should be omitted");
Expand All @@ -486,6 +511,11 @@ mod tests {
value: "v3".to_string(),
reason: Some("breaking change".to_string()),
scope: Some(DecisionScope::Shared),
authority: None,
affected_paths: None,
tags: None,
review_after: None,
reversibility: None,
};
let json = serde_json::to_string(&dp).expect("serialize");
assert!(json.contains("\"scope\":\"shared\""));
Expand Down Expand Up @@ -664,4 +694,46 @@ mod tests {
let decoded: TaskBriefIntent = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, TaskBriefIntent::Fix);
}

// ── DecisionPayload V10 fields tests ─────────────────────────

#[test]
fn test_decision_payload_serde_backward_compat() {
let json = r#"{"key":"db.engine","value":"sqlite","reason":"embedded"}"#;
let dp: DecisionPayload = serde_json::from_str(json).unwrap();
assert_eq!(dp.key, "db.engine");
assert_eq!(dp.authority, None);
assert_eq!(dp.affected_paths, None);
assert_eq!(dp.tags, None);
assert_eq!(dp.review_after, None);
assert_eq!(dp.reversibility, None);
}

#[test]
fn test_decision_payload_serde_with_new_fields() {
let json = r#"{
"key": "db.engine",
"value": "sqlite",
"reason": "embedded",
"authority": "agent",
"affected_paths": ["crates/edda-ledger/**"],
"tags": ["arch"],
"review_after": "2026-06-01",
"reversibility": "hard"
}"#;
let dp: DecisionPayload = serde_json::from_str(json).unwrap();
assert_eq!(dp.authority.as_deref(), Some("agent"));
assert_eq!(
dp.affected_paths,
Some(vec!["crates/edda-ledger/**".to_string()])
);
assert_eq!(dp.tags, Some(vec!["arch".to_string()]));
assert_eq!(dp.review_after.as_deref(), Some("2026-06-01"));
assert_eq!(dp.reversibility.as_deref(), Some("hard"));

// Round-trip: serialize and deserialize
let serialized = serde_json::to_string(&dp).unwrap();
let dp2: DecisionPayload = serde_json::from_str(&serialized).unwrap();
assert_eq!(dp, dp2);
}
}
2 changes: 2 additions & 0 deletions crates/edda-derive/src/context/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ pub(crate) mod digest_helpers {
)
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn make_digest_note_with_tasks(
branch: &str,
session_id: &str,
Expand All @@ -317,6 +318,7 @@ pub(crate) mod digest_helpers {
})
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn make_digest_note_full(
branch: &str,
session_id: &str,
Expand Down
Loading
Loading