1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use hirn_core::id::MemoryId;
6use hirn_core::record::MemoryRecord;
7use hirn_core::revision::{LogicalMemoryId, RevisionId, RevisionOperation, RevisionState};
8use hirn_core::semantic::SemanticRecord;
9use hirn_core::timestamp::Timestamp;
10use hirn_core::types::Layer;
11use hirn_core::{HirnError, HirnResult};
12
13use crate::db::HirnDB;
14use crate::inspect::InspectResult;
15use crate::resource_presentation::{ResourcePreviewPackage, ResourceScoreAttribution};
16pub use crate::scoring::ScoreBreakdown;
17use crate::trace::TraceResult;
18
19use super::ast::{AggFunction, ForgetMode};
20
21#[derive(Debug, Clone)]
22pub enum QueryResult {
23 Records(RecordResults),
24 Aggregated(AggregatedResults),
25 Created(CreatedResult),
26 Forgotten(ForgottenResult),
27 Corrected(CorrectedResult),
28 Superseded(SupersededResult),
29 Merged(MergedResult),
30 Retracted(RetractedResult),
31 Inspected(InspectResult),
32 History(HistoryResult),
33 Traced(TraceResult),
34 Consolidated(ConsolidatedResult),
35 WatchAck(WatchAckResult),
36 ExplainPlan(ExplainResult),
37 Policy(PolicyResult),
38 SvoEvents(SvoEventResults),
39 Causal(CausalQueryResult),
40}
41
42#[derive(Debug, Clone)]
43pub struct ExplainResult {
44 pub plan_text: String,
45 pub actual_result: Option<Box<QueryResult>>,
46 pub diagnostics: Option<crate::diagnostics::QueryDiagnostics>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PolicyResult {
51 pub message: String,
52 pub policies: Vec<(String, String)>,
53}
54
55#[derive(Debug, Clone)]
56pub struct SvoEventResults {
57 pub events: Vec<SvoEventResult>,
58 pub events_returned: usize,
59}
60
61#[derive(Debug, Clone)]
62pub struct SvoEventResult {
63 pub source_memory_id: String,
64 pub subject: String,
65 pub verb: String,
66 pub object: String,
67 pub time_start: Option<String>,
68 pub time_end: Option<String>,
69 pub confidence: f32,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CausalQueryResult {
74 pub kind: CausalQueryKind,
75 pub rows: Vec<CausalRow>,
76 pub query_time_ms: f64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub enum CausalQueryKind {
81 ExplainCauses,
82 WhatIf,
83 Counterfactual,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CausalRow {
88 pub columns: Vec<(String, String)>,
89}
90
91impl fmt::Display for CausalQueryKind {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self {
94 Self::ExplainCauses => write!(f, "EXPLAIN CAUSES"),
95 Self::WhatIf => write!(f, "WHAT_IF"),
96 Self::Counterfactual => write!(f, "COUNTERFACTUAL"),
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
102pub struct RecordResults {
103 pub records: Vec<ScoredMemory>,
104 pub query_time_ms: f64,
105 pub records_scanned: usize,
106 pub records_returned: usize,
107 pub context: Option<String>,
108 pub conflicts: Option<Vec<super::context::ConflictPair>>,
109 pub conflict_groups: Option<Vec<super::context::ConflictGroup>>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ScoredMemory {
114 pub record: MemoryRecord,
115 pub revision: Option<hirn_core::revision::RevisionRef>,
116 pub score: f32,
117 pub score_breakdown: ScoreBreakdown,
118 pub resource_evidence: Vec<crate::retrieval::recall::ResourceEvidenceSummary>,
119 pub(crate) resource_preview_packages: Vec<ResourcePreviewPackage>,
120 pub resource_score_attribution: Vec<ResourceScoreAttribution>,
121}
122
123#[derive(Debug, Clone)]
124pub struct CreatedResult {
125 pub id: MemoryId,
126 pub layer: Layer,
127}
128
129#[derive(Debug, Clone)]
130pub struct ForgottenResult {
131 pub target: String,
132 pub mode: ForgetMode,
133}
134
135#[derive(Debug, Clone)]
136pub struct CorrectedResult {
137 pub logical_memory_id: LogicalMemoryId,
138 pub prior_revision_id: RevisionId,
139 pub new_revision_id: RevisionId,
140}
141
142#[derive(Debug, Clone)]
143pub struct SupersededResult {
144 pub logical_memory_id: LogicalMemoryId,
145 pub prior_revision_id: RevisionId,
146 pub new_revision_id: RevisionId,
147 pub reason: Option<String>,
148}
149
150#[derive(Debug, Clone)]
151pub struct MergedResult {
152 pub target_logical_memory_id: LogicalMemoryId,
153 pub prior_target_revision_id: RevisionId,
154 pub new_target_revision_id: RevisionId,
155 pub source_logical_memory_ids: Vec<LogicalMemoryId>,
156 pub source_revision_ids: Vec<RevisionId>,
157 pub reason: Option<String>,
158}
159
160#[derive(Debug, Clone)]
161pub struct RetractedResult {
162 pub logical_memory_id: LogicalMemoryId,
163 pub prior_revision_id: RevisionId,
164 pub tombstone_revision_id: RevisionId,
165 pub reason: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct SemanticRevisionEntry {
170 pub memory_id: MemoryId,
171 pub revision_id: RevisionId,
172 pub version: u32,
173 pub operation: RevisionOperation,
174 pub state: RevisionState,
175 pub reason: Option<String>,
176 pub created_at: Timestamp,
177 pub superseded_by: Option<MemoryId>,
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct SemanticRevisionSummary {
182 pub logical_memory_id: LogicalMemoryId,
183 pub current_revision_id: RevisionId,
184 pub head_revision_id: RevisionId,
185 pub current_state: RevisionState,
186 pub logical_state: RevisionState,
187 pub revision_count: usize,
188 pub revisions: Vec<SemanticRevisionEntry>,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct SemanticHistoryItem {
193 pub record: SemanticRecord,
194 pub revision: SemanticRevisionEntry,
195}
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct HistoryResult {
199 pub semantic_revision: SemanticRevisionSummary,
200 pub items: Vec<SemanticHistoryItem>,
201}
202
203#[must_use]
204pub fn revision_query_result_to_json(result: &QueryResult) -> Option<serde_json::Value> {
205 match result {
206 QueryResult::Corrected(c) => Some(serde_json::json!({
207 "type": "corrected",
208 "logical_memory_id": c.logical_memory_id.to_string(),
209 "prior_revision_id": c.prior_revision_id.to_string(),
210 "new_revision_id": c.new_revision_id.to_string(),
211 })),
212 QueryResult::Superseded(s) => Some(serde_json::json!({
213 "type": "superseded",
214 "logical_memory_id": s.logical_memory_id.to_string(),
215 "prior_revision_id": s.prior_revision_id.to_string(),
216 "new_revision_id": s.new_revision_id.to_string(),
217 "reason": s.reason,
218 })),
219 QueryResult::Merged(m) => Some(serde_json::json!({
220 "type": "merged",
221 "target_logical_memory_id": m.target_logical_memory_id.to_string(),
222 "prior_target_revision_id": m.prior_target_revision_id.to_string(),
223 "new_target_revision_id": m.new_target_revision_id.to_string(),
224 "source_logical_memory_ids": m.source_logical_memory_ids.iter().map(ToString::to_string).collect::<Vec<_>>(),
225 "source_revision_ids": m.source_revision_ids.iter().map(ToString::to_string).collect::<Vec<_>>(),
226 "reason": m.reason,
227 })),
228 QueryResult::Retracted(r) => Some(serde_json::json!({
229 "type": "retracted",
230 "logical_memory_id": r.logical_memory_id.to_string(),
231 "prior_revision_id": r.prior_revision_id.to_string(),
232 "tombstone_revision_id": r.tombstone_revision_id.to_string(),
233 "reason": r.reason,
234 })),
235 QueryResult::History(h) => Some(serde_json::json!({
236 "type": "history",
237 "semantic_revision": serde_json::to_value(&h.semantic_revision).unwrap_or(serde_json::Value::Null),
238 "items": serde_json::to_value(&h.items).unwrap_or(serde_json::Value::Null),
239 })),
240 _ => None,
241 }
242}
243
244pub(crate) async fn load_semantic_revision_summary(
245 db: &HirnDB,
246 record: &SemanticRecord,
247) -> HirnResult<SemanticRevisionSummary> {
248 let history = db.semantic().history(record.id).await?;
249 summarize_semantic_revision_chain(record, &history)
250}
251
252pub(crate) fn summarize_semantic_revision_chain(
253 current: &SemanticRecord,
254 history: &[SemanticRecord],
255) -> HirnResult<SemanticRevisionSummary> {
256 let head = history.last().ok_or_else(|| {
257 HirnError::NotFound(format!(
258 "semantic revision history missing for {}",
259 current.logical_memory_id
260 ))
261 })?;
262
263 if head.logical_memory_id != current.logical_memory_id {
264 return Err(HirnError::InvalidInput(format!(
265 "semantic revision history mismatch for {}",
266 current.logical_memory_id
267 )));
268 }
269
270 Ok(SemanticRevisionSummary {
271 logical_memory_id: current.logical_memory_id,
272 current_revision_id: current.revision_id,
273 head_revision_id: head.revision_id,
274 current_state: current.revision_state_against(head),
275 logical_state: head.logical_state(),
276 revision_count: history.len(),
277 revisions: history
278 .iter()
279 .enumerate()
280 .map(|(index, revision)| SemanticRevisionEntry {
281 memory_id: revision.id,
282 revision_id: revision.revision_id,
283 version: revision.version,
284 operation: revision.revision_operation,
285 state: revision.revision_state_against(head),
286 reason: revision.revision_reason.clone(),
287 created_at: revision.created_at,
288 superseded_by: semantic_revision_superseded_by(history, index),
289 })
290 .collect(),
291 })
292}
293
294fn semantic_revision_superseded_by(history: &[SemanticRecord], index: usize) -> Option<MemoryId> {
295 let revision = &history[index];
296 revision
297 .superseded_by
298 .or_else(|| history.get(index + 1).map(|next| next.id))
299}
300
301pub(crate) fn build_semantic_history_items(
302 history: &[SemanticRecord],
303 summary: &SemanticRevisionSummary,
304) -> HirnResult<Vec<SemanticHistoryItem>> {
305 if history.len() != summary.revisions.len() {
306 return Err(HirnError::InvalidInput(format!(
307 "semantic history item mismatch for {}",
308 summary.logical_memory_id
309 )));
310 }
311
312 history
313 .iter()
314 .zip(summary.revisions.iter())
315 .map(|(record, revision)| {
316 if (record.id, record.revision_id) != (revision.memory_id, revision.revision_id) {
317 return Err(HirnError::InvalidInput(format!(
318 "semantic history revision mismatch for {}",
319 summary.logical_memory_id
320 )));
321 }
322
323 Ok(SemanticHistoryItem {
324 record: record.clone(),
325 revision: revision.clone(),
326 })
327 })
328 .collect()
329}
330
331#[derive(Debug, Clone)]
332pub struct ConsolidatedResult {
333 pub records_processed: usize,
334}
335
336#[derive(Debug, Clone)]
337pub struct WatchAckResult {
338 pub message: String,
339}
340
341#[derive(Debug, Clone)]
342pub struct AggregatedResults {
343 pub group_field: String,
344 pub function: AggFunction,
345 pub groups: Vec<AggregatedGroup>,
346 pub query_time_ms: f64,
347 pub formatted: Option<String>,
348}
349
350#[derive(Debug, Clone)]
351pub struct AggregatedGroup {
352 pub key: String,
353 pub value: f64,
354}
355
356#[derive(Debug, Clone)]
357pub struct ProjectedRecord {
358 pub fields: std::collections::BTreeMap<String, serde_json::Value>,
359 #[cfg_attr(not(test), allow(dead_code))]
360 pub score: f32,
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn revision_query_result_json_serializes_merge_shape() {
369 let result = QueryResult::Merged(MergedResult {
370 target_logical_memory_id: LogicalMemoryId::new(),
371 prior_target_revision_id: RevisionId::new(),
372 new_target_revision_id: RevisionId::new(),
373 source_logical_memory_ids: vec![LogicalMemoryId::new(), LogicalMemoryId::new()],
374 source_revision_ids: vec![RevisionId::new(), RevisionId::new()],
375 reason: Some("dedupe".to_owned()),
376 });
377
378 let json = revision_query_result_to_json(&result).expect("expected revision JSON");
379
380 assert_eq!(json["type"], "merged");
381 assert_eq!(json["reason"], "dedupe");
382 assert_eq!(
383 json["source_logical_memory_ids"].as_array().unwrap().len(),
384 2
385 );
386 assert_eq!(json["source_revision_ids"].as_array().unwrap().len(), 2);
387 }
388
389 #[test]
390 fn revision_query_result_json_skips_non_revision_results() {
391 let result = QueryResult::WatchAck(WatchAckResult {
392 message: "ok".to_owned(),
393 });
394
395 assert!(revision_query_result_to_json(&result).is_none());
396 }
397}