use serde::Serialize;
use super::partition::StatusClass;
use crate::backlog_order::OverrideReason;
use crate::comparison::ClaimTier;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ReasonKind {
Eligibility {
status: Option<String>,
class: StatusClass,
},
BlockedBy { items: Vec<String> },
Blocking { items: Vec<String> },
Score {
base: f64,
value_dim: f64,
risk_dim: f64,
leverage: f64,
optionality: f64,
total: f64,
},
EvictedEdge {
from: String,
to: String,
reason: OverrideReason,
},
CycleDegraded { nodes: Vec<String> },
ValuePin {
value: f64,
conflict: Vec<String>,
by: Option<String>,
date: Option<String>,
basis: Option<String>,
contested: Option<ContestedClaim>,
},
ValueClaim {
value: f64,
tier: ClaimTier,
conflict: Vec<String>,
by: Option<String>,
date: Option<String>,
contested: Option<ContestedClaim>,
},
#[cfg_attr(
not(test),
expect(dead_code, reason = "SL-222 PHASE-09: used in tests")
)]
ValueUnmigratedFacet,
ValueProjected {
value: f64,
lower: Option<f64>,
upper: Option<f64>,
human: u32,
agent: u32,
},
ValueGauge { value: f64, judgements: u32 },
CostPin {
est_cost: f64,
lower: f64,
upper: f64,
beta: f64,
by: Option<String>,
date: Option<String>,
basis: Option<String>,
contested: Option<ContestedClaim>,
},
CostClaim {
est_cost: f64,
lower: f64,
upper: f64,
beta: f64,
tier: ClaimTier,
by: Option<String>,
date: Option<String>,
conflict: Vec<String>,
},
CostClassAnchor { est_cost: f64 },
#[cfg_attr(
not(test),
expect(dead_code, reason = "SL-222 PHASE-09: used in tests")
)]
CostUnmigratedFacet,
CostProjected {
est_cost: f64,
lower: Option<f64>,
upper: Option<f64>,
human: u32,
agent: u32,
},
CostBareAnchor {
est_cost: f64,
max_estimate: Option<f64>,
margin: f64,
},
CostGauge {
est_cost: f64,
max_estimate: Option<f64>,
margin: f64,
judgements: u32,
},
PriorityDomainDisclosure { count: usize },
AgentEvidenceDemoted,
Tension {
preferred: String,
surfaced: String,
cause: TensionCauseView,
grade: TensionGradeView,
},
ZeroWeightExcluded { count: usize },
}
impl ReasonKind {
pub(crate) fn value_source_token(&self) -> Option<&'static str> {
match self {
ReasonKind::ValuePin { .. } => Some("pin"),
ReasonKind::ValueClaim { tier, .. } => Some(match tier {
ClaimTier::Pin => "pin",
ClaimTier::Human => "human-claim",
ClaimTier::Agent => "agent-claim",
ClaimTier::Migrated => "migrated-claim",
}),
ReasonKind::ValueUnmigratedFacet => Some("unmigrated-facet"),
ReasonKind::ValueProjected { .. } => Some("projected"),
ReasonKind::ValueGauge { .. } => Some("gauge"),
_ => None,
}
}
pub(crate) fn cost_source_token(&self) -> Option<&'static str> {
match self {
ReasonKind::CostPin { .. } => Some("pin"),
ReasonKind::CostClaim { tier, .. } => Some(match tier {
ClaimTier::Pin => "pin",
ClaimTier::Human => "human-claim",
ClaimTier::Agent => "agent-claim",
ClaimTier::Migrated => "migrated-claim",
}),
ReasonKind::CostClassAnchor { .. } => Some("class-anchor"),
ReasonKind::CostUnmigratedFacet => Some("unmigrated-facet"),
ReasonKind::CostProjected { .. } => Some("projected"),
ReasonKind::CostGauge { .. } => Some("gauge"),
ReasonKind::CostBareAnchor { .. } => Some("bare-anchor"),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ContestedClaim {
pub(crate) low: f64,
pub(crate) high: f64,
pub(crate) rows: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum TensionCauseView {
Structure { edge_from: String, verb: EdgeVerb },
Composition {
risk_dim: f64,
leverage: f64,
optionality: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EdgeVerb {
After,
Needs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TensionGradeView {
Determined { human: u32, agent: u32 },
AgentProposed { agent: u32 },
Projected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Actionability {
Actionable,
Blocked,
}
impl Actionability {
pub(crate) fn token(self) -> &'static str {
match self {
Actionability::Actionable => "actionable",
Actionability::Blocked => "blocked",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SurveyRow {
pub(crate) id: String,
pub(crate) title: String,
pub(crate) kind: String,
pub(crate) status: String,
pub(crate) act: Actionability,
pub(crate) score: f64,
pub(crate) blockers: Vec<String>,
pub(crate) reasons: Vec<ReasonKind>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct NextRow {
pub(crate) id: String,
pub(crate) title: String,
pub(crate) kind: String,
pub(crate) status: String,
pub(crate) act: Actionability,
pub(crate) score: f64,
pub(crate) reasons: Vec<ReasonKind>,
pub(crate) blockers: Vec<String>,
pub(crate) blocking: Vec<String>,
pub(crate) cost_source: Option<ReasonKind>,
pub(crate) value_source: Option<ReasonKind>,
pub(crate) tags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct NextView {
pub(crate) rows: Vec<NextRow>,
pub(crate) tensions: Vec<ReasonKind>,
pub(crate) zero_weight: Option<ReasonKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BlockersView {
pub(crate) id: String,
pub(crate) transitive: bool,
pub(crate) blocked_by: Vec<String>,
pub(crate) blocking: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ActionabilityBlock {
pub(crate) eligible: bool,
pub(crate) actionable: bool,
pub(crate) blockers: Vec<String>,
pub(crate) blocking: Vec<String>,
pub(crate) score: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Explanation {
pub(crate) id: String,
pub(crate) eligibility: ReasonKind,
pub(crate) blocker_chain: Vec<ReasonKind>,
pub(crate) evictions: Vec<ReasonKind>,
pub(crate) score: ReasonKind,
pub(crate) value_source: Option<ReasonKind>,
pub(crate) cost_source: Option<ReasonKind>,
pub(crate) priority_disclosure: Option<ReasonKind>,
pub(crate) agent_demotion: Option<ReasonKind>,
pub(crate) tensions: Vec<ReasonKind>,
pub(crate) on_frontier: bool,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ActionabilityNode {
pub(crate) id: String,
pub(crate) title: String,
pub(crate) kind: String,
pub(crate) status: String,
pub(crate) actionability: String,
pub(crate) score: f64,
pub(crate) rank: u32,
pub(crate) blockers: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ActionabilityEdge {
pub(crate) source: String,
pub(crate) target: String,
pub(crate) kind: String,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ActionabilityView {
pub(crate) kind: String,
pub(crate) policy_version: String,
pub(crate) nodes: Vec<ActionabilityNode>,
pub(crate) edges: Vec<ActionabilityEdge>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_source_token_vocabulary_golden_post_flip() {
let shapes: Vec<(ReasonKind, &str)> = vec![
(
ReasonKind::ValuePin {
value: 6.5,
conflict: vec![],
by: None,
date: None,
basis: None,
contested: None,
},
"pin",
),
(
ReasonKind::ValueClaim {
value: 6.2,
tier: ClaimTier::Human,
conflict: vec![],
by: None,
date: None,
contested: None,
},
"human-claim",
),
(
ReasonKind::ValueProjected {
value: 4.0,
lower: Some(3.2),
upper: Some(9.1),
human: 7,
agent: 2,
},
"projected",
),
(
ReasonKind::ValueGauge {
value: 1.0,
judgements: 3,
},
"gauge",
),
(
ReasonKind::ValueClaim {
value: 3.0,
tier: ClaimTier::Agent,
conflict: vec![],
by: None,
date: None,
contested: None,
},
"agent-claim",
),
(
ReasonKind::ValueClaim {
value: 3.0,
tier: ClaimTier::Migrated,
conflict: vec![],
by: None,
date: None,
contested: None,
},
"migrated-claim",
),
(ReasonKind::ValueUnmigratedFacet, "unmigrated-facet"),
];
let tokens: Vec<&str> = shapes
.iter()
.map(|(reason, _)| reason.value_source_token().expect("a value source"))
.collect();
let expected: Vec<&str> = shapes.iter().map(|&(_, token)| token).collect();
assert_eq!(tokens, expected, "per-shape token map");
let vocabulary: std::collections::BTreeSet<&str> = tokens.into_iter().collect();
let pinned: std::collections::BTreeSet<&str> = [
"pin",
"human-claim",
"agent-claim",
"migrated-claim",
"unmigrated-facet",
"projected",
"gauge",
]
.into_iter()
.collect();
assert_eq!(vocabulary, pinned, "full post-flip token set (D11)");
assert!(!vocabulary.contains("authored"), "authored removed (D11)");
assert_eq!(
ReasonKind::BlockedBy { items: vec![] }.value_source_token(),
None
);
}
#[test]
fn value_claim_pin_tier_reads_pin_token() {
let reason = ReasonKind::ValueClaim {
value: 1.0,
tier: ClaimTier::Pin,
conflict: vec![],
by: None,
date: None,
contested: None,
};
assert_eq!(reason.value_source_token(), Some("pin"));
}
#[test]
fn cost_source_token_vocabulary_golden_post_flip() {
let shapes: Vec<(ReasonKind, &str)> = vec![
(
ReasonKind::CostPin {
est_cost: 5.9,
lower: 2.0,
upper: 8.0,
beta: 0.65,
by: Some("david".into()),
date: Some("2026-07-17".into()),
basis: Some("ASM-014".into()),
contested: None,
},
"pin",
),
(
ReasonKind::CostClaim {
est_cost: 5.9,
lower: 2.0,
upper: 8.0,
beta: 0.65,
tier: ClaimTier::Human,
by: Some("david".into()),
date: Some("2026-07-17".into()),
conflict: vec![],
},
"human-claim",
),
(
ReasonKind::CostClaim {
est_cost: 2.7,
lower: 1.0,
upper: 4.0,
beta: 0.65,
tier: ClaimTier::Agent,
by: Some("claude".into()),
date: Some("2026-07-12".into()),
conflict: vec![],
},
"agent-claim",
),
(
ReasonKind::CostClaim {
est_cost: 2.1,
lower: 1.0,
upper: 3.0,
beta: 0.65,
tier: ClaimTier::Migrated,
by: None,
date: Some("2026-07-17".into()),
conflict: vec![],
},
"migrated-claim",
),
(
ReasonKind::CostClassAnchor { est_cost: 4.0 },
"class-anchor",
),
(ReasonKind::CostUnmigratedFacet, "unmigrated-facet"),
(
ReasonKind::CostProjected {
est_cost: 3.4,
lower: Some(2.0),
upper: Some(5.65),
human: 3,
agent: 1,
},
"projected",
),
(
ReasonKind::CostGauge {
est_cost: 11.0,
max_estimate: Some(10.0),
margin: 1.0,
judgements: 3,
},
"gauge",
),
(
ReasonKind::CostBareAnchor {
est_cost: 11.0,
max_estimate: Some(10.0),
margin: 1.0,
},
"bare-anchor",
),
];
let tokens: Vec<&str> = shapes
.iter()
.map(|(reason, _)| reason.cost_source_token().expect("a cost source"))
.collect();
let expected: Vec<&str> = shapes.iter().map(|&(_, token)| token).collect();
assert_eq!(tokens, expected, "per-shape token map");
let vocabulary: std::collections::BTreeSet<&str> = tokens.into_iter().collect();
let pinned: std::collections::BTreeSet<&str> = [
"pin",
"human-claim",
"agent-claim",
"migrated-claim",
"class-anchor",
"unmigrated-facet",
"projected",
"gauge",
"bare-anchor",
]
.into_iter()
.collect();
assert_eq!(vocabulary, pinned, "full post-flip cost_source token set");
assert!(
!vocabulary.contains("authored"),
"authored removed from cost_source"
);
assert_eq!(
ReasonKind::BlockedBy { items: vec![] }.cost_source_token(),
None
);
}
}