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,
}
fn est_cost(
key: EntityKey,
cost_feed: &comparison::CostFeed,
ctx: CostCtx,
_ec: &config::EstimateCost,
_f: &EntityFacets,
estimate_claims: &comparison::ClaimResolutionGeneric<comparison::EstimatePayload>,
) -> f64 {
let canonical = key.canonical();
if let Some(claim) = estimate_claims.anchored.get(&canonical) {
return claim.operative;
}
if let Some(&projected) = cost_feed.get(&canonical) {
return floor_eps(projected);
}
if let Some(claim) = estimate_claims.priors.get(&canonical) {
return claim.operative;
}
ctx.absent
}
#[cfg_attr(
not(test),
expect(dead_code, reason = "SL-222 PHASE-09: used in tests")
)]
pub(crate) fn authored_est_cost(bounds: (f64, f64), ec: &config::EstimateCost) -> f64 {
crate::estimate::operative_cost(bounds, ec.skew)
}
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.operative)
};
claim_rung(&claims.anchored)
.or_else(|| projected.get(&canonical).map(|&(v, _)| v))
.or_else(|| claim_rung(&claims.priors))
.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,
estimate_claims: &comparison::ClaimResolutionGeneric<comparison::EstimatePayload>,
) -> 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(key, cost_feed, ctx, &cfg.estimate, f, estimate_claims);
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,
pub(crate) estimate_claims: comparison::ClaimResolutionGeneric<comparison::EstimatePayload>,
}
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 est = est_cost(
*key,
&self.cost_feed,
self.cost_ctx,
&cfg.estimate,
f,
&self.estimate_claims,
);
let bare = !self.estimate_claims.anchored.contains_key(&key.canonical());
Some((multiplier, est, bare))
}
}
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,
pipeline.bare_anchor,
&pipeline.estimate_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 facet_uppers: BTreeMap<String, f64> = BTreeMap::new();
let margin = cfg.estimate.margin;
let estimate_skew = cfg.estimate.skew;
let value_cfg = ProjectionCfg {
gauge_step: cfg.gauge.step,
..comparison::VALUE_PROJECTION_PARAMS
};
let empty_est_anchors = AnchorMap::new();
comparison::load_pipeline(
root,
&statuses,
&empty_est_anchors,
&value_cfg,
&comparison::ProjectionCfg {
gauge_step: cfg.estimate.gauge_step,
gauge_center: 1.0, },
&facet_uppers,
margin,
estimate_skew,
)
}
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
}
#[expect(
clippy::too_many_arguments,
reason = "PHASE-06 threading: estimate_claims added as 9th param"
)]
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,
bare_anchor: f64,
estimate_claims: &comparison::ClaimResolutionGeneric<comparison::EstimatePayload>,
) -> anyhow::Result<PriorityGraph> {
let ctx = CostCtx {
absent: bare_anchor,
};
let base_by_key: BTreeMap<EntityKey, BaseScore> = scanned
.iter()
.map(|entity| {
let base = base_score(
&EntityFacets {
risk: entity.risk.clone(),
tags: entity.tags.clone(),
},
entity.kind,
entity.key,
projected,
claims,
cost_feed,
cfg,
ctx,
estimate_claims,
);
(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 {
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(),
estimate_claims: estimate_claims.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");
write_value_anchor(root, "fv-sl1", "SL-001", 25.0);
write_est_anchor(root, "fe-sl1", "SL-001", 0.0, 10.0);
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",
);
let item = format!("ISS-{id:03}");
if !estimate.is_empty() {
let (l, u) = parse_estimate_pair(estimate);
write_est_anchor(root, &format!("fe{id}"), &item, l, u);
}
if !value.is_empty() {
let v = parse_value_facet(value);
write_value_anchor(root, &format!("fv{id}"), &item, v);
}
}
fn parse_estimate_pair(s: &str) -> (f64, f64) {
let mut lower = 0.0;
let mut upper = 0.0;
for line in s.lines() {
if let Some(rest) = line.strip_prefix("lower = ") {
lower = rest.parse().unwrap_or(0.0);
} else if let Some(rest) = line.strip_prefix("upper = ") {
upper = rest.parse().unwrap_or(0.0);
}
}
(lower, upper)
}
fn parse_value_facet(s: &str) -> f64 {
for line in s.lines() {
if let Some(rest) = line.strip_prefix("value = ") {
return rest.parse().unwrap_or(0.0);
}
}
0.0
}
#[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 {
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 no_est_claims =
comparison::ClaimResolutionGeneric::<comparison::EstimatePayload>::default();
let bs = base_score(
&facets,
asm_kind,
asm_key,
&no_projection,
&no_claims,
&no_feed,
&cfg,
ctx,
&no_est_claims,
);
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,
&no_est_claims,
);
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,
&no_est_claims,
);
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 {
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 {
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(DEFAULT_VALUE),
"no projection, zero claim rows: falls through to DEFAULT_VALUE (facet deleted)"
);
}
#[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");
write_value_anchor(root, "fv-sl1", "SL-001", 2.0 / 3.0);
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,
est_lower: None,
est_upper: 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",
);
let item = format!("ISS-{id:03}");
if !estimate.is_empty() {
let (l, u) = parse_estimate_pair(estimate);
write_est_anchor(root, &format!("fe{id}"), &item, l, u);
}
if !value.is_empty() {
let v = parse_value_facet(value);
write_value_anchor(root, &format!("fv{id}"), &item, v);
}
}
#[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");
write_value_anchor(root, "v-sl1", "SL-001", 4.0);
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_value_anchor(root, "v-sl1", "SL-001", 4.0);
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");
write_value_anchor(root, "v-sl2", "SL-002", 4.0);
write_value_anchor(root, "v-sl3", "SL-003", 4.0);
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");
write_value_anchor(root, "v-sl1", "SL-001", 4.0);
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");
write_value_anchor(root, "v-sl1", "SL-001", 5.0);
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");
write_value_anchor(root, "v-sl1", "SL-001", 4.0);
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 {
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 cfg = config::load(root);
let pipeline = load_comparison_pipeline(root, &scanned, &cfg).unwrap();
let via_load = build_from(&scanned, root).unwrap();
let via_cfg = 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();
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 authored_est_cost_formula_site() {
let _ec = config::EstimateCost {
skew: 0.65,
..config::EstimateCost::default()
};
assert_eq!(
crate::estimate::operative_cost((1.0, 9.0), 0.65),
1.0 + 0.65 * 8.0
);
assert_eq!(crate::estimate::operative_cost((0.0, 0.0), 0.65), 1e-12); assert_eq!(
crate::estimate::operative_cost((2.0, 6.0), 0.65),
2.0 + 0.65 * 4.0
);
}
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,
est_lower: None,
est_upper: 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 write_est_anchor(root: &Path, uid: &str, item: &str, lower: f64, upper: f64) {
use comparison::RowForm;
let j = comparison::Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: comparison::DOMAIN_ESTIMATE.to_string(),
frame: comparison::FRAME_COST_ANCHOR.to_string(),
form: RowForm::Anchor,
magnitude: None,
supersedes: None,
lens: None,
rater: comparison::RaterKind::Human,
by: Some("david".to_string()),
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 = comparison::ComparisonSession {
schema: comparison::COMPARISON_SCHEMA.to_string(),
version: comparison::COMPARISON_VERSION,
session: comparison::SessionHeader {
uid: uid.to_string(),
date: "2026-07-17".to_string(),
audience: None,
},
judgements: vec![j],
tombstones: Vec::new(),
};
let text = comparison::to_toml(&session).unwrap();
let fname = format!(".doctrine/comparisons/2026-07-17-{uid}.toml");
write(root, &fname, &text);
}
fn write_value_anchor(root: &Path, uid: &str, item: &str, value: f64) {
use comparison::RowForm;
let j = 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: RowForm::Anchor,
magnitude: Some(value),
supersedes: None,
lens: None,
rater: 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 = comparison::ComparisonSession {
schema: comparison::COMPARISON_SCHEMA.to_string(),
version: comparison::COMPARISON_VERSION,
session: comparison::SessionHeader {
uid: uid.to_string(),
date: "2026-07-16".to_string(),
audience: None,
},
judgements: vec![j],
tombstones: Vec::new(),
};
let text = comparison::to_toml(&session).unwrap();
let fname = format!(".doctrine/comparisons/2026-07-16-{uid}.toml");
write(root, &fname, &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
}
fn no_est_facets() -> EntityFacets {
EntityFacets {
risk: None,
tags: Vec::new(),
}
}
fn est_facets(_lower: f64, _upper: f64) -> EntityFacets {
EntityFacets {
risk: None,
tags: Vec::new(),
}
}
fn no_est_claims() -> comparison::ClaimResolutionGeneric<comparison::EstimatePayload> {
comparison::ClaimResolutionGeneric::default()
}
fn anchored_est_claim(
item: &str,
lower: f64,
upper: f64,
tier: comparison::ClaimTier,
) -> comparison::ClaimResolutionGeneric<comparison::EstimatePayload> {
use comparison::EstimatePayload;
let operative = crate::estimate::operative_cost((lower, upper), 0.65);
let claim = comparison::ResolvedClaimGeneric {
operative,
payload: EstimatePayload(lower, upper),
tier,
conflict: None,
rows: 1,
attribution: None,
};
let anchored: BTreeMap<String, _> = [(item.to_string(), claim)].into();
comparison::ClaimResolutionGeneric {
anchored,
priors: BTreeMap::new(),
lensed: BTreeMap::new(),
findings: Vec::new(),
}
}
fn prior_est_claim(
item: &str,
lower: f64,
upper: f64,
tier: comparison::ClaimTier,
) -> comparison::ClaimResolutionGeneric<comparison::EstimatePayload> {
use comparison::EstimatePayload;
let operative = crate::estimate::operative_cost((lower, upper), 0.65);
let claim = comparison::ResolvedClaimGeneric {
operative,
payload: EstimatePayload(lower, upper),
tier,
conflict: None,
rows: 1,
attribution: None,
};
let priors: BTreeMap<String, _> = [(item.to_string(), claim)].into();
comparison::ClaimResolutionGeneric {
anchored: BTreeMap::new(),
priors,
lensed: BTreeMap::new(),
findings: Vec::new(),
}
}
#[test]
fn est_cost_ladder_r1_anchored_claim_beats_feed() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 99.0 };
let feed: comparison::CostFeed = [("ISS-001".to_string(), 1000.0)].into();
let claims = anchored_est_claim("ISS-001", 2.0, 6.0, comparison::ClaimTier::Human);
let result = est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims);
let expected = crate::estimate::operative_cost((2.0, 6.0), 0.65);
assert!(
(result - expected).abs() < 1e-9,
"r1 anchored claim: expected {expected}, got {result}"
);
}
#[test]
fn est_cost_ladder_r2_feed_beats_rest() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 7.0 };
let feed: comparison::CostFeed = [("ISS-001".to_string(), 4.85)].into();
let claims = no_est_claims();
assert_eq!(
est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims),
4.85
);
assert_eq!(
est_cost(key("ISS", 2), &feed, ctx, &ec, &no_est_facets(), &claims),
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();
let claims = no_est_claims();
assert_eq!(
est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims),
EPSILON
);
}
#[test]
fn est_cost_ladder_r2_projections_beats_r3_agent_prior() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 99.0 };
let feed: comparison::CostFeed = [("ISS-001".to_string(), 4.85)].into();
let claims = prior_est_claim("ISS-001", 1.0, 5.0, comparison::ClaimTier::Agent);
let result = est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims);
assert!(
(result - 4.85).abs() < 1e-9,
"r2 projection wins, got {result}"
);
}
#[test]
fn est_cost_ladder_r3_agent_prior_no_projection() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 99.0 };
let feed = comparison::CostFeed::new(); let claims = prior_est_claim("ISS-001", 1.0, 5.0, comparison::ClaimTier::Agent);
let result = est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims);
let expected = crate::estimate::operative_cost((1.0, 5.0), 0.65);
assert!(
(result - expected).abs() < 1e-9,
"r3 agent prior: expected {expected}, got {result}"
);
}
#[test]
fn est_cost_ladder_r4_migrated_prior_no_projection() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 99.0 };
let feed = comparison::CostFeed::new(); let claims = prior_est_claim("ISS-001", 3.0, 7.0, comparison::ClaimTier::Migrated);
let result = est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims);
let expected = crate::estimate::operative_cost((3.0, 7.0), 0.65);
assert!(
(result - expected).abs() < 1e-9,
"r4 migrated prior: expected {expected}, got {result}"
);
}
#[test]
fn est_cost_ladder_r5_deleted_falls_to_bare_anchor() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 99.0 };
let feed = comparison::CostFeed::new();
let claims = no_est_claims();
let result = est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims);
assert!(
(result - 99.0).abs() < 1e-9,
"r5 deleted: expected bare anchor 99.0, got {result}"
);
}
#[test]
fn est_cost_ladder_r6_bare_anchor() {
let ec = config::EstimateCost::default();
let ctx = CostCtx { absent: 11.0 };
let feed = comparison::CostFeed::new();
let claims = no_est_claims();
assert_eq!(
est_cost(key("ISS", 1), &feed, ctx, &ec, &no_est_facets(), &claims),
11.0
);
}
#[test]
fn est_cost_ladder_evidenced_bare_item_takes_cost_feed() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_anchor(root, "a1", "ISS-001", 2.0, 6.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,
"r1 anchored claim"
);
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, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_anchor(root, "a1", "ISS-001", 2.0, 6.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, "", "", "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) - 1.0).abs() < 1e-9);
assert!((est_of(&before, "ISS", 3) - 1.0).abs() < 1e-9);
write_est_anchor(root, "a1", "ISS-003", 1.0, 3.0);
let after = build(root).unwrap();
assert!(
(est_of(&after, "ISS", 3) - 2.3).abs() < 1e-9,
"r1 anchored claim"
);
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, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_anchor(root, "a1", "ISS-001", 6.0, 6.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"
);
}
#[test]
fn est_cost_row_less_human_claim_resolves_at_rung_1() {
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_est_anchor(root, "a1", "ISS-001", 5.0, 7.0);
let pg = build(root).unwrap();
let c1 = est_of(&pg, "ISS", 1);
let expected = crate::estimate::operative_cost((5.0, 7.0), 0.65);
assert!(
(c1 - expected).abs() < 1e-9,
"row-less human claim still wins at rung 1 (scope R1): {c1}"
);
let c2 = est_of(&pg, "ISS", 2);
assert_eq!(c2, pg.cost_ctx.absent, "ISS-002 bare: absent anchor");
}
#[test]
fn gauge_masked_with_agent_claim_takes_rung_3_otherwise_bare() {
let dir = tmp();
let root = dir.path();
seed_issue_with_facets(root, 1, "", "", "value = 5.0", "");
seed_issue_with_facets(root, 2, "", "", "value = 5.0", "");
write_est_session(
root,
vec![est_row(
"e1",
"ISS-001",
"ISS-002",
comparison::Response::PreferA,
)],
);
let pg = build(root).unwrap();
let abs = pg.cost_ctx.absent;
let c1_bare = est_of(&pg, "ISS", 1);
assert!(
(c1_bare - abs).abs() < 1e-9,
"ISS-001 gauge-masked without claim = bare anchor"
);
let c2_bare = est_of(&pg, "ISS", 2);
assert!(
(c2_bare - abs).abs() < 1e-9,
"ISS-002 gauge-masked without claim = bare anchor"
);
write_est_anchor(root, "a1", "ISS-001", 1.0, 3.0);
let pg2 = build(root).unwrap();
let c1 = est_of(&pg2, "ISS", 1);
let agent_op = crate::estimate::operative_cost((1.0, 3.0), 0.65);
assert!(
(c1 - agent_op).abs() < 1e-9,
"ISS-001 agent claim wins at rung 3: {c1} vs {agent_op}"
);
let c2 = est_of(&pg2, "ISS", 2);
assert!(
(c2 - pg2.cost_ctx.absent).abs() > 1e-6,
"ISS-002 gets projected cost (not bare) because the agent anchor\
row grounds the component: c2={c2}, absent={}",
pg2.cost_ctx.absent
);
assert!(
c2 > 0.0,
"ISS-002 cost is positive (projected from the agent claim anchor)"
);
}
#[test]
fn scoring_inert_kinds_estimate_resolve_not_consumed() {
let no_facets = EntityFacets {
risk: None,
tags: Vec::new(),
};
let cfg = config::PriorityConfig::default();
let ctx = CostCtx { absent: 99.0 };
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 key = EntityKey { prefix, id: 1 };
let canonical = key.canonical();
let claims = anchored_est_claim(&canonical, 1.0, 5.0, comparison::ClaimTier::Human);
assert!(
claims.anchored.contains_key(&canonical),
"{prefix}: claim captured (kind-blind)"
);
let result = est_cost(
key,
&comparison::CostFeed::new(),
ctx,
&cfg.estimate,
&no_facets,
&claims,
);
let expected = crate::estimate::operative_cost((1.0, 5.0), 0.65);
assert_eq!(
result, expected,
"{prefix}: rung 1 wins (est_cost is kind-blind)"
);
}
}
#[test]
fn adjacent_rung_dominance_chain_est() {
let cfg = config::PriorityConfig::default();
let ctx = CostCtx { absent: 99.0 };
let pin_only = anchored_est_claim("ISS-001", 1.0, 3.0, comparison::ClaimTier::Pin);
let pin_val = pin_only.anchored["ISS-001"].operative;
assert!((pin_val - 2.3).abs() < 1e-9, "pin operative: {pin_val}");
let human = anchored_est_claim("ISS-001", 4.0, 6.0, comparison::ClaimTier::Human);
let human_val = human.anchored["ISS-001"].operative;
assert!(
(human_val - 5.3).abs() < 1e-9,
"human operative: {human_val}"
);
let feed_item =
|item: &str, cost: f64| -> comparison::CostFeed { [(item.to_string(), cost)].into() };
let iss1_key = EntityKey {
prefix: "ISS",
id: 1,
};
let with_feed = feed_item("ISS-001", 1000.0);
let result = est_cost(
iss1_key,
&with_feed,
ctx,
&cfg.estimate,
&no_est_facets(),
&human,
);
assert!(
(result - 5.3).abs() < 1e-9,
"rung 1 (human) beats rung 2 (projection): {result}"
);
let agent = prior_est_claim("ISS-001", 1.0, 5.0, comparison::ClaimTier::Agent);
let result = est_cost(
iss1_key,
&with_feed,
ctx,
&cfg.estimate,
&no_est_facets(),
&agent,
);
assert!(
(result - 1000.0).abs() < 1e-9,
"rung 2 (projection) beats rung 3 (agent prior): {result}"
);
let agent_item = "ISS-001";
let migrated_item = "ISS-002";
let agent_claim_2 = prior_est_claim(agent_item, 5.0, 7.0, comparison::ClaimTier::Agent);
let migrated_claim_2 =
prior_est_claim(migrated_item, 1.0, 3.0, comparison::ClaimTier::Migrated);
let agent_key = EntityKey {
prefix: "ISS",
id: 1,
};
let migrated_key = EntityKey {
prefix: "ISS",
id: 2,
};
let facet = est_facets(9.0, 10.0);
let result = est_cost(
agent_key,
&comparison::CostFeed::new(),
ctx,
&cfg.estimate,
&facet,
&agent_claim_2,
);
let agent_op = agent_claim_2.priors[agent_item].operative;
assert!(
(result - agent_op).abs() < 1e-9,
"rung 3 (agent prior) beats rung 4 (migrated): {result}"
);
let result = est_cost(
migrated_key,
&comparison::CostFeed::new(),
ctx,
&cfg.estimate,
&facet,
&migrated_claim_2,
);
let migrated_op = migrated_claim_2.priors[migrated_item].operative;
assert!(
(result - migrated_op).abs() < 1e-9,
"rung 4 (migrated prior) beats rung 5 (facet): {result}"
);
assert!(
agent_op > migrated_op,
"agent prior and migrated prior both beat facet; rung 3 before rung 4 in the ladder"
);
let migrated = prior_est_claim("ISS-001", 1.0, 3.0, comparison::ClaimTier::Migrated);
let migrated_val = migrated.priors["ISS-001"].operative;
let no_claims = no_est_claims();
let result = est_cost(
iss1_key,
&comparison::CostFeed::new(),
ctx,
&cfg.estimate,
&no_est_facets(),
&migrated,
);
assert!(
(result - migrated_val).abs() < 1e-9,
"rung 4 (migrated) beats rung 6 (absent): {result}"
);
let result = est_cost(
iss1_key,
&comparison::CostFeed::new(),
ctx,
&cfg.estimate,
&no_est_facets(),
&no_claims,
);
assert!(
(result - ctx.absent).abs() < 1e-9,
"rung 6 (bare anchor) fallsthrough to absent, got {result}"
);
let result = est_cost(
iss1_key,
&comparison::CostFeed::new(),
ctx,
&cfg.estimate,
&no_est_facets(),
&no_claims,
);
assert_eq!(
result, ctx.absent,
"rung 6 (bare anchor) fallsthrough to absent"
);
}
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),
est_lower: None,
est_upper: None,
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 {
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 {
risk: None,
tags: vec![],
};
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(DEFAULT_VALUE),
"facet deleted (PHASE-09): falls through to DEFAULT_VALUE, not 9.0"
);
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"].operative, 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(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!(
pipeline.value.anchors.is_empty(),
"no claim rows ⇒ no compile anchors"
);
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) won: {}",
bs1.value_dim
);
let findings = crate::priority::findings::detect(&pg, &cfg, None);
assert!(
!findings.iter().any(|f| matches!(
f,
crate::priority::findings::Finding::UnmigratedFacet { .. }
)),
"No UnmigratedFacet findings after deletion: {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 - DEFAULT_VALUE).abs() < 1e-9,
"no projection, no claims: DEFAULT_VALUE, got {}",
bs1.value_dim
);
}
#[test]
fn scoring_inert_kinds_claims_resolve_but_are_never_consumed_all_kinds() {
let no_facets = crate::facet::EntityFacets {
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].operative, 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(),
pipeline.bare_anchor,
&crate::comparison::ClaimResolutionGeneric::<
crate::comparison::EstimatePayload,
>::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);
}
}