1use crate::{
4 AbstractionLevel, ContentHash, EdgeId, EdgeType, EntityRef, EvolutionPhase, Provenance,
5 SnapshotId, SummarizationPolicyId, SummarizationTrigger, Timestamp, TrajectoryId,
6};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13pub struct EdgeParticipant {
14 pub entity_ref: EntityRef,
16 pub role: Option<String>,
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24pub struct Edge {
25 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
26 pub edge_id: EdgeId,
27 pub edge_type: EdgeType,
28 pub participants: Vec<EdgeParticipant>,
30 pub weight: Option<f32>,
32 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
34 pub trajectory_id: Option<TrajectoryId>,
35 pub provenance: Provenance,
37 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
38 pub created_at: Timestamp,
39 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
40 pub metadata: Option<serde_json::Value>,
41}
42
43impl Edge {
44 pub fn is_binary(&self) -> bool {
46 self.participants.len() == 2
47 }
48
49 pub fn is_hyperedge(&self) -> bool {
51 self.participants.len() > 2
52 }
53
54 pub fn participants_with_role(&self, role: &str) -> Vec<&EdgeParticipant> {
56 self.participants
57 .iter()
58 .filter(|p| p.role.as_deref() == Some(role))
59 .collect()
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
70pub struct EvolutionMetrics {
71 pub retrieval_accuracy: f32,
73 pub token_efficiency: f32,
75 pub latency_p50_ms: i64,
77 pub latency_p99_ms: i64,
79 pub cost_estimate: f32,
81 pub benchmark_queries: i32,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
89pub struct EvolutionSnapshot {
90 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
92 pub snapshot_id: SnapshotId,
93 pub name: String,
95 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "byte"))]
97 pub config_hash: ContentHash,
98 pub config_source: String,
100 pub phase: EvolutionPhase,
102 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
103 pub created_at: Timestamp,
104 pub metrics: Option<EvolutionMetrics>,
106 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
107 pub metadata: Option<serde_json::Value>,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
118pub struct SummarizationPolicy {
119 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
120 pub summarization_policy_id: SummarizationPolicyId,
121 pub name: String,
123 pub triggers: Vec<SummarizationTrigger>,
125 pub target_level: AbstractionLevel,
127 pub source_level: AbstractionLevel,
129 pub max_sources: i32,
131 pub create_edges: bool,
133 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
134 pub created_at: Timestamp,
135 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
136 pub metadata: Option<serde_json::Value>,
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::{EntityIdType, EntityType, ExtractionMethod, Provenance};
143
144 fn make_edge(n_participants: usize) -> Edge {
145 let participants: Vec<EdgeParticipant> = (0..n_participants)
146 .map(|i| EdgeParticipant {
147 entity_ref: EntityRef {
148 entity_type: EntityType::Artifact,
149 id: uuid::Uuid::now_v7(),
150 },
151 role: if i == 0 {
152 Some("source".into())
153 } else {
154 Some("target".into())
155 },
156 })
157 .collect();
158 Edge {
159 edge_id: EdgeId::new(uuid::Uuid::now_v7()),
160 edge_type: EdgeType::RelatesTo,
161 participants,
162 weight: Some(0.8),
163 trajectory_id: None,
164 provenance: Provenance {
165 source_turn: 1,
166 extraction_method: ExtractionMethod::Explicit,
167 confidence: Some(0.9),
168 },
169 created_at: chrono::Utc::now(),
170 metadata: None,
171 }
172 }
173
174 #[test]
175 fn binary_edge_is_binary() {
176 let edge = make_edge(2);
177 assert!(edge.is_binary());
178 assert!(!edge.is_hyperedge());
179 }
180
181 #[test]
182 fn hyperedge_is_hyperedge() {
183 let edge = make_edge(3);
184 assert!(!edge.is_binary());
185 assert!(edge.is_hyperedge());
186 }
187
188 #[test]
189 fn single_participant_is_neither() {
190 let edge = make_edge(1);
191 assert!(!edge.is_binary());
192 assert!(!edge.is_hyperedge());
193 }
194
195 #[test]
196 fn participants_with_role_filters_correctly() {
197 let edge = make_edge(3);
198 let sources = edge.participants_with_role("source");
199 assert_eq!(sources.len(), 1);
200 let targets = edge.participants_with_role("target");
201 assert_eq!(targets.len(), 2);
202 let unknown = edge.participants_with_role("unknown");
203 assert!(unknown.is_empty());
204 }
205
206 #[test]
207 fn edge_serde_roundtrip() {
208 let edge = make_edge(2);
209 let json = serde_json::to_string(&edge).unwrap();
210 let d: Edge = serde_json::from_str(&json).unwrap();
211 assert_eq!(edge.edge_id, d.edge_id);
212 assert_eq!(edge.participants.len(), d.participants.len());
213 }
214
215 #[test]
216 fn evolution_metrics_serde_roundtrip() {
217 let m = EvolutionMetrics {
218 retrieval_accuracy: 0.95,
219 token_efficiency: 0.8,
220 latency_p50_ms: 100,
221 latency_p99_ms: 500,
222 cost_estimate: 0.05,
223 benchmark_queries: 100,
224 };
225 let json = serde_json::to_string(&m).unwrap();
226 let d: EvolutionMetrics = serde_json::from_str(&json).unwrap();
227 assert_eq!(m, d);
228 }
229
230 #[test]
231 fn summarization_policy_serde_roundtrip() {
232 let p = SummarizationPolicy {
233 summarization_policy_id: <SummarizationPolicyId as EntityIdType>::new(
234 uuid::Uuid::now_v7(),
235 ),
236 name: "test-policy".into(),
237 triggers: vec![crate::SummarizationTrigger::Manual],
238 target_level: crate::AbstractionLevel::Summary,
239 source_level: crate::AbstractionLevel::Raw,
240 max_sources: 10,
241 create_edges: true,
242 created_at: chrono::Utc::now(),
243 metadata: None,
244 };
245 let json = serde_json::to_string(&p).unwrap();
246 let d: SummarizationPolicy = serde_json::from_str(&json).unwrap();
247 assert_eq!(p.name, d.name);
248 assert_eq!(p.max_sources, d.max_sources);
249 }
250}