use std::collections::BTreeMap;
use crate::catalog::scan::ScanMode;
use cordage::{
Arity, CyclePolicy, Direction, EdgeAttrs, Graph, GraphBuilder, OrderLayer, OrderSpec,
OverlayConfig, OverlayId,
};
use crate::comparison::{
self, AnchorMap, EntityLifecycle, Projection as ValueProjection, ProjectionCfg, StatusMap,
};
use crate::facet::EntityFacets;
use crate::priority::config;
use crate::priority::partition::{self, StatusClass};
use crate::projection::Projection;
use crate::relation::RelationLabel;
use crate::relation_graph::{self, EntityKey};
use crate::{dep_seq, entity, integrity};
#[derive(Debug, Clone, Copy)]
pub(crate) struct BaseScore {
pub(crate) value_dim: f64,
pub(crate) risk_dim: f64,
}
impl BaseScore {
pub(crate) fn total(&self) -> f64 {
let t = self.value_dim + self.risk_dim;
if t.is_finite() { t } else { 0.0 }
}
}
const EPSILON: f64 = 1e-12;
fn floor_eps(x: f64) -> f64 {
if x < EPSILON { EPSILON } else { x }
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CostCtx {
pub(crate) absent: f64,
}
pub(crate) fn authored_est_cost(bounds: (f64, f64), ec: &config::EstimateCost) -> f64 {
let (lower, upper) = bounds;
floor_eps(lower + ec.skew * (upper - lower))
}
fn est_cost(
bounds: Option<(f64, f64)>,
key: EntityKey,
cost_feed: &comparison::CostFeed,
ctx: CostCtx,
ec: &config::EstimateCost,
) -> f64 {
match bounds {
Some(b) => authored_est_cost(b, ec),
None => match cost_feed.get(&key.canonical()) {
Some(&projected) => floor_eps(projected),
None => ctx.absent,
},
}
}
pub(crate) const DEFAULT_VALUE: f64 = 1.0;
fn effective_raw_value(
kind: &entity::Kind,
f: &EntityFacets,
key: EntityKey,
projected: &ValueProjection,
claims: &comparison::ClaimResolution,
) -> Option<f64> {
let value_bearing = crate::kinds::is_value_bearing(kind.prefix);
let canonical = key.canonical();
let claim_rung = |map: &BTreeMap<String, comparison::ResolvedClaim>| -> Option<f64> {
if !value_bearing {
return None;
}
map.get(&canonical).map(|c| c.value)
};
claim_rung(&claims.anchored)
.or_else(|| projected.get(&canonical).map(|&(v, _)| v))
.or_else(|| claim_rung(&claims.priors))
.or_else(|| f.value.as_ref().map(|v| v.value))
.or_else(|| value_bearing.then_some(DEFAULT_VALUE))
}
fn tag_term(f: &EntityFacets, cfg: &config::PriorityConfig) -> f64 {
(1.0 + f.tags.iter().map(|t| cfg.tag_coeff(t) - 1.0).sum::<f64>()).max(0.0)
}
#[expect(
clippy::too_many_arguments,
reason = "the pure scoring inputs (projection, claims, cost feed) thread from one shell seam"
)]
fn base_score(
f: &EntityFacets,
kind: &entity::Kind,
key: EntityKey,
projected: &ValueProjection,
claims: &comparison::ClaimResolution,
cost_feed: &comparison::CostFeed,
cfg: &config::PriorityConfig,
ctx: CostCtx,
) -> BaseScore {
let tag_term = tag_term(f, cfg);
let value_dim = {
let raw = match effective_raw_value(kind, f, key, projected, claims) {
Some(v) => {
let cost = est_cost(
f.estimate.as_ref().map(|e| (e.lower, e.upper)),
key,
cost_feed,
ctx,
&cfg.estimate,
);
let kw = cfg.kind_weight(kind.prefix);
cfg.coefficients.value * v * kw * tag_term / cost
}
None => 0.0,
};
if raw.is_finite() { raw } else { 0.0 }
};
let risk_dim = {
let raw = cfg.coefficients.risk * f64::from(crate::risk::exposure(f.risk.as_ref()));
if raw.is_finite() { raw } else { 0.0 }
};
BaseScore {
value_dim,
risk_dim,
}
}
pub(crate) struct NodeAttr {
pub(crate) kind: &'static entity::Kind,
pub(crate) status: Option<String>,
pub(crate) promoted: bool,
pub(crate) title: String,
pub(crate) base_score: BaseScore,
pub(crate) facets: EntityFacets,
}
pub(crate) struct PriorityGraph {
pub(crate) graph: Graph,
pub(crate) projection: Projection<EntityKey>,
pub(crate) attrs: BTreeMap<EntityKey, NodeAttr>,
pub(crate) leverage: BTreeMap<EntityKey, f64>,
pub(crate) optionality: BTreeMap<EntityKey, f64>,
pub(crate) score: BTreeMap<EntityKey, f64>,
pub(crate) dep_overlay: OverlayId,
pub(crate) seq_overlay: OverlayId,
pub(crate) cost_ctx: CostCtx,
pub(crate) cost_feed: comparison::CostFeed,
}
impl PriorityGraph {
pub(crate) fn item_costing(
&self,
key: &EntityKey,
cfg: &config::PriorityConfig,
) -> Option<(f64, f64, bool)> {
let attr = self.attrs.get(key)?;
let f = &attr.facets;
let multiplier =
cfg.coefficients.value * cfg.kind_weight(attr.kind.prefix) * tag_term(f, cfg);
let bounds = f.estimate.as_ref().map(|e| (e.lower, e.upper));
let est = est_cost(bounds, *key, &self.cost_feed, self.cost_ctx, &cfg.estimate);
Some((multiplier, est, f.estimate.is_none()))
}
}
const REF_LABELS: &[RelationLabel] = &[
RelationLabel::References,
RelationLabel::Supersedes,
RelationLabel::DescendsFrom,
RelationLabel::Parent,
RelationLabel::Members,
RelationLabel::Interactions,
RelationLabel::Fulfils,
RelationLabel::Related,
RelationLabel::Reviews,
RelationLabel::OwningSlice,
];
const CONSEQUENCE_LABELS: &[RelationLabel] = &[
RelationLabel::References,
RelationLabel::DescendsFrom,
RelationLabel::Parent,
RelationLabel::Members,
];
pub(crate) fn build(root: &std::path::Path) -> anyhow::Result<PriorityGraph> {
build_from(
&relation_graph::scan_entities(root, &mut vec![], ScanMode::default())?,
root,
)
}
pub(crate) fn build_from(
scanned: &[relation_graph::ScannedEntity],
root: &std::path::Path,
) -> anyhow::Result<PriorityGraph> {
let cfg = config::load(root);
let pipeline = load_comparison_pipeline(root, scanned, &cfg)?;
let cost_feed = comparison::cost_feed(&pipeline.estimate.projection);
build_from_with_cfg(
scanned,
root,
&cfg,
&pipeline.value.projection,
&cost_feed,
&pipeline.value_claims,
)
}
pub(crate) fn load_comparison_pipeline(
root: &std::path::Path,
scanned: &[relation_graph::ScannedEntity],
cfg: &config::PriorityConfig,
) -> anyhow::Result<comparison::Pipeline> {
let statuses = comparison_status_map(scanned);
let est_anchors = comparison_est_anchor_map(scanned, cfg);
let value_cfg = ProjectionCfg {
gauge_step: cfg.gauge.step,
..comparison::VALUE_PROJECTION_PARAMS
};
let est_cfg = ProjectionCfg {
gauge_step: cfg.estimate.gauge_step,
gauge_center: bare_cost_anchor(scanned, &cfg.estimate),
};
comparison::load_pipeline(root, &statuses, &est_anchors, &value_cfg, &est_cfg)
}
fn bare_cost_anchor(scanned: &[relation_graph::ScannedEntity], ec: &config::EstimateCost) -> f64 {
let max_upper = scanned
.iter()
.filter(|entity| {
partition::status_class(entity.kind, entity.status.as_deref()) != StatusClass::Terminal
})
.filter_map(|entity| entity.estimate.as_ref().map(|e| e.upper))
.max_by(f64::total_cmp);
match max_upper {
Some(mu) => mu + ec.margin,
None => 1.0,
}
}
pub(crate) fn load_comparison_pipeline_for_root(
root: &std::path::Path,
) -> anyhow::Result<comparison::Pipeline> {
let scanned = relation_graph::scan_entities(root, &mut vec![], ScanMode::default())?;
let cfg = config::load(root);
load_comparison_pipeline(root, &scanned, &cfg)
}
fn comparison_status_map(scanned: &[relation_graph::ScannedEntity]) -> StatusMap {
let mut map = StatusMap::new();
for entity in scanned {
if partition::status_class(entity.kind, entity.status.as_deref()) == StatusClass::Terminal {
map.insert(entity.key.canonical(), EntityLifecycle::Terminal);
}
}
for entity in scanned {
for edge in &entity.outbound {
if edge.label == RelationLabel::Supersedes
&& let Ok((kref, id)) = integrity::parse_canonical_ref(&edge.target)
{
let target_key = EntityKey {
prefix: kref.kind.prefix,
id,
};
map.insert(
target_key.canonical(),
EntityLifecycle::Superseded {
by: entity.key.canonical(),
},
);
}
}
}
map
}
fn comparison_est_anchor_map(
scanned: &[relation_graph::ScannedEntity],
cfg: &config::PriorityConfig,
) -> AnchorMap {
scanned
.iter()
.filter(|entity| {
comparison::admissible_estimate_pair(entity.kind.prefix, entity.kind.prefix).is_ok()
})
.filter_map(|entity| {
entity.estimate.as_ref().map(|e| {
(
entity.key.canonical(),
authored_est_cost((e.lower, e.upper), &cfg.estimate),
)
})
})
.collect()
}
pub(crate) fn build_from_with_cfg(
scanned: &[relation_graph::ScannedEntity],
root: &std::path::Path,
cfg: &config::PriorityConfig,
projected: &ValueProjection,
cost_feed: &comparison::CostFeed,
claims: &comparison::ClaimResolution,
) -> anyhow::Result<PriorityGraph> {
let ctx = CostCtx {
absent: bare_cost_anchor(scanned, &cfg.estimate),
};
let base_by_key: BTreeMap<EntityKey, BaseScore> = scanned
.iter()
.map(|entity| {
let base = base_score(
&EntityFacets {
estimate: entity.estimate.clone(),
value: entity.value.clone(),
risk: entity.risk.clone(),
tags: entity.tags.clone(),
},
entity.kind,
entity.key,
projected,
claims,
cost_feed,
cfg,
ctx,
);
(entity.key, base)
})
.collect();
let mut order: Vec<EntityKey> = scanned.iter().map(|e| e.key).collect();
order.sort_by(|a, b| {
let ba = base_by_key.get(a).map_or(0.0, BaseScore::total);
let bb = base_by_key.get(b).map_or(0.0, BaseScore::total);
bb.total_cmp(&ba).then_with(|| a.cmp(b))
});
let mut builder = GraphBuilder::new();
let mut ref_by_label: BTreeMap<RelationLabel, OverlayId> = BTreeMap::new();
for &label in REF_LABELS {
let ov = builder.overlay(OverlayConfig::new(CyclePolicy::Reject, Arity::Unbounded));
ref_by_label.insert(label, ov);
}
let dep_overlay = builder.overlay(OverlayConfig::new(CyclePolicy::Reject, Arity::Unbounded));
let seq_overlay = builder.overlay(OverlayConfig::new(CyclePolicy::Evict, Arity::Unbounded));
let mut projection: Projection<EntityKey> = Projection::new();
for &key in &order {
assert!(
projection.resolve(key).is_none(),
"priority::graph: duplicate EntityKey {} (canonical ids unique by prefix)",
key.canonical()
);
projection.intern(&mut builder, key);
}
let mut dep_seq: BTreeMap<EntityKey, (dep_seq::DepSeq, bool)> = BTreeMap::new();
for entity in scanned {
dep_seq.insert(
entity.key,
relation_graph::dep_seq_for(root, entity.kind, entity.key.id)?,
);
}
let mut attrs: BTreeMap<EntityKey, NodeAttr> = BTreeMap::new();
for entity in scanned {
let base = base_by_key.get(&entity.key).copied().unwrap_or(BaseScore {
value_dim: 0.0,
risk_dim: 0.0,
});
attrs.insert(
entity.key,
NodeAttr {
kind: entity.kind,
status: entity.status.clone(),
promoted: dep_seq
.get(&entity.key)
.is_some_and(|(_ds, promoted)| *promoted),
title: entity.title.clone(),
base_score: base,
facets: EntityFacets {
estimate: entity.estimate.clone(),
value: entity.value.clone(),
risk: entity.risk.clone(),
tags: entity.tags.clone(),
},
},
);
}
for entity in scanned {
let Some(src) = projection.resolve(entity.key) else {
debug_assert!(false, "priority::graph: edge-pass key not interned");
continue;
};
for edge in &entity.outbound {
if let Some(dst) = resolve(&projection, &edge.target)
&& let Some(&ov) = ref_by_label.get(&edge.label)
{
builder.edge(ov, src, dst, EdgeAttrs::new(0, 0));
}
}
if let Some((ds, _promoted)) = dep_seq.get(&entity.key) {
for prereq_ref in &ds.needs {
if let Some(prereq) = resolve(&projection, prereq_ref) {
builder.edge(dep_overlay, prereq, src, EdgeAttrs::new(0, 0));
}
}
for (idx, edge) in ds.after.iter().enumerate() {
if let Some(prereq) = resolve(&projection, &edge.to) {
let age = u64::try_from(idx).map_err(|e| {
anyhow::anyhow!("priority::graph: after-edge index overflows u64: {e}")
})?;
builder.edge(seq_overlay, prereq, src, EdgeAttrs::new(edge.rank, age));
}
}
}
}
builder.order_spec(OrderSpec::new(vec![
OrderLayer::new(dep_overlay, Direction::Along),
OrderLayer::new(seq_overlay, Direction::Along),
]));
let graph = builder.build().map_err(|e| {
anyhow::anyhow!(
"priority::graph: cordage rejected well-formed adapter input (internal bug): {e:?}"
)
})?;
let (leverage, optionality, score) = consequence_post_pass(
&graph,
&projection,
&attrs,
&ref_by_label,
dep_overlay,
cfg,
projected,
claims,
);
Ok(PriorityGraph {
graph,
projection,
attrs,
leverage,
optionality,
score,
dep_overlay,
seq_overlay,
cost_ctx: ctx,
cost_feed: cost_feed.clone(),
})
}
#[expect(
clippy::too_many_arguments,
reason = "the pure scoring inputs (projection, claims) thread from build_from_with_cfg's one seam"
)]
fn consequence_post_pass(
graph: &Graph,
projection: &Projection<EntityKey>,
attrs: &BTreeMap<EntityKey, NodeAttr>,
ref_by_label: &BTreeMap<RelationLabel, OverlayId>,
dep_overlay: OverlayId,
cfg: &config::PriorityConfig,
projected: &ValueProjection,
claims: &comparison::ClaimResolution,
) -> (
BTreeMap<EntityKey, f64>,
BTreeMap<EntityKey, f64>,
BTreeMap<EntityKey, f64>,
) {
use std::collections::BTreeSet;
let ek = |nid: cordage::NodeId| -> Option<EntityKey> { projection.key_of(nid) };
let base_of = |nid: cordage::NodeId| -> f64 {
ek(nid)
.and_then(|k| attrs.get(&k))
.map_or(0.0, |a| a.base_score.total())
};
let value_dim_of = |nid: cordage::NodeId| -> f64 {
ek(nid)
.and_then(|k| attrs.get(&k))
.map_or(0.0, |a| a.base_score.value_dim)
};
let risk_dim_of = |nid: cordage::NodeId| -> f64 {
ek(nid)
.and_then(|k| attrs.get(&k))
.map_or(0.0, |a| a.base_score.risk_dim)
};
let raw_value_of = |nid: cordage::NodeId| -> f64 {
let Some(key) = ek(nid) else {
return 0.0;
};
attrs
.get(&key)
.and_then(|a| effective_raw_value(a.kind, &a.facets, key, projected, claims))
.unwrap_or(0.0)
};
let cycles = graph.provenance().cycles();
let mut node_to_component: BTreeMap<cordage::NodeId, usize> = BTreeMap::new();
let mut component_members: Vec<BTreeSet<cordage::NodeId>> = Vec::new();
for cyc in cycles {
if cyc.overlay() != dep_overlay {
continue;
}
let comp_idx = component_members.len();
for &n in cyc.nodes() {
node_to_component.insert(n, comp_idx);
}
component_members.push(cyc.nodes().clone());
}
for nid in graph.ordered() {
node_to_component.entry(nid).or_insert_with(|| {
let comp_idx = component_members.len();
component_members.push(BTreeSet::from([nid]));
comp_idx
});
}
let component_count = component_members.len();
let comp_of = |nid: cordage::NodeId| -> Option<usize> { node_to_component.get(&nid).copied() };
let mut comp_dependents: Vec<BTreeSet<cordage::NodeId>> =
vec![BTreeSet::new(); component_count];
let mut comp_succ: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); component_count];
for (c, ((dependents, succ), members)) in comp_dependents
.iter_mut()
.zip(comp_succ.iter_mut())
.zip(component_members.iter())
.enumerate()
{
for &m in members {
for (d, _) in graph.out_edges(dep_overlay, m) {
match comp_of(d) {
Some(dc) if dc != c => {
dependents.insert(d);
succ.insert(dc);
}
_ => {} }
}
}
}
let mut topo: Vec<usize> = Vec::with_capacity(component_count);
let mut visited = vec![false; component_count];
for start in 0..component_count {
if visited.get(start).copied().unwrap_or(true) {
continue;
}
let mut stack: Vec<(usize, bool)> = vec![(start, false)];
while let Some((c, emit)) = stack.pop() {
if emit {
topo.push(c);
continue;
}
if visited.get(c).copied().unwrap_or(true) {
continue;
}
if let Some(slot) = visited.get_mut(c) {
*slot = true;
}
stack.push((c, true));
if let Some(succ) = comp_succ.get(c) {
for &sc in succ {
if !visited.get(sc).copied().unwrap_or(true) {
stack.push((sc, false));
}
}
}
}
}
let mut leverage_by_node: BTreeMap<cordage::NodeId, f64> = BTreeMap::new();
for &c in &topo {
let Some(dependents) = comp_dependents.get(c) else {
continue;
};
let mut sum = 0.0f64;
for &d in dependents {
sum += base_of(d) + leverage_by_node.get(&d).copied().unwrap_or(0.0);
}
let lev = cfg.consequence.dep_coeff * sum;
let lev = if lev.is_finite() { lev } else { 0.0 };
if let Some(members) = component_members.get(c) {
for &m in members {
leverage_by_node.insert(m, lev);
}
}
}
let mut optionality_by_node: BTreeMap<cordage::NodeId, f64> = BTreeMap::new();
for nid in graph.ordered() {
let mut sum = 0.0f64;
for &label in CONSEQUENCE_LABELS {
if let Some(&ov) = ref_by_label.get(&label) {
for (src, _) in graph.in_edges(ov, nid) {
sum += base_of(src);
}
}
}
let opt = cfg.consequence.ref_coeff * sum;
let opt = if opt.is_finite() { opt } else { 0.0 };
optionality_by_node.insert(nid, opt);
}
let fulfils_ov = ref_by_label.get(&RelationLabel::Fulfils).copied();
let mut burndown_by_node: BTreeMap<cordage::NodeId, f64> = BTreeMap::new();
if let Some(ov) = fulfils_ov {
for nid in graph.ordered() {
let raw_val = raw_value_of(nid);
if raw_val <= 0.0 {
burndown_by_node.insert(nid, 0.0);
continue;
}
let mut delivered = 0.0f64;
for (src, _) in graph.in_edges(ov, nid) {
let Some(src_key) = ek(src) else { continue };
let Some(src_attr) = attrs.get(&src_key) else {
continue;
};
let gate = match src_attr.status.as_deref() {
Some("started" | "audit" | "reconcile" | "done") => 1.0,
_ => 0.0,
};
if gate > 0.0 {
delivered += gate * raw_value_of(src);
}
}
let r = (delivered / raw_val).clamp(0.0, 1.0);
let burn = value_dim_of(nid) * (1.0 - r);
let burn = if burn.is_finite() { burn } else { 0.0 };
burndown_by_node.insert(nid, burn);
}
}
let mut leverage: BTreeMap<EntityKey, f64> = BTreeMap::new();
let mut optionality: BTreeMap<EntityKey, f64> = BTreeMap::new();
let mut score: BTreeMap<EntityKey, f64> = BTreeMap::new();
for nid in graph.ordered() {
if let Some(k) = ek(nid) {
let lev = leverage_by_node.get(&nid).copied().unwrap_or(0.0);
let opt = optionality_by_node.get(&nid).copied().unwrap_or(0.0);
let burn = burndown_by_node
.get(&nid)
.copied()
.unwrap_or(value_dim_of(nid));
let sc = risk_dim_of(nid) + lev + opt + burn;
let sc = if sc.is_finite() { sc } else { 0.0 };
leverage.insert(k, lev);
optionality.insert(k, opt);
score.insert(k, sc);
}
}
(leverage, optionality, score)
}
fn resolve(projection: &Projection<EntityKey>, reference: &str) -> Option<cordage::NodeId> {
let (kref, id) = integrity::parse_canonical_ref(reference).ok()?;
projection.resolve(EntityKey {
prefix: kref.kind.prefix,
id,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
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 migrate_body(source: &crate::entity::Kind, rels: &str) -> String {
use crate::relation::RelationLabel;
let mut typed = String::new();
let mut rows = String::new();
for line in rels.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let key = trimmed.split('=').next().unwrap_or("").trim();
let is_simple_list = trimmed.contains('[') && !trimmed.contains('{');
let migrated = is_simple_list
&& RelationLabel::from_name(key)
.and_then(|l| crate::relation::lookup(source, l, None))
.is_some_and(|r| {
r.tier == crate::relation::Tier::One
&& r.link != crate::relation::LinkPolicy::LifecycleOnly
});
if migrated {
let inner = trimmed
.split_once('[')
.and_then(|(_, rest)| rest.rsplit_once(']'))
.map(|(refs, _)| refs)
.unwrap_or("");
for t in inner.split(',') {
let t = t.trim().trim_matches('"');
if !t.is_empty() {
rows.push_str(&format!(
"[[relation]]\nlabel = \"{key}\"\ntarget = \"{t}\"\n"
));
}
}
} else {
typed.push_str(line);
typed.push('\n');
}
}
let typed_table = if typed.trim().is_empty() {
String::new()
} else {
format!("[relationships]\n{typed}")
};
format!("{typed_table}{rows}")
}
fn seed_slice(root: &Path, id: u32, rels: &str) {
write(
root,
&format!(".doctrine/slice/{id:03}/slice-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"s\"\ntitle = \"S\"\nstatus = \"proposed\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n{}",
migrate_body(&crate::slice::SLICE_KIND, rels)
),
);
write(
root,
&format!(".doctrine/slice/{id:03}/slice-{id:03}.md"),
"scope\n",
);
}
fn seed_requirement(root: &Path, id: u32) {
write(
root,
&format!(".doctrine/requirement/{id:03}/requirement-{id:03}.toml"),
&format!("id = {id}\nslug = \"r\"\ntitle = \"R\"\nstatus = \"active\"\n"),
);
write(
root,
&format!(".doctrine/requirement/{id:03}/requirement-{id:03}.md"),
"r\n",
);
}
fn seed_issue(root: &Path, id: u32, status: &str, resolution: &str, rels: &str) {
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\
{}",
migrate_body(&crate::backlog::ISSUE_KIND, rels)
),
);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
}
fn seed_risk(root: &Path, id: u32, status: &str, rels: &str) {
write(
root,
&format!(".doctrine/backlog/risk/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"k\"\ntitle = \"K\"\nkind = \"risk\"\nstatus = \"{status}\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
{}",
migrate_body(&crate::backlog::RISK_KIND, rels)
),
);
write(
root,
&format!(".doctrine/backlog/risk/{id:03}/backlog-{id:03}.md"),
"k\n",
);
}
fn seed_rec(root: &Path, id: u32, owning_slice: &str) {
write(
root,
&format!(".doctrine/rec/{id:03}/rec-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"r\"\ntitle = \"R\"\n\
[rec]\nmove = \"accept\"\nowning_slice = \"{owning_slice}\"\n"
),
);
}
fn seed_review(root: &Path, id: u32, target: &str, findings: &str) {
write(
root,
&format!(".doctrine/review/{id:03}/review-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"r\"\ntitle = \"R\"\n\
[review]\nfacet = \"reconciliation\"\nraiser = \"a\"\nresponder = \"b\"\n\
[target]\nref = \"{target}\"\n{findings}"
),
);
}
fn key(prefix: &'static str, id: u32) -> EntityKey {
EntityKey { prefix, id }
}
#[test]
fn builds_over_multi_kind_corpus_node_set_equals_scanned() {
let dir = tmp();
let root = dir.path();
seed_slice(
root,
1,
"[[relation]]\nlabel = \"references\"\nrole = \"implements\"\ntarget = \"REQ-005\"\n",
);
seed_requirement(root, 5);
seed_issue(root, 1, "open", "", "slices = [\"SL-001\"]\n");
seed_rec(root, 1, "SL-001");
seed_review(root, 1, "SL-001", "");
let pg = build(root).unwrap();
let scanned: std::collections::BTreeSet<EntityKey> =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default())
.unwrap()
.iter()
.map(|e| e.key)
.collect();
let minted: std::collections::BTreeSet<EntityKey> = pg.attrs.keys().copied().collect();
assert_eq!(minted, scanned, "every scanned entity is a node");
for k in &scanned {
assert!(
pg.projection.resolve(*k).is_some(),
"{} minted",
k.canonical()
);
}
assert_eq!(pg.attrs.len(), scanned.len());
for (k, attr) in &pg.attrs {
assert_eq!(
attr.kind.prefix, k.prefix,
"NodeAttr.kind matches the key prefix"
);
}
}
#[test]
fn node_attr_status_promoted_per_kind() {
let dir = tmp();
let root = dir.path();
seed_slice(root, 1, "");
seed_requirement(root, 5);
seed_issue(root, 1, "resolved", "promoted", "");
seed_issue(root, 2, "open", "", "");
seed_rec(root, 1, "SL-001");
seed_review(
root,
1,
"SL-001",
"[[finding]]\nid = \"F-1\"\nstatus = \"open\"\nseverity = \"minor\"\n\
title = \"t\"\ndetail = \"d\"\n",
);
seed_review(
root,
2,
"SL-001",
"[[finding]]\nid = \"F-1\"\nstatus = \"verified\"\nseverity = \"minor\"\n\
title = \"t\"\ndetail = \"d\"\n",
);
let pg = build(root).unwrap();
assert_eq!(pg.attrs[&key("SL", 1)].status.as_deref(), Some("proposed"));
assert!(!pg.attrs[&key("SL", 1)].promoted);
assert_eq!(pg.attrs[&key("REQ", 5)].status.as_deref(), Some("active"));
assert_eq!(pg.attrs[&key("REC", 1)].status, None);
assert_eq!(pg.attrs[&key("ISS", 1)].status.as_deref(), Some("resolved"));
assert!(
pg.attrs[&key("ISS", 1)].promoted,
"resolution=promoted ⇒ promoted"
);
assert!(!pg.attrs[&key("ISS", 2)].promoted);
assert_eq!(pg.attrs[&key("RV", 1)].status.as_deref(), Some("active"));
assert_eq!(pg.attrs[&key("RV", 2)].status.as_deref(), Some("done"));
}
#[test]
fn mint_order_base_desc_then_canonical_asc_and_permutation_invariant() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 10.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "lower = 0.0\nupper = 10.0", "value = 25.0", "");
seed_issue_with_facets(root, 3, "", "lower = 0.0\nupper = 10.0", "value = 15.0", "");
let pg = build(root).unwrap();
let n1 = pg.projection.resolve(key("ISS", 1)).unwrap();
let n2 = pg.projection.resolve(key("ISS", 2)).unwrap();
let n3 = pg.projection.resolve(key("ISS", 3)).unwrap();
assert!(
n2 < n3,
"ISS-002 (base 25/6.5) mints before ISS-003 (base 15/6.5)"
);
assert!(
n3 < n1,
"ISS-003 (base 15/6.5) mints before ISS-001 (base 5/6.5)"
);
let dir2 = tmp();
let root2 = dir2.path();
seed_issue_with_facets(
root2,
3,
"",
"lower = 0.0\nupper = 10.0",
"value = 15.0",
"",
);
seed_issue_with_facets(
root2,
2,
"",
"lower = 0.0\nupper = 10.0",
"value = 25.0",
"",
);
seed_issue_with_facets(root2, 1, "", "lower = 0.0\nupper = 10.0", "value = 5.0", "");
let pg2 = build(root2).unwrap();
assert_eq!(pg.score, pg2.score, "score map is permutation-invariant");
let m1 = pg2.projection.resolve(key("ISS", 1)).unwrap();
let m2 = pg2.projection.resolve(key("ISS", 2)).unwrap();
let m3 = pg2.projection.resolve(key("ISS", 3)).unwrap();
assert!(m2 < m3 && m3 < m1, "mint order is permutation-invariant");
}
#[test]
fn mint_order_is_blind_to_consequence_topology() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "");
seed_issue_with_facets(root, 2, "", "lower = 0.0\nupper = 10.0", "value = 25.0", "");
seed_slice(root, 1, "slices = [\"ISS-001\"]\n");
seed_slice(root, 2, "slices = [\"ISS-001\"]\n");
let pg = build(root).unwrap();
let n1 = pg.projection.resolve(key("ISS", 1)).unwrap();
let n2 = pg.projection.resolve(key("ISS", 2)).unwrap();
assert!(
n2 < n1,
"ISS-002 (base 25/6.5≈3.846) mints before the heavily-referenced ISS-001 (base 0) — mint is base-only (I3)"
);
assert!((pg.score.get(&key("ISS", 1)).copied().unwrap_or(0.0) - 1.0 / 11.0).abs() < 1e-9);
}
#[test]
fn dep_seq_edges_emitted_for_backlog_unresolved_contributes_no_edge() {
let dir = tmp();
let root = dir.path();
seed_issue(
root,
1,
"open",
"",
"needs = [\"RSK-001\", \"ISS-099\"]\nafter = [{ to = \"ISS-002\", rank = 0 }]\n",
);
seed_issue(root, 2, "open", "", "");
seed_risk(root, 1, "open", "");
let pg = build(root).unwrap();
let iss1 = pg.projection.resolve(key("ISS", 1)).unwrap();
let rsk1 = pg.projection.resolve(key("RSK", 1)).unwrap();
let dep_preds: Vec<_> = pg
.graph
.in_edges(pg.dep_overlay, iss1)
.map(|(s, _)| s)
.collect();
assert_eq!(
dep_preds,
vec![rsk1],
"only the resolvable needs prereq edges (B→A); unresolved adds nothing"
);
let iss2 = pg.projection.resolve(key("ISS", 2)).unwrap();
let seq_preds: Vec<_> = pg
.graph
.in_edges(pg.seq_overlay, iss1)
.map(|(s, _)| s)
.collect();
assert!(
seq_preds.contains(&iss2),
"after edge oriented predecessor→src"
);
}
#[test]
fn nodes_authoring_no_dep_seq_carry_no_edges() {
let dir = tmp();
let root = dir.path();
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"proposed\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[estimate]\nlower = 0.0\nupper = 10.0\n\
[value]\nvalue = 25.0\n\
[[relation]]\nlabel = \"references\"\nrole = \"implements\"\ntarget = \"REQ-005\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 10.0", "value = 25.0", "");
seed_issue(root, 2, "open", "", "");
seed_requirement(root, 5);
seed_slice(root, 2, "");
let pg = build(root).unwrap();
let sl1 = pg.projection.resolve(key("SL", 1)).unwrap();
let sl2 = pg.projection.resolve(key("SL", 2)).unwrap();
assert_eq!(pg.graph.in_edges(pg.dep_overlay, sl1).count(), 0);
assert_eq!(pg.graph.in_edges(pg.seq_overlay, sl1).count(), 0);
assert_eq!(pg.graph.in_edges(pg.dep_overlay, sl2).count(), 0);
assert!(
(pg.optionality.get(&key("REQ", 5)).copied().unwrap_or(0.0) - 25.0 / 6.5).abs() < 1e-9,
"resolvable consequence ref produces its edge (witnessed via optionality)"
);
}
#[test]
fn slice_needs_lands_on_dep_overlay_cross_kind() {
let dir = tmp();
let root = dir.path();
seed_slice(root, 1, "needs = [\"SL-002\"]\n");
seed_slice(root, 2, "");
let pg = build(root).unwrap();
let sl1 = pg.projection.resolve(key("SL", 1)).unwrap();
let sl2 = pg.projection.resolve(key("SL", 2)).unwrap();
let dep_preds: Vec<_> = pg
.graph
.in_edges(pg.dep_overlay, sl1)
.map(|(s, _)| s)
.collect();
assert_eq!(
dep_preds,
vec![sl2],
"slice→slice needs lands on the dep overlay (B→A flip), like backlog"
);
}
#[test]
fn slice_after_lands_on_seq_overlay_with_rank_and_array_index_age() {
let dir = tmp();
let root = dir.path();
seed_slice(
root,
1,
"after = [{ to = \"SL-002\", rank = 7 }, { to = \"SL-003\" }]\n",
);
seed_slice(root, 2, "");
seed_slice(root, 3, "");
let pg = build(root).unwrap();
let sl1 = pg.projection.resolve(key("SL", 1)).unwrap();
let sl2 = pg.projection.resolve(key("SL", 2)).unwrap();
let sl3 = pg.projection.resolve(key("SL", 3)).unwrap();
let seq: BTreeMap<_, _> = pg
.graph
.in_edges(pg.seq_overlay, sl1)
.map(|(s, a)| (s, (a.rank(), a.age())))
.collect();
assert_eq!(
seq.get(&sl2).copied(),
Some((7, 0)),
"first after edge: authored rank 7, age = array index 0"
);
assert_eq!(
seq.get(&sl3).copied(),
Some((0, 1)),
"second after edge: default rank 0, age = array index 1"
);
}
#[test]
fn free_text_outbound_target_produces_no_edge() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "drift = [\"some-free-text\"]\n");
let pg = build(root).unwrap();
let n = pg.projection.resolve(key("ISS", 1)).unwrap();
assert_eq!(
pg.graph.out_edges(pg.dep_overlay, n).count(),
0,
"free-text drift target produces no dep edge"
);
assert_eq!(
pg.score.get(&key("ISS", 1)).copied().unwrap_or(0.0),
1.0,
"free-text drift target: valueless item score = 1.0 (default)"
);
}
fn seed_issue_with_facets(
root: &Path,
id: u32,
rels: &str,
estimate: &str,
value: &str,
risk_facet: &str,
) {
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
{}\n[estimate]\n{}\n[value]\n{}\n[facet]\n{}\n",
migrate_body(&crate::backlog::ISSUE_KIND, rels),
estimate,
value,
risk_facet,
),
);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
}
#[test]
fn base_score_all_facets_present() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(
root,
1,
"",
"lower = 2.0\nupper = 8.0",
"value = 10.0",
"likelihood = \"high\"\nimpact = \"critical\"",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 10.0 / 5.9).abs() < 1e-9,
"value_dim should be 10/5.9"
);
assert!((bs.risk_dim - 24.0).abs() < 1e-9, "risk_dim should be 24.0");
assert!(
(bs.total() - (10.0 / 5.9 + 24.0)).abs() < 1e-9,
"total should be 10/5.9 + 24"
);
}
#[test]
fn base_score_value_only_risk_absent() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 2.0", "value = 5.0", "");
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 5.0 / 1.3).abs() < 1e-9,
"value_dim should be 5.0/1.3"
);
assert!((bs.risk_dim - 0.0).abs() < 1e-9, "risk_dim should be 0");
assert!(
(bs.total() - 5.0 / 1.3).abs() < 1e-9,
"total should be 5.0/1.3"
);
}
#[test]
fn base_score_risk_only_value_absent() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(
root,
1,
"",
"",
"",
"likelihood = \"low\"\nimpact = \"medium\"",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 1.0).abs() < 1e-9,
"value_dim should be 1.0 (default)"
);
assert!((bs.risk_dim - 4.0).abs() < 1e-9, "risk_dim should be 4.0");
assert!((bs.total() - 5.0).abs() < 1e-9, "total should be 5.0");
}
#[test]
fn base_score_neither_facet_present() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "");
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 1.0).abs() < 1e-9,
"value_dim should be 1.0 (default)"
);
assert!((bs.risk_dim - 0.0).abs() < 1e-9, "risk_dim should be 0");
assert!((bs.total() - 1.0).abs() < 1e-9, "total should be 1.0");
}
#[test]
fn base_score_bare_item_empty_corpus_fallback_cost_one() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(
root,
1,
"",
"", "value = 3.0",
"",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!((bs.value_dim - 3.0).abs() < 1e-9, "value_dim should be 3.0");
}
#[test]
fn base_score_valueless_sl_equals_explicit_value_one() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 10.0", "", "");
seed_issue_with_facets(root, 2, "", "lower = 0.0\nupper = 10.0", "value = 1.0", "");
let pg = build(root).unwrap();
let bs1 = pg.attrs[&key("ISS", 1)].base_score;
let bs2 = pg.attrs[&key("ISS", 2)].base_score;
let expected = 1.0 / 6.5;
assert!(
(bs1.value_dim - expected).abs() < 1e-9,
"valueless SL value_dim = 1.0/6.5 = {expected}, got {}",
bs1.value_dim
);
assert!(
(bs2.value_dim - expected).abs() < 1e-9,
"explicit value=1.0 SL value_dim = 1.0/6.5 = {expected}, got {}",
bs2.value_dim
);
}
#[test]
fn base_score_valueless_asm_and_rev_value_dim_zero() {
let facets = crate::facet::EntityFacets {
estimate: None,
value: None,
risk: None,
tags: vec![],
};
let asm_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "ASM")
.map(|k| k.kind)
.expect("ASM in KINDS");
let rev_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "REV")
.map(|k| k.kind)
.expect("REV in KINDS");
let iss_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "ISS")
.map(|k| k.kind)
.expect("ISS in KINDS");
let no_projection = ValueProjection::new();
let no_claims = comparison::ClaimResolution::default();
let asm_key = EntityKey {
prefix: asm_kind.prefix,
id: 1,
};
let rev_key = EntityKey {
prefix: rev_kind.prefix,
id: 1,
};
let iss_key = EntityKey {
prefix: iss_kind.prefix,
id: 1,
};
assert_eq!(
effective_raw_value(asm_kind, &facets, asm_key, &no_projection, &no_claims),
None
);
assert_eq!(
effective_raw_value(rev_kind, &facets, rev_key, &no_projection, &no_claims),
None
);
assert_eq!(
effective_raw_value(iss_kind, &facets, iss_key, &no_projection, &no_claims),
Some(DEFAULT_VALUE),
"ISS is value-bearing → default"
);
let cfg = config::PriorityConfig::default();
let ctx = CostCtx { absent: 1.0 };
let no_feed = comparison::CostFeed::new();
let bs = base_score(
&facets,
asm_kind,
asm_key,
&no_projection,
&no_claims,
&no_feed,
&cfg,
ctx,
);
assert!(
(bs.value_dim - 0.0).abs() < 1e-9,
"ASM value_dim should be 0"
);
let bs = base_score(
&facets,
rev_kind,
rev_key,
&no_projection,
&no_claims,
&no_feed,
&cfg,
ctx,
);
assert!(
(bs.value_dim - 0.0).abs() < 1e-9,
"REV value_dim should be 0"
);
let bs = base_score(
&facets,
iss_kind,
iss_key,
&no_projection,
&no_claims,
&no_feed,
&cfg,
ctx,
);
assert!(
(bs.value_dim - 1.0).abs() < 1e-9,
"ISS value_dim should be 1.0 (default)"
);
}
#[test]
fn base_score_authored_value_preserved_no_clamp() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 10.0", "value = 0.3", "");
seed_issue_with_facets(root, 2, "", "lower = 0.0\nupper = 10.0", "value = 0.0", "");
let pg = build(root).unwrap();
let bs1 = pg.attrs[&key("ISS", 1)].base_score;
let bs2 = pg.attrs[&key("ISS", 2)].base_score;
assert!(
(bs1.value_dim - 0.3 / 6.5).abs() < 1e-9,
"authored 0.3 should be 0.3/6.5, not clamped to 1.0"
);
assert!(
(bs2.value_dim - 0.0).abs() < 1e-9,
"authored 0.0 stays 0.0 (not defaulted to 1.0)"
);
}
#[test]
fn effective_raw_value_provenance_chain_authored_over_projected_over_gauge_over_default() {
let iss_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "ISS")
.map(|k| k.kind)
.expect("ISS in KINDS");
let key1 = EntityKey {
prefix: "ISS",
id: 1,
};
let no_facets = crate::facet::EntityFacets {
estimate: None,
value: None,
risk: None,
tags: vec![],
};
let no_claims = comparison::ClaimResolution::default();
let empty = ValueProjection::new();
assert_eq!(
effective_raw_value(iss_kind, &no_facets, key1, &empty, &no_claims),
Some(DEFAULT_VALUE)
);
let mut projected = ValueProjection::new();
projected.insert(
key1.canonical(),
(2.5, crate::comparison::ValueProvenance::Projected),
);
assert_eq!(
effective_raw_value(iss_kind, &no_facets, key1, &projected, &no_claims),
Some(2.5)
);
let mut gauged = ValueProjection::new();
gauged.insert(
key1.canonical(),
(2.5, crate::comparison::ValueProvenance::Gauge),
);
assert_eq!(
effective_raw_value(iss_kind, &no_facets, key1, &gauged, &no_claims),
Some(2.5),
"Gauge and Projected provenance resolve to the same raw value"
);
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 9.0", "");
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let authored = scanned
.iter()
.find(|e| e.key == key1)
.expect("ISS-001 scanned");
let authored_facets = crate::facet::EntityFacets {
estimate: authored.estimate.clone(),
value: authored.value.clone(),
risk: authored.risk.clone(),
tags: authored.tags.clone(),
};
assert_eq!(
effective_raw_value(iss_kind, &authored_facets, key1, &gauged, &no_claims),
Some(2.5),
"projection out-ranks the unmigrated facet (rung 2 > rung 5)"
);
assert_eq!(
effective_raw_value(iss_kind, &authored_facets, key1, &empty, &no_claims),
Some(9.0),
"no projection, zero claim rows: the facet still contributes (rung 5)"
);
}
#[test]
fn ledger_fed_comparison_projects_into_value_dim() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "");
seed_issue(root, 2, "open", "", "");
write_comparison_session(root, "ISS-001", "ISS-002");
let pg = build(root).unwrap();
let bs1 = pg.attrs[&key("ISS", 1)].base_score;
let bs2 = pg.attrs[&key("ISS", 2)].base_score;
let expected_winner = 2.0 * DEFAULT_VALUE * 2.0 / 3.0;
let expected_loser = 2.0 * DEFAULT_VALUE * 1.0 / 3.0;
assert!(
(bs1.value_dim - expected_winner).abs() < 1e-9,
"preferred ISS-001 gauge-projects to 4/3, got {}",
bs1.value_dim
);
assert!(
(bs2.value_dim - expected_loser).abs() < 1e-9,
"non-preferred ISS-002 gauge-projects to 2/3, got {}",
bs2.value_dim
);
assert!(bs1.value_dim > bs2.value_dim);
}
#[test]
fn gauge_fed_burndown_golden() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "");
seed_issue(root, 2, "open", "", "");
write_comparison_session(root, "ISS-001", "ISS-002");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"done\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 0.6666666666666666\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-001\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
let pg = build(root).unwrap();
let s1 = pg.score[&key("ISS", 1)];
let expected_value_dim = 2.0 * DEFAULT_VALUE * 2.0 / 3.0;
let expected_burn = expected_value_dim * 0.5;
assert!(
(s1 - expected_burn).abs() < 1e-6,
"gauge-fed value burns down identically to an authored one, got {s1}"
);
}
fn write_comparison_session(root: &Path, winner: &str, loser: &str) {
let judgement = comparison::Judgement {
uid: "j1".to_string(),
seq: 0,
a: winner.to_string(),
b: Some(loser.to_string()),
response: Some(comparison::Response::PreferA),
domain: comparison::DOMAIN_VALUE.to_string(),
frame: comparison::FRAME_EQUAL_EFFORT.to_string(),
form: comparison::RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater: comparison::RaterKind::Agent,
by: None,
note: None,
date: Some("2026-07-11".to_string()),
observed_at: None,
basis: None,
admission: None,
};
let session = comparison::ComparisonSession {
schema: comparison::COMPARISON_SCHEMA.to_string(),
version: comparison::COMPARISON_VERSION,
session: comparison::SessionHeader {
uid: "s1".to_string(),
date: "2026-07-11".to_string(),
audience: None,
},
judgements: vec![judgement],
tombstones: Vec::new(),
};
let text = comparison::to_toml(&session).unwrap();
write(root, ".doctrine/comparisons/2026-07-11-s1.toml", &text);
}
#[test]
fn leverage_flows_out_edges_dep_overlay() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "needs = [\"ISS-002\"]\n", "", "value = 10.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 3.0", "");
let pg = build(root).unwrap();
let lev2 = pg.leverage[&key("ISS", 2)];
let lev1 = pg.leverage[&key("ISS", 1)];
assert!((lev1 - 0.0).abs() < 1e-9, "ISS-001 has no dependents");
assert!((lev2 - 5.0).abs() < 1e-9, "ISS-002 gets 0.5 * 10.0");
}
#[test]
fn optionality_flows_in_edges_over_consequence_labels_one_hop() {
let dir = tmp();
let root = dir.path();
seed_slice(root, 1, "slices = [\"ISS-001\"]\n");
seed_issue_with_facets(root, 1, "", "", "value = 7.0", "");
let pg = build(root).unwrap();
let opt = pg.optionality[&key("ISS", 1)];
assert!(
(opt - 0.0).abs() < 1e-9,
"SL-001 has no value → optionality=0"
);
let opt_sl = pg.optionality[&key("SL", 1)];
assert!(
(opt_sl - 0.0).abs() < 1e-9,
"SL-001 is not a ref target of a consequence label"
);
}
#[test]
fn reviews_and_owning_slice_edges_contribute_zero_optionality() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 5.0", "");
seed_review(root, 1, "ISS-001", "");
seed_rec(root, 1, "ISS-001");
let pg = build(root).unwrap();
let opt = pg.optionality[&key("ISS", 1)];
assert!(
(opt - 0.0).abs() < 1e-9,
"reviews/owning_slice contribute 0"
);
}
#[test]
fn dangling_target_contributes_zero() {
let dir = tmp();
let root = dir.path();
seed_slice(root, 1, "slices = [\"ISS-099\"]\n");
seed_issue_with_facets(root, 1, "", "", "value = 3.0", "");
let pg = build(root).unwrap();
assert!(pg.optionality.get(&key("ISS", 1)).copied().unwrap_or(0.0) == 0.0);
}
#[test]
fn leverage_recursive_chain() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "needs = [\"ISS-002\"]\n", "", "value = 2.0", "");
seed_issue_with_facets(root, 2, "needs = [\"ISS-003\"]\n", "", "value = 3.0", "");
seed_issue_with_facets(root, 3, "", "", "value = 5.0", "");
let pg = build(root).unwrap();
let lev_1 = pg.leverage[&key("ISS", 1)];
let lev_2 = pg.leverage[&key("ISS", 2)];
let lev_3 = pg.leverage[&key("ISS", 3)];
assert!((lev_1 - 0.0).abs() < 1e-9, "ISS-001 has no dependents");
assert!((lev_2 - 1.0).abs() < 1e-9, "ISS-002 gets 0.5 * ISS-001");
assert!(
(lev_3 - 2.0).abs() < 1e-9,
"ISS-003 gets 0.5 * (ISS-002+l2)"
);
}
#[test]
fn leverage_diamond_double_counts_shared_leaf() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(
root,
1,
"needs = [\"ISS-002\", \"ISS-003\"]\n",
"",
"value = 10.0",
"",
);
seed_issue_with_facets(root, 2, "needs = [\"ISS-004\"]\n", "", "value = 1.0", "");
seed_issue_with_facets(root, 3, "needs = [\"ISS-004\"]\n", "", "value = 1.0", "");
seed_issue_with_facets(root, 4, "", "", "value = 5.0", "");
let pg = build(root).unwrap();
let lev_1 = pg.leverage[&key("ISS", 1)];
let lev_2 = pg.leverage[&key("ISS", 2)];
let lev_3 = pg.leverage[&key("ISS", 3)];
let lev_4 = pg.leverage[&key("ISS", 4)];
assert!((lev_1 - 0.0).abs() < 1e-9);
assert!((lev_2 - 5.0).abs() < 1e-9);
assert!((lev_3 - 5.0).abs() < 1e-9);
assert!(
(lev_4 - 6.0).abs() < 1e-9,
"D double-counted through both paths"
);
}
#[test]
fn ref_optionality_is_one_hop_no_transitive_accumulation() {
let dir = tmp();
let root = dir.path();
seed_slice(root, 1, "");
seed_issue_with_facets(root, 1, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 3.0", "");
let pg = build(root).unwrap();
assert!(
(pg.optionality[&key("ISS", 2)] - 0.0).abs() < 1e-9,
"ISS-002 has no referencers"
);
assert!(
(pg.optionality[&key("ISS", 1)] - 0.0).abs() < 1e-9,
"ISS-001 has no referencers"
);
assert!(
(pg.optionality[&key("SL", 1)] - 0.0).abs() < 1e-9,
"SL-001 has no referencers"
);
}
#[test]
fn equal_scores_tiebreak_id_asc() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 10.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 10.0", "");
let pg = build(root).unwrap();
let s1 = pg.score[&key("ISS", 1)];
let s2 = pg.score[&key("ISS", 2)];
assert!((s1 - s2).abs() < 1e-9, "equal bases yield equal scores");
let keys: Vec<_> = pg.score.keys().collect();
assert!(keys[0] < keys[1], "BTreeMap orders by id asc");
}
#[test]
fn near_max_coefficients_produce_no_nan_or_inf() {
let dir = tmp();
let root = dir.path();
let max_val = config::COEFF_MAX;
write(
root,
".doctrine/doctrine.toml",
&format!(
"[priority]\ncoefficients = {{ value = {max_val}, risk = {max_val} }}\n\
consequence = {{ dep_coeff = 1.0, ref_coeff = {max_val} }}\n"
),
);
seed_issue_with_facets(root, 1, "needs = [\"ISS-002\"]\n", "", "value = 1e6", "");
seed_issue_with_facets(
root,
2,
"",
"",
"value = 1e6",
"likelihood = \"critical\"\nimpact = \"critical\"",
);
let pg = build(root).unwrap();
for (_k, &s) in &pg.score {
assert!(s.is_finite(), "score should be finite, got {s}");
}
for (_k, &lev) in &pg.leverage {
assert!(lev.is_finite(), "leverage should be finite, got {lev}");
}
for (_k, &opt) in &pg.optionality {
assert!(opt.is_finite(), "optionality should be finite, got {opt}");
}
}
#[test]
fn self_loop_yields_finite_leverage() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "needs = [\"ISS-001\"]\n", "", "value = 5.0", "");
let pg = build(root).unwrap();
let lev = pg.leverage[&key("ISS", 1)];
assert!(lev.is_finite(), "self-loop leverage should be finite");
}
#[test]
fn multi_member_scc_with_external_dependent() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "needs = [\"ISS-002\"]\n", "", "value = 1.0", "");
seed_issue_with_facets(root, 2, "needs = [\"ISS-001\"]\n", "", "value = 1.0", "");
seed_issue_with_facets(root, 3, "needs = [\"ISS-002\"]\n", "", "value = 10.0", "");
let pg = build(root).unwrap();
let lev_a = pg.leverage[&key("ISS", 1)];
let lev_b = pg.leverage[&key("ISS", 2)];
let lev_c = pg.leverage[&key("ISS", 3)];
assert!(lev_c == 0.0, "C has no dependents");
assert!(
(lev_a - lev_b).abs() < 1e-9,
"A and B report the same component leverage"
);
assert!((lev_a - 5.0).abs() < 1e-9, "component leverage = 0.5 * 10");
assert!(lev_a.is_finite(), "leverage should be finite");
}
#[test]
fn scc_leverage_uses_component_topo_order_under_seq_perturbation() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "needs = [\"ISS-002\"]\n", "", "value = 0.0", ""); seed_issue_with_facets(
root,
2,
"needs = [\"ISS-001\"]\nafter = [{ to = \"ISS-003\", rank = 0 }]\n",
"",
"value = 0.0",
"",
); seed_issue_with_facets(root, 3, "needs = [\"ISS-001\"]\n", "", "value = 2.0", ""); seed_issue_with_facets(root, 4, "needs = [\"ISS-003\"]\n", "", "value = 8.0", ""); let pg = build(root).unwrap();
let lev_a = pg.leverage[&key("ISS", 1)];
let lev_b = pg.leverage[&key("ISS", 2)];
let lev_d = pg.leverage[&key("ISS", 3)];
let lev_e = pg.leverage[&key("ISS", 4)];
assert!((lev_e - 0.0).abs() < 1e-9, "E has no dependents");
assert!((lev_d - 4.0).abs() < 1e-9, "D = 0.5 * base(E)");
assert!(
(lev_a - lev_b).abs() < 1e-9,
"A and B share component leverage"
);
assert!(
(lev_a - 3.0).abs() < 1e-9,
"{{A,B}} picks up D's RESOLVED leverage: 0.5*(2+4)=3, not 0.5*2=1"
);
}
#[test]
fn scc_external_dependent_counted_once_per_component() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "needs = [\"ISS-002\"]\n", "", "value = 0.0", ""); seed_issue_with_facets(root, 2, "needs = [\"ISS-001\"]\n", "", "value = 0.0", ""); seed_issue_with_facets(
root,
3,
"needs = [\"ISS-001\", \"ISS-002\"]\n",
"",
"value = 10.0",
"",
); let pg = build(root).unwrap();
let lev_a = pg.leverage[&key("ISS", 1)];
let lev_b = pg.leverage[&key("ISS", 2)];
let lev_d = pg.leverage[&key("ISS", 3)];
assert!((lev_d - 0.0).abs() < 1e-9, "D has no dependents");
assert!(
(lev_a - lev_b).abs() < 1e-9,
"A and B share component leverage"
);
assert!(
(lev_a - 5.0).abs() < 1e-9,
"D counted once per component: 0.5*10=5, not 0.5*20=10"
);
}
fn seed_issue_with_tags(root: &Path, id: u32, tags: &str, value: &str, estimate: &str) {
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
tags = [{tags}]\n\
[estimate]\n{estimate}\n\
[value]\n{value}\n",
),
);
write(
root,
&format!(".doctrine/backlog/issue/{id:03}/backlog-{id:03}.md"),
"b\n",
);
}
#[test]
fn base_score_empty_tags_identity() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 10.0", "value = 10.0", "");
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 10.0 / 6.5).abs() < 1e-9,
"empty tags → identity"
);
}
#[test]
fn base_score_with_tag_coefficient() {
let dir = tmp();
let root = dir.path();
write(
root,
".doctrine/doctrine.toml",
"[priority]\ntag_coefficients = { \"area:foo\" = 2.0 }\n",
);
seed_issue_with_tags(
root,
1,
"\"area:foo\"",
"value = 10.0",
"lower = 0.0\nupper = 10.0",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 20.0 / 6.5).abs() < 1e-9,
"tag coeff 2.0 doubles value_dim"
);
}
#[test]
fn base_score_multiple_tags() {
let dir = tmp();
let root = dir.path();
write(
root,
".doctrine/doctrine.toml",
"[priority]\ntag_coefficients = { a = 1.5, b = 2.0 }\n",
);
seed_issue_with_tags(
root,
1,
"\"a\", \"b\"",
"value = 6.0",
"lower = 2.0\nupper = 4.0",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 15.0 / 3.3).abs() < 1e-9,
"tag_term 2.5 → value_dim = 15.0/3.3"
);
}
#[test]
fn base_score_demoting_tag() {
let dir = tmp();
let root = dir.path();
write(
root,
".doctrine/doctrine.toml",
"[priority]\ntag_coefficients = { wontfix = 0.5 }\n",
);
seed_issue_with_tags(
root,
1,
"\"wontfix\"",
"value = 20.0",
"lower = 0.0\nupper = 10.0",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 10.0 / 6.5).abs() < 1e-9,
"demoting tag halves value_dim"
);
}
#[test]
fn base_score_multi_demote_floors_at_zero() {
let dir = tmp();
let root = dir.path();
write(
root,
".doctrine/doctrine.toml",
"[priority]\ntag_coefficients = { x = 0.0, y = 0.0 }\n",
);
seed_issue_with_tags(
root,
1,
"\"x\", \"y\"",
"value = 10.0",
"lower = 0.0\nupper = 10.0",
);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 0.0).abs() < 1e-9,
"multi-demote floors at zero, not negative"
);
}
#[test]
fn burndown_lowers_score() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 10.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 10.0", "");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"done\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 4.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-002\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
let pg = build(root).unwrap();
let s1 = pg.score[&key("ISS", 1)];
let s2 = pg.score[&key("ISS", 2)];
assert!((s1 - 10.0).abs() < 1e-9, "ISS-001 unchanged, got {s1}");
assert!((s2 - 6.0).abs() < 1e-9, "ISS-002 burndown to 6.0, got {s2}");
assert!(
s2 < s1,
"burndown strictly lowers the fulfilled item's score"
);
}
#[test]
fn burndown_lifecycle_gate() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 10.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 10.0", "");
seed_issue_with_facets(root, 3, "", "", "value = 10.0", "");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"ready\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 4.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-001\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
write(
root,
".doctrine/slice/002/slice-002.toml",
"id = 2\nslug = \"s\"\ntitle = \"S\"\nstatus = \"started\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 4.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-002\"\n",
);
write(root, ".doctrine/slice/002/slice-002.md", "scope\n");
write(
root,
".doctrine/slice/003/slice-003.toml",
"id = 3\nslug = \"s\"\ntitle = \"S\"\nstatus = \"done\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 4.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-003\"\n",
);
write(root, ".doctrine/slice/003/slice-003.md", "scope\n");
let pg = build(root).unwrap();
assert!(
(pg.score[&key("ISS", 1)] - 10.0).abs() < 1e-9,
"ready status burns nothing: Fulfils burndown lifecycle gate"
);
assert!(
(pg.score[&key("ISS", 2)] - 6.0).abs() < 1e-9,
"started status burns fully: Fulfils burndown lifecycle gate"
);
assert!(
(pg.score[&key("ISS", 3)] - 6.0).abs() < 1e-9,
"done (via started) burns fully: Fulfils burndown lifecycle gate"
);
}
#[test]
fn burndown_non_conservation() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 10.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 10.0", "");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"done\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 4.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-001\"\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-002\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
let pg = build(root).unwrap();
let s1 = pg.score[&key("ISS", 1)];
let s2 = pg.score[&key("ISS", 2)];
assert!((s1 - 6.0).abs() < 1e-9, "ISS-001 burndown to 6.0, got {s1}");
assert!((s2 - 6.0).abs() < 1e-9, "ISS-002 burndown to 6.0, got {s2}");
assert!(
(s1 - s2).abs() < 1e-9,
"Fulfils burndown is non-conserving across multi-item"
);
}
#[test]
fn originates_from_inert_for_priority() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "originates_from = [\"SL-001\"]\n");
seed_slice(root, 1, "");
let pg = build(root).unwrap();
assert!(
pg.optionality.get(&key("ISS", 1)).copied().unwrap_or(0.0) == 0.0,
"originates_from is not a CONSEQUENCE_LABELS member → optionality = 0"
);
assert!(
pg.optionality.get(&key("SL", 1)).copied().unwrap_or(0.0) == 0.0,
"SL target of originates_from gets no optionality"
);
assert!(
(pg.score[&key("ISS", 1)] - 1.0).abs() < 1e-9,
"originates_from: valueless item score = 1.0 (default); still no lev/opt change"
);
}
#[test]
fn burndown_exact_value_divergence_trap() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 0.0\nupper = 20.0", "value = 10.0", "");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"done\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 5.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-001\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
let pg = build(root).unwrap();
let expected: f64 = 10.0 / 13.0 * 0.5; let got = pg.score[&key("ISS", 1)];
assert!(
(got - expected).abs() < 1e-9,
"Fulfils burndown uses raw_value denominator ({expected}) not value_dim, got {got}"
);
}
#[test]
fn burndown_decomposition() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 10.0", "");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"done\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[value]\nvalue = 4.0\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-001\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
let pg = build(root).unwrap();
assert!(
pg.optionality[&key("ISS", 1)] == 0.0,
"Fulfils not in CONSEQUENCE_LABELS: optionality from fulfils = 0"
);
let s = pg.score[&key("ISS", 1)];
assert!(
(s - 6.0).abs() < 1e-9,
"Fulfils burndown decomposition: score=6.0, got {s}"
);
let baseline = 10.0;
let delta = baseline - s;
assert!(
(delta - 4.0).abs() < 1e-9,
"decomposition: delta={delta} equals burndown term ONLY (no slices/optionality double-count)"
);
}
#[test]
fn burndown_valueless_fulfilling_slice_delivers_default_value() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 10.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 10.0", "");
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s\"\ntitle = \"S\"\nstatus = \"started\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[[relation]]\nlabel = \"fulfils\"\ntarget = \"ISS-002\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
let pg = build(root).unwrap();
let s1 = pg.score[&key("ISS", 1)];
let s2 = pg.score[&key("ISS", 2)];
assert!(
(s1 - 10.0).abs() < 1e-9,
"ISS-001 unfulfilled baseline 10.0, got {s1}"
);
assert!(
(s2 - 9.0).abs() < 1e-9,
"ISS-002 burndown by valueless slice to 9.0, got {s2}"
);
assert!(
s2 < s1,
"valueless fulfilling slice reduces the item's score"
);
assert!(
s2 > 0.0,
"score is positive (delivered > 0 from default value), got {s2}"
);
}
#[test]
fn burndown_non_value_bearing_source_contributes_zero() {
let facets = crate::facet::EntityFacets {
estimate: None,
value: None,
risk: None,
tags: vec![],
};
let rev_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "REV")
.map(|k| k.kind)
.expect("REV in KINDS");
let asm_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "ASM")
.map(|k| k.kind)
.expect("ASM in KINDS");
let iss_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "ISS")
.map(|k| k.kind)
.expect("ISS in KINDS");
let no_projection = ValueProjection::new();
let no_claims = comparison::ClaimResolution::default();
let rev_key = EntityKey {
prefix: rev_kind.prefix,
id: 1,
};
let asm_key = EntityKey {
prefix: asm_kind.prefix,
id: 1,
};
let iss_key = EntityKey {
prefix: iss_kind.prefix,
id: 1,
};
assert_eq!(
effective_raw_value(rev_kind, &facets, rev_key, &no_projection, &no_claims),
None
);
assert_eq!(
effective_raw_value(asm_kind, &facets, asm_key, &no_projection, &no_claims),
None
);
assert_eq!(
effective_raw_value(iss_kind, &facets, iss_key, &no_projection, &no_claims),
Some(DEFAULT_VALUE)
);
}
#[test]
fn build_from_equals_build_from_with_cfg_over_loaded_config() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(
root,
1,
"needs = [\"RSK-001\"]",
"lower = 0.0\nupper = 10.0",
"value = 25.0",
"",
);
seed_issue_with_facets(root, 2, "", "lower = 1.0\nupper = 4.0", "value = 5.0", "");
seed_risk(root, 1, "open", "");
seed_slice(root, 1, "references = [\"REQ-005\"]");
seed_requirement(root, 5);
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let via_load = build_from(&scanned, root).unwrap();
let via_cfg = build_from_with_cfg(
&scanned,
root,
&config::load(root),
&ValueProjection::new(),
&comparison::CostFeed::new(),
&comparison::ClaimResolution::default(),
)
.unwrap();
assert_eq!(via_load.score, via_cfg.score, "score map identical");
assert_eq!(
via_load.leverage, via_cfg.leverage,
"leverage map identical"
);
assert_eq!(
via_load.optionality, via_cfg.optionality,
"optionality map identical"
);
let base = |pg: &PriorityGraph| -> std::collections::BTreeMap<EntityKey, (f64, f64)> {
pg.attrs
.iter()
.map(|(k, a)| (*k, (a.base_score.value_dim, a.base_score.risk_dim)))
.collect()
};
assert_eq!(base(&via_load), base(&via_cfg), "base scores identical");
let order = |pg: &PriorityGraph| -> Vec<EntityKey> {
pg.graph
.ordered()
.iter()
.filter_map(|n| pg.projection.key_of(*n))
.collect()
};
assert_eq!(order(&via_load), order(&via_cfg), "minted order identical");
}
#[test]
fn est_anchor_builder_equals_graph_authored_branch_est_cost() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 1.0\nupper = 9.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "lower = 0.0\nupper = 0.0", "value = 5.0", "");
seed_issue(root, 3, "open", "", ""); write(
root,
".doctrine/knowledge/question/001/record-001.toml",
&format!(
"schema = \"{}\"\nversion = 1\nid = 1\nslug = \"q1\"\ntitle = \"Q1\"\n\
record_kind = \"question\"\nstatus = \"open\"\ncreated = \"2026-01-01\"\n\
updated = \"2026-01-01\"\ntags = []\n[estimate]\nlower = 2.0\nupper = 6.0\n",
crate::test_support::SCHEMA_KNOWLEDGE
),
);
write(
root,
".doctrine/knowledge/question/001/record-001.md",
"q\n",
);
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = config::load(root);
let anchors = comparison_est_anchor_map(&scanned, &cfg);
let ctx = CostCtx { absent: 999.0 };
for (canonical, prefix, id, bounds) in [
("ISS-001", "ISS", 1, (1.0, 9.0)),
("ISS-002", "ISS", 2, (0.0, 0.0)),
("QUE-001", "QUE", 1, (2.0, 6.0)),
] {
assert_eq!(
anchors.get(canonical).copied(),
Some(est_cost(
Some(bounds),
key(prefix, id),
&comparison::CostFeed::new(),
ctx,
&cfg.estimate
)),
"{canonical}: builder anchor == authored-branch est_cost"
);
}
assert!(
!anchors.contains_key("ISS-003"),
"no authored estimate ⇒ no anchor"
);
}
fn est_row(
uid: &str,
a: &str,
b: &str,
response: comparison::Response,
) -> comparison::Judgement {
comparison::Judgement {
uid: uid.to_string(),
seq: 0,
a: a.to_string(),
b: Some(b.to_string()),
response: Some(response),
domain: comparison::DOMAIN_ESTIMATE.to_string(),
frame: comparison::FRAME_MORE_WORK.to_string(),
form: comparison::RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater: comparison::RaterKind::Agent,
by: None,
note: None,
date: Some("2026-07-11".to_string()),
observed_at: None,
basis: None,
admission: None,
}
}
fn write_est_session(root: &Path, rows: Vec<comparison::Judgement>) {
let session = comparison::ComparisonSession {
schema: comparison::COMPARISON_SCHEMA.to_string(),
version: comparison::COMPARISON_VERSION,
session: comparison::SessionHeader {
uid: "e1".to_string(),
date: "2026-07-11".to_string(),
audience: None,
},
judgements: rows,
tombstones: Vec::new(),
};
let text = comparison::to_toml(&session).unwrap();
write(root, ".doctrine/comparisons/2026-07-11-e1.toml", &text);
}
fn est_of(pg: &PriorityGraph, prefix: &'static str, id: u32) -> f64 {
let cfg = config::PriorityConfig::default();
pg.item_costing(&key(prefix, id), &cfg).unwrap().1
}
#[test]
fn est_cost_ladder_authored_bounds_beat_cost_feed() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 7.0 };
for fed in [0.001, 1000.0] {
let feed: comparison::CostFeed = [("ISS-001".to_string(), fed)].into();
assert_eq!(
est_cost(Some((2.0, 6.0)), key("ISS", 1), &feed, ctx, &ec),
authored_est_cost((2.0, 6.0), &ec),
"authored beats feed entry {fed}"
);
}
}
#[test]
fn est_cost_ladder_feed_then_bare_anchor() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 7.0 };
let feed: comparison::CostFeed = [("ISS-001".to_string(), 4.85)].into();
assert_eq!(est_cost(None, key("ISS", 1), &feed, ctx, &ec), 4.85);
assert_eq!(
est_cost(None, key("ISS", 2), &feed, ctx, &ec),
7.0,
"unevidenced-bare falls through to ctx.absent"
);
}
#[test]
fn est_cost_ladder_epsilon_floors_the_feed_branch() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 7.0 };
let feed: comparison::CostFeed = [("ISS-001".to_string(), 0.0)].into();
assert_eq!(est_cost(None, key("ISS", 1), &feed, ctx, &ec), EPSILON);
}
#[test]
fn est_cost_ladder_evidenced_bare_item_takes_cost_feed() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 2.0\nupper = 6.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_session(
root,
vec![est_row(
"e1",
"ISS-002",
"ISS-001",
comparison::Response::PreferA,
)],
);
let pg = build(root).unwrap();
assert!(
(est_of(&pg, "ISS", 1) - 4.6).abs() < 1e-9,
"authored branch"
);
assert!(
(est_of(&pg, "ISS", 2) - (4.6 + config::EST_GAUGE_STEP)).abs() < 1e-9,
"evidenced-bare fed at the P5 placement, not the 7.0 bare anchor"
);
assert_eq!(pg.cost_feed.len(), 2, "anchor + P5 head both fed");
}
#[test]
fn est_cost_ladder_gauge_masked_items_stay_at_bare_anchor() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 2.0\nupper = 6.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 3, "", "", "value = 5.0", "");
write_est_session(
root,
vec![est_row(
"e1",
"ISS-002",
"ISS-003",
comparison::Response::PreferA,
)],
);
let pg = build(root).unwrap();
assert!(pg.cost_feed.is_empty(), "gauge tier absent from the feed");
for id in [2, 3] {
assert!(
(est_of(&pg, "ISS", id) - 7.0).abs() < 1e-9,
"ISS-{id:03} gauge-masked ⇒ bare anchor"
);
}
}
#[test]
fn est_cost_ladder_merge_hoisted_member_fed_at_class_anchor() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 2.0\nupper = 6.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_session(
root,
vec![est_row(
"e1",
"ISS-002",
"ISS-001",
comparison::Response::Equal,
)],
);
let pg = build(root).unwrap();
assert_eq!(
pg.cost_feed.get("ISS-002"),
Some(&4.6),
"merged member fed at the class anchor"
);
assert!((est_of(&pg, "ISS", 2) - 4.6).abs() < 1e-9);
assert!((est_of(&pg, "ISS", 1) - 4.6).abs() < 1e-9);
}
#[test]
fn est_cost_ladder_regime_flip_first_anchor_flips_component() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "lower = 2.0\nupper = 6.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 3, "", "", "value = 5.0", "");
write_est_session(
root,
vec![est_row(
"e1",
"ISS-002",
"ISS-003",
comparison::Response::PreferA,
)],
);
let before = build(root).unwrap();
assert!((est_of(&before, "ISS", 2) - 7.0).abs() < 1e-9);
assert!((est_of(&before, "ISS", 3) - 7.0).abs() < 1e-9);
seed_issue_with_facets(root, 3, "", "lower = 1.0\nupper = 3.0", "value = 5.0", "");
let after = build(root).unwrap();
assert!((est_of(&after, "ISS", 3) - 2.3).abs() < 1e-9, "authored");
assert!(
(est_of(&after, "ISS", 2) - (2.3 + config::EST_GAUGE_STEP)).abs() < 1e-9,
"member flipped bare → projected (the owned discontinuity)"
);
}
#[test]
fn est_cost_ladder_projected_may_exceed_bare_anchor() {
let dir = tmp();
let root = dir.path();
write(
root,
crate::dtoml::DOCTRINE_TOML,
"[priority.estimate]\ngauge_step = 5.0\n",
);
seed_issue_with_facets(root, 1, "", "lower = 6.0\nupper = 6.0", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_session(
root,
vec![est_row(
"e1",
"ISS-002",
"ISS-001",
comparison::Response::PreferA,
)],
);
let pg = build(root).unwrap();
assert!((pg.cost_ctx.absent - 7.0).abs() < 1e-9, "bare anchor");
let cfg = config::load(root);
let fed = pg.item_costing(&key("ISS", 2), &cfg).unwrap().1;
assert!((fed - 11.0).abs() < 1e-9, "P5 head at anchor + step: {fed}");
assert!(
fed > pg.cost_ctx.absent,
"projected exceeds the bare anchor"
);
}
fn claim_anchor(
uid: &str,
item: &str,
magnitude: f64,
rater: comparison::RaterKind,
pin: bool,
) -> comparison::Judgement {
let migrated = matches!(rater, comparison::RaterKind::Migrated);
comparison::Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: comparison::DOMAIN_VALUE.to_string(),
frame: comparison::FRAME_VALUE_ANCHOR.to_string(),
form: comparison::RowForm::Anchor,
magnitude: Some(magnitude),
supersedes: None,
lens: None,
rater,
by: pin.then(|| "op".to_string()),
note: None,
date: (!migrated).then(|| "2026-07-16".to_string()),
observed_at: migrated.then(|| "2026-07-16".to_string()),
basis: None,
admission: pin.then_some(comparison::AdmissionKind::Pin),
}
}
fn claims_of(rows: &[comparison::Judgement]) -> comparison::ClaimResolution {
let tagged: Vec<(&comparison::Judgement, comparison::ResolutionStatus)> = rows
.iter()
.map(|j| (j, comparison::ResolutionStatus::Active))
.collect();
comparison::resolve_claims(&tagged)
}
#[test]
fn value_ladder_rungs_and_adjacent_dominance() {
use comparison::RaterKind;
let iss_kind = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == "ISS")
.map(|k| k.kind)
.expect("ISS in KINDS");
let key1 = key("ISS", 1);
let no_facets = crate::facet::EntityFacets {
estimate: None,
value: None,
risk: None,
tags: vec![],
};
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 9.0", "");
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let facet_9 = crate::facet::EntityFacets {
estimate: None,
value: scanned
.iter()
.find(|e| e.key == key1)
.and_then(|e| e.value.clone()),
risk: None,
tags: vec![],
};
assert!(facet_9.value.is_some(), "fixture authored the facet");
let no_projection = ValueProjection::new();
let mut projection_2_5 = ValueProjection::new();
projection_2_5.insert(
key1.canonical(),
(2.5, crate::comparison::ValueProvenance::Projected),
);
let erv = |f: &EntityFacets,
projected: &ValueProjection,
claims: &comparison::ClaimResolution| {
effective_raw_value(iss_kind, f, key1, projected, claims)
};
let pin_over_human = claims_of(&[
claim_anchor("h", "ISS-001", 5.0, RaterKind::Human, false),
claim_anchor("p", "ISS-001", 6.0, RaterKind::Human, true),
]);
assert_eq!(
erv(&facet_9, &projection_2_5, &pin_over_human),
Some(6.0),
"pin > human (tier contest) and anchored > projection > facet"
);
let human = claims_of(&[claim_anchor("h", "ISS-001", 5.0, RaterKind::Human, false)]);
assert_eq!(
erv(&no_facets, &projection_2_5, &human),
Some(5.0),
"human claim > projection"
);
let agent = claims_of(&[claim_anchor("a", "ISS-001", 3.0, RaterKind::Agent, false)]);
assert!(agent.priors.contains_key("ISS-001"), "agent → priors (D4)");
assert_eq!(
erv(&no_facets, &projection_2_5, &agent),
Some(2.5),
"projection > agent prior"
);
let agent_over_migrated = claims_of(&[
claim_anchor("a", "ISS-001", 3.0, RaterKind::Agent, false),
claim_anchor("m", "ISS-001", 2.0, RaterKind::Migrated, false),
]);
assert_eq!(
erv(&no_facets, &no_projection, &agent_over_migrated),
Some(3.0),
"agent prior > migrated prior"
);
let migrated = claims_of(&[claim_anchor(
"m",
"ISS-001",
2.0,
RaterKind::Migrated,
false,
)]);
assert_eq!(
erv(&facet_9, &no_projection, &migrated),
Some(2.0),
"migrated prior > unmigrated facet"
);
let no_claims = comparison::ClaimResolution::default();
assert_eq!(
erv(&facet_9, &no_projection, &no_claims),
Some(9.0),
"unmigrated facet > default (transitional rung, D6)"
);
assert_eq!(
erv(&no_facets, &no_projection, &no_claims),
Some(DEFAULT_VALUE)
);
}
#[test]
fn row_less_human_claim_resolves_at_rung_1_through_the_full_pipeline() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "");
write_value_claim_session(
root,
vec![claim_anchor(
"c1",
"ISS-001",
7.0,
comparison::RaterKind::Human,
false,
)],
);
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = config::load(root);
let pipeline = load_comparison_pipeline(root, &scanned, &cfg).unwrap();
assert!(
pipeline.value.projection.get("ISS-001").is_none(),
"no comparison rows ⇒ no projection entry"
);
assert_eq!(pipeline.value_claims.anchored["ISS-001"].value, 7.0);
let pg = build(root).unwrap();
let bs = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs.value_dim - 7.0).abs() < 1e-9,
"the row-less claim scored: {}",
bs.value_dim
);
}
#[test]
fn compared_facet_bearing_item_resolves_at_rung_2_and_presence_finding_fires() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 9.0", "");
seed_issue(root, 2, "open", "", "");
write_comparison_session(root, "ISS-001", "ISS-002");
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = config::load(root);
let pipeline = load_comparison_pipeline(root, &scanned, &cfg).unwrap();
assert!(
pipeline.value.anchors.is_empty(),
"no claim rows ⇒ no compile anchors — the facet never enters"
);
let pg = build(root).unwrap();
let bs1 = pg.attrs[&key("ISS", 1)].base_score;
assert!(
(bs1.value_dim - 4.0 / 3.0).abs() < 1e-9,
"rung 2 (projection) beat the unmigrated facet: {}",
bs1.value_dim
);
let findings = crate::priority::findings::detect(&pg, &cfg, None);
assert!(
findings.iter().any(|f| matches!(
f,
crate::priority::findings::Finding::UnmigratedFacet { entity, value, .. }
if entity == "ISS-001" && (*value - 9.0).abs() < 1e-9
)),
"UnmigratedFacet fires on PRESENCE: {findings:?}"
);
std::fs::remove_dir_all(root.join(".doctrine/comparisons")).unwrap();
let pg2 = build(root).unwrap();
let bs1 = pg2.attrs[&key("ISS", 1)].base_score;
assert!(
(bs1.value_dim - 9.0).abs() < 1e-9,
"no projection: rung 5 serves the facet"
);
}
#[test]
fn scoring_inert_kinds_claims_resolve_but_are_never_consumed_all_kinds() {
let no_facets = crate::facet::EntityFacets {
estimate: None,
value: None,
risk: None,
tags: vec![],
};
let no_projection = ValueProjection::new();
for prefix in crate::kinds::ALL_KINDS {
let Some(kind) = crate::integrity::KINDS
.iter()
.find(|k| k.kind.prefix == *prefix)
.map(|k| k.kind)
else {
panic!("{prefix} missing from integrity::KINDS");
};
let subject = format!("{prefix}-001");
let claims = claims_of(&[claim_anchor(
"c1",
&subject,
7.0,
comparison::RaterKind::Human,
false,
)]);
assert_eq!(claims.anchored[&subject].value, 7.0, "{prefix} captured");
let entity_key = EntityKey { prefix, id: 1 };
let resolved =
effective_raw_value(kind, &no_facets, entity_key, &no_projection, &claims);
if crate::kinds::is_value_bearing(prefix) {
assert_eq!(resolved, Some(7.0), "{prefix} consumes at rung 1");
} else {
assert_eq!(resolved, None, "{prefix} is consumption-inert (D7)");
}
}
}
#[test]
fn empty_claims_and_no_facets_score_bitwise_identically() {
let dir = tmp();
let root = dir.path();
seed_issue(root, 1, "open", "", "");
seed_issue(root, 2, "open", "", "");
write_comparison_session(root, "ISS-001", "ISS-002");
let scanned =
relation_graph::scan_entities(root, &mut vec![], ScanMode::default()).unwrap();
let cfg = config::load(root);
let pipeline = load_comparison_pipeline(root, &scanned, &cfg).unwrap();
assert_eq!(
pipeline.value_claims,
comparison::ClaimResolution::default(),
"no anchor rows ⇒ the pipeline's claims are empty"
);
assert!(!pipeline.value.projection.is_empty(), "non-trivial corpus");
let cost_feed = comparison::cost_feed(&pipeline.estimate.projection);
let via_load = build_from(&scanned, root).unwrap();
let explicit_empty = build_from_with_cfg(
&scanned,
root,
&cfg,
&pipeline.value.projection,
&cost_feed,
&comparison::ClaimResolution::default(),
)
.unwrap();
assert_eq!(via_load.score, explicit_empty.score, "score map bitwise");
assert_eq!(via_load.leverage, explicit_empty.leverage);
assert_eq!(via_load.optionality, explicit_empty.optionality);
for (k, attr) in &via_load.attrs {
let other = &explicit_empty.attrs[k];
assert!(
attr.base_score.value_dim.to_bits() == other.base_score.value_dim.to_bits()
&& attr.base_score.risk_dim.to_bits() == other.base_score.risk_dim.to_bits(),
"{} base dims bitwise-identical",
k.canonical()
);
}
}
fn write_value_claim_session(root: &Path, rows: Vec<comparison::Judgement>) {
let session = comparison::ComparisonSession {
schema: comparison::COMPARISON_SCHEMA.to_string(),
version: comparison::COMPARISON_VERSION,
session: comparison::SessionHeader {
uid: "v1".to_string(),
date: "2026-07-16".to_string(),
audience: None,
},
judgements: rows,
tombstones: Vec::new(),
};
let text = comparison::to_toml(&session).unwrap();
write(root, ".doctrine/comparisons/2026-07-16-v1.toml", &text);
}
}