Skip to main content

oxibrain_core/
knowledge.rs

1//! Knowledge domain types (DESIGN §5.4). Entities, statements, assertions,
2//! mentions, beliefs — the projection types derived from the ledger.
3
4use crate::types::TrustTier;
5use oxibrain_ports::Timestamp;
6use serde::{Deserialize, Serialize};
7
8pub type EntityId = String;
9pub type EntityKeyId = String;
10pub type StatementId = String;
11pub type AssertionId = String;
12pub type MentionId = String;
13
14pub type EntityTypeRef = String;
15pub type PredicateRef = String;
16
17/// Opaque, permanent identity. Names live in EntityKey, not here (P3).
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Entity {
20    pub id: EntityId,
21    pub space: String,
22    pub ty: EntityTypeRef,
23    pub canonical_key: Option<EntityKeyId>,
24    pub created_at: Timestamp,
25    pub merged_into: Option<EntityId>,
26}
27
28/// A (type, normalized name) handle. Aliases are additional keys on one entity.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct EntityKey {
31    pub id: EntityKeyId,
32    pub space: String,
33    pub entity: EntityId,
34    pub ty: EntityTypeRef,
35    pub normalized: String,
36    pub surface: String,
37    pub origin: KeyOrigin,
38}
39
40#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum KeyOrigin {
43    Extracted,
44    UserDeclared,
45    Imported,
46}
47
48impl KeyOrigin {
49    pub fn as_db(&self) -> &'static str {
50        match self {
51            Self::Extracted => "extracted",
52            Self::UserDeclared => "user_declared",
53            Self::Imported => "imported",
54        }
55    }
56    pub fn parse_db(s: &str) -> Option<Self> {
57        match s {
58            "extracted" => Some(Self::Extracted),
59            "user_declared" => Some(Self::UserDeclared),
60            "imported" => Some(Self::Imported),
61            _ => None,
62        }
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct EntityMerge {
68    pub id: String,
69    pub loser: EntityId,
70    pub winner: EntityId,
71    pub decided_by: MergeDecision,
72    pub provenance: String,
73    pub evidence: Vec<MentionId>,
74    pub decided_at: Timestamp,
75    pub undone_at: Option<Timestamp>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
80pub enum MergeDecision {
81    Rule { score: f64 },
82    User,
83    Import,
84}
85
86impl MergeDecision {
87    /// (decided_by column, score column) for persistence.
88    pub fn db_columns(&self) -> (&'static str, Option<f64>) {
89        match self {
90            Self::Rule { score } => ("rule", Some(*score)),
91            Self::User => ("user", None),
92            Self::Import => ("import", None),
93        }
94    }
95    pub fn parse_db(kind: &str, score: Option<f64>) -> Option<Self> {
96        match kind {
97            "rule" => Some(Self::Rule {
98                score: score.unwrap_or(0.0),
99            }),
100            "user" => Some(Self::User),
101            "import" => Some(Self::Import),
102            _ => None,
103        }
104    }
105}
106
107/// An atemporal proposition. Content-addressed → deduplicated by construction.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Statement {
110    pub id: StatementId,
111    pub space: String,
112    pub subject: EntityId,
113    pub predicate: PredicateRef,
114    pub object: Object,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(tag = "kind", rename_all = "snake_case")]
119pub enum Object {
120    Entity(EntityId),
121    Literal(TypedValue),
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type", content = "value", rename_all = "snake_case")]
126pub enum TypedValue {
127    Text(String),
128    Date(String),
129    DateTime(String),
130    Quantity { value: f64, unit: String },
131    Number(f64),
132    Bool(bool),
133    Enum(String),
134}
135
136/// "Episode E, via extractor R, claimed S held over I, with confidence c."
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Assertion {
139    pub id: AssertionId,
140    pub statement: StatementId,
141    pub episode: String,
142    pub extractor: Option<String>,
143    pub polarity: Polarity,
144    pub claimed_from: Timestamp,
145    pub claimed_to: Timestamp,
146    pub confidence: f32,
147    pub recorded_at: Timestamp,
148    pub retracted_at: Option<Timestamp>,
149}
150
151#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum Polarity {
154    Affirm,
155    Deny,
156}
157
158impl Polarity {
159    pub fn as_db(&self) -> i64 {
160        match self {
161            Self::Affirm => 1,
162            Self::Deny => -1,
163        }
164    }
165    pub fn parse_db(v: i64) -> Option<Self> {
166        match v {
167            1 => Some(Self::Affirm),
168            -1 => Some(Self::Deny),
169            _ => None,
170        }
171    }
172}
173
174/// The verbatim text this assertion came from.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct Mention {
177    pub id: MentionId,
178    pub assertion: AssertionId,
179    pub role: MentionRole,
180    pub surface: String,
181    pub span: (u32, u32),
182    pub resolved_to: Option<EntityId>,
183    pub method: ResolutionMethod,
184}
185
186#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum MentionRole {
189    Subject,
190    Object,
191}
192
193impl MentionRole {
194    pub fn as_db(&self) -> &'static str {
195        match self {
196            Self::Subject => "subject",
197            Self::Object => "object",
198        }
199    }
200    pub fn parse_db(s: &str) -> Option<Self> {
201        match s {
202            "subject" => Some(Self::Subject),
203            "object" => Some(Self::Object),
204            _ => None,
205        }
206    }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(tag = "method", rename_all = "snake_case")]
211pub enum ResolutionMethod {
212    ExactKey,
213    Alias,
214    Lexical { score: f64 },
215    Embedding { score: f64 },
216    New,
217    User,
218}
219
220impl ResolutionMethod {
221    /// (method column, score column) for persistence. score is NULL for non-scoring methods.
222    pub fn db_columns(&self) -> (&'static str, Option<f64>) {
223        match self {
224            Self::ExactKey => ("exact_key", None),
225            Self::Alias => ("alias", None),
226            Self::Lexical { score } => ("lexical", Some(*score)),
227            Self::Embedding { score } => ("embedding", Some(*score)),
228            Self::New => ("new", None),
229            Self::User => ("user", None),
230        }
231    }
232    pub fn parse_db(method: &str, score: Option<f64>) -> Option<Self> {
233        match method {
234            "exact_key" => Some(Self::ExactKey),
235            "alias" => Some(Self::Alias),
236            "lexical" => Some(Self::Lexical {
237                score: score.unwrap_or(0.0),
238            }),
239            "embedding" => Some(Self::Embedding {
240                score: score.unwrap_or(0.0),
241            }),
242            "new" => Some(Self::New),
243            "user" => Some(Self::User),
244            _ => None,
245        }
246    }
247}
248
249/// Current-slice cache of the temporal fold. Fully derived.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct Belief {
252    pub statement: StatementId,
253    pub valid_from: Timestamp,
254    pub valid_to: Timestamp,
255    pub support: Support,
256    pub confidence: f32,
257    pub status: BeliefStatus,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct Support {
262    pub affirm_count: u32,
263    pub deny_count: u32,
264    pub distinct_episodes: u32,
265    /// Sorted by TrustTier ordinal (Trusted < SemiTrusted < Untrusted) for
266    /// deterministic serialization. Each entry: (tier, count of episodes).
267    pub trust_weights: Vec<(TrustTier, u32)>,
268}
269
270#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum BeliefStatus {
273    Active,
274    Superseded,
275    Contradicted,
276    Retracted,
277}
278
279impl BeliefStatus {
280    pub fn as_db(&self) -> &'static str {
281        match self {
282            Self::Active => "active",
283            Self::Superseded => "superseded",
284            Self::Contradicted => "contradicted",
285            Self::Retracted => "retracted",
286        }
287    }
288    pub fn parse_db(s: &str) -> Option<Self> {
289        match s {
290            "active" => Some(Self::Active),
291            "superseded" => Some(Self::Superseded),
292            "contradicted" => Some(Self::Contradicted),
293            "retracted" => Some(Self::Retracted),
294            _ => None,
295        }
296    }
297}
298
299/// Canonical string representation of an Object for StatementId hashing (DESIGN §5.6).
300/// Prefix-based to avoid JSON float formatting issues.
301pub fn object_repr(object: &Object) -> String {
302    match object {
303        Object::Entity(id) => format!("e:{id}"),
304        Object::Literal(TypedValue::Text(s)) => format!("t:{s}"),
305        Object::Literal(TypedValue::Date(s)) => format!("d:{s}"),
306        Object::Literal(TypedValue::DateTime(s)) => format!("dt:{s}"),
307        Object::Literal(TypedValue::Number(n)) => format!("n:{}", canonical_f64(*n)),
308        Object::Literal(TypedValue::Bool(b)) => format!("b:{b}"),
309        Object::Literal(TypedValue::Quantity { value, unit }) => {
310            format!("q:{}:{unit}", canonical_f64(*value))
311        }
312        Object::Literal(TypedValue::Enum(s)) => format!("en:{s}"),
313    }
314}
315
316/// Canonical f64: avoid -0.0, normalize integer-valued floats.
317fn canonical_f64(n: f64) -> String {
318    if n == 0.0 {
319        "0".to_string()
320    } else if n.fract() == 0.0 && n.abs() < 1e15 {
321        format!("{}", n as i64)
322    } else {
323        format!("{n}")
324    }
325}
326
327/// Canonical string representation of a claim for AssertionId hashing.
328/// Uses f32::to_bits() for deterministic float representation.
329pub fn claim_repr(
330    polarity: Polarity,
331    claimed_from: Timestamp,
332    claimed_to: Timestamp,
333    confidence: f32,
334) -> String {
335    format!(
336        "{}:{}:{}:{}",
337        match polarity {
338            Polarity::Affirm => "a",
339            Polarity::Deny => "d",
340        },
341        claimed_from.millis(),
342        claimed_to.millis(),
343        confidence.to_bits(),
344    )
345}