Skip to main content

hirn_engine/observability/
trace.rs

1//! Trace builder: fluent API for provenance lineage queries.
2
3use hirn_core::id::MemoryId;
4use hirn_core::provenance::Provenance;
5use hirn_core::record::MemoryRecord;
6use hirn_core::types::{AgentId, Namespace, Origin};
7use hirn_core::{HirnError, HirnResult};
8
9use crate::causal::TraceReport;
10use crate::db::HirnDB;
11use crate::ql::context::ConflictGroup;
12use crate::ql::results::SemanticRevisionSummary;
13use crate::retrieval::recall::ResourceEvidenceSummary;
14
15/// Result of a trace query executed via the builder API.
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct TraceResult {
18    /// The traced memory record.
19    pub record: MemoryRecord,
20    /// Full provenance chain.
21    pub provenance: Provenance,
22    /// Source episodes (for semantic/consolidated records).
23    pub source_episodes: Vec<MemoryId>,
24    /// Records derived from this record.
25    pub derived_records: Vec<MemoryId>,
26    /// Number of mutations in the provenance log.
27    pub mutation_count: usize,
28    /// Computed trust score in [0.0, 1.0].
29    pub trust_score: f32,
30    /// Textual lineage tree representation.
31    pub lineage_tree: String,
32    /// Semantic revision chain summary, when tracing a semantic record.
33    pub semantic_revision: Option<SemanticRevisionSummary>,
34    /// Visible grouped contradiction state for this record.
35    pub conflict_groups: Vec<ConflictGroup>,
36    /// Linked resource evidence with authorization-sensitive hydration flags.
37    pub resource_evidence: Vec<ResourceEvidenceSummary>,
38}
39
40impl From<TraceReport> for TraceResult {
41    fn from(report: TraceReport) -> Self {
42        Self {
43            record: report.record,
44            provenance: report.provenance,
45            source_episodes: report.source_episodes,
46            derived_records: report.derived_records,
47            mutation_count: report.mutation_count,
48            trust_score: report.trust_score,
49            lineage_tree: report.lineage_tree,
50            semantic_revision: None,
51            conflict_groups: Vec::new(),
52            resource_evidence: Vec::new(),
53        }
54    }
55}
56
57/// Builder for provenance trace queries.
58///
59/// ```ignore
60/// let result = db.trace(memory_id)
61///     .execute()?;
62///
63/// println!("Trust: {:.2}", result.trust_score);
64/// println!("Lineage:\n{}", result.lineage_tree);
65/// ```
66pub struct TraceBuilder<'a> {
67    db: &'a HirnDB,
68    id: MemoryId,
69    allowed_namespaces: Option<Vec<Namespace>>,
70    agent_id: Option<String>,
71    exact_conflict_target: bool,
72}
73
74impl<'a> TraceBuilder<'a> {
75    pub(crate) fn new(db: &'a HirnDB, id: MemoryId) -> Self {
76        Self {
77            db,
78            id,
79            allowed_namespaces: None,
80            agent_id: None,
81            exact_conflict_target: false,
82        }
83    }
84
85    #[must_use]
86    pub fn allowed_namespaces(mut self, allowed_namespaces: Vec<Namespace>) -> Self {
87        self.allowed_namespaces = Some(allowed_namespaces);
88        self
89    }
90
91    #[must_use]
92    pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
93        self.agent_id = Some(agent_id.into());
94        self
95    }
96
97    #[must_use]
98    pub fn exact_conflict_target(mut self, exact_conflict_target: bool) -> Self {
99        self.exact_conflict_target = exact_conflict_target;
100        self
101    }
102
103    /// Execute the trace query.
104    pub async fn execute(self) -> HirnResult<TraceResult> {
105        let record = self.db.get_memory(self.id).await?;
106        if let Some(allowed_namespaces) = self.allowed_namespaces.as_deref() {
107            let namespace = record.effective_namespace();
108            if !allowed_namespaces.contains(&namespace) {
109                return Err(HirnError::AccessDenied(format!(
110                    "TRACE cannot access namespace '{}'",
111                    namespace.as_str()
112                )));
113            }
114        }
115
116        let conflict_groups = if self.exact_conflict_target {
117            crate::ql::context::detect_conflicts_for_exact_record(
118                self.db,
119                &record,
120                self.allowed_namespaces.as_deref(),
121            )
122            .await
123            .groups
124        } else {
125            crate::ql::context::detect_conflicts_for_record(
126                self.db,
127                &record,
128                self.allowed_namespaces.as_deref(),
129            )
130            .await
131            .groups
132        };
133        let semantic_revision = match &record {
134            MemoryRecord::Semantic(record) => {
135                Some(crate::ql::results::load_semantic_revision_summary(self.db, record).await?)
136            }
137            _ => None,
138        };
139
140        let (provenance, source_episodes) = match &record {
141            MemoryRecord::Episodic(e) => (e.provenance.clone(), vec![]),
142            MemoryRecord::Semantic(s) => (s.provenance.clone(), s.source_episodes.clone()),
143            MemoryRecord::Working(_) => (
144                Provenance::with_origin(Origin::DirectObservation, AgentId::well_known("system")),
145                vec![],
146            ),
147            MemoryRecord::Procedural(p) => (p.provenance.clone(), p.source_episodes.clone()),
148        };
149
150        let report = crate::causal::build_trace_report(
151            self.db.graph_store(),
152            record,
153            provenance,
154            source_episodes,
155        )
156        .await?;
157
158        let mut result = TraceResult::from(report);
159        result.semantic_revision = semantic_revision;
160        result.conflict_groups = conflict_groups;
161        let agent_id = self.agent_id.as_deref().unwrap_or("anonymous");
162        result.resource_evidence = self
163            .db
164            .resource_evidence_summaries_for_record(&result.record, agent_id)
165            .await?;
166        Ok(result)
167    }
168}