Skip to main content

hirn_engine/ql/
results.rs

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}