#![allow(missing_docs)]
use mempill_types::{Belief, Cardinality, Claim};
use crate::config::EngineConfig;
use crate::engine::gate::{ConflictType, Proposal};
use crate::engine::valid_time_helpers;
#[derive(Debug)]
pub(crate) struct ReconcilerInput<'a> {
pub candidate: &'a Claim,
pub incumbent: Option<&'a Belief>,
pub superseded_claim_refs: &'a [mempill_types::ClaimRef],
pub measured_confidence: f32,
pub cardinality_proposal: Cardinality,
pub oracle_present: bool,
pub succession_threshold: f32,
pub n_gt_1_live_incumbents: bool,
}
pub(crate) fn reconcile(input: ReconcilerInput<'_>, _config: &EngineConfig) -> Proposal {
let conflict_type = classify_conflict(&input);
Proposal {
candidate: input.candidate.clone(),
incumbent: input.incumbent.cloned(),
conflict_type,
measured_confidence: input.measured_confidence,
cardinality_proposal: input.cardinality_proposal,
oracle_present: input.oracle_present,
}
}
fn classify_conflict(input: &ReconcilerInput<'_>) -> ConflictType {
let incumbent = match input.incumbent {
Some(b) => b,
None => return ConflictType::NoConflict,
};
let lineage = input.candidate.derived_from();
for ancestor_ref in lineage {
if input.superseded_claim_refs.contains(ancestor_ref) {
return ConflictType::DependsOnSuperseded;
}
}
let cand_subject = &input.candidate.fact().subject;
let cand_predicate = &input.candidate.fact().predicate;
let cand_value = &input.candidate.fact().value;
let incumb_subject = &incumbent.fact.subject;
let incumb_predicate = &incumbent.fact.predicate;
let incumb_value = &incumbent.fact.value;
if cand_subject == incumb_subject && cand_predicate == incumb_predicate {
if cand_value == incumb_value {
ConflictType::NoConflict
} else {
let threshold = input.succession_threshold;
let cand_vt = input.candidate.valid_time();
let incumb_vt = &incumbent.valid_time;
let is_succession = !input.n_gt_1_live_incumbents
&& valid_time_helpers::valid_time_is_trusted(cand_vt, threshold)
&& valid_time_helpers::valid_time_is_trusted(incumb_vt, threshold)
&& valid_time_helpers::valid_times_non_overlapping(cand_vt, incumb_vt);
if is_succession {
ConflictType::Succession
} else {
ConflictType::SameLineConflict
}
}
} else if cand_subject == incumb_subject && cand_predicate != incumb_predicate {
ConflictType::CrossLineConflict
} else {
ConflictType::NoConflict
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::gate::ConflictType;
use mempill_types::{
AgentId, Cardinality, Claim, ClaimRef, Confidence, Criticality, CurrencySignal,
CurrencyState, ExternalAnchor, ExternalKind, Fact, ProvenanceLabel, TransactionTime,
ValidTime,
};
use chrono::{TimeZone, Utc};
fn tx() -> TransactionTime {
TransactionTime(Utc.with_ymd_and_hms(2026, 6, 22, 0, 0, 0).unwrap())
}
fn tx_past() -> TransactionTime {
TransactionTime(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap())
}
fn no_vt() -> ValidTime {
ValidTime { start: None, end: None, valid_time_confidence: 0.0 }
}
fn make_claim(
subject: &str,
predicate: &str,
value: serde_json::Value,
derived_from: Vec<ClaimRef>,
) -> Claim {
Claim::new(
ClaimRef::new_random(),
AgentId("agent-rc".into()),
Fact {
subject: subject.into(),
predicate: predicate.into(),
value,
},
Cardinality::Functional,
ProvenanceLabel::External(ExternalKind::ExternalFirstHand),
ExternalAnchor { nearest_external_anchor: None, derivation_depth: 0 },
tx(),
no_vt(),
Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
Criticality::Medium,
derived_from,
None,
None,
)
}
fn make_belief(subject: &str, predicate: &str, value: serde_json::Value) -> Belief {
Belief {
claim_ref: ClaimRef::new_random(),
fact: Fact {
subject: subject.into(),
predicate: predicate.into(),
value,
},
provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
valid_time: no_vt(),
transaction_time: tx_past(),
confidence: Confidence { value_confidence: 0.8, valid_time_confidence: 0.0 },
currency_signal: CurrencySignal {
last_refreshed_at: tx_past(),
state: CurrencyState::Fresh,
corroboration_count: 0,
},
criticality: Criticality::Medium,
}
}
fn cfg() -> EngineConfig {
EngineConfig::default()
}
fn input<'a>(
candidate: &'a Claim,
incumbent: Option<&'a Belief>,
superseded: &'a [ClaimRef],
oracle: bool,
) -> ReconcilerInput<'a> {
ReconcilerInput {
candidate,
incumbent,
superseded_claim_refs: superseded,
measured_confidence: 0.85,
cardinality_proposal: Cardinality::Functional,
oracle_present: oracle,
succession_threshold: 0.7,
n_gt_1_live_incumbents: false,
}
}
#[test]
fn same_line_different_value_is_same_line_conflict() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let inp = input(&candidate, Some(&incumbent), &[], false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::SameLineConflict,
"same (subject, predicate) with different values must be SameLineConflict");
}
#[test]
fn same_line_conflict_proposal_carries_candidate_and_incumbent() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let inp = input(&candidate, Some(&incumbent), &[], false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.candidate.fact().value, serde_json::json!("Paris"));
assert!(proposal.incumbent.is_some());
assert_eq!(
proposal.incumbent.as_ref().unwrap().fact.value,
serde_json::json!("Berlin")
);
}
#[test]
fn cross_line_different_predicate_same_subject_is_cross_line_conflict() {
let candidate = make_claim("user", "allergies", serde_json::json!("none"), vec![]);
let incumbent = make_belief("user", "medications", serde_json::json!("penicillin"));
let inp = input(&candidate, Some(&incumbent), &[], false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::CrossLineConflict,
"same subject, different predicate → CrossLineConflict (v0.1 structural detection)");
}
#[test]
fn cross_line_conflict_builds_correct_proposal_for_gate() {
let candidate = make_claim("user", "country", serde_json::json!("France"), vec![]);
let incumbent = make_belief("user", "location", serde_json::json!("Berlin"));
let inp = input(&candidate, Some(&incumbent), &[], true);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::CrossLineConflict);
assert!(proposal.oracle_present, "oracle_present must be threaded into the proposal");
}
#[test]
fn same_line_same_value_is_no_conflict() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let incumbent = make_belief("user", "city", serde_json::json!("Paris"));
let inp = input(&candidate, Some(&incumbent), &[], false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::NoConflict,
"identical value re-statement must be NoConflict (idempotent write)");
}
#[test]
fn same_line_same_value_different_type_is_same_line_conflict() {
let candidate = make_claim("user", "age", serde_json::json!("30"), vec![]);
let incumbent = make_belief("user", "age", serde_json::json!(30));
let inp = input(&candidate, Some(&incumbent), &[], false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::SameLineConflict,
"JSON type mismatch (string vs number) must be treated as different values → SameLineConflict");
}
#[test]
fn derived_from_superseded_claim_is_depends_on_superseded() {
let superseded_ref = ClaimRef::new_random();
let candidate = make_claim(
"user", "city", serde_json::json!("Paris"),
vec![superseded_ref.clone()],
);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let superseded = vec![superseded_ref];
let inp = input(&candidate, Some(&incumbent), &superseded, false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::DependsOnSuperseded,
"candidate derived_from a superseded claim must classify as DependsOnSuperseded (V3-8)");
}
#[test]
fn derived_from_non_superseded_claim_does_not_trigger_depends_on_superseded() {
let ancestor_ref = ClaimRef::new_random();
let candidate = make_claim(
"user", "city", serde_json::json!("Paris"),
vec![ancestor_ref.clone()],
);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let superseded: Vec<ClaimRef> = vec![];
let inp = input(&candidate, Some(&incumbent), &superseded, false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::SameLineConflict);
}
#[test]
fn depends_on_superseded_fires_before_same_line_check() {
let superseded_ref = ClaimRef::new_random();
let candidate = make_claim(
"user", "city", serde_json::json!("Paris"),
vec![superseded_ref.clone()],
);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let superseded = vec![superseded_ref];
let inp = input(&candidate, Some(&incumbent), &superseded, false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::DependsOnSuperseded,
"DependsOnSuperseded check (step 2) fires before same-line check (step 3/4)");
}
#[test]
fn no_incumbent_is_no_conflict() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let inp = input(&candidate, None, &[], false);
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.conflict_type, ConflictType::NoConflict,
"no incumbent = first write on subject-line = NoConflict");
assert!(proposal.incumbent.is_none());
}
#[test]
fn proposal_carries_measured_confidence() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let inp = ReconcilerInput {
candidate: &candidate,
incumbent: None,
superseded_claim_refs: &[],
measured_confidence: 0.73,
cardinality_proposal: Cardinality::Functional,
oracle_present: false,
succession_threshold: 0.7,
n_gt_1_live_incumbents: false,
};
let proposal = reconcile(inp, &cfg());
assert!((proposal.measured_confidence - 0.73).abs() < f32::EPSILON,
"measured_confidence must be threaded into the Proposal unchanged");
}
#[test]
fn proposal_carries_oracle_present_flag() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let inp = input(&candidate, None, &[], true);
let proposal = reconcile(inp, &cfg());
assert!(proposal.oracle_present, "oracle_present=true must be in the Proposal (A24)");
}
#[test]
fn proposal_carries_cardinality_proposal() {
let candidate = make_claim("user", "tags", serde_json::json!(["rust"]), vec![]);
let inp = ReconcilerInput {
candidate: &candidate,
incumbent: None,
superseded_claim_refs: &[],
measured_confidence: 0.8,
cardinality_proposal: Cardinality::SetValued,
oracle_present: false,
succession_threshold: 0.7,
n_gt_1_live_incumbents: false,
};
let proposal = reconcile(inp, &cfg());
assert_eq!(proposal.cardinality_proposal, Cardinality::SetValued);
}
#[test]
fn reconcile_is_deterministic_same_line_conflict() {
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let cfg = cfg();
let p1 = reconcile(input(&candidate, Some(&incumbent), &[], false), &cfg);
let p2 = reconcile(input(&candidate, Some(&incumbent), &[], false), &cfg);
assert_eq!(p1.conflict_type, p2.conflict_type,
"reconcile() must be deterministic for SameLineConflict");
assert_eq!(
format!("{:?}", p1.conflict_type),
format!("{:?}", p2.conflict_type)
);
}
#[test]
fn reconcile_is_deterministic_across_all_conflict_types() {
let cfg = cfg();
let c1 = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let p1a = reconcile(input(&c1, None, &[], false), &cfg);
let p1b = reconcile(input(&c1, None, &[], false), &cfg);
assert_eq!(p1a.conflict_type, p1b.conflict_type);
let c2 = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let inc = make_belief("user", "city", serde_json::json!("Berlin"));
let p2a = reconcile(input(&c2, Some(&inc), &[], false), &cfg);
let p2b = reconcile(input(&c2, Some(&inc), &[], false), &cfg);
assert_eq!(p2a.conflict_type, p2b.conflict_type);
let c3 = make_claim("user", "country", serde_json::json!("France"), vec![]);
let inc3 = make_belief("user", "city", serde_json::json!("Berlin"));
let p3a = reconcile(input(&c3, Some(&inc3), &[], false), &cfg);
let p3b = reconcile(input(&c3, Some(&inc3), &[], false), &cfg);
assert_eq!(p3a.conflict_type, p3b.conflict_type);
let sup_ref = ClaimRef::new_random();
let c4 = make_claim("user", "city", serde_json::json!("Paris"), vec![sup_ref.clone()]);
let inc4 = make_belief("user", "city", serde_json::json!("Berlin"));
let sup = vec![sup_ref];
let p4a = reconcile(input(&c4, Some(&inc4), &sup, false), &cfg);
let p4b = reconcile(input(&c4, Some(&inc4), &sup, false), &cfg);
assert_eq!(p4a.conflict_type, p4b.conflict_type);
}
#[test]
fn reconciler_proposal_is_consumable_by_gate() {
use crate::engine::gate::{adjudicate, Route};
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let incumbent = make_belief("user", "city", serde_json::json!("Berlin"));
let inp = input(&candidate, Some(&incumbent), &[], false);
let proposal = reconcile(inp, &cfg());
let decision = adjudicate(&proposal, &cfg());
assert_eq!(decision.route, Route::HeavyPath);
}
#[test]
fn no_conflict_proposal_routes_cheap_path_in_gate() {
use crate::engine::gate::{adjudicate, Route};
let candidate = make_claim("user", "city", serde_json::json!("Paris"), vec![]);
let inp = input(&candidate, None, &[], false);
let proposal = reconcile(inp, &cfg());
let decision = adjudicate(&proposal, &cfg());
assert_eq!(decision.route, Route::CheapPath);
}
}