use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::detect::{self, DetectionOutput};
use crate::event::{Event, EvidenceEvent};
use crate::model::{
Claim, Contradiction, HypothesisStanding, RemediationEvent, RemediationMove,
RequirementCandidate, RequirementMeta,
};
use crate::readiness::ReadinessReport;
use crate::session::CoverageSchema;
use crate::signal::Signal;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AcceptStance {
pub remediation_id: String,
pub target: String,
pub rationale: String,
pub nonconforming_claims: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Registry {
pub entity_ids: Vec<String>,
pub predicate_tokens: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InterviewState {
pub session_id: String,
pub coverage_schema: CoverageSchema,
pub claims: Vec<ClaimWithStanding>,
pub contradictions: Vec<Contradiction>,
pub requirements: Vec<RequirementCandidate>,
pub accepted_stances: Vec<AcceptStance>,
pub registry: Registry,
pub signals: Vec<Signal>,
pub readiness: ReadinessReport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaimWithStanding {
#[serde(flatten)]
pub claim: Claim,
pub standing: HypothesisStanding,
pub evidence: Vec<String>,
}
#[derive(Debug, Default)]
pub(crate) struct RawFold {
pub session_id: String,
pub coverage_schema: CoverageSchema,
pub claims: Vec<Claim>,
pub requirement_meta: BTreeMap<String, RequirementMeta>,
pub evidence: BTreeMap<String, Vec<EvidenceEvent>>,
pub remediations: Vec<RemediationEvent>,
}
impl RawFold {
fn claim(&self, id: &str) -> Option<&Claim> {
self.claims.iter().find(|c| c.id == id)
}
}
pub(crate) fn raw_fold(events: &[Event]) -> RawFold {
let mut fold = RawFold::default();
for event in events {
match event {
Event::SessionOpened {
session_id,
coverage_schema,
} => {
fold.session_id = session_id.clone();
fold.coverage_schema = coverage_schema.clone();
}
Event::ClaimAdded { claim, requirement } => {
fold.claims.push((**claim).clone());
if let Some(meta) = requirement {
fold.requirement_meta.insert(claim.id.clone(), meta.clone());
}
}
Event::EvidenceAdded { evidence } => {
fold.evidence
.entry(evidence.claim_id.clone())
.or_default()
.push(evidence.clone());
}
Event::Remediated { remediation } => {
apply_qualify(&mut fold, remediation);
fold.remediations.push(remediation.clone());
}
}
}
fold
}
fn apply_qualify(fold: &mut RawFold, remediation: &RemediationEvent) {
let RemediationMove::Qualify {
claim,
condition,
time_scope,
} = &remediation.r#move
else {
return;
};
let Some(c) = fold.claims.iter_mut().find(|c| &c.id == claim) else {
return;
};
if let Some(cond) = condition {
c.condition = Some(cond.clone());
}
if let Some(ts) = time_scope {
c.time_scope = Some(ts.clone());
}
}
fn build_registry(fold: &RawFold) -> Registry {
let mut entities: BTreeSet<String> = BTreeSet::new();
let mut predicates: BTreeSet<String> = BTreeSet::new();
for c in &fold.claims {
entities.insert(c.subject.id.clone());
if let crate::model::Object::Entity(e) = &c.object {
entities.insert(e.id.clone());
}
predicates.insert(c.predicate.clone());
}
Registry {
entity_ids: entities.into_iter().collect(),
predicate_tokens: predicates.into_iter().collect(),
}
}
fn disproven_by_remediation(fold: &RawFold) -> BTreeSet<String> {
let mut disproven = BTreeSet::new();
for r in &fold.remediations {
match &r.r#move {
RemediationMove::Retract { claim } => {
disproven.insert(claim.clone());
}
RemediationMove::Supersede { old, .. } => {
disproven.insert(old.clone());
}
_ => {}
}
}
disproven
}
fn compute_standings(
fold: &RawFold,
detection: &DetectionOutput,
disproven: &BTreeSet<String>,
) -> BTreeMap<String, HypothesisStanding> {
let accept_targets: BTreeSet<&str> = detection
.accepted_stances
.iter()
.map(|s| s.target.as_str())
.collect();
let mut contradicted: BTreeSet<String> = BTreeSet::new();
for c in &detection.contradictions {
for id in &c.claims {
if accept_targets.contains(id.as_str()) {
continue;
}
contradicted.insert(id.clone());
}
}
for id in &detection.nonconforming_claims {
contradicted.insert(id.clone());
}
let mut standings = BTreeMap::new();
for claim in &fold.claims {
let standing = compute_one(fold, claim, disproven, &contradicted);
standings.insert(claim.id.clone(), standing);
}
standings
}
fn compute_one(
fold: &RawFold,
claim: &Claim,
disproven: &BTreeSet<String>,
contradicted: &BTreeSet<String>,
) -> HypothesisStanding {
if disproven.contains(&claim.id) {
return HypothesisStanding::Disproven;
}
let evidence = fold
.evidence
.get(&claim.id)
.map(Vec::as_slice)
.unwrap_or(&[]);
let has_user_refutation = evidence
.iter()
.any(|e| e.source_type == crate::model::EvidenceSourceType::UserRefutation);
if has_user_refutation && !user_reasserted_after_refutation(evidence) {
return HypothesisStanding::Disproven;
}
let has_support = evidence.iter().any(|e| e.source_type.is_support());
let app_refutes = detect::app_research_refutes(evidence);
if contradicted.contains(&claim.id) {
return HypothesisStanding::Challenged;
}
if app_refutes && !detect::app_refutation_suppressed_by_user(claim, evidence) {
return HypothesisStanding::Challenged;
}
if has_support {
return HypothesisStanding::Withstood;
}
HypothesisStanding::Open
}
fn user_reasserted_after_refutation(evidence: &[EvidenceEvent]) -> bool {
let last_user = evidence
.iter()
.rev()
.find(|e| e.source_type.is_user_sourced());
matches!(
last_user.map(|e| e.source_type),
Some(crate::model::EvidenceSourceType::UserAssent)
)
}
fn derive_requirements(
fold: &RawFold,
standings: &BTreeMap<String, HypothesisStanding>,
) -> Vec<RequirementCandidate> {
use crate::model::{Category, Object, Priority, RequirementQualityCheck};
let mut out = Vec::new();
for claim in &fold.claims {
if claim.category != Category::Requirement {
continue;
}
if standings.get(&claim.id) == Some(&HypothesisStanding::Disproven) {
continue;
}
let meta = fold
.requirement_meta
.get(&claim.id)
.cloned()
.unwrap_or_default();
let capability = meta.capability.unwrap_or_else(|| claim.predicate.clone());
let has_explicit_statement = meta.statement.is_some();
let statement = meta.statement.unwrap_or_else(|| {
let obj = match &claim.object {
Object::Entity(e) => e.id.clone(),
Object::Value { value } => value.clone(),
};
format!("{} {} {}", claim.subject.id, claim.predicate, obj)
});
let priority = meta.priority;
let owner = meta.owner;
let mut quality_checks = Vec::new();
let has_acceptance = !meta.acceptance_criteria.is_empty();
quality_checks.push(RequirementQualityCheck {
name: "has_acceptance".into(),
passed: has_acceptance,
detail: None,
});
quality_checks.push(RequirementQualityCheck {
name: "has_owner".into(),
passed: owner.is_some(),
detail: None,
});
quality_checks.push(RequirementQualityCheck {
name: "testable".into(),
passed: has_acceptance,
detail: None,
});
let object_is_concrete = match &claim.object {
Object::Entity(e) => !e.id.trim().is_empty(),
Object::Value { value } => !value.trim().is_empty(),
};
quality_checks.push(RequirementQualityCheck {
name: "unambiguous".into(),
passed: has_explicit_statement || object_is_concrete,
detail: None,
});
out.push(RequirementCandidate {
id: format!("req:{}", claim.id),
statement,
subject: claim.subject.clone(),
capability,
condition: claim.condition.clone(),
acceptance_criteria: meta.acceptance_criteria,
priority: priority.or({
None::<Priority>
}),
owner,
rationale: meta.rationale,
source_claims: vec![claim.id.clone()],
quality_checks,
});
}
out
}
pub fn project(events: &[Event]) -> InterviewState {
let fold = raw_fold(events);
let disproven = disproven_by_remediation(&fold);
let detection = detect::detect(&fold, &disproven);
let standings = compute_standings(&fold, &detection, &disproven);
let requirements = derive_requirements(&fold, &standings);
let registry = build_registry(&fold);
let claims: Vec<ClaimWithStanding> = fold
.claims
.iter()
.map(|c| ClaimWithStanding {
claim: c.clone(),
standing: standings
.get(&c.id)
.copied()
.unwrap_or(HypothesisStanding::Open),
evidence: fold
.evidence
.get(&c.id)
.map(|evs| evs.iter().map(|e| e.id.clone()).collect())
.unwrap_or_default(),
})
.collect();
let readiness = crate::readiness::assess(
&fold.coverage_schema,
&claims,
&detection.contradictions,
&detection.accepted_stances,
&requirements,
);
let signals = crate::signal::compute_signals(&claims, &detection, &readiness);
InterviewState {
session_id: fold.session_id.clone(),
coverage_schema: fold.coverage_schema.clone(),
claims,
contradictions: detection.contradictions,
requirements,
accepted_stances: detection.accepted_stances,
registry,
signals,
readiness,
}
}
pub(crate) use RawFold as InternalRawFold;
impl RawFold {
pub(crate) fn claim_by_id(&self, id: &str) -> Option<&Claim> {
self.claim(id)
}
}