1use std::collections::{BTreeMap, BTreeSet};
20
21use serde::{Deserialize, Serialize};
22
23use crate::detect::{self, DetectionOutput};
24use crate::event::{Event, EvidenceEvent};
25use crate::model::{
26 Claim, Contradiction, HypothesisStanding, RemediationEvent, RemediationMove,
27 RequirementCandidate, RequirementMeta,
28};
29use crate::readiness::ReadinessReport;
30use crate::session::CoverageSchema;
31use crate::signal::Signal;
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct AcceptStance {
37 pub remediation_id: String,
38 pub target: String,
39 pub rationale: String,
40 pub nonconforming_claims: Vec<String>,
42}
43
44#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
47pub struct Registry {
48 pub entity_ids: Vec<String>,
49 pub predicate_tokens: Vec<String>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct InterviewState {
55 pub session_id: String,
56 pub coverage_schema: CoverageSchema,
57 pub claims: Vec<ClaimWithStanding>,
59 pub contradictions: Vec<Contradiction>,
60 pub requirements: Vec<RequirementCandidate>,
61 pub accepted_stances: Vec<AcceptStance>,
62 pub registry: Registry,
63 pub signals: Vec<Signal>,
64 pub readiness: ReadinessReport,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ClaimWithStanding {
70 #[serde(flatten)]
71 pub claim: Claim,
72 pub standing: HypothesisStanding,
73 pub evidence: Vec<String>,
75}
76
77#[derive(Debug, Default)]
79pub(crate) struct RawFold {
80 pub session_id: String,
81 pub coverage_schema: CoverageSchema,
82 pub claims: Vec<Claim>,
84 pub requirement_meta: BTreeMap<String, RequirementMeta>,
86 pub evidence: BTreeMap<String, Vec<EvidenceEvent>>,
88 pub remediations: Vec<RemediationEvent>,
90}
91
92impl RawFold {
93 fn claim(&self, id: &str) -> Option<&Claim> {
94 self.claims.iter().find(|c| c.id == id)
95 }
96}
97
98pub(crate) fn raw_fold(events: &[Event]) -> RawFold {
101 let mut fold = RawFold::default();
102 for event in events {
103 match event {
104 Event::SessionOpened {
105 session_id,
106 coverage_schema,
107 } => {
108 fold.session_id = session_id.clone();
109 fold.coverage_schema = coverage_schema.clone();
110 }
111 Event::ClaimAdded { claim, requirement } => {
112 fold.claims.push((**claim).clone());
113 if let Some(meta) = requirement {
114 fold.requirement_meta.insert(claim.id.clone(), meta.clone());
115 }
116 }
117 Event::EvidenceAdded { evidence } => {
118 fold.evidence
119 .entry(evidence.claim_id.clone())
120 .or_default()
121 .push(evidence.clone());
122 }
123 Event::Remediated { remediation } => {
124 apply_qualify(&mut fold, remediation);
125 fold.remediations.push(remediation.clone());
126 }
127 }
128 }
129 fold
130}
131
132fn apply_qualify(fold: &mut RawFold, remediation: &RemediationEvent) {
134 let RemediationMove::Qualify {
135 claim,
136 condition,
137 time_scope,
138 } = &remediation.r#move
139 else {
140 return;
141 };
142 let Some(c) = fold.claims.iter_mut().find(|c| &c.id == claim) else {
143 return;
144 };
145 if let Some(cond) = condition {
146 c.condition = Some(cond.clone());
147 }
148 if let Some(ts) = time_scope {
149 c.time_scope = Some(ts.clone());
150 }
151}
152
153fn build_registry(fold: &RawFold) -> Registry {
156 let mut entities: BTreeSet<String> = BTreeSet::new();
157 let mut predicates: BTreeSet<String> = BTreeSet::new();
158 for c in &fold.claims {
159 entities.insert(c.subject.id.clone());
160 if let crate::model::Object::Entity(e) = &c.object {
161 entities.insert(e.id.clone());
162 }
163 predicates.insert(c.predicate.clone());
164 }
165 Registry {
166 entity_ids: entities.into_iter().collect(),
167 predicate_tokens: predicates.into_iter().collect(),
168 }
169}
170
171fn disproven_by_remediation(fold: &RawFold) -> BTreeSet<String> {
174 let mut disproven = BTreeSet::new();
175 for r in &fold.remediations {
176 match &r.r#move {
177 RemediationMove::Retract { claim } => {
178 disproven.insert(claim.clone());
179 }
180 RemediationMove::Supersede { old, .. } => {
181 disproven.insert(old.clone());
182 }
183 _ => {}
184 }
185 }
186 disproven
187}
188
189fn compute_standings(
196 fold: &RawFold,
197 detection: &DetectionOutput,
198 disproven: &BTreeSet<String>,
199) -> BTreeMap<String, HypothesisStanding> {
200 let accept_targets: BTreeSet<&str> = detection
205 .accepted_stances
206 .iter()
207 .map(|s| s.target.as_str())
208 .collect();
209
210 let mut contradicted: BTreeSet<String> = BTreeSet::new();
213 for c in &detection.contradictions {
214 for id in &c.claims {
215 if accept_targets.contains(id.as_str()) {
216 continue;
217 }
218 contradicted.insert(id.clone());
219 }
220 }
221 for id in &detection.nonconforming_claims {
223 contradicted.insert(id.clone());
224 }
225
226 let mut standings = BTreeMap::new();
227 for claim in &fold.claims {
228 let standing = compute_one(fold, claim, disproven, &contradicted);
229 standings.insert(claim.id.clone(), standing);
230 }
231 standings
232}
233
234fn compute_one(
235 fold: &RawFold,
236 claim: &Claim,
237 disproven: &BTreeSet<String>,
238 contradicted: &BTreeSet<String>,
239) -> HypothesisStanding {
240 if disproven.contains(&claim.id) {
241 return HypothesisStanding::Disproven;
242 }
243
244 let evidence = fold
245 .evidence
246 .get(&claim.id)
247 .map(Vec::as_slice)
248 .unwrap_or(&[]);
249 let has_user_refutation = evidence
250 .iter()
251 .any(|e| e.source_type == crate::model::EvidenceSourceType::UserRefutation);
252
253 if has_user_refutation && !user_reasserted_after_refutation(evidence) {
257 return HypothesisStanding::Disproven;
258 }
259
260 let has_support = evidence.iter().any(|e| e.source_type.is_support());
261 let app_refutes = detect::app_research_refutes(evidence);
265
266 if contradicted.contains(&claim.id) {
269 return HypothesisStanding::Challenged;
270 }
271
272 if app_refutes && !detect::app_refutation_suppressed_by_user(claim, evidence) {
277 return HypothesisStanding::Challenged;
278 }
279
280 if has_support {
281 return HypothesisStanding::Withstood;
282 }
283
284 HypothesisStanding::Open
285}
286
287fn user_reasserted_after_refutation(evidence: &[EvidenceEvent]) -> bool {
290 let last_user = evidence
291 .iter()
292 .rev()
293 .find(|e| e.source_type.is_user_sourced());
294 matches!(
295 last_user.map(|e| e.source_type),
296 Some(crate::model::EvidenceSourceType::UserAssent)
297 )
298}
299
300fn derive_requirements(
303 fold: &RawFold,
304 standings: &BTreeMap<String, HypothesisStanding>,
305) -> Vec<RequirementCandidate> {
306 use crate::model::{Category, Object, Priority, RequirementQualityCheck};
307
308 let mut out = Vec::new();
309 for claim in &fold.claims {
310 if claim.category != Category::Requirement {
311 continue;
312 }
313 if standings.get(&claim.id) == Some(&HypothesisStanding::Disproven) {
315 continue;
316 }
317 let meta = fold
318 .requirement_meta
319 .get(&claim.id)
320 .cloned()
321 .unwrap_or_default();
322 let capability = meta.capability.unwrap_or_else(|| claim.predicate.clone());
323 let has_explicit_statement = meta.statement.is_some();
324 let statement = meta.statement.unwrap_or_else(|| {
325 let obj = match &claim.object {
326 Object::Entity(e) => e.id.clone(),
327 Object::Value { value } => value.clone(),
328 };
329 format!("{} {} {}", claim.subject.id, claim.predicate, obj)
330 });
331 let priority = meta.priority;
332 let owner = meta.owner;
333
334 let mut quality_checks = Vec::new();
335 let has_acceptance = !meta.acceptance_criteria.is_empty();
336 quality_checks.push(RequirementQualityCheck {
337 name: "has_acceptance".into(),
338 passed: has_acceptance,
339 detail: None,
340 });
341 quality_checks.push(RequirementQualityCheck {
342 name: "has_owner".into(),
343 passed: owner.is_some(),
344 detail: None,
345 });
346 quality_checks.push(RequirementQualityCheck {
348 name: "testable".into(),
349 passed: has_acceptance,
350 detail: None,
351 });
352 let object_is_concrete = match &claim.object {
357 Object::Entity(e) => !e.id.trim().is_empty(),
358 Object::Value { value } => !value.trim().is_empty(),
359 };
360 quality_checks.push(RequirementQualityCheck {
361 name: "unambiguous".into(),
362 passed: has_explicit_statement || object_is_concrete,
363 detail: None,
364 });
365
366 out.push(RequirementCandidate {
367 id: format!("req:{}", claim.id),
368 statement,
369 subject: claim.subject.clone(),
370 capability,
371 condition: claim.condition.clone(),
372 acceptance_criteria: meta.acceptance_criteria,
373 priority: priority.or({
374 None::<Priority>
377 }),
378 owner,
379 rationale: meta.rationale,
380 source_claims: vec![claim.id.clone()],
381 quality_checks,
382 });
383 }
384 out
385}
386
387pub fn project(events: &[Event]) -> InterviewState {
390 let fold = raw_fold(events);
391 let disproven = disproven_by_remediation(&fold);
392
393 let detection = detect::detect(&fold, &disproven);
396 let standings = compute_standings(&fold, &detection, &disproven);
397 let requirements = derive_requirements(&fold, &standings);
398 let registry = build_registry(&fold);
399
400 let claims: Vec<ClaimWithStanding> = fold
401 .claims
402 .iter()
403 .map(|c| ClaimWithStanding {
404 claim: c.clone(),
405 standing: standings
406 .get(&c.id)
407 .copied()
408 .unwrap_or(HypothesisStanding::Open),
409 evidence: fold
410 .evidence
411 .get(&c.id)
412 .map(|evs| evs.iter().map(|e| e.id.clone()).collect())
413 .unwrap_or_default(),
414 })
415 .collect();
416
417 let readiness = crate::readiness::assess(
418 &fold.coverage_schema,
419 &claims,
420 &detection.contradictions,
421 &detection.accepted_stances,
422 &requirements,
423 );
424
425 let signals = crate::signal::compute_signals(&claims, &detection, &readiness);
426
427 InterviewState {
428 session_id: fold.session_id.clone(),
429 coverage_schema: fold.coverage_schema.clone(),
430 claims,
431 contradictions: detection.contradictions,
432 requirements,
433 accepted_stances: detection.accepted_stances,
434 registry,
435 signals,
436 readiness,
437 }
438}
439
440pub(crate) use RawFold as InternalRawFold;
442
443impl RawFold {
444 pub(crate) fn claim_by_id(&self, id: &str) -> Option<&Claim> {
446 self.claim(id)
447 }
448}