use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::Context;
use super::compile::{self, AnchorMap, ClassId, ConstraintSet, QuarantinePolicy, RaterCounts};
use super::project::{self, Projection, ProjectionCfg, ValueProvenance};
use super::resolve::{
self, MalformedSupersession, ResolutionStatus, RowState, StatusMap, rater_key,
};
use super::{
COMPARISONS_DIR, ClaimResolution, ClaimResolutionGeneric, ComparisonSession, DOMAIN_ESTIMATE,
DOMAIN_PRIORITY, EstimatePayload, Judgement, Response, RowForm, resolve_claims,
resolve_claims_generic,
};
pub(crate) fn load_sessions(root: &Path) -> anyhow::Result<Vec<ComparisonSession>> {
let dir = root.join(".doctrine").join(COMPARISONS_DIR);
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => {
return Err(
anyhow::Error::new(e).context(format!("read comparisons dir {}", dir.display()))
);
}
};
let mut paths: Vec<PathBuf> = entries
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
.collect();
paths.sort();
let mut sessions = Vec::new();
for path in paths {
let text = std::fs::read_to_string(&path)
.with_context(|| format!("read comparison session {}", path.display()))?;
let session = super::parse(&text)
.with_context(|| format!("parse comparison session {}", path.display()))?;
sessions.push(session);
}
Ok(sessions)
}
pub(crate) struct RowSummary {
pub uid: String,
pub a: String,
pub b: Option<String>,
pub response: Option<Response>,
pub domain: String,
pub frame: String,
pub rater_token: &'static str,
pub by: Option<String>,
pub note: Option<String>,
pub date: String,
pub claim: Option<&'static str>,
pub state: RowState,
}
impl RowSummary {
pub(crate) fn display_token(&self) -> String {
self.claim
.map_or_else(|| self.state.display_token(), str::to_string)
}
}
pub(crate) struct Pipeline {
pub rows: Vec<RowSummary>,
pub value: DomainSystem,
pub estimate: DomainSystem,
pub constraining_by_class: BTreeMap<ClassId, RaterCounts>,
pub est_constraining_by_class: BTreeMap<ClassId, RaterCounts>,
pub malformed: Vec<MalformedSupersession>,
pub priority_domain_count: usize,
pub value_claims: ClaimResolution,
pub estimate_claims: ClaimResolutionGeneric<EstimatePayload>,
pub bare_anchor: f64,
pub active_pairwise: Vec<Judgement>,
}
impl Pipeline {
pub(crate) fn active_pairwise(&self) -> &[Judgement] {
&self.active_pairwise
}
}
pub(crate) struct DomainSystem {
pub constraint_set: ConstraintSet,
pub anchors: AnchorMap,
pub projection: Projection,
}
impl DomainSystem {
fn compiled(rows: &[&Judgement], anchors: &AnchorMap, cfg: &ProjectionCfg) -> Self {
let constraint_set = compile::compile(rows, anchors, QuarantinePolicy::Symmetric);
let projection = project::project(&constraint_set, cfg);
DomainSystem {
constraint_set,
anchors: anchors.clone(),
projection,
}
}
fn empty(anchors: &AnchorMap) -> Self {
DomainSystem {
constraint_set: compile::compile(&[], anchors, QuarantinePolicy::Symmetric),
anchors: anchors.clone(),
projection: Projection::new(),
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "PHASE-04 pipeline wiring: load_pipeline receives 7 params — root + 5 shared (statuses, est_anchors, value_cfg, est_cfg) + 3 new PHASE-04 params (facet_uppers, margin, estimate_skew). Acceptable for an integration seam."
)]
pub(crate) fn load_pipeline(
root: &Path,
statuses: &StatusMap,
est_anchors: &AnchorMap,
value_cfg: &ProjectionCfg,
est_cfg: &ProjectionCfg,
facet_uppers: &BTreeMap<String, f64>,
margin: f64,
estimate_skew: f64,
) -> anyhow::Result<Pipeline> {
let sessions = load_sessions(root)?;
pipeline_from_sessions(
&sessions,
statuses,
est_anchors,
value_cfg,
est_cfg,
facet_uppers,
margin,
estimate_skew,
)
}
#[expect(
clippy::too_many_arguments,
reason = "PHASE-04 pipeline wiring: 8 params for the core pipeline function — 5 shared domain params + 3 new PHASE-04 params (facet_uppers, margin, estimate_skew). Acceptable for a central integration seam."
)]
pub(crate) fn pipeline_from_sessions(
sessions: &[ComparisonSession],
statuses: &StatusMap,
_est_anchors: &AnchorMap,
value_cfg: &ProjectionCfg,
est_cfg: &ProjectionCfg,
facet_uppers: &BTreeMap<String, f64>,
margin: f64,
estimate_skew: f64,
) -> anyhow::Result<Pipeline> {
if sessions.is_empty() {
let max_upper = facet_uppers.values().copied().max_by(f64::total_cmp);
let bare_anchor = match max_upper {
Some(mu) => mu + margin,
None => 1.0,
};
return Ok(Pipeline {
rows: Vec::new(),
value: DomainSystem::empty(&AnchorMap::new()),
estimate: DomainSystem::empty(&AnchorMap::new()),
constraining_by_class: BTreeMap::new(),
est_constraining_by_class: BTreeMap::new(),
malformed: Vec::new(),
priority_domain_count: 0,
value_claims: ClaimResolution::default(),
estimate_claims: ClaimResolutionGeneric::default(),
bare_anchor,
active_pairwise: Vec::new(),
});
}
let resolution = resolve::resolve(sessions, statuses)?;
let value_claims = resolve_claims(&resolution.rows);
let estimate_claims = resolve_claims_generic::<EstimatePayload>(
&resolution.rows,
DOMAIN_ESTIMATE,
&estimate_skew,
);
let value_anchors = value_claims.anchor_map();
let max_anchor_upper = resolution
.rows
.iter()
.filter(|(j, status)| {
j.domain == DOMAIN_ESTIMATE
&& matches!(j.form, RowForm::Anchor)
&& matches!(
status,
ResolutionStatus::Active | ResolutionStatus::InertLens
)
&& j.lens.is_none()
})
.filter_map(|(j, _)| j.est_upper)
.max_by(f64::total_cmp);
let max_facet_upper = facet_uppers.values().copied().max_by(f64::total_cmp);
let max_upper = max_anchor_upper
.into_iter()
.chain(max_facet_upper)
.max_by(f64::total_cmp);
let bare_anchor = match max_upper {
Some(mu) => mu + margin,
None => 1.0,
};
let est_cfg = ProjectionCfg {
gauge_center: bare_anchor,
..*est_cfg
};
let active: Vec<&Judgement> = resolution
.rows
.iter()
.filter(|(j, status)| {
matches!(status, ResolutionStatus::Active) && !matches!(j.form, RowForm::Anchor)
})
.map(|(j, _)| *j)
.collect();
let (est_active, value_active): (Vec<&Judgement>, Vec<&Judgement>) =
active.iter().partition(|j| j.domain == DOMAIN_ESTIMATE);
let est_anchors_from_claims = estimate_claims.anchor_map();
let gated_est_anchors: AnchorMap = est_anchors_from_claims
.iter()
.filter(|(item, _)| {
est_active
.iter()
.any(|j| j.a == **item || j.b.as_deref() == Some(item.as_str()))
})
.map(|(item, &v)| (item.clone(), v))
.collect();
let value = DomainSystem::compiled(&value_active, &value_anchors, value_cfg);
let estimate = DomainSystem::compiled(&est_active, &gated_est_anchors, &est_cfg);
let constraining_by_class =
compile::constraining_counts_by_class(&value.constraint_set, &value_active);
let est_constraining_by_class =
compile::constraining_counts_by_class(&estimate.constraint_set, &est_active);
let priority_domain_count = resolution
.rows
.iter()
.filter(|(j, _)| j.domain == DOMAIN_PRIORITY)
.count();
let rows = resolution
.rows
.iter()
.map(|(j, status)| {
let is_anchor = matches!(j.form, RowForm::Anchor);
let compilation =
(!is_anchor && matches!(status, ResolutionStatus::Active)).then(|| {
if j.domain == DOMAIN_ESTIMATE {
estimate.constraint_set.status_of(j)
} else {
value.constraint_set.status_of(j)
}
});
let claim = (is_anchor && matches!(status, ResolutionStatus::Active))
.then(|| {
if j.domain == DOMAIN_ESTIMATE {
claim_token_estimate(&estimate_claims, &j.a)
} else {
claim_token(&value_claims, &j.a)
}
})
.flatten();
RowSummary {
uid: j.uid.clone(),
a: j.a.clone(),
b: j.b.clone(),
response: j.response,
domain: j.domain.clone(),
frame: j.frame.clone(),
rater_token: rater_key(&j.rater),
by: j.by.clone(),
note: j.note.clone(),
date: j.ordering_date().to_string(),
claim,
state: RowState::new(status.clone(), compilation),
}
})
.collect();
let active_pairwise: Vec<Judgement> = value_active.iter().map(|&j| j.clone()).collect();
Ok(Pipeline {
rows,
value,
estimate,
constraining_by_class,
est_constraining_by_class,
malformed: resolution.malformed,
priority_domain_count,
value_claims,
estimate_claims,
bare_anchor,
active_pairwise,
})
}
fn claim_token(claims: &ClaimResolution, item: &str) -> Option<&'static str> {
if let Some(claim) = claims.anchored.get(item) {
return Some(if claim.conflict.is_some() {
"conflicted"
} else {
"anchored"
});
}
claims.priors.get(item).map(|claim| {
if claim.conflict.is_some() {
"conflicted"
} else {
"prior"
}
})
}
fn claim_token_estimate(
claims: &ClaimResolutionGeneric<EstimatePayload>,
item: &str,
) -> Option<&'static str> {
if let Some(claim) = claims.anchored.get(item) {
return Some(if claim.conflict.is_some() {
"conflicted"
} else {
"anchored"
});
}
claims.priors.get(item).map(|claim| {
if claim.conflict.is_some() {
"conflicted"
} else {
"prior"
}
})
}
pub(crate) type CostFeed = BTreeMap<String, f64>;
pub(crate) fn cost_feed(est_projection: &Projection) -> CostFeed {
est_projection
.iter()
.filter(|(_, (_, provenance))| *provenance != ValueProvenance::Gauge)
.map(|(entity, &(cost, _))| (entity.clone(), cost))
.collect()
}
#[cfg(test)]
mod tests {
use super::{AnchorMap, Projection, ProjectionCfg, StatusMap, load_pipeline, load_sessions};
use crate::comparison::{
COMPARISON_SCHEMA, COMPARISON_VERSION, ClaimResolution, ComparisonSession, DOMAIN_ESTIMATE,
DOMAIN_VALUE, FRAME_COST_ANCHOR, FRAME_EQUAL_EFFORT, FRAME_MORE_WORK, FRAME_VALUE_ANCHOR,
Judgement, QuarantinePolicy, QuarantineReason, RaterKind, Response, RowForm, SessionHeader,
VALUE_PROJECTION_PARAMS, ValueProvenance, cost_feed, pipeline_from_sessions,
};
use crate::priority::config::EST_GAUGE_STEP;
use std::collections::BTreeMap;
const CFG: ProjectionCfg = VALUE_PROJECTION_PARAMS;
const EST_SKEW: f64 = 0.65;
const EST_MARGIN: f64 = 1.0;
fn empty_facet_uppers() -> BTreeMap<String, f64> {
BTreeMap::new()
}
const EST_CENTER: f64 = 11.0;
const EST_CFG: ProjectionCfg = ProjectionCfg {
gauge_step: EST_GAUGE_STEP,
gauge_center: EST_CENTER,
};
fn write(root: &std::path::Path, rel: &str, body: &str) {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
fn judgement(uid: &str, a: &str, b: &str) -> Judgement {
Judgement {
uid: uid.to_string(),
seq: 0,
a: a.to_string(),
b: Some(b.to_string()),
response: Some(Response::PreferA),
domain: DOMAIN_VALUE.to_string(),
frame: FRAME_EQUAL_EFFORT.to_string(),
form: RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater: RaterKind::Human,
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 session(uid: &str, date: &str, judgements: Vec<Judgement>) -> ComparisonSession {
ComparisonSession {
schema: COMPARISON_SCHEMA.to_string(),
version: COMPARISON_VERSION,
session: SessionHeader {
uid: uid.to_string(),
date: date.to_string(),
audience: None,
},
judgements,
tombstones: Vec::new(),
}
}
#[test]
fn missing_comparisons_dir_is_an_empty_load() {
let dir = tempfile::tempdir().unwrap();
let sessions = load_sessions(dir.path()).unwrap();
assert!(sessions.is_empty());
}
#[test]
fn empty_comparisons_dir_is_an_empty_load() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".doctrine/comparisons")).unwrap();
let sessions = load_sessions(dir.path()).unwrap();
assert!(sessions.is_empty());
}
#[test]
fn sessions_load_in_filename_order() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write(
root,
".doctrine/comparisons/2026-07-11-zzz.toml",
&crate::comparison::to_toml(&session("last", "2026-07-11", vec![])).unwrap(),
);
write(
root,
".doctrine/comparisons/2026-07-09-aaa.toml",
&crate::comparison::to_toml(&session("first", "2026-07-09", vec![])).unwrap(),
);
write(
root,
".doctrine/comparisons/2026-07-10-mmm.toml",
&crate::comparison::to_toml(&session("middle", "2026-07-10", vec![])).unwrap(),
);
let sessions = load_sessions(root).unwrap();
let uids: Vec<&str> = sessions.iter().map(|s| s.session.uid.as_str()).collect();
assert_eq!(uids, vec!["first", "middle", "last"]);
}
#[test]
fn non_toml_files_in_the_dir_are_ignored() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write(root, ".doctrine/comparisons/README.md", "not a session\n");
let sessions = load_sessions(root).unwrap();
assert!(sessions.is_empty());
}
#[test]
fn no_sessions_on_disk_is_an_empty_projection() {
let dir = tempfile::tempdir().unwrap();
let pipeline = load_pipeline(
dir.path(),
&StatusMap::new(),
&Default::default(),
&CFG,
&EST_CFG,
&empty_facet_uppers(),
EST_MARGIN,
EST_SKEW,
)
.unwrap();
assert!(pipeline.value.projection.is_empty());
assert!(pipeline.estimate.projection.is_empty());
assert!(pipeline.rows.is_empty());
assert_eq!(pipeline.priority_domain_count, 0);
}
#[test]
fn pipeline_composes_through_to_a_projection() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write(
root,
".doctrine/comparisons/2026-07-11-s1.toml",
&crate::comparison::to_toml(&session(
"s1",
"2026-07-11",
vec![judgement("j1", "SL-100", "SL-200")],
))
.unwrap(),
);
let pipeline = load_pipeline(
root,
&StatusMap::new(),
&Default::default(),
&CFG,
&EST_CFG,
&empty_facet_uppers(),
EST_MARGIN,
EST_SKEW,
)
.unwrap();
let projected = &pipeline.value.projection;
assert!(projected.contains_key("SL-100"));
assert!(projected.contains_key("SL-200"));
let (winner, _) = projected["SL-100"];
let (loser, _) = projected["SL-200"];
assert!(winner > loser, "the preferred side outranks the other");
assert_eq!(pipeline.rows.len(), 1);
assert_eq!(pipeline.rows[0].state.display_token(), "active");
}
fn est_judgement(uid: &str, costlier: &str, cheaper: &str) -> Judgement {
let mut j = judgement(uid, costlier, cheaper);
j.domain = DOMAIN_ESTIMATE.to_string();
j.frame = FRAME_MORE_WORK.to_string();
j
}
fn pipeline_of(judgements: Vec<Judgement>, est_anchors: &AnchorMap) -> super::Pipeline {
let sessions = vec![session("s1", "2026-07-11", judgements)];
pipeline_from_sessions(
&sessions,
&StatusMap::new(),
est_anchors,
&CFG,
&EST_CFG,
&empty_facet_uppers(),
EST_MARGIN,
EST_SKEW,
)
.unwrap()
}
fn pipeline_with_est_anchors(
judgements: Vec<Judgement>,
est_anchors: &AnchorMap,
) -> super::Pipeline {
let mut all = judgements;
for (i, (item, &cost)) in est_anchors.iter().enumerate() {
all.push(est_anchor_row(&format!("a-{item}"), item, cost, cost));
all.push(Judgement {
uid: format!("ag-{item}"),
seq: i as u32 + 100,
a: item.to_string(),
b: None,
response: Some(Response::PreferA),
domain: DOMAIN_ESTIMATE.to_string(),
frame: FRAME_MORE_WORK.to_string(),
form: RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater: RaterKind::Agent,
by: None,
note: None,
date: Some("2026-07-17".to_string()),
observed_at: None,
basis: None,
est_lower: None,
est_upper: None,
admission: None,
});
}
let sessions = vec![session("s1", "2026-07-11", all)];
pipeline_from_sessions(
&sessions,
&StatusMap::new(),
est_anchors,
&CFG,
&EST_CFG,
&empty_facet_uppers(),
EST_MARGIN,
EST_SKEW,
)
.unwrap()
}
#[test]
fn each_domain_system_compiles_only_its_own_rows() {
let pipeline = pipeline_of(
vec![
judgement("v1", "SL-100", "SL-200"),
est_judgement("e1", "SL-100", "SL-300"),
],
&AnchorMap::new(),
);
let value_entities: Vec<&String> = pipeline.value.constraint_set.classes.keys().collect();
assert_eq!(value_entities, ["SL-100", "SL-200"], "value rows only");
let est_entities: Vec<&String> = pipeline.estimate.constraint_set.classes.keys().collect();
assert_eq!(est_entities, ["SL-100", "SL-300"], "estimate rows only");
let active_uids: Vec<&str> = pipeline
.active_pairwise()
.iter()
.map(|j| j.uid.as_str())
.collect();
assert_eq!(active_uids, ["v1"]);
}
#[test]
fn est_anchors_from_authored_est_cost_are_row_gated() {
let ec = crate::priority::config::EstimateCost::default();
let cost = |bounds| crate::priority::graph::authored_est_cost(bounds, &ec);
let est_anchors: AnchorMap = [
("SL-100".to_string(), cost((1.0, 3.0))),
("SL-300".to_string(), cost((5.0, 9.0))),
("SL-400".to_string(), cost((2.0, 4.0))), ]
.into_iter()
.collect();
let pipeline = pipeline_with_est_anchors(
vec![est_judgement("e1", "SL-300", "SL-100")],
&[
("SL-100".to_string(), cost((1.0, 3.0))),
("SL-300".to_string(), cost((5.0, 9.0))),
]
.into_iter()
.collect(),
);
let gated: Vec<&String> = pipeline.estimate.anchors.keys().collect();
assert_eq!(gated, ["SL-100", "SL-300"], "row-gated anchor set");
let cs = &pipeline.estimate.constraint_set;
let class_of = |e: &str| cs.classes.get(e).unwrap();
assert_eq!(cs.anchors.get(class_of("SL-100")), Some(&cost((1.0, 3.0))));
assert_eq!(cs.anchors.get(class_of("SL-300")), Some(&cost((5.0, 9.0))));
let cold = pipeline_of(vec![judgement("v1", "SL-100", "SL-200")], &est_anchors);
assert!(cold.estimate.anchors.is_empty(), "no rows ⇒ no est anchors");
assert!(cold.estimate.constraint_set.classes.is_empty());
assert!(cold.estimate.projection.is_empty());
}
#[test]
fn c2_cost_anchor_merge_conflict_quarantines_the_equal_row() {
let est_anchors: AnchorMap = [("SL-100".to_string(), 1.0), ("SL-300".to_string(), 5.0)]
.into_iter()
.collect();
let mut equal_row = est_judgement("e1", "SL-100", "SL-300");
equal_row.response = Some(Response::Equal);
let pipeline = pipeline_with_est_anchors(vec![equal_row], &est_anchors);
assert!(
matches!(
pipeline.estimate.constraint_set.quarantined.get("e1"),
Some(QuarantineReason::AnchorConflict { .. })
),
"C2 cost-anchor merge conflict quarantines the row: {:?}",
pipeline.estimate.constraint_set.quarantined
);
assert!(pipeline.value.constraint_set.quarantined.is_empty());
}
#[test]
fn c4_stale_estimate_violation_closure_golden() {
let est_anchors: AnchorMap = [
("SL-100".to_string(), 1.0), ("SL-300".to_string(), 5.0),
]
.into_iter()
.collect();
let pipeline =
pipeline_with_est_anchors(vec![est_judgement("e1", "SL-100", "SL-300")], &est_anchors);
assert_eq!(
pipeline.estimate.constraint_set.quarantined.get("e1"),
Some(&QuarantineReason::AnchorConflict {
pairs: vec![("SL-100".to_string(), "SL-300".to_string())],
}),
"the C4 closure names the contradicted anchor pair"
);
let row = pipeline.rows.iter().find(|r| r.uid == "e1").unwrap();
assert_eq!(row.domain, DOMAIN_ESTIMATE);
assert_eq!(row.state.display_token(), "quarantined(anchors)");
}
#[test]
fn quarantine_maps_are_disjoint_and_rowstate_joins_by_owning_domain() {
let est_anchors: AnchorMap = [("SL-100".to_string(), 1.0), ("SL-300".to_string(), 5.0)]
.into_iter()
.collect();
let pipeline = pipeline_with_est_anchors(
vec![
judgement("v1", "SL-100", "SL-200"),
judgement("v2", "SL-200", "SL-500"),
judgement("v3", "SL-500", "SL-100"),
est_judgement("e1", "SL-100", "SL-300"),
],
&est_anchors,
);
let value_uids: Vec<&String> = pipeline.value.constraint_set.quarantined.keys().collect();
let est_uids: Vec<&String> = pipeline
.estimate
.constraint_set
.quarantined
.keys()
.collect();
assert_eq!(value_uids, ["v1", "v2", "v3"], "value cycle quarantined");
assert_eq!(est_uids, ["e1"], "est conflict quarantined");
assert!(
value_uids.iter().all(|uid| !est_uids.contains(uid)),
"quarantine maps disjoint by row uid"
);
for uid in ["v1", "v2", "v3"] {
let row = pipeline.rows.iter().find(|r| r.uid == uid).unwrap();
assert_eq!(row.state.display_token(), "quarantined(cycle)");
}
let e1 = pipeline.rows.iter().find(|r| r.uid == "e1").unwrap();
assert_eq!(e1.state.display_token(), "quarantined(anchors)");
}
use ValueProvenance::{Authored, Gauge, Projected};
const EPS: f64 = 1e-4;
fn est_pipeline_with_uppers(
judgements: Vec<Judgement>,
est_anchors: &AnchorMap,
est_cfg: &ProjectionCfg,
facet_uppers: &BTreeMap<String, f64>,
margin: f64,
) -> super::Pipeline {
let mut all = judgements;
for (i, (item, &cost)) in est_anchors.iter().enumerate() {
all.push(est_anchor_row(&format!("a-{item}"), item, cost, cost));
all.push(Judgement {
uid: format!("ag-{item}"),
seq: i as u32 + 100,
a: item.to_string(),
b: None,
response: Some(Response::PreferA),
domain: DOMAIN_ESTIMATE.to_string(),
frame: FRAME_MORE_WORK.to_string(),
form: RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater: RaterKind::Agent,
by: None,
note: None,
date: Some("2026-07-17".to_string()),
observed_at: None,
basis: None,
est_lower: None,
est_upper: None,
admission: None,
});
}
let sessions = vec![session("s1", "2026-07-11", all)];
pipeline_from_sessions(
&sessions,
&StatusMap::new(),
est_anchors,
&CFG,
est_cfg,
facet_uppers,
margin,
EST_SKEW,
)
.unwrap()
}
fn est_pipeline(
judgements: Vec<Judgement>,
est_anchors: &AnchorMap,
est_cfg: &ProjectionCfg,
) -> super::Pipeline {
let mut all = judgements;
for (i, (item, &cost)) in est_anchors.iter().enumerate() {
all.push(est_anchor_row(&format!("a-{item}"), item, cost, cost));
all.push(Judgement {
uid: format!("ag-{item}"),
seq: i as u32 + 100,
a: item.to_string(),
b: None,
response: Some(Response::PreferA),
domain: DOMAIN_ESTIMATE.to_string(),
frame: FRAME_MORE_WORK.to_string(),
form: RowForm::Order,
magnitude: None,
supersedes: None,
lens: None,
rater: RaterKind::Agent,
by: None,
note: None,
date: Some("2026-07-17".to_string()),
observed_at: None,
basis: None,
est_lower: None,
est_upper: None,
admission: None,
});
}
est_pipeline_with_uppers(all, est_anchors, est_cfg, &empty_facet_uppers(), EST_MARGIN)
}
fn anchors_of(pairs: &[(&str, f64)]) -> AnchorMap {
pairs.iter().map(|&(e, v)| (e.to_string(), v)).collect()
}
fn cost_golden(p: &Projection, e: &str, expected: f64, prov: ValueProvenance) {
let (got, got_prov) = *p.get(e).unwrap_or_else(|| panic!("missing entity {e}"));
assert!(
(got - expected).abs() < EPS,
"{e}: got {got:.4}, want {expected:.4}"
);
assert_eq!(got_prov, prov, "{e} provenance");
assert!(!got.is_nan(), "{e} is NaN");
}
#[test]
fn est_projection_anchored_chain_golden() {
let pipeline = est_pipeline(
vec![
est_judgement("e1", "SL-100", "SL-200"),
est_judgement("e2", "SL-200", "SL-300"),
est_judgement("e3", "SL-300", "SL-400"),
],
&anchors_of(&[("SL-100", 8.0), ("SL-400", 2.0)]),
&EST_CFG,
);
let p = &pipeline.estimate.projection;
cost_golden(p, "SL-100", 8.0, Authored);
cost_golden(p, "SL-200", 6.0, Projected);
cost_golden(p, "SL-300", 4.0, Projected);
cost_golden(p, "SL-400", 2.0, Authored);
let feed = cost_feed(p);
assert_eq!(feed.len(), 4, "every non-Gauge placement is fed");
for (entity, &(cost, _)) in p {
assert_eq!(feed.get(entity), Some(&cost), "{entity} fed at its cost");
}
}
#[test]
fn est_projection_p5_above_top_anchor_golden() {
let pipeline = est_pipeline(
vec![est_judgement("e1", "SL-100", "SL-200")],
&anchors_of(&[("SL-200", 5.0)]),
&EST_CFG,
);
let p = &pipeline.estimate.projection;
cost_golden(p, "SL-200", 5.0, Authored);
cost_golden(p, "SL-100", 5.0 + EST_GAUGE_STEP, Projected);
assert_eq!(
cost_feed(p).get("SL-100"),
Some(&(5.0 + EST_GAUGE_STEP)),
"the P5 head is fed"
);
}
#[test]
fn est_projection_p6_compression_below_small_anchor_golden() {
let pipeline = est_pipeline(
vec![
est_judgement("e1", "SL-100", "SL-200"),
est_judgement("e2", "SL-200", "SL-300"),
est_judgement("e3", "SL-300", "SL-400"),
],
&anchors_of(&[("SL-100", 0.5)]),
&EST_CFG,
);
let p = &pipeline.estimate.projection;
cost_golden(p, "SL-100", 0.5, Authored);
cost_golden(p, "SL-200", 0.375, Projected);
cost_golden(p, "SL-300", 0.25, Projected);
cost_golden(p, "SL-400", 0.125, Projected);
let feed = cost_feed(p);
for e in ["SL-200", "SL-300", "SL-400"] {
let cost = feed[e];
assert!(cost > 0.0 && cost < 0.5, "{e} forced into (0, 0.5): {cost}");
}
}
#[test]
fn gauge_component_present_in_projection_absent_from_feed() {
let facet_uppers: BTreeMap<String, f64> =
[("SL-100".to_string(), 10.0)].into_iter().collect();
let pipeline = est_pipeline_with_uppers(
vec![
est_judgement("e1", "SL-100", "SL-200"),
est_judgement("e2", "SL-500", "SL-600"),
],
&anchors_of(&[("SL-100", 5.0)]),
&EST_CFG,
&facet_uppers,
EST_MARGIN,
);
let p = &pipeline.estimate.projection;
cost_golden(p, "SL-500", 2.0 * EST_CENTER * 2.0 / 3.0, Gauge);
cost_golden(p, "SL-600", 2.0 * EST_CENTER / 3.0, Gauge);
let (hi, _) = p["SL-500"];
let (lo, _) = p["SL-600"];
assert!(
((hi + lo) / 2.0 - EST_CENTER).abs() < EPS,
"gauge centers on the corpus's own bare anchor"
);
let feed = cost_feed(p);
let fed: Vec<&String> = feed.keys().collect();
assert_eq!(fed, ["SL-100", "SL-200"], "only the anchored component");
assert!(!feed.contains_key("SL-500") && !feed.contains_key("SL-600"));
}
#[test]
fn est_gauge_step_sweep_order_and_provenance_invariant() {
let rows = || {
vec![
est_judgement("e1", "SL-100", "SL-200"),
est_judgement("e2", "SL-200", "SL-300"),
est_judgement("e3", "SL-300", "SL-400"),
]
};
let anchors = anchors_of(&[("SL-200", 0.5)]);
let order = ["SL-100", "SL-200", "SL-300", "SL-400"];
let mut step_milli = 5_u32;
while step_milli <= 1000 {
let cfg = ProjectionCfg {
gauge_step: f64::from(step_milli) / 1000.0,
gauge_center: EST_CENTER,
};
let p = est_pipeline(rows(), &anchors, &cfg).estimate.projection;
for pair in order.windows(2) {
let (w, _) = p[pair[0]];
let (l, _) = p[pair[1]];
assert!(
w > l,
"order {}>{} broke at step {step_milli}",
pair[0],
pair[1]
);
}
assert_eq!(p["SL-200"].1, Authored, "anchor provenance at {step_milli}");
for e in ["SL-100", "SL-300", "SL-400"] {
assert_eq!(p[e].1, Projected, "{e} provenance at {step_milli}");
}
for (e, cost) in cost_feed(&p) {
assert!(
cost > 0.0,
"fed {e} not positive at step {step_milli}: {cost}"
);
}
step_milli += 50;
}
}
fn est_anchor_row(uid: &str, item: &str, lower: f64, upper: f64) -> Judgement {
Judgement {
uid: uid.to_string(),
seq: 0,
a: item.to_string(),
b: None,
response: None,
domain: DOMAIN_ESTIMATE.to_string(),
frame: FRAME_COST_ANCHOR.to_string(),
form: RowForm::Anchor,
magnitude: None,
est_lower: Some(lower),
est_upper: Some(upper),
supersedes: None,
lens: None,
rater: RaterKind::Human,
by: None,
note: None,
date: Some("2026-07-17".to_string()),
observed_at: None,
basis: None,
admission: None,
}
}
fn as_agent(mut j: Judgement) -> Judgement {
j.rater = RaterKind::Agent;
j
}
fn with_lens(mut j: Judgement, lens: &str) -> Judgement {
j.lens = Some(lens.to_string());
j
}
fn bare_pipeline(
sessions: Vec<ComparisonSession>,
facet_uppers: &BTreeMap<String, f64>,
margin: f64,
) -> super::Pipeline {
pipeline_from_sessions(
&sessions,
&StatusMap::new(),
&AnchorMap::new(),
&CFG,
&EST_CFG,
facet_uppers,
margin,
EST_SKEW,
)
.unwrap()
}
#[test]
fn bare_anchor_dominates_all_active_estimate_uppers() {
let pipeline = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![
est_anchor_row("h1", "SL-100", 2.0, 8.0),
as_agent(est_anchor_row("a1", "SL-200", 5.0, 15.0)),
est_anchor_row("h2", "SL-300", 1.0, 3.0),
],
)],
&empty_facet_uppers(),
EST_MARGIN,
);
assert!((pipeline.bare_anchor - 16.0).abs() < EPS);
for (item, upper) in [("SL-100", 8.0), ("SL-200", 15.0), ("SL-300", 3.0)] {
assert!(
pipeline.bare_anchor >= upper + EST_MARGIN,
"bare_anchor {:.4} < {item} upper {upper:.4} + margin {EST_MARGIN:.4}",
pipeline.bare_anchor
);
}
assert!((pipeline.bare_anchor - 15.0 - EST_MARGIN).abs() < EPS);
}
#[test]
fn bare_anchor_includes_losing_tier_upper() {
let pipeline = bare_pipeline(
vec![
session(
"s1",
"2026-07-17",
vec![est_anchor_row("h1", "SL-100", 1.0, 10.0)],
),
session(
"s2",
"2026-07-17",
vec![est_anchor_row("h2", "SL-100", 2.0, 8.0)],
),
session(
"s3",
"2026-07-17",
vec![as_agent(est_anchor_row("a1", "SL-100", 15.0, 20.0))],
),
],
&empty_facet_uppers(),
EST_MARGIN,
);
assert!(
(pipeline.bare_anchor - 21.0).abs() < EPS,
"bare_anchor={}",
pipeline.bare_anchor
);
assert!(pipeline.estimate_claims.anchored.contains_key("SL-100"));
assert!(
!pipeline.estimate_claims.priors.contains_key("SL-100"),
"priors empty: losing-tier Agent rows vanish from the fold"
);
let claim = &pipeline.estimate_claims.anchored["SL-100"];
let expected_op = 1.5 + 0.65 * (9.0 - 1.5);
assert!(
(claim.operative - expected_op).abs() < 1e-12,
"operative={}",
claim.operative
);
assert!(
(pipeline.bare_anchor - claim.operative).abs() > 10.0,
"bare_anchor and claim operative should differ"
);
}
#[test]
fn bare_anchor_includes_conflict_individual_uppers() {
let pipeline = bare_pipeline(
vec![
session(
"s1",
"2026-07-17",
vec![est_anchor_row("h1", "SL-100", 5.0, 20.0)],
),
session(
"s2",
"2026-07-17",
vec![est_anchor_row("h2", "SL-100", 1.0, 2.0)],
),
],
&empty_facet_uppers(),
EST_MARGIN,
);
assert!(
(pipeline.bare_anchor - 21.0).abs() < EPS,
"bare_anchor={}",
pipeline.bare_anchor
);
assert!(
pipeline.estimate_claims.anchored["SL-100"]
.conflict
.is_some(),
"same-tier disagreement should produce conflict"
);
}
#[test]
fn bare_anchor_excludes_lensed_upper() {
let pipeline = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![
est_anchor_row("h1", "SL-100", 2.0, 8.0),
with_lens(est_anchor_row("l1", "SL-200", 10.0, 50.0), "user-value"),
],
)],
&empty_facet_uppers(),
EST_MARGIN,
);
assert!((pipeline.bare_anchor - 9.0).abs() < EPS);
let l1 = row_of(&pipeline, "l1");
assert_eq!(l1.display_token(), "inert(lens)");
}
#[test]
fn bare_anchor_excludes_superseded_and_tombstoned_uppers() {
let mut h2 = est_anchor_row("h2", "SL-100", 1.0, 10.0);
h2.supersedes = Some("h1".to_string());
let h1 = est_anchor_row("h1", "SL-100", 50.0, 100.0);
let h3 = est_anchor_row("h3", "SL-200", 20.0, 50.0);
let h4 = est_anchor_row("h4", "SL-300", 2.0, 5.0);
let s = session("s1", "2026-07-17", vec![h1, h2, h3, h4]);
let s = crate::comparison::ComparisonSession {
tombstones: vec![crate::comparison::Tombstone {
uid: "t1".to_string(),
seq: 0,
target: "h3".to_string(),
date: "2026-07-17".to_string(),
note: None,
}],
..s
};
let pipeline = bare_pipeline(vec![s], &empty_facet_uppers(), EST_MARGIN);
assert!((pipeline.bare_anchor - 11.0).abs() < EPS);
assert_eq!(row_of(&pipeline, "h1").display_token(), "superseded→h2");
assert_eq!(row_of(&pipeline, "h3").display_token(), "tombstoned");
assert_eq!(row_of(&pipeline, "h2").display_token(), "anchored");
assert_eq!(row_of(&pipeline, "h4").display_token(), "anchored");
}
#[test]
fn bare_anchor_facet_uppers_join_anchor_uppers() {
let facet_uppers: BTreeMap<String, f64> =
[("SL-100".to_string(), 30.0)].into_iter().collect();
let pipeline = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![est_anchor_row("h1", "SL-100", 2.0, 8.0)],
)],
&facet_uppers,
EST_MARGIN,
);
assert!((pipeline.bare_anchor - 31.0).abs() < EPS);
let facet_low: BTreeMap<String, f64> = [("SL-100".to_string(), 2.0)].into_iter().collect();
let pipeline2 = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![est_anchor_row("h1", "SL-100", 2.0, 50.0)],
)],
&facet_low,
EST_MARGIN,
);
assert!((pipeline2.bare_anchor - 51.0).abs() < EPS);
}
#[test]
fn bare_anchor_empty_corpus_falls_back_to_one() {
let pipeline = bare_pipeline(vec![], &empty_facet_uppers(), EST_MARGIN);
assert!((pipeline.bare_anchor - 1.0).abs() < EPS);
let pipeline2 = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![judgement("j1", "SL-100", "SL-200")],
)],
&empty_facet_uppers(),
EST_MARGIN,
);
assert!((pipeline2.bare_anchor - 1.0).abs() < EPS);
}
#[test]
fn bare_anchor_equals_absent_and_gauge_center() {
let pipeline = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![
est_anchor_row("h1", "SL-100", 2.0, 8.0),
est_anchor_row("h2", "SL-200", 3.0, 12.0),
as_agent(est_anchor_row("a1", "SL-300", 10.0, 25.0)),
est_anchor_row("h3", "SL-400", 1.0, 5.0),
est_judgement("e1", "SL-500", "SL-600"),
],
)],
&empty_facet_uppers(),
EST_MARGIN,
);
let expected = 26.0;
assert!((pipeline.bare_anchor - expected).abs() < EPS);
let (hi_got, hi_prov) = pipeline.estimate.projection["SL-500"];
let (lo_got, lo_prov) = pipeline.estimate.projection["SL-600"];
assert_eq!(hi_prov, Gauge);
assert_eq!(lo_prov, Gauge);
let midpoint = (hi_got + lo_got) / 2.0;
assert!(
(midpoint - expected).abs() < EPS,
"gauge midpoint {midpoint:.4} ≠ bare_anchor {expected:.4}"
);
}
#[test]
fn bare_anchor_facet_only_falls_back_to_facet_upper() {
let facet_uppers: BTreeMap<String, f64> =
[("ISS-001".to_string(), 12.0), ("ISS-002".to_string(), 7.0)]
.into_iter()
.collect();
let pipeline = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![judgement("j1", "SL-100", "SL-200")],
)],
&facet_uppers,
EST_MARGIN,
);
assert!((pipeline.bare_anchor - 13.0).abs() < EPS);
let pipeline2 = bare_pipeline(
vec![session(
"s1",
"2026-07-17",
vec![judgement("j1", "SL-100", "SL-200")],
)],
&empty_facet_uppers(),
EST_MARGIN,
);
assert!((pipeline2.bare_anchor - 1.0).abs() < EPS);
}
fn anchor_row(uid: &str, item: &str, magnitude: f64) -> Judgement {
let mut j = judgement(uid, item, "unused");
j.b = None;
j.response = None;
j.form = RowForm::Anchor;
j.frame = FRAME_VALUE_ANCHOR.to_string();
j.magnitude = Some(magnitude);
j
}
fn row_of<'a>(pipeline: &'a super::Pipeline, uid: &str) -> &'a super::RowSummary {
pipeline
.rows
.iter()
.find(|r| r.uid == uid)
.unwrap_or_else(|| panic!("row {uid} present"))
}
#[test]
fn anchor_rows_terminate_at_claims_and_never_reach_compile() {
let mut agent_anchor = anchor_row("a1", "SL-300", 2.0);
agent_anchor.rater = RaterKind::Agent;
let with_anchors = pipeline_of(
vec![
judgement("j1", "SL-100", "SL-200"),
anchor_row("h1", "SL-100", 6.0),
agent_anchor,
],
&AnchorMap::new(),
);
assert_eq!(with_anchors.value_claims.anchored["SL-100"].operative, 6.0);
assert_eq!(with_anchors.value_claims.priors["SL-300"].operative, 2.0);
let pairwise: Vec<&str> = with_anchors
.active_pairwise()
.iter()
.map(|j| j.uid.as_str())
.collect();
assert_eq!(pairwise, ["j1"]);
let claim_anchors = with_anchors.value_claims.anchor_map();
assert_eq!(
claim_anchors.get("SL-100"),
Some(&6.0),
"the human claim anchors the compile (the flip, D12)"
);
assert!(
!claim_anchors.contains_key("SL-300"),
"the agent claim never enters the anchor map (D4)"
);
assert_eq!(with_anchors.value.anchors, claim_anchors);
let pairwise_refs: Vec<&Judgement> = with_anchors.active_pairwise().iter().collect();
let direct =
super::compile::compile(&pairwise_refs, &claim_anchors, QuarantinePolicy::Symmetric);
assert_eq!(with_anchors.value.constraint_set, direct);
let h1 = row_of(&with_anchors, "h1");
assert_eq!(h1.b, None);
assert_eq!(h1.response, None);
assert_eq!(h1.state.compilation, None);
assert_eq!(h1.claim, Some("anchored"));
assert_eq!(h1.display_token(), "anchored");
let a1 = row_of(&with_anchors, "a1");
assert_eq!(a1.state.compilation, None);
assert_eq!(a1.display_token(), "prior");
assert_eq!(row_of(&with_anchors, "j1").display_token(), "active");
}
#[test]
fn anchor_row_tokens_cover_conflict_lens_and_supersession() {
let mut lensed = anchor_row("l1", "SL-100", 9.0);
lensed.lens = Some("user-value".to_string());
let s1 = session(
"s1",
"2026-07-11",
vec![
anchor_row("p", "SL-100", 4.0),
anchor_row("q", "SL-100", 8.0), lensed,
],
);
let s2 = session("s2", "2026-07-11", vec![anchor_row("r", "SL-100", 2.0)]);
let pipeline = pipeline_from_sessions(
&[s1, s2],
&StatusMap::new(),
&AnchorMap::new(),
&CFG,
&EST_CFG,
&empty_facet_uppers(),
EST_MARGIN,
EST_SKEW,
)
.unwrap();
let claim = &pipeline.value_claims.anchored["SL-100"];
assert_eq!(claim.operative, 5.0);
assert!(claim.conflict.is_some());
assert_eq!(row_of(&pipeline, "q").display_token(), "conflicted");
assert_eq!(row_of(&pipeline, "r").display_token(), "conflicted");
assert_eq!(row_of(&pipeline, "p").display_token(), "superseded→q");
assert_eq!(row_of(&pipeline, "l1").claim, None);
assert_eq!(row_of(&pipeline, "l1").display_token(), "inert(lens)");
let key = ("user-value".to_string(), "SL-100".to_string());
assert_eq!(pipeline.value_claims.lensed[&key].operative, 9.0);
}
#[test]
fn corpus_without_anchor_rows_scores_identically_with_empty_claims() {
let rows = vec![
judgement("j1", "SL-100", "SL-200"),
judgement("j2", "SL-200", "SL-300"),
];
let pipeline = pipeline_of(rows, &AnchorMap::new());
assert_eq!(pipeline.value_claims, ClaimResolution::default());
assert!(pipeline.value.anchors.is_empty(), "no claims ⇒ no anchors");
let rows = [
judgement("j1", "SL-100", "SL-200"),
judgement("j2", "SL-200", "SL-300"),
];
let refs: Vec<&Judgement> = rows.iter().collect();
let direct = super::compile::compile(&refs, &AnchorMap::new(), QuarantinePolicy::Symmetric);
assert_eq!(pipeline.value.constraint_set, direct);
assert_eq!(
pipeline.value.projection,
super::project::project(&direct, &CFG)
);
}
#[test]
fn cost_feed_fed_costs_positive_property() {
const PAIRS: [(&str, &str); 6] = [
("SL-100", "SL-200"),
("SL-100", "SL-300"),
("SL-100", "SL-400"),
("SL-200", "SL-300"),
("SL-200", "SL-400"),
("SL-300", "SL-400"),
];
let configs: [Vec<(&str, f64)>; 3] = [
vec![],
vec![("SL-100", 0.5), ("SL-400", 0.01)],
vec![("SL-100", 8.0), ("SL-200", 3.0), ("SL-400", 1.0)],
];
for cfg in configs {
let anchors = anchors_of(&cfg);
for mask in 0..4_u32.pow(6) {
let mut rows = Vec::new();
for (i, (a, b)) in PAIRS.iter().enumerate() {
let uid = format!("j{i}");
match (mask / 4_u32.pow(u32::try_from(i).unwrap())) % 4 {
1 => rows.push(est_judgement(&uid, a, b)),
2 => rows.push(est_judgement(&uid, b, a)),
3 => {
let mut eq = est_judgement(&uid, a, b);
eq.response = Some(Response::Equal);
rows.push(eq);
}
_ => {}
}
}
if rows.is_empty() {
continue;
}
let p = est_pipeline(rows, &anchors, &EST_CFG).estimate.projection;
for (e, cost) in cost_feed(&p) {
assert!(
cost > 0.0 && !cost.is_nan(),
"fed {e} violates positivity under mask {mask}: {cost}"
);
}
}
}
}
}