1use serde::{Deserialize, Serialize};
8
9use crate::model::{HypothesisStanding, RequirementCandidate};
10use crate::projection::InterviewState;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ExportedHypothesis {
15 pub claim_id: String,
16 pub statement: String,
17 pub standing: HypothesisStanding,
18 pub source_turn: String,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct DimensionSection {
24 pub dimension: String,
25 pub hypotheses: Vec<ExportedHypothesis>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct AcceptedTension {
32 pub target: String,
33 pub rationale: String,
34 pub nonconforming_claims: Vec<String>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct SpecDocument {
40 pub session_id: String,
41 pub ready: bool,
42 pub dimensions: Vec<DimensionSection>,
43 pub requirements: Vec<RequirementCandidate>,
44 pub accepted_tensions: Vec<AcceptedTension>,
45 pub blockers: Vec<String>,
47}
48
49pub(crate) fn build(state: &InterviewState) -> SpecDocument {
51 let mut dimensions = Vec::new();
52 for dim in &state.coverage_schema.dimensions {
53 let mut hypotheses = Vec::new();
54 for cw in &state.claims {
55 if !cw.standing.is_withstood_or_better() {
56 continue;
57 }
58 if cw.claim.dimensions.iter().any(|t| t == &dim.tag) {
59 hypotheses.push(ExportedHypothesis {
60 claim_id: cw.claim.id.clone(),
61 statement: statement_for(&cw.claim),
62 standing: cw.standing,
63 source_turn: cw.claim.source.turn_id.clone(),
64 });
65 }
66 }
67 dimensions.push(DimensionSection {
68 dimension: dim.tag.clone(),
69 hypotheses,
70 });
71 }
72
73 let accepted_tensions = state
74 .accepted_stances
75 .iter()
76 .map(|s| AcceptedTension {
77 target: s.target.clone(),
78 rationale: s.rationale.clone(),
79 nonconforming_claims: s.nonconforming_claims.clone(),
80 })
81 .collect();
82
83 SpecDocument {
84 session_id: state.session_id.clone(),
85 ready: state.readiness.ready,
86 dimensions,
87 requirements: state.requirements.clone(),
88 accepted_tensions,
89 blockers: state.readiness.blockers.clone(),
90 }
91}
92
93fn statement_for(claim: &crate::model::Claim) -> String {
94 let obj = match &claim.object {
95 crate::model::Object::Entity(e) => e.id.clone(),
96 crate::model::Object::Value { value } => value.clone(),
97 };
98 let polarity = match claim.polarity {
99 crate::model::Polarity::Positive => "",
100 crate::model::Polarity::Negative => "NOT ",
101 };
102 let scope = claim
103 .condition
104 .as_ref()
105 .map(|c| format!(" [when {c}]"))
106 .unwrap_or_default();
107 format!(
108 "{}{} {} {}{scope}",
109 polarity, claim.subject.id, claim.predicate, obj
110 )
111}