ant_types/observation.rs
1//! Antares-native observations.
2//!
3//! An **Observation** is an immutable, source-bound, evidence-backed
4//! atomic fact extracted from a real event/source. Observations live
5//! in their own append-only plane and feed the Belief layer.
6//!
7//! Examples:
8//! - "Marcus from legal joined the review" → subject=person_marcus,
9//! predicate=joined_review, source_event_id=meeting_001
10//! - "SOC2 was mentioned in the call" → subject=meeting_001,
11//! predicate=mentioned_topic, object_value="SOC2"
12//! - "pricing page opened 5×" → subject=person_marcus,
13//! predicate=page_open_count, object_value=5
14//!
15//! This module ships ONLY the observation plane; beliefs are their
16//! own record kind.
17
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21use crate::author::AuthorStamp;
22use crate::evidence::EvidenceId;
23use crate::ids::{ProjectId, TenantId, VertexId};
24
25/// Stable identifier for an observation.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct ObservationId(pub String);
29
30/// An atomic, source-bound fact about a subject.
31///
32/// Append-only: once an observation is stored, it cannot be modified.
33/// Re-submitting identical content under the same id is a no-op
34/// (idempotent). Re-submitting different content under the same id is
35/// a conflict.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct Observation {
38 /// Observation id, unique within the scope.
39 pub id: ObservationId,
40 /// Owning tenant.
41 pub tenant_id: TenantId,
42 /// Owning project.
43 pub project_id: ProjectId,
44
45 /// Identifier of the source event (e.g. "meeting_001",
46 /// "email_002", "crm_webhook_2026-04-29T18:02"). Optional because
47 /// some observations are aggregated (e.g. "champion silent for 14
48 /// days") and don't tie to a single event.
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub source_event_id: Option<String>,
51
52 /// URI of the source artifact (e.g. "antares://transcripts/m1#1240-1295").
53 /// Optional and may duplicate `evidence_ids[0].source_uri`.
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub source_uri: Option<String>,
56
57 /// Subject of the observation — typically a deal, person, meeting,
58 /// or email vertex. Optional for observations that aren't anchored
59 /// to a specific entity (e.g. aggregate behavioral signals).
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub subject_id: Option<VertexId>,
62
63 /// What was observed about the subject. Free-form string — common
64 /// values include "joined_review", "mentioned_topic", "viewed",
65 /// "opened_email", "forwarded_to", "went_silent", "usage_dropped".
66 pub predicate: String,
67
68 /// Object of the predicate when the observation is relational.
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub object_id: Option<VertexId>,
71
72 /// Object as a literal value when the observation isn't relational
73 /// (e.g. "SOC2 was mentioned" → object_value = "SOC2";
74 /// "page opens count" → object_value = 5).
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub object_value: Option<serde_json::Value>,
77
78 /// Wall-clock time the underlying event happened.
79 pub observed_at: DateTime<Utc>,
80 /// Wall-clock time the extractor produced this observation.
81 pub extracted_at: DateTime<Utc>,
82
83 /// Confidence in `[0,1]`; `None` is treated as 1.0.
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub confidence: Option<f32>,
86
87 /// First-class evidence references. Each Observation should point
88 /// at one or more Evidence records that back it.
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub evidence_ids: Vec<EvidenceId>,
91
92 /// Version tag of the producing extractor, verbatim.
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub extractor_version: Option<String>,
95
96 /// Free-form metadata for extractor-specific extras.
97 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
98 pub metadata: serde_json::Value,
99
100 /// Which user authored this observation. `None` for older
101 /// records and for anonymous calls. Set by the writer from the
102 /// resolved authentication context at write time.
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub author: Option<AuthorStamp>,
105}