use std::path::Path;
use std::collections::{BTreeMap, BTreeSet};
use crate::catalog::scan::ScanMode;
use crate::comparison::{
self, ClaimTier, ConstraintSet, Judgement, Projection as ValueProjection, QuarantinePolicy,
RaterCounts, Reachability, compile_human_only, constraining_counts_by_class, determined,
};
use crate::relation_graph::{self, EntityKey};
use super::channels;
use super::config::PriorityConfig;
use super::elicit::pair_side;
use super::graph::{self, NodeAttr, PriorityGraph};
use super::order::{frontier_order, surviving_seq_predecessors};
use super::partition::{StatusClass, status_class};
use super::tension::{
self, DetectInputs, EdgeKind, EvidenceGrade, PredEdge, Tension, TensionCause,
};
use super::view::{
Actionability, ActionabilityBlock, ActionabilityEdge, ActionabilityNode, ActionabilityView,
BlockersView, ContestedClaim, EdgeVerb, Explanation, NextRow, NextView, ReasonKind, SurveyRow,
TensionCauseView, TensionGradeView,
};
fn attr(g: &PriorityGraph, key: EntityKey) -> Option<&NodeAttr> {
g.attrs.get(&key)
}
fn kind_of(g: &PriorityGraph, key: EntityKey) -> String {
attr(g, key).map_or_else(|| key.prefix.to_string(), |a| a.kind.prefix.to_string())
}
fn title_of(g: &PriorityGraph, key: EntityKey) -> String {
attr(g, key).map_or_else(|| key.canonical(), |a| a.title.clone())
}
fn status_of(g: &PriorityGraph, key: EntityKey) -> String {
attr(g, key)
.and_then(|a| a.status.clone())
.unwrap_or_else(|| "—".to_string())
}
fn class_of(g: &PriorityGraph, key: EntityKey) -> StatusClass {
match attr(g, key) {
Some(a) => status_class(a.kind, a.status.as_deref()),
None => StatusClass::Unrecognised,
}
}
fn eligibility_reason(g: &PriorityGraph, key: EntityKey) -> ReasonKind {
ReasonKind::Eligibility {
status: attr(g, key).and_then(|a| a.status.clone()),
class: class_of(g, key),
}
}
fn score_reason(g: &PriorityGraph, key: EntityKey) -> ReasonKind {
ReasonKind::Score {
base: channels::base(g, key),
value_dim: channels::value_dim(g, key),
risk_dim: channels::risk_dim(g, key),
leverage: channels::leverage(g, key),
optionality: channels::optionality(g, key),
total: channels::score(g, key),
}
}
fn refs(keys: &[EntityKey]) -> Vec<String> {
keys.iter().map(|k| k.canonical()).collect()
}
fn actionability(g: &PriorityGraph, key: EntityKey) -> Actionability {
if channels::blocked(g, key) {
Actionability::Blocked
} else {
Actionability::Actionable
}
}
struct SurveyDecorated {
key: EntityKey,
act: Actionability,
score: f64,
blockers: Vec<String>,
}
fn act_rank(a: Actionability) -> u8 {
match a {
Actionability::Actionable => 0,
Actionability::Blocked => 1,
}
}
pub(crate) fn survey_for_map(g: &PriorityGraph, all: bool) -> Vec<SurveyRow> {
let mut rows: Vec<SurveyDecorated> = g
.attrs
.keys()
.copied()
.filter(|&k| {
if all {
return true;
}
channels::eligible(g, k) && !channels::promoted(g, k)
})
.map(|k| SurveyDecorated {
key: k,
act: actionability(g, k),
score: channels::score(g, k),
blockers: refs(&channels::blocked_by(g, k)),
})
.collect();
rows.sort_by(|a, b| {
let act = act_rank(a.act).cmp(&act_rank(b.act));
let score = b.score.total_cmp(&a.score); act.then(score).then_with(|| a.key.cmp(&b.key))
});
rows.into_iter()
.map(|d| {
let mut reasons = vec![eligibility_reason(g, d.key)];
if !d.blockers.is_empty() {
reasons.push(ReasonKind::BlockedBy {
items: d.blockers.clone(),
});
}
reasons.push(score_reason(g, d.key));
SurveyRow {
id: d.key.canonical(),
title: title_of(g, d.key),
kind: kind_of(g, d.key),
status: status_of(g, d.key),
act: d.act,
score: d.score,
blockers: d.blockers,
reasons,
}
})
.collect()
}
pub(crate) fn survey(root: &Path, all: bool, hide_blocked: bool) -> anyhow::Result<Vec<SurveyRow>> {
let g = graph::build(root)?;
let mut rows = survey_for_map(&g, all);
if hide_blocked {
rows.retain(|r| r.act == Actionability::Actionable);
}
Ok(rows)
}
pub(crate) fn survey_view_for_map(g: &PriorityGraph, all: bool) -> ActionabilityView {
use std::collections::{BTreeMap, BTreeSet, VecDeque};
let rows: Vec<_> = survey_for_map(g, all)
.into_iter()
.filter(|r| {
crate::kinds::is_value_bearing(r.kind.as_str())
})
.collect();
let key_by_id: BTreeMap<String, EntityKey> = rows
.iter()
.filter_map(|r| parse_key(&r.id).ok().map(|k| (r.id.clone(), k)))
.collect();
let node_keys: BTreeSet<EntityKey> = key_by_id.values().copied().collect();
let mut blockers_of: BTreeMap<EntityKey, Vec<EntityKey>> = BTreeMap::new();
let mut dependents_of: BTreeMap<EntityKey, Vec<EntityKey>> = BTreeMap::new();
let mut indeg: BTreeMap<EntityKey, usize> = BTreeMap::new();
for &k in &node_keys {
let blockers: Vec<EntityKey> = channels::blocked_by(g, k)
.into_iter()
.filter(|b| node_keys.contains(b))
.collect();
indeg.insert(k, blockers.len());
for &b in &blockers {
dependents_of.entry(b).or_default().push(k);
}
blockers_of.insert(k, blockers);
}
let mut ranks: BTreeMap<EntityKey, u32> = BTreeMap::new();
let mut queue: VecDeque<EntityKey> = indeg
.iter()
.filter(|(_, d)| **d == 0)
.map(|(&k, _)| k)
.collect();
while let Some(k) = queue.pop_front() {
let rank = blockers_of.get(&k).map_or(0, |bs| {
bs.iter()
.filter_map(|b| ranks.get(b))
.max()
.map_or(0, |r| r + 1)
});
ranks.insert(k, rank);
if let Some(deps) = dependents_of.get(&k) {
for &dep in deps {
if let Some(d) = indeg.get_mut(&dep) {
*d -= 1;
if *d == 0 {
queue.push_back(dep);
}
}
}
}
}
for &k in &node_keys {
if !ranks.contains_key(&k) {
let rank = blockers_of.get(&k).map_or(0, |bs| {
bs.iter()
.filter_map(|b| ranks.get(b))
.max()
.map_or(0, |r| r + 1)
});
ranks.insert(k, rank);
}
}
let mut edges: Vec<ActionabilityEdge> = Vec::new();
for &k in &node_keys {
for blocker in &channels::blocked_by(g, k) {
if node_keys.contains(blocker) {
edges.push(ActionabilityEdge {
source: blocker.canonical(),
target: k.canonical(),
kind: "needs".into(),
});
}
}
}
for &k in &node_keys {
if let Some(n) = g.projection.resolve(k) {
for (pred, _) in g.graph.in_edges(g.seq_overlay, n) {
if let Some(pred_key) = g.projection.key_of(pred)
&& node_keys.contains(&pred_key)
{
edges.push(ActionabilityEdge {
source: pred_key.canonical(),
target: k.canonical(),
kind: "after".into(),
});
}
}
}
}
let nodes: Vec<ActionabilityNode> = rows
.into_iter()
.filter_map(|r| {
let k = parse_key(&r.id).ok()?;
let rank = ranks.get(&k).copied().unwrap_or(0);
let actionability = match r.act {
Actionability::Actionable => "actionable",
Actionability::Blocked => "blocked",
};
Some(ActionabilityNode {
id: r.id,
title: r.title,
kind: r.kind,
status: r.status,
actionability: actionability.into(),
score: r.score,
rank,
blockers: r.blockers,
})
})
.collect();
ActionabilityView {
kind: "actionability_graph".into(),
policy_version: "priority.v3".into(),
nodes,
edges,
}
}
pub(crate) fn next(root: &Path) -> anyhow::Result<NextView> {
let scanned = relation_graph::scan_entities(root, &mut vec![], ScanMode::default())?;
let cfg = super::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg)?;
let cost_feed = comparison::cost_feed(&pipeline.estimate.projection);
let g = graph::build_from_with_cfg(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&cost_feed,
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)?;
let actionable_set: std::collections::BTreeSet<EntityKey> = g
.attrs
.keys()
.copied()
.filter(|&k| channels::actionable(&g, k) && !channels::promoted(&g, k))
.collect();
let actionable: Vec<EntityKey> = actionable_set.iter().copied().collect();
let preds = surviving_seq_predecessors(&g, &actionable_set);
let order = frontier_order(&actionable, &|k| channels::score(&g, k), &preds);
let rows = order
.into_iter()
.map(|k| {
let blocking = refs(&channels::blocking(&g, k));
let mut reasons = vec![eligibility_reason(&g, k)];
if !blocking.is_empty() {
reasons.push(ReasonKind::Blocking {
items: blocking.clone(),
});
}
reasons.push(score_reason(&g, k));
let tags = attr(&g, k).map_or(Vec::new(), |a| a.facets.tags.clone());
let value_source = value_source_reason(&g, k, &pipeline);
let cost_source = cost_source_reason(&g, k, &pipeline, &cfg);
NextRow {
id: k.canonical(),
title: title_of(&g, k),
kind: kind_of(&g, k),
status: status_of(&g, k),
act: Actionability::Actionable,
score: channels::score(&g, k),
reasons,
blockers: Vec::new(),
blocking,
cost_source,
value_source,
tags,
}
})
.collect();
let tensions = graded_tensions(&g, &pipeline, &cfg, usize::MAX)
.iter()
.map(tension_reason)
.collect();
let zero_weight = match frontier_zero_weight(&g, &cfg, usize::MAX) {
0 => None,
count => Some(ReasonKind::ZeroWeightExcluded { count }),
};
Ok(NextView {
rows,
tensions,
zero_weight,
})
}
fn parse_key(id: &str) -> anyhow::Result<EntityKey> {
let (kref, qid) = crate::integrity::parse_canonical_ref(id)?;
Ok(EntityKey {
prefix: kref.kind.prefix,
id: qid,
})
}
pub(crate) fn blockers(root: &Path, id: &str, transitive: bool) -> anyhow::Result<BlockersView> {
let key = parse_key(id)?;
let g = graph::build(root)?;
relation_graph::require_minted(&g.projection, key)?;
let (blocked_by, blocking) = if transitive {
(
channels::blocked_by_transitive(&g, key),
channels::blocking_transitive(&g, key),
)
} else {
(channels::blocked_by(&g, key), channels::blocking(&g, key))
};
Ok(BlockersView {
id: key.canonical(),
transitive,
blocked_by: refs(&blocked_by),
blocking: refs(&blocking),
})
}
pub(crate) fn explain(root: &Path, id: &str) -> anyhow::Result<Explanation> {
let key = parse_key(id)?;
let scanned = relation_graph::scan_entities(root, &mut vec![], ScanMode::default())?;
let cfg = super::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg)?;
let cost_feed = comparison::cost_feed(&pipeline.estimate.projection);
let g = graph::build_from_with_cfg(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&cost_feed,
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)?;
relation_graph::require_minted(&g.projection, key)?;
let eligibility = eligibility_reason(&g, key);
let chain = channels::blocked_by_transitive(&g, key);
let blocker_chain = if chain.is_empty() {
Vec::new()
} else {
vec![ReasonKind::BlockedBy {
items: refs(&chain),
}]
};
let evictions = channels::evicted_seq_edges(&g, key)
.into_iter()
.map(|(from, to, reason)| ReasonKind::EvictedEdge {
from: from.canonical(),
to: to.canonical(),
reason,
})
.collect();
let cycle = channels::dep_cycles(&g)
.into_iter()
.find(|c| c.contains(&key));
let score = score_reason(&g, key);
let mut blocker_chain = blocker_chain;
if let Some(component) = cycle {
let nodes = component.into_iter().map(EntityKey::canonical).collect();
blocker_chain.push(ReasonKind::CycleDegraded { nodes });
}
let value_source = value_source_reason(&g, key, &pipeline);
let cost_source = cost_source_reason(&g, key, &pipeline, &cfg);
let priority_disclosure =
(pipeline.priority_domain_count > 0).then_some(ReasonKind::PriorityDomainDisclosure {
count: pipeline.priority_domain_count,
});
let id_ref = key.canonical();
let on_frontier = channels::actionable(&g, key) && !channels::promoted(&g, key);
let tensions: Vec<ReasonKind> = graded_tensions(&g, &pipeline, &cfg, usize::MAX)
.iter()
.filter(|t| t.preferred == key || t.surfaced == key)
.map(tension_reason)
.collect();
Ok(Explanation {
id: id_ref,
eligibility,
blocker_chain,
evictions,
score,
value_source,
cost_source,
priority_disclosure,
agent_demotion: cfg
.compare
.demote_agent_evidence
.then_some(ReasonKind::AgentEvidenceDemoted),
tensions,
on_frontier,
})
}
struct FrontierProjections {
order: Vec<EntityKey>,
value_dim: BTreeMap<EntityKey, f64>,
multiplier: BTreeMap<EntityKey, f64>,
full_score: BTreeMap<EntityKey, f64>,
risk_dim: BTreeMap<EntityKey, f64>,
leverage: BTreeMap<EntityKey, f64>,
optionality: BTreeMap<EntityKey, f64>,
preds: BTreeMap<EntityKey, Vec<PredEdge>>,
}
impl FrontierProjections {
fn inputs(&self, page_k: usize) -> DetectInputs<'_> {
DetectInputs {
delivery_order: &self.order,
page_k,
value_dim: &self.value_dim,
multiplier: &self.multiplier,
full_score: &self.full_score,
risk_dim: &self.risk_dim,
leverage: &self.leverage,
optionality: &self.optionality,
preds: &self.preds,
}
}
}
fn frontier_projections(g: &PriorityGraph, cfg: &PriorityConfig) -> FrontierProjections {
let actionable_set: BTreeSet<EntityKey> = g
.attrs
.keys()
.copied()
.filter(|&k| channels::actionable(g, k) && !channels::promoted(g, k))
.collect();
let actionable: Vec<EntityKey> = actionable_set.iter().copied().collect();
let seq_preds = surviving_seq_predecessors(g, &actionable_set);
let order = frontier_order(&actionable, &|k| channels::score(g, k), &seq_preds);
let map = |f: &dyn Fn(EntityKey) -> f64| -> BTreeMap<EntityKey, f64> {
actionable.iter().map(|&k| (k, f(k))).collect()
};
let mut preds: BTreeMap<EntityKey, Vec<PredEdge>> = BTreeMap::new();
for &k in &actionable {
let mut edges: Vec<PredEdge> = seq_preds
.get(&k)
.into_iter()
.flatten()
.map(|&pred| PredEdge {
pred,
kind: EdgeKind::Seq,
})
.collect();
for pred in channels::blocked_by(g, k) {
if actionable_set.contains(&pred) {
edges.push(PredEdge {
pred,
kind: EdgeKind::Dep,
});
}
}
if !edges.is_empty() {
preds.insert(k, edges);
}
}
FrontierProjections {
value_dim: map(&|k| channels::value_dim(g, k)),
risk_dim: map(&|k| channels::risk_dim(g, k)),
leverage: map(&|k| channels::leverage(g, k)),
optionality: map(&|k| channels::optionality(g, k)),
full_score: map(&|k| channels::score(g, k)),
multiplier: map(&|k| g.item_costing(&k, cfg).map_or(0.0, |(m, _, _)| m)),
order,
preds,
}
}
pub(crate) fn frontier_zero_weight(
g: &PriorityGraph,
cfg: &PriorityConfig,
page_k: usize,
) -> usize {
tension::zero_weight_excluded(&frontier_projections(g, cfg).inputs(page_k))
}
pub(crate) fn tension_reason(t: &Tension) -> ReasonKind {
let cause = match t.cause {
TensionCause::Structure { edge } => TensionCauseView::Structure {
edge_from: edge.from.canonical(),
verb: match edge.kind {
EdgeKind::Seq => EdgeVerb::After,
EdgeKind::Dep => EdgeVerb::Needs,
},
},
TensionCause::Composition { deltas } => TensionCauseView::Composition {
risk_dim: deltas.risk_dim,
leverage: deltas.leverage,
optionality: deltas.optionality,
},
};
let grade = match t.grade {
EvidenceGrade::Determined { counts } => TensionGradeView::Determined {
human: counts.human,
agent: counts.agent,
},
EvidenceGrade::AgentProposed { counts } => TensionGradeView::AgentProposed {
agent: counts.agent,
},
EvidenceGrade::Projected => TensionGradeView::Projected,
};
ReasonKind::Tension {
preferred: t.preferred.canonical(),
surfaced: t.surfaced.canonical(),
cause,
grade,
}
}
pub(crate) fn graded_tensions(
g: &PriorityGraph,
pipeline: &comparison::Pipeline,
cfg: &PriorityConfig,
page_k: usize,
) -> Vec<Tension> {
let proj = frontier_projections(g, cfg);
let detected = tension::detect(&proj.inputs(page_k));
if detected.is_empty() {
return Vec::new();
}
let active: Vec<&Judgement> = pipeline.active_pairwise().iter().collect();
let full_reach = Reachability::build(&pipeline.value.constraint_set);
let knob_on = cfg.compare.demote_agent_evidence;
let human = knob_on.then(|| {
let cs = compile_human_only(
&active,
&pipeline.value.anchors,
QuarantinePolicy::Symmetric,
);
let reach = Reachability::build(&cs);
let counts = constraining_counts_by_class(&cs, &active);
(cs, reach, counts)
});
let (verdict_cs, verdict_reach, verdict_counts) = match &human {
Some((cs, reach, counts)) => (cs, reach, counts),
None => (
&pipeline.value.constraint_set,
&full_reach,
&pipeline.constraining_by_class,
),
};
detected
.into_iter()
.map(|t| {
let grade = grade_pair(
g,
cfg,
t.preferred,
t.surfaced,
(verdict_cs, verdict_reach, verdict_counts),
(
&pipeline.value.constraint_set,
&full_reach,
&pipeline.constraining_by_class,
),
knob_on,
);
t.with_grade(grade)
})
.collect()
}
type SystemView<'a> = (
&'a ConstraintSet,
&'a Reachability,
&'a BTreeMap<comparison::ClassId, RaterCounts>,
);
fn grade_pair(
g: &PriorityGraph,
cfg: &PriorityConfig,
preferred: EntityKey,
surfaced: EntityKey,
verdict: SystemView<'_>,
full: SystemView<'_>,
knob_on: bool,
) -> EvidenceGrade {
let (Some((ma, ca, _)), Some((mb, cb, _))) = (
g.item_costing(&preferred, cfg),
g.item_costing(&surfaced, cfg),
) else {
return EvidenceGrade::Projected;
};
let (pa, pb) = (preferred.canonical(), surfaced.canonical());
let evaluate = |sys: SystemView<'_>| -> (bool, RaterCounts) {
let (cs, reach, by_class) = sys;
let sa = pair_side(cs, &pa, ma * cb);
let sb = pair_side(cs, &pb, mb * ca);
let det = determined(reach, &sa, &sb).is_determined();
(det, pair_counts(by_class, &sa.class, &sb.class))
};
let (det_verdict, verdict_counts) = evaluate(verdict);
let (det_full, full_counts) = if knob_on {
evaluate(full)
} else {
(det_verdict, verdict_counts)
};
tension::grade(knob_on, det_verdict, verdict_counts, det_full, full_counts)
}
fn pair_counts(
by_class: &BTreeMap<comparison::ClassId, RaterCounts>,
a: &str,
b: &str,
) -> RaterCounts {
let mut out = by_class.get(a).copied().unwrap_or_default();
if a != b {
let other = by_class.get(b).copied().unwrap_or_default();
out.human += other.human;
out.agent += other.agent;
}
out
}
pub(crate) fn claim_reason(claim: &comparison::ResolvedClaim, conflict: Vec<String>) -> ReasonKind {
let contested = claim.conflict.as_ref().map(|c| ContestedClaim {
low: c.low,
high: c.high,
rows: claim.rows,
});
let attr = claim.attribution.clone().unwrap_or_default();
let date = if matches!(claim.tier, comparison::ClaimTier::Migrated) {
attr.observed_at
} else {
attr.date
};
match claim.tier {
comparison::ClaimTier::Pin => ReasonKind::ValuePin {
value: claim.operative,
conflict,
by: attr.by,
date,
basis: attr.basis,
contested,
},
tier => ReasonKind::ValueClaim {
value: claim.operative,
tier,
conflict,
by: attr.by,
date,
contested,
},
}
}
pub(crate) fn value_source_reason(
_g: &PriorityGraph,
key: EntityKey,
pipeline: &comparison::Pipeline,
) -> Option<ReasonKind> {
if !crate::kinds::is_value_bearing(key.prefix) {
return None;
}
resolve_value_reason(pipeline, &key.canonical())
}
pub(crate) fn resolve_value_reason(
pipeline: &comparison::Pipeline,
canonical: &str,
) -> Option<ReasonKind> {
let claims = &pipeline.value_claims;
if let Some(claim) = claims.anchored.get(canonical) {
let conflict = anchor_conflict_citation(&pipeline.value.constraint_set, canonical);
return Some(claim_reason(claim, conflict));
}
if let Some(&(value, provenance)) = pipeline.value.projection.get(canonical) {
return Some(match provenance {
comparison::ValueProvenance::Authored | comparison::ValueProvenance::Projected => {
let (lower, upper) = class_bounds(&pipeline.value.constraint_set, canonical);
let counts = class_rater_counts(pipeline, canonical);
ReasonKind::ValueProjected {
value,
lower,
upper,
human: counts.human,
agent: counts.agent,
}
}
comparison::ValueProvenance::Gauge => {
let counts = class_rater_counts(pipeline, canonical);
ReasonKind::ValueGauge {
value,
judgements: counts.total(),
}
}
});
}
if let Some(claim) = claims.priors.get(canonical) {
return Some(claim_reason(claim, Vec::new()));
}
None
}
pub(crate) fn show_value_line(
root: &Path,
canonical: &str,
kind: &str,
value_unit: &str,
) -> anyhow::Result<Option<String>> {
let pipeline = graph::load_comparison_pipeline_for_root(root)?;
Ok(value_line_from_pipeline(
&pipeline, canonical, kind, value_unit,
))
}
pub(crate) fn value_line_from_pipeline(
pipeline: &comparison::Pipeline,
canonical: &str,
kind: &str,
value_unit: &str,
) -> Option<String> {
let reason = resolve_value_reason(pipeline, canonical)?;
let inert = (!crate::kinds::is_value_bearing(kind)).then_some(kind);
super::render::show_value_render(&reason, value_unit, inert)
}
pub(crate) fn show_estimate_line(
root: &Path,
canonical: &str,
kind: &str,
est_unit: &str,
) -> anyhow::Result<Option<String>> {
let pipeline = graph::load_comparison_pipeline_for_root(root)?;
Ok(estimate_line_from_pipeline(
&pipeline, canonical, kind, est_unit,
))
}
pub(crate) fn estimate_line_from_pipeline(
pipeline: &comparison::Pipeline,
canonical: &str,
kind: &str,
est_unit: &str,
) -> Option<String> {
if pipeline.estimate.projection.is_empty() {
return None;
}
let cost_reason = pipeline_cost_reason(pipeline, canonical)?;
let inert = (!crate::kinds::is_value_bearing(kind)).then_some(kind);
super::render::show_cost_render(&cost_reason, est_unit, inert)
}
fn pipeline_cost_reason(pipeline: &comparison::Pipeline, canonical: &str) -> Option<ReasonKind> {
if let Some(claim) = pipeline.estimate_claims.anchored.get(canonical) {
let est_cost = claim.operative;
let attr = claim.attribution.clone().unwrap_or_default();
return match claim.tier {
ClaimTier::Pin => Some(ReasonKind::CostPin {
est_cost,
lower: est_cost,
upper: est_cost,
beta: 0.65,
by: attr.by,
date: attr.date,
basis: attr.basis,
contested: claim.conflict.as_ref().map(|c| ContestedClaim {
low: c.low,
high: c.high,
rows: claim.rows,
}),
}),
_ => Some(ReasonKind::CostClaim {
est_cost,
lower: est_cost,
upper: est_cost,
beta: 0.65,
tier: claim.tier,
by: attr.by,
date: if matches!(claim.tier, ClaimTier::Migrated) {
attr.observed_at
} else {
attr.date
},
conflict: Vec::new(),
}),
};
}
if let Some(&(cost, provenance)) = pipeline.estimate.projection.get(canonical) {
return Some(match provenance {
comparison::ValueProvenance::Authored => ReasonKind::CostClassAnchor { est_cost: cost },
comparison::ValueProvenance::Projected => ReasonKind::CostProjected {
est_cost: cost,
lower: None,
upper: None,
human: 0,
agent: 0,
},
comparison::ValueProvenance::Gauge => ReasonKind::CostGauge {
est_cost: cost,
max_estimate: None,
margin: 0.0,
judgements: 0,
},
});
}
if let Some(claim) = pipeline.estimate_claims.priors.get(canonical) {
let est_cost = claim.operative;
let attr = claim.attribution.clone().unwrap_or_default();
return Some(ReasonKind::CostClaim {
est_cost,
lower: est_cost,
upper: est_cost,
beta: 0.65,
tier: claim.tier,
by: attr.by,
date: if matches!(claim.tier, ClaimTier::Migrated) {
attr.observed_at
} else {
attr.date
},
conflict: Vec::new(),
});
}
None
}
pub(crate) fn cost_source_reason(
g: &PriorityGraph,
key: EntityKey,
pipeline: &comparison::Pipeline,
cfg: &PriorityConfig,
) -> Option<ReasonKind> {
let _attrs = g.attrs.get(&key)?;
if !crate::kinds::is_value_bearing(key.prefix) {
return None;
}
if pipeline.estimate.projection.is_empty() {
return None;
}
let canonical = key.canonical();
let margin = cfg.estimate.margin;
let absent = g.cost_ctx.absent;
let max_estimate = max_authored_upper(pipeline, cfg);
let estimate_skew = cfg.estimate.skew;
if let Some(claim) = pipeline.estimate_claims.anchored.get(&canonical) {
let est_cost = claim.operative;
let lb = claim.operative;
let ub = claim.operative;
let conflict = anchor_conflict_citation(&pipeline.estimate.constraint_set, &canonical);
let contested = claim.conflict.as_ref().map(|c| ContestedClaim {
low: c.low,
high: c.high,
rows: claim.rows,
});
let attr = claim.attribution.clone().unwrap_or_default();
let date = attr.date;
return match claim.tier {
ClaimTier::Pin => Some(ReasonKind::CostPin {
est_cost,
lower: lb,
upper: ub,
beta: estimate_skew,
by: attr.by,
date,
basis: attr.basis,
contested,
}),
_ => Some(ReasonKind::CostClaim {
est_cost,
lower: lb,
upper: ub,
beta: estimate_skew,
tier: claim.tier,
by: attr.by,
date: if matches!(claim.tier, ClaimTier::Migrated) {
attr.observed_at
} else {
date
},
conflict,
}),
};
}
if let Some(&(cost, provenance)) = pipeline.estimate.projection.get(&canonical) {
return Some(match provenance {
comparison::ValueProvenance::Authored => ReasonKind::CostClassAnchor { est_cost: cost },
comparison::ValueProvenance::Projected => {
let (lower, upper) = class_bounds(&pipeline.estimate.constraint_set, &canonical);
let counts = est_class_rater_counts(pipeline, &canonical);
ReasonKind::CostProjected {
est_cost: cost,
lower,
upper,
human: counts.human,
agent: counts.agent,
}
}
comparison::ValueProvenance::Gauge => {
let counts = est_class_rater_counts(pipeline, &canonical);
ReasonKind::CostGauge {
est_cost: absent,
max_estimate,
margin,
judgements: counts.total(),
}
}
});
}
if let Some(claim) = pipeline.estimate_claims.priors.get(&canonical) {
let est_cost = claim.operative;
let lb = claim.operative;
let ub = claim.operative;
let attr = claim.attribution.clone().unwrap_or_default();
return Some(ReasonKind::CostClaim {
est_cost,
lower: lb,
upper: ub,
beta: estimate_skew,
tier: claim.tier,
by: attr.by,
date: if matches!(claim.tier, ClaimTier::Migrated) {
attr.observed_at
} else {
attr.date
},
conflict: Vec::new(),
});
}
Some(ReasonKind::CostBareAnchor {
est_cost: absent,
max_estimate,
margin,
})
}
fn est_class_rater_counts(pipeline: &comparison::Pipeline, canonical: &str) -> RaterCounts {
pipeline
.estimate
.constraint_set
.classes
.get(canonical)
.and_then(|class| pipeline.est_constraining_by_class.get(class))
.copied()
.unwrap_or_default()
}
fn max_authored_upper(pipeline: &comparison::Pipeline, cfg: &PriorityConfig) -> Option<f64> {
let margin = cfg.estimate.margin;
let has_anchored = !pipeline.estimate_claims.anchored.is_empty();
if !has_anchored && (pipeline.bare_anchor - 1.0).abs() < 1e-9 {
return None;
}
Some(pipeline.bare_anchor - margin)
}
fn class_rater_counts(
pipeline: &comparison::Pipeline,
canonical: &str,
) -> crate::comparison::RaterCounts {
pipeline
.value
.constraint_set
.classes
.get(canonical)
.and_then(|class| pipeline.constraining_by_class.get(class))
.copied()
.unwrap_or_default()
}
fn class_bounds(cs: &comparison::ConstraintSet, canonical: &str) -> (Option<f64>, Option<f64>) {
let Some(class) = cs.classes.get(canonical) else {
return (None, None);
};
let Some(bounds) = cs.bounds.get(class) else {
return (None, None);
};
(bound_value(bounds.lower), bound_value(bounds.upper))
}
fn bound_value(b: comparison::Bound) -> Option<f64> {
match b {
comparison::Bound::Unbounded => None,
comparison::Bound::Open(v) | comparison::Bound::Closed(v) => Some(v),
}
}
pub(crate) fn class_bounds_structural(
cs: &comparison::ConstraintSet,
canonical: &str,
) -> Option<comparison::ValueBounds> {
let class = cs.classes.get(canonical)?;
cs.bounds.get(class).copied()
}
fn anchor_conflict_citation(cs: &comparison::ConstraintSet, canonical: &str) -> Vec<String> {
let Some(class) = cs.classes.get(canonical) else {
return Vec::new();
};
let mut others = std::collections::BTreeSet::new();
for reason in cs.quarantined.values() {
if let comparison::QuarantineReason::AnchorConflict { pairs } = reason {
for (x, y) in pairs {
if x == class {
others.insert(y.clone());
}
if y == class {
others.insert(x.clone());
}
}
}
}
others.into_iter().collect()
}
pub(crate) fn actionability_block_from(
scanned: &[relation_graph::ScannedEntity],
root: &Path,
id: &str,
) -> anyhow::Result<ActionabilityBlock> {
let key = parse_key(id)?;
let g = graph::build_from(scanned, root)?;
relation_graph::require_minted(&g.projection, key)?;
Ok(ActionabilityBlock {
eligible: channels::eligible(&g, key),
actionable: channels::actionable(&g, key),
blockers: refs(&channels::blocked_by(&g, key)),
blocking: refs(&channels::blocking(&g, key)),
score: channels::score(&g, key),
})
}
pub(crate) fn findings(root: &Path) -> anyhow::Result<Vec<super::findings::Finding>> {
let scanned = relation_graph::scan_entities(root, &mut vec![], ScanMode::default())?;
let cfg = super::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg)?;
let cost_feed = comparison::cost_feed(&pipeline.estimate.projection);
let base = graph::build_from_with_cfg(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&cost_feed,
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)?;
let betas = beta_endpoints(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&cost_feed,
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)?;
let mut findings = super::findings::detect(&base, &cfg, betas.as_ref());
findings.extend(super::findings::comparison_findings(&pipeline));
findings.sort_by(|a, b| {
a.kind_label()
.cmp(b.kind_label())
.then(b.magnitude().total_cmp(&a.magnitude()))
});
Ok(findings)
}
fn has_nonterminal_interval(
scanned: &[relation_graph::ScannedEntity],
estimate_claims: &crate::comparison::ClaimResolutionGeneric<crate::comparison::EstimatePayload>,
) -> bool {
use crate::comparison::EstimatePayload;
use crate::priority::partition::{StatusClass, status_class};
let non_terminal: std::collections::BTreeSet<String> = scanned
.iter()
.filter(|e| status_class(e.kind, e.status.as_deref()) != StatusClass::Terminal)
.map(|e| e.key.canonical())
.collect();
estimate_claims.anchored.iter().any(|(item, claim)| {
let EstimatePayload(lower, upper) = claim.payload;
lower < upper && non_terminal.contains(item.as_str())
})
}
#[expect(
clippy::too_many_arguments,
reason = "PHASE-06 threading: estimate_claims added as 8th param to the post-flip integration"
)]
pub(crate) fn beta_endpoints(
scanned: &[relation_graph::ScannedEntity],
root: &Path,
cfg: &super::config::PriorityConfig,
projected: &ValueProjection,
cost_feed: &comparison::CostFeed,
claims: &comparison::ClaimResolution,
bare_anchor: f64,
estimate_claims: &crate::comparison::ClaimResolutionGeneric<crate::comparison::EstimatePayload>,
) -> anyhow::Result<Option<super::findings::BetaEndpoints>> {
if !has_nonterminal_interval(scanned, estimate_claims) {
return Ok(None);
}
let mut lo_cfg = cfg.clone();
lo_cfg.estimate.skew = super::findings::BETA_LO;
let mut hi_cfg = cfg.clone();
hi_cfg.estimate.skew = super::findings::BETA_HI;
let lo = graph::build_from_with_cfg(
scanned,
root,
&lo_cfg,
projected,
cost_feed,
claims,
bare_anchor,
estimate_claims,
)?;
let hi = graph::build_from_with_cfg(
scanned,
root,
&hi_cfg,
projected,
cost_feed,
claims,
bare_anchor,
estimate_claims,
)?;
Ok(Some(super::findings::BetaEndpoints { lo, hi }))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
use crate::priority::graph::build;
fn write(root: &Path, rel: &str, body: &str) {
let path = root.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
}
fn tmp() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn seed_issue(root: &Path, id: u32, status: &str, resolution: &str, axes: &[(&str, &[&str])]) {
let rels = crate::relation::rels_block(&crate::backlog::ISSUE_KIND, axes);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"{status}\"\n\
resolution = \"{resolution}\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
{rels}"
),
);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
}
#[test]
fn survey_rank_topological_chain_a_to_b_to_c() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[("needs", &["ISS-002"])]);
seed_issue(root, 2, "open", "", &[("needs", &["ISS-003"])]);
seed_issue(root, 3, "open", "", &[]);
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
let n1 = view.nodes.iter().find(|n| n.id == "ISS-001").unwrap();
let n2 = view.nodes.iter().find(|n| n.id == "ISS-002").unwrap();
let n3 = view.nodes.iter().find(|n| n.id == "ISS-003").unwrap();
assert_eq!(n3.rank, 0, "ISS-003 has no blockers → rank 0");
assert_eq!(n2.rank, 1, "ISS-002 blocked by ISS-003 (rank 0) → rank 1");
assert_eq!(n1.rank, 2, "ISS-001 blocked by ISS-002 (rank 1) → rank 2");
}
#[test]
fn survey_needs_edges_present() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[("needs", &["ISS-002"])]);
seed_issue(root, 2, "open", "", &[("needs", &["ISS-003"])]);
seed_issue(root, 3, "open", "", &[]);
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
assert!(
view.edges
.iter()
.any(|e| e.source == "ISS-003" && e.target == "ISS-002" && e.kind == "needs")
);
assert!(
view.edges
.iter()
.any(|e| e.source == "ISS-002" && e.target == "ISS-001" && e.kind == "needs")
);
}
#[test]
fn survey_after_edges_present() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 2, "open", "", &[]);
write(
root,
".doctrine/backlog/issue/001/backlog-001.toml",
"id = 1\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[relationships]\nafter = [{ to = \"ISS-002\", rank = 0 }]\n",
);
write(root, ".doctrine/backlog/issue/001/backlog-001.md", "b\n");
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
assert!(
view.edges
.iter()
.any(|e| e.source == "ISS-002" && e.target == "ISS-001" && e.kind == "after")
);
}
#[test]
fn survey_empty_graph() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "closed", "", &[]);
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
assert!(view.nodes.is_empty());
assert!(view.edges.is_empty());
}
#[test]
fn survey_excludes_terminal() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[]);
seed_issue(root, 2, "closed", "", &[]);
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
assert_eq!(view.nodes.len(), 1, "only the eligible (open) node");
assert_eq!(view.nodes[0].id, "ISS-001");
assert!(view.nodes.iter().all(|n| n.id != "ISS-002"));
}
#[test]
fn survey_terminal_blocker_no_edge() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[("needs", &["ISS-002"])]);
seed_issue(root, 2, "closed", "", &[]);
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
assert_eq!(view.nodes.len(), 1);
let n1 = &view.nodes[0];
assert_eq!(n1.id, "ISS-001");
assert!(view.edges.is_empty(), "terminal → eligible edge suppressed");
assert_eq!(n1.actionability, "actionable");
assert_eq!(n1.rank, 0);
}
#[test]
fn survey_for_map_matches_survey_byte_for_byte() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[("needs", &["ISS-003"])]);
seed_issue(root, 2, "open", "", &[("needs", &["ISS-003"])]);
seed_issue(root, 3, "open", "", &[]);
let g = build(root).unwrap();
let from_survey = survey(root, false, false).unwrap();
let from_for_map = survey_for_map(&g, false);
assert_eq!(
from_survey, from_for_map,
"survey_for_map must match survey output exactly"
);
}
#[test]
fn actionability_view_set_preserved_after_value_bearing_promotion() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[]);
write(
root,
".doctrine/requirement/005/requirement-005.toml",
"id = 5\nslug = \"r\"\ntitle = \"R\"\nstatus = \"active\"\n",
);
write(root, ".doctrine/requirement/005/requirement-005.md", "r\n");
let g = build(root).unwrap();
let view = survey_view_for_map(&g, false);
assert_eq!(view.nodes.len(), 1, "only the ISS appears, not REQ-005");
assert_eq!(view.nodes[0].id, "ISS-001");
let kind_set: std::collections::BTreeSet<&str> =
view.nodes.iter().map(|n| n.kind.as_str()).collect();
let expected: std::collections::BTreeSet<&str> = ["ISS"].iter().copied().collect();
assert_eq!(kind_set, expected, "only work/value-bearing kinds appear");
}
fn seed_valued(root: &Path, id: u32, value: f64, rel_lines: &str) {
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"i\"\ntitle = \"I{id}\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[estimate]\nlower = 0.0\nupper = 10.0\n[value]\nvalue = {value}\n\
[relationships]\n{rel_lines}"
),
);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
let item = format!("ISS-{id:03}");
write_value_anchor(root, &format!("fv{id}"), &item, value);
write_est_anchor(root, &format!("fe{id}"), &item, 0.0, 10.0);
}
fn write_value_anchor(root: &Path, uid: &str, item: &str, val: f64) {
use crate::comparison::RowForm;
let j = crate::comparison::Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: crate::comparison::DOMAIN_VALUE.to_string(),
frame: crate::comparison::FRAME_VALUE_ANCHOR.to_string(),
form: RowForm::Anchor,
magnitude: Some(val),
supersedes: None,
lens: None,
rater: crate::comparison::RaterKind::Human,
by: None,
note: None,
date: Some("2026-07-16".to_string()),
observed_at: None,
basis: None,
est_lower: None,
est_upper: None,
admission: None,
};
let session = crate::comparison::ComparisonSession {
schema: crate::comparison::COMPARISON_SCHEMA.to_string(),
version: crate::comparison::COMPARISON_VERSION,
session: crate::comparison::SessionHeader {
uid: uid.to_string(),
date: "2026-07-16".to_string(),
audience: None,
},
judgements: vec![j],
tombstones: Vec::new(),
};
let text = crate::comparison::to_toml(&session).unwrap();
write(
root,
&format!(".doctrine/comparisons/2026-07-16-{uid}.toml"),
&text,
);
}
fn write_est_anchor(root: &Path, uid: &str, item: &str, lower: f64, upper: f64) {
use crate::comparison::RowForm;
let j = crate::comparison::Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: crate::comparison::DOMAIN_ESTIMATE.to_string(),
frame: crate::comparison::FRAME_COST_ANCHOR.to_string(),
form: RowForm::Anchor,
magnitude: None,
supersedes: None,
lens: None,
rater: crate::comparison::RaterKind::Human,
by: None,
note: None,
date: Some("2026-07-17".to_string()),
observed_at: None,
basis: None,
est_lower: Some(lower),
est_upper: Some(upper),
admission: None,
};
let session = crate::comparison::ComparisonSession {
schema: crate::comparison::COMPARISON_SCHEMA.to_string(),
version: crate::comparison::COMPARISON_VERSION,
session: crate::comparison::SessionHeader {
uid: uid.to_string(),
date: "2026-07-17".to_string(),
audience: None,
},
judgements: vec![j],
tombstones: Vec::new(),
};
let text = crate::comparison::to_toml(&session).unwrap();
write(
root,
&format!(".doctrine/comparisons/2026-07-17-{uid}.toml"),
&text,
);
}
fn next_ids(root: &Path) -> Vec<String> {
next(root).unwrap().rows.into_iter().map(|r| r.id).collect()
}
fn survey_ids(root: &Path) -> Vec<String> {
survey(root, false, false)
.unwrap()
.into_iter()
.map(|r| r.id)
.collect()
}
#[test]
fn vt5_blocker_of_one_high_value_outranks_blocker_of_five_ideas() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", &[]); write(
root,
".doctrine/backlog/risk/001/backlog-001.toml",
"id = 1\nslug = \"k\"\ntitle = \"K1\"\nkind = \"risk\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n",
);
write(root, ".doctrine/backlog/risk/001/backlog-001.md", "k\n");
write(
root,
".doctrine/backlog/risk/002/backlog-002.toml",
"id = 2\nslug = \"k\"\ntitle = \"K2\"\nkind = \"risk\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n",
);
write(root, ".doctrine/backlog/risk/002/backlog-002.md", "k\n");
seed_valued(root, 10, 100.0, "needs = [\"RSK-001\"]\n");
for id in 20..25 {
seed_valued(root, id, 0.0, "needs = [\"RSK-002\"]\n");
}
let g = build(root).unwrap();
let rsk1 = EntityKey {
prefix: "RSK",
id: 1,
};
let rsk2 = EntityKey {
prefix: "RSK",
id: 2,
};
assert!(
(channels::score(&g, rsk1) - (50.0 / 6.5 + 1.0 / 11.0)).abs() < 1e-9,
"RSK-001 leverages the one high-value dependent: got {}",
channels::score(&g, rsk1)
);
assert!(
(channels::score(&g, rsk2) - 1.0 / 11.0).abs() < 1e-9,
"RSK-002 gates only zero-value ideas → score = value_dim only: got {}",
channels::score(&g, rsk2)
);
let ids = survey_ids(root);
let p1 = ids.iter().position(|x| x == "RSK-001").unwrap();
let p2 = ids.iter().position(|x| x == "RSK-002").unwrap();
assert!(
p1 < p2,
"RSK-001 outranks RSK-002 by score (not inbound count): {ids:?}"
);
}
#[test]
fn vt5_deep_blocker_of_valuable_cone_outranks_shallow_blocker_of_modest_item() {
let dir = tmp();
let root = dir.path();
seed_valued(root, 1, 0.0, ""); seed_valued(root, 2, 0.0, "needs = [\"ISS-001\"]\n"); seed_valued(root, 3, 200.0, "needs = [\"ISS-002\"]\n"); seed_valued(root, 10, 0.0, ""); seed_valued(root, 11, 10.0, "needs = [\"ISS-010\"]\n");
let g = build(root).unwrap();
let k = |id| EntityKey { prefix: "ISS", id };
let deep = channels::score(&g, k(1));
let shallow = channels::score(&g, k(10));
assert!(
(deep - 50.0 / 6.5).abs() < 1e-9,
"deep blocker recursive leverage = 50/6.5: got {deep}"
);
assert!(
(shallow - 5.0 / 6.5).abs() < 1e-9,
"shallow blocker leverage = 5/6.5: got {shallow}"
);
let ids = survey_ids(root);
let pd = ids.iter().position(|x| x == "ISS-001").unwrap();
let ps = ids.iter().position(|x| x == "ISS-010").unwrap();
assert!(
pd < ps,
"deep blocker of a valuable cone outranks the shallow one: {ids:?}"
);
}
#[test]
fn vt7_y_fixture_incomparable_arms_order_by_score() {
let dir = tmp();
let root = dir.path();
seed_valued(root, 1, 0.0, "");
seed_valued(root, 2, 50.0, "after = [{ to = \"ISS-001\", rank = 0 }]\n");
seed_valued(root, 3, 100.0, "after = [{ to = \"ISS-001\", rank = 0 }]\n");
let ids = next_ids(root);
let p1 = ids.iter().position(|x| x == "ISS-001").unwrap();
let p2 = ids.iter().position(|x| x == "ISS-002").unwrap();
let p3 = ids.iter().position(|x| x == "ISS-003").unwrap();
assert!(p1 < p2 && p1 < p3, "shared upstream leads: {ids:?}");
assert!(p3 < p2, "higher-score arm ISS-003 before ISS-002: {ids:?}");
}
#[test]
fn vt7_same_chain_seq_keeps_structural_order_over_score() {
let dir = tmp();
let root = dir.path();
seed_valued(root, 1, 10.0, "");
seed_valued(root, 2, 100.0, "after = [{ to = \"ISS-001\", rank = 0 }]\n");
let ids = next_ids(root);
let p1 = ids.iter().position(|x| x == "ISS-001").unwrap();
let p2 = ids.iter().position(|x| x == "ISS-002").unwrap();
assert!(
p1 < p2,
"low-score predecessor ISS-001 precedes high-score ISS-002 (structural): {ids:?}"
);
}
#[test]
fn vt7_evicted_seq_edge_does_not_reimpose_precedence() {
let dir = tmp();
let root = dir.path();
seed_valued(root, 1, 10.0, "after = [{ to = \"ISS-002\", rank = 0 }]\n");
seed_valued(root, 2, 100.0, "after = [{ to = \"ISS-001\", rank = 0 }]\n");
let g = build(root).unwrap();
let evicted_total: usize = g
.attrs
.keys()
.map(|&k| channels::evicted_seq_edges(&g, k).len())
.sum();
assert!(
evicted_total > 0,
"the seq 2-cycle must produce an eviction"
);
let ids = next_ids(root);
let p1 = ids.iter().position(|x| x == "ISS-001").unwrap();
let p2 = ids.iter().position(|x| x == "ISS-002").unwrap();
let surviving_pred_of_2 = {
let preds = surviving_seq_predecessors(
&g,
&g.attrs
.keys()
.copied()
.filter(|&k| channels::actionable(&g, k) && !channels::promoted(&g, k))
.collect(),
);
preds
.get(&EntityKey {
prefix: "ISS",
id: 2,
})
.map(|s| {
s.contains(&EntityKey {
prefix: "ISS",
id: 1,
})
})
.unwrap_or(false)
};
if surviving_pred_of_2 {
assert!(
p1 < p2,
"surviving edge orders ISS-001 before ISS-002: {ids:?}"
);
} else {
assert!(
p2 < p1,
"evicted edge does NOT re-impose precedence; higher-score ISS-002 leads: {ids:?}"
);
}
}
fn set_interval_claim(root: &Path, uid: &str, item: &str, lower: f64, upper: f64) {
capture_cost_anchor(root, uid, item, lower, upper);
}
fn set_point_claim(root: &Path, uid: &str, item: &str, val: f64) {
capture_cost_anchor(root, uid, item, val, val);
}
#[test]
fn beta_endpoints_some_over_interval_estimate_none_over_estimate_free() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(root, 2, "[value]\nvalue = 10.0\n");
set_interval_claim(root, "a1", "ISS-001", 2.0, 8.0);
set_point_claim(root, "a2", "ISS-002", 5.0);
capture_more_work(root, "mw", "ISS-001", "ISS-002", "prefer-a", "human");
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = super::super::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg).unwrap();
let betas = beta_endpoints(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&comparison::cost_feed(&pipeline.estimate.projection),
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)
.unwrap();
assert!(
betas.is_some(),
"a non-terminal interval claim arms the β sweep"
);
let dir2 = tmp();
let root2 = dir2.path();
seed_cost(root2, 1, "[value]\nvalue = 10.0\n");
seed_cost(root2, 2, "[value]\nvalue = 10.0\n");
set_point_claim(root2, "b1", "ISS-001", 5.0);
set_point_claim(root2, "b2", "ISS-002", 3.0);
capture_more_work(root2, "mw2", "ISS-001", "ISS-002", "prefer-a", "human");
let scanned2 =
relation_graph::scan_entities(root2, &mut vec![], ScanMode::default()).unwrap();
let cfg2 = super::super::config::load(root2);
let pipeline2 = graph::load_comparison_pipeline(root2, &scanned2, &cfg2).unwrap();
assert!(
beta_endpoints(
&scanned2,
root2,
&cfg2,
&pipeline2.value.projection,
&comparison::cost_feed(&pipeline2.estimate.projection),
&pipeline2.value_claims,
pipeline2.bare_anchor,
&pipeline2.estimate_claims,
)
.unwrap()
.is_none(),
"point-only claims do not arm the β sweep"
);
let dir3 = tmp();
let root3 = dir3.path();
seed_cost(root3, 1, "[value]\nvalue = 10.0\n");
seed_cost(root3, 2, "[value]\nvalue = 10.0\n");
capture_more_work(root3, "mw3", "ISS-001", "ISS-002", "prefer-a", "human");
let scanned3 =
relation_graph::scan_entities(root3, &mut vec![], ScanMode::default()).unwrap();
let cfg3 = super::super::config::load(root3);
let pipeline3 = graph::load_comparison_pipeline(root3, &scanned3, &cfg3).unwrap();
assert!(
beta_endpoints(
&scanned3,
root3,
&cfg3,
&pipeline3.value.projection,
&comparison::cost_feed(&pipeline3.estimate.projection),
&pipeline3.value_claims,
pipeline3.bare_anchor,
&pipeline3.estimate_claims,
)
.unwrap()
.is_none(),
"an estimate-free corpus yields None"
);
}
#[test]
fn beta_endpoints_none_when_only_terminal_has_interval() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
write(
root,
".doctrine/backlog/issue/001/backlog-001.toml",
"id = 1\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"closed\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 10.0\n[relationships]\n",
);
set_interval_claim(root, "a1", "ISS-001", 2.0, 8.0);
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = super::super::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg).unwrap();
assert!(
beta_endpoints(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&comparison::cost_feed(&pipeline.estimate.projection),
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)
.unwrap()
.is_none(),
"a terminal-only interval does not arm the sweep"
);
}
#[test]
fn va1_explain_exposes_full_score_breakdown() {
let dir = tmp();
let root = dir.path();
seed_valued(root, 1, 50.0, "");
seed_valued(root, 2, 100.0, "needs = [\"ISS-001\"]\n");
let ex = explain(root, "ISS-001").unwrap();
let expected_base = 50.0 / 6.5;
let expected_lev = 50.0 / 6.5;
match ex.score {
ReasonKind::Score {
base,
value_dim,
risk_dim,
leverage,
optionality,
total,
} => {
assert!((base - expected_base).abs() < 1e-9, "base = 50/6.5");
assert!(
(value_dim - expected_base).abs() < 1e-9,
"value_dim = 50/6.5"
);
assert!(risk_dim.abs() < 1e-9, "risk_dim 0");
assert!((leverage - expected_lev).abs() < 1e-9, "leverage = 50/6.5");
assert!(optionality.abs() < 1e-9, "optionality 0");
assert!(
(total - (expected_base + expected_lev)).abs() < 1e-9,
"total = base+lev"
);
}
other => panic!("explain score must be a Score reason, got {other:?}"),
}
let expected_total = expected_base + expected_lev;
let human = crate::priority::render::explain_human(&ex);
assert!(
human.contains(&format!(
"score: {expected_total:.1} (base {expected_base:.1} [value {expected_base:.1}, risk 0.0], leverage {expected_lev:.1}, optionality 0.0)"
)),
"human explain renders the full breakdown: {human}"
);
}
fn prefer_a_over_b(root: &Path, rater: &str) {
write(
root,
".doctrine/comparisons/2026-01-01-vt3.toml",
&format!(
"schema = \"doctrine.comparison-session\"\nversion = 2\n\n\
[session]\nuid = \"vt3-sess\"\ndate = \"2026-01-01\"\n\n\
[[judgement]]\nuid = \"vt3-row\"\nseq = 0\na = \"ISS-001\"\nb = \"ISS-002\"\n\
response = \"prefer-a\"\ndomain = \"value\"\nframe = \"equal-effort\"\n\
form = \"order\"\nrater = \"{rater}\"\ndate = \"2026-01-01\"\n"
),
);
}
fn assemble_tensions(root: &Path) -> Vec<Tension> {
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = crate::priority::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg).unwrap();
let g = graph::build_from_with_cfg(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&comparison::cost_feed(&pipeline.estimate.projection),
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)
.unwrap();
graded_tensions(&g, &pipeline, &cfg, usize::MAX)
}
#[test]
fn vt3_structural_tension_human_row_graded_determined() {
let dir = tmp();
let root = dir.path();
seed_valued(root, 1, 8.0, "after = [{ to = \"ISS-002\", rank = 0 }]\n");
seed_valued(root, 2, 1.0, "");
prefer_a_over_b(root, "human");
let ts = assemble_tensions(root);
assert_eq!(ts.len(), 1, "one structural tension: {ts:?}");
assert_eq!(ts[0].surfaced.canonical(), "ISS-002");
assert_eq!(ts[0].preferred.canonical(), "ISS-001");
assert!(matches!(
ts[0].cause,
tension::TensionCause::Structure { .. }
));
match ts[0].grade {
EvidenceGrade::Determined { counts } => assert!(
counts.human >= 1 && counts.agent == 0,
"human-determined ⇒ human counts only (no agent rows cited): {counts:?}"
),
other => panic!("expected Determined, got {other:?}"),
}
}
#[test]
fn vt3_agent_row_knob_on_graded_agent_proposed() {
let dir = tmp();
let root = dir.path();
write(
root,
".doctrine/doctrine.toml",
"[priority.compare]\ndemote_agent_evidence = true\n",
);
seed_valued(root, 1, 8.0, "after = [{ to = \"ISS-002\", rank = 0 }]\n");
seed_valued(root, 2, 1.0, "");
prefer_a_over_b(root, "agent");
let ts = assemble_tensions(root);
assert_eq!(ts.len(), 1, "one structural tension: {ts:?}");
match ts[0].grade {
EvidenceGrade::AgentProposed { counts } => assert!(
counts.agent >= 1,
"agent-proposed carries the full-system agent counts: {counts:?}"
),
other => {
panic!("expected AgentProposed (human system indeterminate), got {other:?}")
}
}
}
fn seed_cost(root: &Path, id: u32, facets: &str) {
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"i\"\ntitle = \"I{id}\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
{facets}[relationships]\n"
),
);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
}
fn capture_cost_anchor(root: &Path, uid: &str, item: &str, lower: f64, upper: f64) {
use crate::comparison::{
COMPARISON_SCHEMA, COMPARISON_VERSION, ComparisonSession, DOMAIN_ESTIMATE,
FRAME_COST_ANCHOR, Judgement, RaterKind, RowForm, SessionHeader,
};
let j = Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: DOMAIN_ESTIMATE.to_string(),
frame: FRAME_COST_ANCHOR.to_string(),
form: RowForm::Anchor,
magnitude: None,
supersedes: None,
lens: None,
rater: RaterKind::Human,
by: Some("david".to_string()),
note: None,
date: Some("2026-01-01".to_string()),
observed_at: None,
basis: None,
est_lower: Some(lower),
est_upper: Some(upper),
admission: None,
};
let session = ComparisonSession {
schema: COMPARISON_SCHEMA.to_string(),
version: COMPARISON_VERSION,
session: SessionHeader {
uid: uid.to_string(),
date: "2026-01-01".to_string(),
audience: None,
},
judgements: vec![j],
tombstones: Vec::new(),
};
let text = crate::comparison::to_toml(&session).unwrap();
write(
root,
&format!(".doctrine/comparisons/2026-01-01-{uid}.toml"),
&text,
);
}
fn capture_more_work(root: &Path, slot: &str, a: &str, b: &str, resp: &str, rater: &str) {
write(
root,
&format!(".doctrine/comparisons/2026-01-01-{slot}.toml"),
&format!(
"schema = \"doctrine.comparison-session\"\nversion = 2\n\n\
[session]\nuid = \"sess-{slot}\"\ndate = \"2026-01-01\"\n\n\
[[judgement]]\nuid = \"row-{slot}\"\nseq = 0\na = \"{a}\"\nb = \"{b}\"\n\
response = \"{resp}\"\ndomain = \"estimate\"\nframe = \"more-work\"\n\
form = \"order\"\nrater = \"{rater}\"\ndate = \"2026-01-01\"\n"
),
);
}
fn cost_reason(root: &Path, id: &str) -> Option<ReasonKind> {
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = crate::priority::config::load(root);
let pipeline = graph::load_comparison_pipeline(root, &scanned, &cfg).unwrap();
let g = graph::build_from_with_cfg(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&comparison::cost_feed(&pipeline.estimate.projection),
&pipeline.value_claims,
pipeline.bare_anchor,
&pipeline.estimate_claims,
)
.unwrap();
let key = parse_key(id).unwrap();
cost_source_reason(&g, key, &pipeline, &cfg)
}
fn cost_fragment(root: &Path, id: &str) -> String {
cost_reason(root, id)
.and_then(|r| crate::priority::render::cost_source_fragment(&r))
.unwrap_or_default()
}
#[test]
fn cost_source_none_without_est_engagement() {
let dir = tmp();
let root = dir.path();
seed_cost(
root,
1,
"[estimate]\nlower = 2.0\nupper = 8.0\n[value]\nvalue = 10.0\n",
);
assert!(
cost_reason(root, "ISS-001").is_none(),
"no est engagement ⇒ no cost-source block"
);
}
#[test]
fn cost_source_claim_shape_shows_bounds_beta_and_attribution() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(root, 2, "[value]\nvalue = 10.0\n");
capture_cost_anchor(root, "a1", "ISS-001", 2.0, 8.0);
capture_more_work(root, "mw", "ISS-001", "ISS-002", "prefer-a", "human");
match cost_reason(root, "ISS-001") {
Some(ReasonKind::CostClaim {
est_cost,
lower,
upper,
beta,
tier,
by,
date,
conflict,
}) => {
assert!(
(est_cost - 5.9).abs() < 1e-9,
"β-resolved claim: {est_cost}"
);
assert_eq!((lower, upper), (5.9, 5.9)); assert!((beta - 0.65).abs() < 1e-9);
assert_eq!(tier, ClaimTier::Human);
assert_eq!(by.as_deref(), Some("david"));
assert_eq!(date.as_deref(), Some("2026-01-01"));
assert!(conflict.is_empty());
}
other => panic!("expected CostClaim(Human), got {other:?}"),
}
assert!(
cost_fragment(root, "ISS-001")
.contains("est_cost 5.9 — human claim [5.9 ‥ 5.9] · β 0.65 (david, 2026-01-01)"),
"claim fragment: {}",
cost_fragment(root, "ISS-001"),
);
}
#[test]
fn cost_source_projected_shape_rater_split_excludes_noconstraint() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(
root,
2,
"[estimate]\nlower = 8.0\nupper = 8.0\n[value]\nvalue = 10.0\n",
);
capture_cost_anchor(root, "a1", "ISS-002", 8.0, 8.0);
capture_more_work(root, "mw", "ISS-002", "ISS-001", "prefer-a", "human");
capture_more_work(root, "nc", "ISS-001", "ISS-002", "incomparable", "agent");
match cost_reason(root, "ISS-001") {
Some(ReasonKind::CostProjected {
upper,
human,
agent,
..
}) => {
assert_eq!(upper, Some(8.0), "C6 upper bound at the anchor");
assert_eq!((human, agent), (1, 0), "NoConstraint row excluded");
}
other => panic!("expected CostProjected, got {other:?}"),
}
assert!(
cost_fragment(root, "ISS-001").contains(
"projected · bounds (unbounded ‥ 8.0) · from 1 constraining sizing \
judgements (1 human, 0 agent)"
),
"projected fragment: {}",
cost_fragment(root, "ISS-001")
);
}
#[test]
fn cost_source_bare_anchor_shape_decomposes_max_and_margin() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(
root,
2,
"[estimate]\nlower = 10.0\nupper = 10.0\n[value]\nvalue = 10.0\n",
);
seed_cost(
root,
3,
"[estimate]\nlower = 1.0\nupper = 1.0\n[value]\nvalue = 10.0\n",
);
capture_more_work(root, "mw", "ISS-002", "ISS-003", "prefer-a", "human");
match cost_reason(root, "ISS-001") {
Some(ReasonKind::CostBareAnchor {
est_cost,
max_estimate,
margin,
}) => {
assert!((est_cost - 1.0).abs() < 1e-9);
assert_eq!(max_estimate, None);
assert!((margin - 1.0).abs() < 1e-9);
}
other => panic!("expected CostBareAnchor, got {other:?}"),
}
assert_eq!(
cost_fragment(root, "ISS-001"),
"est_cost 1.0 — bare anchor (no estimate in corpus; default 1.0)"
);
}
#[test]
fn cost_source_gauge_flag_shows_bare_anchor_never_the_divisor() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(root, 2, "[value]\nvalue = 10.0\n");
seed_cost(
root,
3,
"[estimate]\nlower = 10.0\nupper = 10.0\n[value]\nvalue = 10.0\n",
);
capture_more_work(root, "mw", "ISS-001", "ISS-002", "prefer-a", "human");
match cost_reason(root, "ISS-001") {
Some(ReasonKind::CostGauge {
est_cost,
max_estimate,
margin,
judgements,
}) => {
assert!(
(est_cost - 1.0).abs() < 1e-9,
"scoring used the bare anchor"
);
assert_eq!(max_estimate, None);
assert!((margin - 1.0).abs() < 1e-9);
assert_eq!(judgements, 1);
}
other => panic!("expected CostGauge, got {other:?}"),
}
let frag = cost_fragment(root, "ISS-001");
assert!(
frag.contains("est_cost 1.0 — bare anchor (no estimate in corpus; default 1.0)"),
"line 1 is the bare anchor (what scoring used): {frag}"
);
assert!(
frag.contains(
"sizing: gauge · ordered by 1 judgements, no estimated item in component — \
estimate any member to calibrate"
),
"line 2 discloses gauge separately — never implies it fed the divisor: {frag}"
);
}
#[test]
fn cost_source_bare_anchor_decomposes_max_and_margin_from_pipeline() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(root, 2, "[value]\nvalue = 10.0\n");
seed_cost(root, 3, "[value]\nvalue = 10.0\n");
set_interval_claim(root, "a1", "ISS-002", 2.0, 8.0);
set_point_claim(root, "a2", "ISS-003", 5.0);
capture_more_work(root, "mw", "ISS-002", "ISS-003", "prefer-a", "human");
match cost_reason(root, "ISS-001") {
Some(ReasonKind::CostBareAnchor {
est_cost,
max_estimate,
margin,
}) => {
assert!((est_cost - 9.0).abs() < 1e-9, "est_cost={est_cost}");
assert!(
(max_estimate.unwrap() - 8.0).abs() < 1e-9,
"max_estimate={max_estimate:?}"
);
assert!((margin - 1.0).abs() < 1e-9, "margin={margin}");
}
other => panic!("expected CostBareAnchor, got {other:?}"),
}
assert_eq!(
cost_fragment(root, "ISS-001"),
"est_cost 9.0 — bare anchor (max estimate 8.0 + margin 1.0)"
);
}
#[test]
fn cost_source_gauge_shows_bare_anchor_with_estimate_claims_present() {
let dir = tmp();
let root = dir.path();
seed_cost(root, 1, "[value]\nvalue = 10.0\n");
seed_cost(root, 2, "[value]\nvalue = 10.0\n");
seed_cost(root, 3, "[value]\nvalue = 10.0\n");
set_interval_claim(root, "a1", "ISS-003", 2.0, 8.0);
capture_more_work(root, "mw", "ISS-001", "ISS-002", "prefer-a", "human");
match cost_reason(root, "ISS-001") {
Some(ReasonKind::CostGauge {
est_cost,
max_estimate,
margin,
judgements,
}) => {
assert!(
(est_cost - 9.0).abs() < 1e-9,
"scoring used the bare anchor: {est_cost}"
);
assert!(
(max_estimate.unwrap() - 8.0).abs() < 1e-9,
"max_estimate={max_estimate:?}"
);
assert!((margin - 1.0).abs() < 1e-9, "margin={margin}");
assert_eq!(judgements, 1);
}
other => panic!("expected CostGauge, got {other:?}"),
}
let frag = cost_fragment(root, "ISS-001");
assert!(
frag.contains("est_cost 9.0 — bare anchor (max estimate 8.0 + margin 1.0)"),
"line 1 is the bare anchor (what scoring used): {frag}"
);
assert!(
frag.contains(
"sizing: gauge · ordered by 1 judgements, no estimated item in component — \
estimate any member to calibrate"
),
"line 2 discloses gauge separately — never implies it fed the divisor: {frag}"
);
}
}