1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! Cross-episode memory-convergence reconciliation — the pure decision core.
//!
//! At structured ingest, every freshly-extracted fact is classified against the
//! existing in-scope rows that assert the SAME `(subject, predicate)` (read via
//! [`lunaris_core::keyspace::fact_spo_key`]). [`classify_fact`] is the pure heart
//! of the hybrid memory-update feature (`memory-update-intelligence`):
//!
//! - **[`FactDecision::Noop`]** — the new triple re-asserts an EXISTING
//! `(subject, predicate, object)`. Its deterministic
//! [`FactId`](lunaris_extract::types::FactId) collides with the prior row, so
//! the write overwrites in place: no duplicate row accrues (sync dedup).
//! - **[`FactDecision::Supersede`]** — the new fact asserts a DIFFERENT object
//! whose validity window OVERLAPS an existing one. This is a cross-episode
//! contradiction; the named loser is routed through the async verify →
//! `apply_supersede` path (latest-assertion-wins) to close its bi-temporal
//! interval.
//! - **[`FactDecision::Append`]** — a brand-new `(subject, predicate)`, or a
//! different object with a DISJOINT window (legitimate temporal succession,
//! e.g. employer A `[2020, 2022)` then employer B `[2023, …)`). Additive,
//! never a false supersede.
//!
//! Keeping the policy a pure function over `(new, prior[])` makes it testable
//! without storage or model weights; the structured-ingest write path supplies
//! `prior` from the spo-index read and acts on the returned decision.
use ;
use EntityId;
use Ulid;
/// A newly-extracted fact triple awaiting classification, with its asserted
/// validity window (half-open `[valid_from, valid_to)`; `valid_to == None`
/// means the assertion is still open / current).
/// One existing row from the `(subject, predicate)` spo-index — the prior facts
/// already stored in this scope for the same subject + predicate.
/// The reconciliation verdict for a new fact against its prior spo-index rows.
/// Classify `new` against the existing `(subject, predicate)` rows `prior`.
///
/// Precedence:
/// 1. An EXACT object match (re-assertion) is a [`FactDecision::Noop`],
/// regardless of the validity window — dedup wins over everything.
/// 2. Otherwise the FIRST prior row with a DIFFERENT object whose window
/// overlaps the new one yields a [`FactDecision::Supersede`] naming that
/// row as the loser (latest-assertion-wins; if several priors overlap, the
/// earliest-seen is named — multi-overlap is itself a pre-existing
/// inconsistency the verifier resolves over successive passes).
/// 3. Otherwise (no prior, or only disjoint windows) it is additive:
/// [`FactDecision::Append`].
/// Do two half-open intervals `[a_from, a_to)` and `[b_from, b_to)` overlap?
/// A `None` upper bound is treated as `+∞` (still-open assertion).