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 pub id: ObservationId,
39 pub tenant_id: TenantId,
40 pub project_id: ProjectId,
41
42 /// Identifier of the source event (e.g. "meeting_001",
43 /// "email_002", "crm_webhook_2026-04-29T18:02"). Optional because
44 /// some observations are aggregated (e.g. "champion silent for 14
45 /// days") and don't tie to a single event.
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub source_event_id: Option<String>,
48
49 /// URI of the source artifact (e.g. "antares://transcripts/m1#1240-1295").
50 /// Optional and may duplicate evidence_ids[0].source_uri.
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub source_uri: Option<String>,
53
54 /// Subject of the observation — typically a deal, person, meeting,
55 /// or email vertex. Optional for observations that aren't anchored
56 /// to a specific entity (e.g. aggregate behavioral signals).
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub subject_id: Option<VertexId>,
59
60 /// What was observed about the subject. Free-form string — common
61 /// values include "joined_review", "mentioned_topic", "viewed",
62 /// "opened_email", "forwarded_to", "went_silent", "usage_dropped".
63 pub predicate: String,
64
65 /// Object of the predicate when the observation is relational.
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub object_id: Option<VertexId>,
68
69 /// Object as a literal value when the observation isn't relational
70 /// (e.g. "SOC2 was mentioned" → object_value = "SOC2";
71 /// "page opens count" → object_value = 5).
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub object_value: Option<serde_json::Value>,
74
75 /// Wall-clock time the underlying event happened.
76 pub observed_at: DateTime<Utc>,
77 /// Wall-clock time the extractor produced this observation.
78 pub extracted_at: DateTime<Utc>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub confidence: Option<f32>,
82
83 /// First-class evidence references. Each Observation should point
84 /// at one or more Evidence records that back it.
85 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub evidence_ids: Vec<EvidenceId>,
87
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub extractor_version: Option<String>,
90
91 /// Free-form metadata for extractor-specific extras.
92 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
93 pub metadata: serde_json::Value,
94
95 /// Which user authored this observation. `None` for older
96 /// records and for anonymous calls. Set by the writer from the
97 /// resolved authentication context at write time.
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub author: Option<AuthorStamp>,
100}