use serde::{Deserialize, Serialize};
use super::action::{Action, DecisionClass};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionTier {
Coordinator,
Decider,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DecisionEnvelope {
pub actor: String,
pub input_artifacts: Vec<String>,
pub reason: String,
pub decision_tier: DecisionTier,
pub model: String,
pub prompt_version: String,
}
impl DecisionEnvelope {
pub fn validate_for(&self, action: &Action) -> Result<(), TierViolation> {
if action.decision_class() == DecisionClass::Consequential
&& self.decision_tier == DecisionTier::Coordinator
{
return Err(TierViolation {
action: action.name().to_string(),
actor: self.actor.clone(),
reason: self.reason.clone(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[error("tier violation: consequential action `{action}` was stamped coordinator-tier (actor `{actor}`, reason: {reason})")]
pub struct TierViolation {
pub action: String,
pub actor: String,
pub reason: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::action::SpinoffScope;
fn envelope(tier: DecisionTier) -> DecisionEnvelope {
DecisionEnvelope {
actor: match tier {
DecisionTier::Coordinator => "coordinator".into(),
DecisionTier::Decider => "decider".into(),
},
input_artifacts: vec!["verify:v1".into()],
reason: "test".into(),
decision_tier: tier,
model: "test-model".into(),
prompt_version: "v1".into(),
}
}
#[test]
fn consequential_on_coordinator_is_a_violation() {
let env = envelope(DecisionTier::Coordinator);
let err = env.validate_for(&Action::DeclareConverged).unwrap_err();
assert_eq!(err.action, "declare_converged");
assert_eq!(err.actor, "coordinator");
}
#[test]
fn consequential_on_decider_is_ok() {
let env = envelope(DecisionTier::Decider);
assert!(env.validate_for(&Action::DeclareConverged).is_ok());
}
#[test]
fn routine_on_either_tier_is_ok() {
let action = Action::AcceptChunk {
chunk_id: "c1".into(),
};
assert!(envelope(DecisionTier::Coordinator)
.validate_for(&action)
.is_ok());
assert!(envelope(DecisionTier::Decider)
.validate_for(&action)
.is_ok());
}
#[test]
fn trivial_spinoff_on_coordinator_is_ok_but_nontrivial_is_not() {
let trivial = Action::ProposeSpinoff {
title: "t".into(),
kind: "improvement".into(),
rationale: "r".into(),
scope: SpinoffScope::Trivial,
};
assert!(envelope(DecisionTier::Coordinator)
.validate_for(&trivial)
.is_ok());
let nontrivial = Action::ProposeSpinoff {
title: "t".into(),
kind: "refactor".into(),
rationale: "r".into(),
scope: SpinoffScope::Substantial,
};
assert!(envelope(DecisionTier::Coordinator)
.validate_for(&nontrivial)
.is_err());
}
#[test]
fn envelope_round_trips_through_serde() {
let env = envelope(DecisionTier::Decider);
let v = serde_json::to_value(&env).unwrap();
assert_eq!(v["decision_tier"], serde_json::json!("decider"));
let back: DecisionEnvelope = serde_json::from_value(v).unwrap();
assert_eq!(back, env);
}
}