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    /// Trust tier of the supporting episode at ingest time.
150    #[serde(default)]
151    pub trust: TrustTier,
152}
153
154#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum Polarity {
157    Affirm,
158    Deny,
159}
160
161impl Polarity {
162    pub fn as_db(&self) -> i64 {
163        match self {
164            Self::Affirm => 1,
165            Self::Deny => -1,
166        }
167    }
168    pub fn parse_db(v: i64) -> Option<Self> {
169        match v {
170            1 => Some(Self::Affirm),
171            -1 => Some(Self::Deny),
172            _ => None,
173        }
174    }
175}
176
177/// The verbatim text this assertion came from.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct Mention {
180    pub id: MentionId,
181    pub assertion: AssertionId,
182    pub role: MentionRole,
183    pub surface: String,
184    pub span: (u32, u32),
185    pub resolved_to: Option<EntityId>,
186    pub method: ResolutionMethod,
187}
188
189#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum MentionRole {
192    Subject,
193    Object,
194}
195
196impl MentionRole {
197    pub fn as_db(&self) -> &'static str {
198        match self {
199            Self::Subject => "subject",
200            Self::Object => "object",
201        }
202    }
203    pub fn parse_db(s: &str) -> Option<Self> {
204        match s {
205            "subject" => Some(Self::Subject),
206            "object" => Some(Self::Object),
207            _ => None,
208        }
209    }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(tag = "method", rename_all = "snake_case")]
214pub enum ResolutionMethod {
215    ExactKey,
216    Alias,
217    Lexical { score: f64 },
218    Embedding { score: f64 },
219    New,
220    User,
221}
222
223impl ResolutionMethod {
224    /// (method column, score column) for persistence. score is NULL for non-scoring methods.
225    pub fn db_columns(&self) -> (&'static str, Option<f64>) {
226        match self {
227            Self::ExactKey => ("exact_key", None),
228            Self::Alias => ("alias", None),
229            Self::Lexical { score } => ("lexical", Some(*score)),
230            Self::Embedding { score } => ("embedding", Some(*score)),
231            Self::New => ("new", None),
232            Self::User => ("user", None),
233        }
234    }
235    pub fn parse_db(method: &str, score: Option<f64>) -> Option<Self> {
236        match method {
237            "exact_key" => Some(Self::ExactKey),
238            "alias" => Some(Self::Alias),
239            "lexical" => Some(Self::Lexical {
240                score: score.unwrap_or(0.0),
241            }),
242            "embedding" => Some(Self::Embedding {
243                score: score.unwrap_or(0.0),
244            }),
245            "new" => Some(Self::New),
246            "user" => Some(Self::User),
247            _ => None,
248        }
249    }
250}
251
252/// Current-slice cache of the temporal fold. Fully derived.
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct Belief {
255    pub statement: StatementId,
256    pub valid_from: Timestamp,
257    pub valid_to: Timestamp,
258    pub support: Support,
259    pub confidence: f32,
260    pub status: BeliefStatus,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct Support {
265    pub affirm_count: u32,
266    pub deny_count: u32,
267    pub distinct_episodes: u32,
268    /// Sorted by TrustTier ordinal (Trusted < SemiTrusted < Untrusted) for
269    /// deterministic serialization. Each entry: (tier, count of episodes).
270    pub trust_weights: Vec<(TrustTier, u32)>,
271}
272
273#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
274#[serde(rename_all = "snake_case")]
275pub enum BeliefStatus {
276    Active,
277    Superseded,
278    Contradicted,
279    Retracted,
280}
281
282impl BeliefStatus {
283    pub fn as_db(&self) -> &'static str {
284        match self {
285            Self::Active => "active",
286            Self::Superseded => "superseded",
287            Self::Contradicted => "contradicted",
288            Self::Retracted => "retracted",
289        }
290    }
291    pub fn parse_db(s: &str) -> Option<Self> {
292        match s {
293            "active" => Some(Self::Active),
294            "superseded" => Some(Self::Superseded),
295            "contradicted" => Some(Self::Contradicted),
296            "retracted" => Some(Self::Retracted),
297            _ => None,
298        }
299    }
300}
301
302/// Canonical string representation of an Object for StatementId hashing (DESIGN §5.6).
303/// Prefix-based to avoid JSON float formatting issues.
304pub fn object_repr(object: &Object) -> String {
305    match object {
306        Object::Entity(id) => format!("e:{id}"),
307        Object::Literal(TypedValue::Text(s)) => format!("t:{s}"),
308        Object::Literal(TypedValue::Date(s)) => format!("d:{s}"),
309        Object::Literal(TypedValue::DateTime(s)) => format!("dt:{s}"),
310        Object::Literal(TypedValue::Number(n)) => format!("n:{}", canonical_f64(*n)),
311        Object::Literal(TypedValue::Bool(b)) => format!("b:{b}"),
312        Object::Literal(TypedValue::Quantity { value, unit }) => {
313            format!("q:{}:{unit}", canonical_f64(*value))
314        }
315        Object::Literal(TypedValue::Enum(s)) => format!("en:{s}"),
316    }
317}
318
319/// Canonical f64: avoid -0.0, normalize integer-valued floats.
320fn canonical_f64(n: f64) -> String {
321    if n == 0.0 {
322        "0".to_string()
323    } else if n.fract() == 0.0 && n.abs() < 1e15 {
324        format!("{}", n as i64)
325    } else {
326        format!("{n}")
327    }
328}
329
330/// Canonical string representation of a claim for AssertionId hashing.
331/// Uses f32::to_bits() for deterministic float representation.
332pub fn claim_repr(
333    polarity: Polarity,
334    claimed_from: Timestamp,
335    claimed_to: Timestamp,
336    confidence: f32,
337) -> String {
338    format!(
339        "{}:{}:{}:{}",
340        match polarity {
341            Polarity::Affirm => "a",
342            Polarity::Deny => "d",
343        },
344        claimed_from.millis(),
345        claimed_to.millis(),
346        confidence.to_bits(),
347    )
348}