Skip to main content

graphrefly/
patterns.rs

1//! Horizontal graph-visible patterns.
2//!
3//! D158 allows semantic-memory patterns when they are ordinary graph nodes with
4//! declared deps and graph-visible facts. This module intentionally owns no
5//! storage restore/hydration, scheduler, vector DB, LLM extraction, retention
6//! loop, consolidation loop, or protocol behavior.
7
8use std::collections::{BTreeMap, HashSet};
9use std::rc::Rc;
10
11use crate::graph::{Graph, GraphNodeOpts};
12use crate::json::JsonValue;
13use crate::node::{Node, NodeOpts};
14use crate::operators::Operator;
15
16/// Stable identity for a semantic-memory fact.
17pub type FactId = String;
18
19/// A single semantic-memory fact. This is pattern vocabulary, not a protocol
20/// message, storage record owner, restore contract, or agentic runtime.
21#[derive(Clone, Debug, PartialEq)]
22pub struct MemoryFragment<T> {
23    /// `id` field for id.
24    pub id: FactId,
25    /// `payload` field for payload.
26    pub payload: T,
27    /// `t_ns` field for t ns.
28    pub t_ns: u128,
29    /// `valid_from` field for valid from.
30    pub valid_from: Option<u128>,
31    /// `valid_to` field for valid to.
32    pub valid_to: Option<u128>,
33    /// `confidence` field for confidence.
34    pub confidence: f64,
35    /// `tags` field for tags.
36    pub tags: Vec<String>,
37    /// `sources` field for sources.
38    pub sources: Vec<FactId>,
39    /// `embedding` field for embedding.
40    pub embedding: Option<Vec<f64>>,
41    /// `parent_fragment_id` field for parent fragment id.
42    pub parent_fragment_id: Option<FactId>,
43    /// `provenance` field for provenance.
44    pub provenance: Option<String>,
45}
46
47impl<T> MemoryFragment<T> {
48    /// Creates or computes `new`.
49    pub fn new(id: impl Into<String>, payload: T, t_ns: u128) -> Self {
50        Self {
51            id: id.into(),
52            payload,
53            t_ns,
54            valid_from: None,
55            valid_to: None,
56            confidence: 1.0,
57            tags: Vec::new(),
58            sources: Vec::new(),
59            embedding: None,
60            parent_fragment_id: None,
61            provenance: None,
62        }
63    }
64}
65
66/// Passive lower-layer KG assertion object vocabulary.
67///
68/// D165 keeps KG assertions independent from the agentic-memory record
69/// envelope. Agentic solution bundles may project to this shape, but lower KG
70/// reducers do not need `AgenticMemoryRecord`.
71#[derive(Clone, Debug, PartialEq)]
72pub enum KnowledgeAssertionObject {
73    /// `Entity` variant.
74    Entity {
75        /// `entity_id` field for `Entity`.
76        entity_id: FactId,
77    },
78    /// `Literal` variant.
79    Literal {
80        /// `value` field for `Literal`.
81        value: JsonValue,
82    },
83}
84
85/// Passive KG assertion fact.
86#[derive(Clone, Debug, PartialEq)]
87pub struct KnowledgeAssertion {
88    /// `id` field for id.
89    pub id: FactId,
90    /// `subject_id` field for subject id.
91    pub subject_id: FactId,
92    /// `predicate` field for predicate.
93    pub predicate: String,
94    /// `object` field for object.
95    pub object: KnowledgeAssertionObject,
96    /// `sources` field for sources.
97    pub sources: Vec<FactId>,
98    /// `confidence` field for confidence.
99    pub confidence: f64,
100    /// `t_ns` field for t ns.
101    pub t_ns: u128,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
105/// `KnowledgeGraphPolicy` data container.
106pub struct KnowledgeGraphPolicy {
107    /// `allowed_predicates` field for allowed predicates.
108    pub allowed_predicates: Vec<String>,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
112/// `KnowledgeGraphEntity` data container.
113pub struct KnowledgeGraphEntity {
114    /// `id` field for id.
115    pub id: FactId,
116    /// `assertion_ids` field for assertion ids.
117    pub assertion_ids: Vec<FactId>,
118    /// `subject_assertion_ids` field for subject assertion ids.
119    pub subject_assertion_ids: Vec<FactId>,
120    /// `object_assertion_ids` field for object assertion ids.
121    pub object_assertion_ids: Vec<FactId>,
122}
123
124#[derive(Clone, Debug, PartialEq)]
125/// `KnowledgeGraphRelation` data container.
126pub struct KnowledgeGraphRelation {
127    /// `assertion_id` field for assertion id.
128    pub assertion_id: FactId,
129    /// `subject_id` field for subject id.
130    pub subject_id: FactId,
131    /// `predicate` field for predicate.
132    pub predicate: String,
133    /// `object` field for object.
134    pub object: KnowledgeAssertionObject,
135    /// `sources` field for sources.
136    pub sources: Vec<FactId>,
137    /// `confidence` field for confidence.
138    pub confidence: f64,
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
142/// `KnowledgeGraphTopic` data container.
143pub struct KnowledgeGraphTopic {
144    /// `predicate` field for predicate.
145    pub predicate: String,
146    /// `assertion_ids` field for assertion ids.
147    pub assertion_ids: Vec<FactId>,
148    /// `entity_ids` field for entity ids.
149    pub entity_ids: Vec<FactId>,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153/// `KnowledgeGraphIndex` data container.
154pub struct KnowledgeGraphIndex {
155    /// `assertion_ids` field for assertion ids.
156    pub assertion_ids: Vec<FactId>,
157    /// `entity_ids` field for entity ids.
158    pub entity_ids: Vec<FactId>,
159    /// `relation_ids` field for relation ids.
160    pub relation_ids: Vec<FactId>,
161    /// `predicates` field for predicates.
162    pub predicates: Vec<String>,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq)]
166/// `KnowledgeGraphCursor` data container.
167pub struct KnowledgeGraphCursor {
168    /// `evaluation` field for evaluation.
169    pub evaluation: u64,
170    /// `valid_assertions` field for valid assertions.
171    pub valid_assertions: usize,
172    /// `invalid_assertions` field for invalid assertions.
173    pub invalid_assertions: usize,
174    /// `entity_count` field for entity count.
175    pub entity_count: usize,
176    /// `relation_count` field for relation count.
177    pub relation_count: usize,
178    /// `predicate_count` field for predicate count.
179    pub predicate_count: usize,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183/// `KnowledgeGraphStatusState` variants.
184pub enum KnowledgeGraphStatusState {
185    /// `Ready` variant.
186    Ready,
187    /// `Empty` variant.
188    Empty,
189    /// `Partial` variant.
190    Partial,
191    /// `Error` variant.
192    Error,
193}
194
195#[derive(Clone, Debug, PartialEq, Eq)]
196/// `KnowledgeGraphStatus` data container.
197pub struct KnowledgeGraphStatus {
198    /// `state` field for state.
199    pub state: KnowledgeGraphStatusState,
200    /// `cursor` field for cursor.
201    pub cursor: KnowledgeGraphCursor,
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq)]
205/// `KnowledgeGraphErrorCode` variants.
206pub enum KnowledgeGraphErrorCode {
207    /// `InvalidAssertion` variant.
208    InvalidAssertion,
209    /// `DuplicateAssertionId` variant.
210    DuplicateAssertionId,
211    /// `PolicyConflict` variant.
212    PolicyConflict,
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216/// `KnowledgeGraphError` data container.
217pub struct KnowledgeGraphError {
218    /// `code` field for code.
219    pub code: KnowledgeGraphErrorCode,
220    /// `message` field for message.
221    pub message: String,
222    /// `index` field for index.
223    pub index: Option<usize>,
224    /// `assertion_id` field for assertion id.
225    pub assertion_id: Option<FactId>,
226    /// `validation_errors` field for validation errors.
227    pub validation_errors: Vec<String>,
228    /// `cursor` field for cursor.
229    pub cursor: KnowledgeGraphCursor,
230}
231
232#[derive(Clone, Debug, PartialEq)]
233/// `KnowledgeGraphSnapshot` data container.
234pub struct KnowledgeGraphSnapshot {
235    /// `assertions` field for assertions.
236    pub assertions: Vec<KnowledgeAssertion>,
237    /// `entities` field for entities.
238    pub entities: Vec<KnowledgeGraphEntity>,
239    /// `relations` field for relations.
240    pub relations: Vec<KnowledgeGraphRelation>,
241    /// `topics` field for topics.
242    pub topics: Vec<KnowledgeGraphTopic>,
243    /// `index` field for index.
244    pub index: KnowledgeGraphIndex,
245    /// `status` field for status.
246    pub status: KnowledgeGraphStatus,
247    /// `errors` field for errors.
248    pub errors: Vec<KnowledgeGraphError>,
249    /// `cursor` field for cursor.
250    pub cursor: KnowledgeGraphCursor,
251}
252
253#[derive(Clone)]
254/// `KnowledgeGraphReducerBundleOptions` data container.
255pub struct KnowledgeGraphReducerBundleOptions {
256    /// `name` field for name.
257    pub name: Option<String>,
258    /// `assertions` field for assertions.
259    pub assertions: Node<Vec<KnowledgeAssertion>>,
260    /// `policy` field for policy.
261    pub policy: Option<Node<KnowledgeGraphPolicy>>,
262}
263
264impl KnowledgeGraphReducerBundleOptions {
265    /// Creates or computes `new`.
266    pub fn new(assertions: Node<Vec<KnowledgeAssertion>>) -> Self {
267        Self {
268            name: None,
269            assertions,
270            policy: None,
271        }
272    }
273
274    /// Updates or reads `named`.
275    pub fn named(mut self, name: impl Into<String>) -> Self {
276        self.name = Some(name.into());
277        self
278    }
279
280    /// Updates or reads `with_policy`.
281    pub fn with_policy(mut self, policy: Node<KnowledgeGraphPolicy>) -> Self {
282        self.policy = Some(policy);
283        self
284    }
285}
286
287#[derive(Clone)]
288/// `KnowledgeGraphReducerBundle` data container.
289pub struct KnowledgeGraphReducerBundle {
290    /// `assertions_input` field for assertions input.
291    pub assertions_input: Node<Vec<KnowledgeAssertion>>,
292    /// `policy_input` field for policy input.
293    pub policy_input: Option<Node<KnowledgeGraphPolicy>>,
294    /// `snapshot` field for snapshot.
295    pub snapshot: Node<KnowledgeGraphSnapshot>,
296    /// `assertions` field for assertions.
297    pub assertions: Node<Vec<KnowledgeAssertion>>,
298    /// `entities` field for entities.
299    pub entities: Node<Vec<KnowledgeGraphEntity>>,
300    /// `relations` field for relations.
301    pub relations: Node<Vec<KnowledgeGraphRelation>>,
302    /// `topics` field for topics.
303    pub topics: Node<Vec<KnowledgeGraphTopic>>,
304    /// `index` field for index.
305    pub index: Node<KnowledgeGraphIndex>,
306    /// `status` field for status.
307    pub status: Node<KnowledgeGraphStatus>,
308    /// `errors` field for errors.
309    pub errors: Node<Vec<KnowledgeGraphError>>,
310    /// `cursor` field for cursor.
311    pub cursor: Node<KnowledgeGraphCursor>,
312}
313
314/// `ShardKey` type alias.
315pub type ShardKey = String;
316
317#[derive(Clone, Debug, Default, PartialEq)]
318/// `FactStore` data container.
319pub struct FactStore<T> {
320    /// `by_id` field for by id.
321    pub by_id: BTreeMap<FactId, MemoryFragment<T>>,
322}
323
324impl<T> FactStore<T> {
325    /// Updates or reads `read_handle`.
326    pub fn read_handle(&self) -> StoreReadHandle<'_, T> {
327        StoreReadHandle { by_id: &self.by_id }
328    }
329}
330
331#[derive(Clone, Copy, Debug)]
332/// `StoreReadHandle` data container.
333pub struct StoreReadHandle<'a, T> {
334    by_id: &'a BTreeMap<FactId, MemoryFragment<T>>,
335}
336
337impl<'a, T> StoreReadHandle<'a, T> {
338    /// Updates or reads `get`.
339    pub fn get(&self, id: &str) -> Option<&'a MemoryFragment<T>> {
340        self.by_id.get(id)
341    }
342
343    /// Updates or reads `has`.
344    pub fn has(&self, id: &str) -> bool {
345        self.by_id.contains_key(id)
346    }
347
348    /// Updates or reads `size`.
349    pub fn size(&self) -> usize {
350        self.by_id.len()
351    }
352
353    /// Updates or reads `values`.
354    pub fn values(&self) -> impl Iterator<Item = &'a MemoryFragment<T>> {
355        self.by_id.values()
356    }
357}
358
359#[derive(Clone, Debug, Default, PartialEq)]
360/// `MemoryQuery` data container.
361pub struct MemoryQuery {
362    /// `tags` field for tags.
363    pub tags: Vec<String>,
364    /// `as_of` field for as of.
365    pub as_of: Option<u128>,
366    /// `min_confidence` field for min confidence.
367    pub min_confidence: Option<f64>,
368    /// `limit` field for limit.
369    pub limit: Option<usize>,
370}
371
372#[derive(Clone, Debug, PartialEq, Eq)]
373/// `MemoryFragmentValidation` data container.
374pub struct MemoryFragmentValidation {
375    /// `ok` field for ok.
376    pub ok: bool,
377    /// `errors` field for errors.
378    pub errors: Vec<String>,
379}
380
381#[derive(Clone, Debug, PartialEq)]
382/// `OutcomeSignal` data container.
383pub struct OutcomeSignal {
384    /// `fact_id` field for fact id.
385    pub fact_id: FactId,
386    /// `reward` field for reward.
387    pub reward: f64,
388}
389
390#[derive(Clone, Debug, PartialEq)]
391/// `CollectionEntry` data container.
392pub struct CollectionEntry<T> {
393    /// `id` field for id.
394    pub id: String,
395    /// `value` field for value.
396    pub value: T,
397    /// `created_at_ns` field for created at ns.
398    pub created_at_ns: u128,
399    /// `last_access_ns` field for last access ns.
400    pub last_access_ns: u128,
401    /// `base_score` field for base score.
402    pub base_score: f64,
403}
404
405#[derive(Clone, Debug, PartialEq)]
406/// `RankedCollectionEntry` data container.
407pub struct RankedCollectionEntry<T> {
408    /// `entry` field for entry.
409    pub entry: CollectionEntry<T>,
410    /// `score` field for score.
411    pub score: f64,
412}
413
414#[derive(Clone, Debug, Default, PartialEq)]
415/// `RetrievalQuery` data container.
416pub struct RetrievalQuery {
417    /// `text` field for text.
418    pub text: Option<String>,
419    /// `vector` field for vector.
420    pub vector: Option<Vec<f64>>,
421    /// `entity_ids` field for entity ids.
422    pub entity_ids: Vec<String>,
423    /// `context` field for context.
424    pub context: Vec<String>,
425}
426
427#[derive(Clone, Debug, PartialEq)]
428/// `VectorSearchResult` data container.
429pub struct VectorSearchResult<TMeta> {
430    /// `id` field for id.
431    pub id: String,
432    /// `score` field for score.
433    pub score: f64,
434    /// `meta` field for meta.
435    pub meta: Option<TMeta>,
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439/// `RetrievalEntrySource` variants.
440pub enum RetrievalEntrySource {
441    /// `Vector` variant.
442    Vector,
443    /// `Graph` variant.
444    Graph,
445    /// `Store` variant.
446    Store,
447}
448
449#[derive(Clone, Debug, PartialEq)]
450/// `RetrievalEntry` data container.
451pub struct RetrievalEntry<TMem> {
452    /// `key` field for key.
453    pub key: String,
454    /// `value` field for value.
455    pub value: TMem,
456    /// `score` field for score.
457    pub score: f64,
458    /// `sources` field for sources.
459    pub sources: Vec<RetrievalEntrySource>,
460    /// `context` field for context.
461    pub context: Vec<String>,
462}
463
464#[derive(Clone, Debug, PartialEq)]
465/// `RetrievalTrace` data container.
466pub struct RetrievalTrace<TMem> {
467    /// `vector_candidates` field for vector candidates.
468    pub vector_candidates: Vec<VectorSearchResult<TMem>>,
469    /// `graph_expanded` field for graph expanded.
470    pub graph_expanded: Vec<String>,
471    /// `ranked` field for ranked.
472    pub ranked: Vec<RetrievalEntry<TMem>>,
473    /// `packed` field for packed.
474    pub packed: Vec<RetrievalEntry<TMem>>,
475}
476
477/// `AdmissionThresholds` type alias.
478pub type AdmissionThresholds = BTreeMap<String, f64>;
479/// `AdmissionScoreFn` type alias.
480pub type AdmissionScoreFn<TRaw> = Rc<dyn Fn(&TRaw) -> BTreeMap<String, f64>>;
481/// `AdmissionScore3DFn` type alias.
482pub type AdmissionScore3DFn<TRaw> = Rc<dyn Fn(&TRaw) -> AdmissionScores>;
483/// `TenantShardFn` type alias.
484pub type TenantShardFn<T> = Rc<dyn Fn(&MemoryFragment<T>) -> String>;
485/// `ShardByFn` type alias.
486pub type ShardByFn<T> = Rc<dyn Fn(&MemoryFragment<T>) -> ShardKey>;
487
488/// `AdmissionScoredOptions` data container.
489pub struct AdmissionScoredOptions<TRaw> {
490    /// `score_fn` field for score fn.
491    pub score_fn: AdmissionScoreFn<TRaw>,
492    /// `thresholds` field for thresholds.
493    pub thresholds: AdmissionThresholds,
494}
495
496#[derive(Clone, Copy, Debug, PartialEq)]
497/// `AdmissionScores` data container.
498pub struct AdmissionScores {
499    /// `persistence` field for persistence.
500    pub persistence: f64,
501    /// `structure` field for structure.
502    pub structure: f64,
503    /// `personal_value` field for personal value.
504    pub personal_value: f64,
505}
506
507/// `AdmissionScore3DOptions` data container.
508pub struct AdmissionScore3DOptions<TRaw> {
509    /// `score_fn` field for score fn.
510    pub score_fn: AdmissionScore3DFn<TRaw>,
511    /// `persistence_threshold` field for persistence threshold.
512    pub persistence_threshold: f64,
513    /// `personal_value_threshold` field for personal value threshold.
514    pub personal_value_threshold: f64,
515    /// `require_structured` field for require structured.
516    pub require_structured: bool,
517}
518
519impl<TRaw> AdmissionScore3DOptions<TRaw> {
520    /// Creates or computes `new`.
521    pub fn new(score_fn: AdmissionScore3DFn<TRaw>) -> Self {
522        Self {
523            score_fn,
524            persistence_threshold: 0.3,
525            personal_value_threshold: 0.3,
526            require_structured: false,
527        }
528    }
529}
530
531#[derive(Clone, Debug, Default, PartialEq, Eq)]
532/// `ShardByTenantOptions` data container.
533pub struct ShardByTenantOptions {
534    /// `tenants` field for tenants.
535    pub tenants: Vec<String>,
536    /// `shard_count` field for shard count.
537    pub shard_count: Option<usize>,
538}
539
540/// `ShardByTenantConfig` data container.
541pub struct ShardByTenantConfig<T> {
542    /// `shard_by` field for shard by.
543    pub shard_by: ShardByFn<T>,
544    /// `shard_count` field for shard count.
545    pub shard_count: usize,
546}
547
548/// Creates or computes `cosine_similarity`.
549pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
550    let n = a.len().max(b.len());
551    let mut dot = 0.0;
552    let mut na = 0.0;
553    let mut nb = 0.0;
554    for i in 0..n {
555        let av = a.get(i).copied().unwrap_or(0.0);
556        let bv = b.get(i).copied().unwrap_or(0.0);
557        dot += av * bv;
558        na += av * av;
559        nb += bv * bv;
560    }
561    if na == 0.0 || nb == 0.0 {
562        return 0.0;
563    }
564    let score = dot / (na.sqrt() * nb.sqrt());
565    if score.is_finite() {
566        score
567    } else {
568        0.0
569    }
570}
571
572/// Creates or computes `memory_fragment_valid_at`.
573pub fn memory_fragment_valid_at<T>(fragment: &MemoryFragment<T>, as_of: Option<u128>) -> bool {
574    match as_of {
575        None => fragment.valid_from.is_none() && fragment.valid_to.is_none(),
576        Some(as_of) => {
577            if fragment
578                .valid_from
579                .is_some_and(|valid_from| valid_from > as_of)
580            {
581                return false;
582            }
583            if fragment.valid_to.is_some_and(|valid_to| valid_to <= as_of) {
584                return false;
585            }
586            true
587        }
588    }
589}
590
591/// Creates or computes `memory_fragment_matches_query`.
592pub fn memory_fragment_matches_query<T>(fragment: &MemoryFragment<T>, query: &MemoryQuery) -> bool {
593    if !memory_fragment_valid_at(fragment, query.as_of) {
594        return false;
595    }
596    if query
597        .min_confidence
598        .is_some_and(|min_confidence| fragment.confidence < min_confidence)
599    {
600        return false;
601    }
602    if !query.tags.is_empty()
603        && !query
604            .tags
605            .iter()
606            .any(|tag| fragment.tags.iter().any(|fragment_tag| fragment_tag == tag))
607    {
608        return false;
609    }
610    true
611}
612
613/// Creates or computes `filter_memory_fragments`.
614pub fn filter_memory_fragments<T: Clone>(
615    fragments: impl IntoIterator<Item = MemoryFragment<T>>,
616    query: &MemoryQuery,
617) -> Vec<MemoryFragment<T>> {
618    let mut ranked: Vec<_> = fragments
619        .into_iter()
620        .filter(|fragment| memory_fragment_matches_query(fragment, query))
621        .collect();
622    ranked.sort_by(|a, b| {
623        b.confidence
624            .total_cmp(&a.confidence)
625            .then_with(|| b.t_ns.cmp(&a.t_ns))
626    });
627    if let Some(limit) = query.limit {
628        ranked.truncate(limit);
629    }
630    ranked
631}
632
633/// Creates or computes `validate_memory_fragment`.
634pub fn validate_memory_fragment<T>(fragment: &MemoryFragment<T>) -> MemoryFragmentValidation {
635    let errors = validate_fragment(fragment);
636    MemoryFragmentValidation {
637        ok: errors.is_empty(),
638        errors,
639    }
640}
641
642/// Creates or computes `admission_scored`.
643pub fn admission_scored<TRaw>(opts: AdmissionScoredOptions<TRaw>) -> impl Fn(&TRaw) -> bool {
644    move |raw| {
645        let scores = (opts.score_fn)(raw);
646        opts.thresholds.iter().all(|(dimension, threshold)| {
647            scores
648                .get(dimension)
649                .copied()
650                .filter(|score| score.is_finite())
651                .is_some_and(|score| score >= *threshold)
652        })
653    }
654}
655
656/// Creates or computes `admission_filter_3d`.
657pub fn admission_filter_3d<TRaw>(opts: AdmissionScore3DOptions<TRaw>) -> impl Fn(&TRaw) -> bool {
658    move |raw| {
659        let scores = (opts.score_fn)(raw);
660        score_at_least(scores.persistence, opts.persistence_threshold)
661            && score_at_least(scores.personal_value, opts.personal_value_threshold)
662            && (!opts.require_structured || score_at_least(scores.structure, f64::MIN_POSITIVE))
663    }
664}
665
666/// Creates or computes `shard_by_tenant`.
667pub fn shard_by_tenant<T: 'static>(
668    tenant_of: TenantShardFn<T>,
669    opts: ShardByTenantOptions,
670) -> ShardByTenantConfig<T> {
671    if !opts.tenants.is_empty() {
672        let mut tenants = Vec::<String>::new();
673        for tenant in opts.tenants {
674            if !tenants.iter().any(|known| known == &tenant) {
675                tenants.push(tenant);
676            }
677        }
678        let index: BTreeMap<_, _> = tenants
679            .iter()
680            .enumerate()
681            .map(|(i, tenant)| (tenant.clone(), i.to_string()))
682            .collect();
683        let overflow = tenants.len().to_string();
684        return ShardByTenantConfig {
685            shard_count: tenants.len() + 1,
686            shard_by: Rc::new(move |fragment| {
687                index
688                    .get(&(tenant_of)(fragment))
689                    .cloned()
690                    .unwrap_or_else(|| overflow.clone())
691            }),
692        };
693    }
694    let shard_count = opts.shard_count.unwrap_or(4).max(1);
695    ShardByTenantConfig {
696        shard_count,
697        shard_by: Rc::new(move |fragment| (tenant_of)(fragment)),
698    }
699}
700
701fn score_at_least(score: f64, min: f64) -> bool {
702    score.is_finite() && score >= min
703}
704
705/// Structured query over semantic-memory facts.
706#[derive(Clone, Debug, Default, PartialEq)]
707pub struct MemoryRetrievalQuery {
708    /// `tags` field for tags.
709    pub tags: Vec<String>,
710    /// `as_of` field for as of.
711    pub as_of: Option<u128>,
712    /// `min_confidence` field for min confidence.
713    pub min_confidence: Option<f64>,
714    /// `limit` field for limit.
715    pub limit: Option<usize>,
716    /// `vector` field for vector.
717    pub vector: Option<Vec<f64>>,
718}
719
720impl MemoryRetrievalQuery {
721    /// Updates or reads `memory_query`.
722    pub fn memory_query(&self) -> MemoryQuery {
723        MemoryQuery {
724            tags: self.tags.clone(),
725            as_of: self.as_of,
726            min_confidence: self.min_confidence,
727            limit: self.limit,
728        }
729    }
730}
731
732#[derive(Clone, Debug, PartialEq, Eq)]
733/// `MemoryRetrievalCursor` data container.
734pub struct MemoryRetrievalCursor {
735    /// `evaluation` field for evaluation.
736    pub evaluation: u64,
737    /// `valid_fragments` field for valid fragments.
738    pub valid_fragments: usize,
739    /// `invalid_fragments` field for invalid fragments.
740    pub invalid_fragments: usize,
741    /// `result_count` field for result count.
742    pub result_count: usize,
743}
744
745#[derive(Clone, Copy, Debug, PartialEq, Eq)]
746/// `MemoryRetrievalStatusState` variants.
747pub enum MemoryRetrievalStatusState {
748    /// `Ready` variant.
749    Ready,
750    /// `Empty` variant.
751    Empty,
752    /// `Partial` variant.
753    Partial,
754    /// `Error` variant.
755    Error,
756}
757
758#[derive(Clone, Debug, PartialEq)]
759/// `MemoryRetrievalStatus` data container.
760pub struct MemoryRetrievalStatus {
761    /// `state` field for state.
762    pub state: MemoryRetrievalStatusState,
763    /// `query` field for query.
764    pub query: MemoryRetrievalQuery,
765    /// `cursor` field for cursor.
766    pub cursor: MemoryRetrievalCursor,
767}
768
769#[derive(Clone, Debug, PartialEq)]
770/// `MemoryRetrievalIndex` data container.
771pub struct MemoryRetrievalIndex<T> {
772    /// `ids` field for ids.
773    pub ids: Vec<FactId>,
774    /// `by_id` field for by id.
775    pub by_id: BTreeMap<FactId, MemoryFragment<T>>,
776    /// `cursor` field for cursor.
777    pub cursor: MemoryRetrievalCursor,
778}
779
780#[derive(Clone, Copy, Debug, PartialEq, Eq)]
781/// `MemoryRetrievalErrorCode` variants.
782pub enum MemoryRetrievalErrorCode {
783    /// `DuplicateFragmentId` variant.
784    DuplicateFragmentId,
785    /// `InvalidFragment` variant.
786    InvalidFragment,
787    /// `InvalidQuery` variant.
788    InvalidQuery,
789    /// `InvalidQueryVector` variant.
790    InvalidQueryVector,
791}
792
793#[derive(Clone, Debug, PartialEq, Eq)]
794/// `MemoryRetrievalError` data container.
795pub struct MemoryRetrievalError {
796    /// `code` field for code.
797    pub code: MemoryRetrievalErrorCode,
798    /// `message` field for message.
799    pub message: String,
800    /// `index` field for index.
801    pub index: Option<usize>,
802    /// `fragment_id` field for fragment id.
803    pub fragment_id: Option<FactId>,
804    /// `validation_errors` field for validation errors.
805    pub validation_errors: Vec<String>,
806    /// `cursor` field for cursor.
807    pub cursor: MemoryRetrievalCursor,
808}
809
810#[derive(Clone, Debug, PartialEq)]
811/// `MemoryAnswer` data container.
812pub struct MemoryAnswer<T> {
813    /// `query` field for query.
814    pub query: MemoryRetrievalQuery,
815    /// `results` field for results.
816    pub results: Vec<MemoryFragment<T>>,
817}
818
819#[derive(Clone, Debug, PartialEq)]
820/// `MemoryRetrievalSnapshot` data container.
821pub struct MemoryRetrievalSnapshot<T> {
822    /// `fragments` field for fragments.
823    pub fragments: Vec<MemoryFragment<T>>,
824    /// `indexed` field for indexed.
825    pub indexed: MemoryRetrievalIndex<T>,
826    /// `ranked` field for ranked.
827    pub ranked: MemoryAnswer<T>,
828    /// `status` field for status.
829    pub status: MemoryRetrievalStatus,
830    /// `errors` field for errors.
831    pub errors: Vec<MemoryRetrievalError>,
832    /// `cursor` field for cursor.
833    pub cursor: MemoryRetrievalCursor,
834}
835
836/// Alias for the aggregate DATA fact emitted by the snapshot node.
837pub type MemoryRetrievalFact<T> = MemoryRetrievalSnapshot<T>;
838
839#[derive(Clone)]
840/// `MemoryRetrievalBundleOptions` data container.
841pub struct MemoryRetrievalBundleOptions<T> {
842    /// `name` field for name.
843    pub name: Option<String>,
844    /// `fragments` field for fragments.
845    pub fragments: Node<Vec<MemoryFragment<T>>>,
846    /// `query` field for query.
847    pub query: Node<MemoryRetrievalQuery>,
848}
849
850impl<T> MemoryRetrievalBundleOptions<T> {
851    /// Creates or computes `new`.
852    pub fn new(fragments: Node<Vec<MemoryFragment<T>>>, query: Node<MemoryRetrievalQuery>) -> Self {
853        Self {
854            name: None,
855            fragments,
856            query,
857        }
858    }
859
860    /// Updates or reads `named`.
861    pub fn named(mut self, name: impl Into<String>) -> Self {
862        self.name = Some(name.into());
863        self
864    }
865}
866
867#[derive(Clone)]
868/// `MemoryRetrievalBundle` data container.
869pub struct MemoryRetrievalBundle<T> {
870    /// `fragments_input` field for fragments input.
871    pub fragments_input: Node<Vec<MemoryFragment<T>>>,
872    /// `query_input` field for query input.
873    pub query_input: Node<MemoryRetrievalQuery>,
874    /// `snapshot` field for snapshot.
875    pub snapshot: Node<MemoryRetrievalSnapshot<T>>,
876    /// `fragments` field for fragments.
877    pub fragments: Node<Vec<MemoryFragment<T>>>,
878    /// `indexed` field for indexed.
879    pub indexed: Node<MemoryRetrievalIndex<T>>,
880    /// `ranked` field for ranked.
881    pub ranked: Node<MemoryAnswer<T>>,
882    /// `status` field for status.
883    pub status: Node<MemoryRetrievalStatus>,
884    /// `errors` field for errors.
885    pub errors: Node<Vec<MemoryRetrievalError>>,
886    /// `cursor` field for cursor.
887    pub cursor: Node<MemoryRetrievalCursor>,
888}
889
890#[derive(Clone, Debug)]
891struct PendingMemoryRetrievalError {
892    code: MemoryRetrievalErrorCode,
893    message: String,
894    index: Option<usize>,
895    fragment_id: Option<FactId>,
896    validation_errors: Vec<String>,
897}
898
899/// Build a graph-visible memory retrieval bundle from explicit fragment/query deps.
900///
901/// Invalid fragments and ranking status are emitted as ordinary DATA facts. The
902/// bundle never owns storage restore or hidden mutation; callers that need
903/// persistence compose D161 collection/storage sidecars outside this pattern.
904pub fn memory_retrieval_bundle<T: Clone + 'static>(
905    graph: &Graph,
906    opts: MemoryRetrievalBundleOptions<T>,
907) -> MemoryRetrievalBundle<T> {
908    let name = opts.name.unwrap_or_else(|| "memoryRetrieval".to_owned());
909    let fragments = opts.fragments;
910    let query = opts.query;
911    let snapshot = graph.init_node(
912        Operator::with_opts("memoryRetrievalSnapshot", pattern_node_config(), |ctx| {
913            let evaluation = ctx
914                .state_get::<u64>()
915                .map(|evaluation| *evaluation + 1)
916                .unwrap_or(1);
917            let raw_fragments = ctx
918                .data::<Vec<MemoryFragment<T>>>(0)
919                .map(|fragments| (*fragments).clone())
920                .unwrap_or_default();
921            let raw_query = ctx
922                .data::<MemoryRetrievalQuery>(1)
923                .map(|query| (*query).clone())
924                .unwrap_or_default();
925            let query_errors = validate_query(&raw_query);
926            let current_query = if query_errors.is_empty() {
927                raw_query
928            } else {
929                MemoryRetrievalQuery::default()
930            };
931            let mut valid = Vec::new();
932            let mut seen = HashSet::new();
933            let mut errors = query_errors;
934
935            for (index, fragment) in raw_fragments.into_iter().enumerate() {
936                let fragment_errors = validate_fragment(&fragment);
937                if !fragment_errors.is_empty() {
938                    errors.push(PendingMemoryRetrievalError {
939                        code: MemoryRetrievalErrorCode::InvalidFragment,
940                        message: "memory_retrieval_bundle: invalid memory fragment".to_owned(),
941                        index: Some(index),
942                        fragment_id: Some(fragment.id.clone()),
943                        validation_errors: fragment_errors,
944                    });
945                    continue;
946                }
947                if !seen.insert(fragment.id.clone()) {
948                    errors.push(PendingMemoryRetrievalError {
949                        code: MemoryRetrievalErrorCode::DuplicateFragmentId,
950                        message: "memory_retrieval_bundle: duplicate fragment id".to_owned(),
951                        index: Some(index),
952                        fragment_id: Some(fragment.id.clone()),
953                        validation_errors: vec![format!("duplicate fragment id '{}'", fragment.id)],
954                    });
955                    continue;
956                }
957                valid.push(fragment);
958            }
959
960            let ranked = if errors
961                .iter()
962                .any(|error| !is_recoverable_fragment_error(error))
963            {
964                Vec::new()
965            } else {
966                rank_fragments(&valid, &current_query)
967            };
968            let cursor = MemoryRetrievalCursor {
969                evaluation,
970                valid_fragments: valid.len(),
971                invalid_fragments: errors
972                    .iter()
973                    .filter(|error| is_recoverable_fragment_error(error))
974                    .count(),
975                result_count: ranked.len(),
976            };
977            ctx.state_set(evaluation);
978            let status = MemoryRetrievalStatus {
979                state: status_state(&errors, ranked.len()),
980                query: current_query.clone(),
981                cursor: cursor.clone(),
982            };
983            let indexed = MemoryRetrievalIndex {
984                ids: valid.iter().map(|fragment| fragment.id.clone()).collect(),
985                by_id: valid
986                    .iter()
987                    .map(|fragment| (fragment.id.clone(), fragment.clone()))
988                    .collect(),
989                cursor: cursor.clone(),
990            };
991            let errors = errors
992                .into_iter()
993                .map(|error| MemoryRetrievalError {
994                    code: error.code,
995                    message: error.message,
996                    index: error.index,
997                    fragment_id: error.fragment_id,
998                    validation_errors: error.validation_errors,
999                    cursor: cursor.clone(),
1000                })
1001                .collect();
1002            ctx.emit(MemoryRetrievalSnapshot {
1003                fragments: valid,
1004                indexed,
1005                ranked: MemoryAnswer {
1006                    query: current_query,
1007                    results: ranked,
1008                },
1009                status,
1010                errors,
1011                cursor,
1012            });
1013        }),
1014        vec![fragments.erased(), query.erased()],
1015        named_graph_node_opts(format!("{name}/snapshot")),
1016    );
1017
1018    MemoryRetrievalBundle {
1019        fragments_input: fragments,
1020        query_input: query,
1021        fragments: retrieval_projection(
1022            graph,
1023            &snapshot,
1024            format!("{name}/fragments"),
1025            "memoryRetrievalFragments",
1026            |snapshot| snapshot.fragments.clone(),
1027        ),
1028        indexed: retrieval_projection(
1029            graph,
1030            &snapshot,
1031            format!("{name}/indexed"),
1032            "memoryRetrievalIndexed",
1033            |snapshot| snapshot.indexed.clone(),
1034        ),
1035        ranked: retrieval_projection(
1036            graph,
1037            &snapshot,
1038            format!("{name}/ranked"),
1039            "memoryRetrievalRanked",
1040            |snapshot| snapshot.ranked.clone(),
1041        ),
1042        status: retrieval_projection(
1043            graph,
1044            &snapshot,
1045            format!("{name}/status"),
1046            "memoryRetrievalStatus",
1047            |snapshot| snapshot.status.clone(),
1048        ),
1049        errors: retrieval_projection(
1050            graph,
1051            &snapshot,
1052            format!("{name}/errors"),
1053            "memoryRetrievalErrors",
1054            |snapshot| snapshot.errors.clone(),
1055        ),
1056        cursor: retrieval_projection(
1057            graph,
1058            &snapshot,
1059            format!("{name}/cursor"),
1060            "memoryRetrievalCursor",
1061            |snapshot| snapshot.cursor.clone(),
1062        ),
1063        snapshot,
1064    }
1065}
1066
1067/// Creates or computes `knowledge_graph_reducer_bundle`.
1068pub fn knowledge_graph_reducer_bundle(
1069    graph: &Graph,
1070    opts: KnowledgeGraphReducerBundleOptions,
1071) -> KnowledgeGraphReducerBundle {
1072    let name = opts.name.unwrap_or_else(|| "knowledgeGraph".to_owned());
1073    let assertions = opts.assertions;
1074    let policy = opts.policy;
1075    let mut deps = vec![assertions.erased()];
1076    if let Some(policy) = &policy {
1077        deps.push(policy.erased());
1078    }
1079    let has_policy = policy.is_some();
1080    let snapshot = graph.init_node(
1081        Operator::with_opts(
1082            "knowledgeGraphReducerSnapshot",
1083            pattern_node_config(),
1084            move |ctx| {
1085                let evaluation = ctx
1086                    .state_get::<u64>()
1087                    .map(|evaluation| *evaluation + 1)
1088                    .unwrap_or(1);
1089                let raw_assertions = ctx
1090                    .data::<Vec<KnowledgeAssertion>>(0)
1091                    .map(|assertions| (*assertions).clone())
1092                    .unwrap_or_default();
1093                let policy = if has_policy {
1094                    ctx.data::<KnowledgeGraphPolicy>(1)
1095                        .map(|policy| (*policy).clone())
1096                        .unwrap_or_default()
1097                } else {
1098                    KnowledgeGraphPolicy::default()
1099                };
1100                let reduced = reduce_knowledge_assertions(raw_assertions, &policy);
1101                let cursor = KnowledgeGraphCursor {
1102                    evaluation,
1103                    valid_assertions: reduced.assertions.len(),
1104                    invalid_assertions: reduced.errors.len(),
1105                    entity_count: reduced.entities.len(),
1106                    relation_count: reduced.relations.len(),
1107                    predicate_count: reduced.topics.len(),
1108                };
1109                let errors = reduced
1110                    .errors
1111                    .into_iter()
1112                    .map(|pending| KnowledgeGraphError {
1113                        code: pending.code,
1114                        message: pending.message,
1115                        index: pending.index,
1116                        assertion_id: pending.assertion_id,
1117                        validation_errors: pending.validation_errors,
1118                        cursor: cursor.clone(),
1119                    })
1120                    .collect::<Vec<_>>();
1121                let status = KnowledgeGraphStatus {
1122                    state: if reduced.assertions.is_empty() && errors.is_empty() {
1123                        KnowledgeGraphStatusState::Empty
1124                    } else if errors.is_empty() {
1125                        KnowledgeGraphStatusState::Ready
1126                    } else if !reduced.assertions.is_empty() {
1127                        KnowledgeGraphStatusState::Partial
1128                    } else {
1129                        KnowledgeGraphStatusState::Error
1130                    },
1131                    cursor: cursor.clone(),
1132                };
1133                ctx.state_set(evaluation);
1134                ctx.emit(KnowledgeGraphSnapshot {
1135                    assertions: reduced.assertions,
1136                    entities: reduced.entities,
1137                    relations: reduced.relations,
1138                    topics: reduced.topics,
1139                    index: reduced.index,
1140                    status,
1141                    errors,
1142                    cursor,
1143                });
1144            },
1145        ),
1146        deps,
1147        named_graph_node_opts(format!("{name}/snapshot")),
1148    );
1149
1150    KnowledgeGraphReducerBundle {
1151        assertions_input: assertions,
1152        policy_input: policy,
1153        assertions: kg_projection(
1154            graph,
1155            &snapshot,
1156            format!("{name}/assertions"),
1157            "knowledgeGraphAssertions",
1158            |snapshot| snapshot.assertions.clone(),
1159        ),
1160        entities: kg_projection(
1161            graph,
1162            &snapshot,
1163            format!("{name}/entities"),
1164            "knowledgeGraphEntities",
1165            |snapshot| snapshot.entities.clone(),
1166        ),
1167        relations: kg_projection(
1168            graph,
1169            &snapshot,
1170            format!("{name}/relations"),
1171            "knowledgeGraphRelations",
1172            |snapshot| snapshot.relations.clone(),
1173        ),
1174        topics: kg_projection(
1175            graph,
1176            &snapshot,
1177            format!("{name}/topics"),
1178            "knowledgeGraphTopics",
1179            |snapshot| snapshot.topics.clone(),
1180        ),
1181        index: kg_projection(
1182            graph,
1183            &snapshot,
1184            format!("{name}/index"),
1185            "knowledgeGraphIndex",
1186            |snapshot| snapshot.index.clone(),
1187        ),
1188        status: kg_projection(
1189            graph,
1190            &snapshot,
1191            format!("{name}/status"),
1192            "knowledgeGraphStatus",
1193            |snapshot| snapshot.status.clone(),
1194        ),
1195        errors: kg_projection(
1196            graph,
1197            &snapshot,
1198            format!("{name}/errors"),
1199            "knowledgeGraphErrors",
1200            |snapshot| snapshot.errors.clone(),
1201        ),
1202        cursor: kg_projection(
1203            graph,
1204            &snapshot,
1205            format!("{name}/cursor"),
1206            "knowledgeGraphCursor",
1207            |snapshot| snapshot.cursor.clone(),
1208        ),
1209        snapshot,
1210    }
1211}
1212
1213fn kg_projection<U, F>(
1214    graph: &Graph,
1215    snapshot: &Node<KnowledgeGraphSnapshot>,
1216    name: String,
1217    factory: &'static str,
1218    select: F,
1219) -> Node<U>
1220where
1221    U: 'static,
1222    F: Fn(&KnowledgeGraphSnapshot) -> U + 'static,
1223{
1224    graph.init_node(
1225        Operator::with_opts(factory, pattern_node_config(), move |ctx| {
1226            for snapshot in ctx.batch::<KnowledgeGraphSnapshot>(0) {
1227                ctx.emit(select(snapshot.as_ref()));
1228            }
1229        }),
1230        vec![snapshot.erased()],
1231        named_graph_node_opts(name),
1232    )
1233}
1234
1235#[derive(Clone, Debug)]
1236struct PendingKnowledgeGraphError {
1237    code: KnowledgeGraphErrorCode,
1238    message: String,
1239    index: Option<usize>,
1240    assertion_id: Option<FactId>,
1241    validation_errors: Vec<String>,
1242}
1243
1244struct ReducedKnowledgeGraph {
1245    assertions: Vec<KnowledgeAssertion>,
1246    entities: Vec<KnowledgeGraphEntity>,
1247    relations: Vec<KnowledgeGraphRelation>,
1248    topics: Vec<KnowledgeGraphTopic>,
1249    index: KnowledgeGraphIndex,
1250    errors: Vec<PendingKnowledgeGraphError>,
1251}
1252
1253fn reduce_knowledge_assertions(
1254    raw_assertions: Vec<KnowledgeAssertion>,
1255    policy: &KnowledgeGraphPolicy,
1256) -> ReducedKnowledgeGraph {
1257    let mut assertions = Vec::new();
1258    let mut errors = Vec::new();
1259    let mut seen = HashSet::new();
1260    for (index, assertion) in raw_assertions.into_iter().enumerate() {
1261        let validation = validate_knowledge_assertion(&assertion, policy);
1262        if !validation.is_empty() {
1263            errors.push(PendingKnowledgeGraphError {
1264                code: if validation.iter().any(|error| error.contains("policy")) {
1265                    KnowledgeGraphErrorCode::PolicyConflict
1266                } else {
1267                    KnowledgeGraphErrorCode::InvalidAssertion
1268                },
1269                message: "knowledge_graph_reducer_bundle: assertion is invalid".to_owned(),
1270                index: Some(index),
1271                assertion_id: Some(assertion.id.clone()),
1272                validation_errors: validation,
1273            });
1274            continue;
1275        }
1276        if !seen.insert(assertion.id.clone()) {
1277            errors.push(PendingKnowledgeGraphError {
1278                code: KnowledgeGraphErrorCode::DuplicateAssertionId,
1279                message: "knowledge_graph_reducer_bundle: duplicate assertion id".to_owned(),
1280                index: Some(index),
1281                assertion_id: Some(assertion.id.clone()),
1282                validation_errors: vec![format!("duplicate assertion id '{}'", assertion.id)],
1283            });
1284            continue;
1285        }
1286        assertions.push(assertion);
1287    }
1288    let (entities, relations, topics, index) = materialize_knowledge_graph(&assertions);
1289    ReducedKnowledgeGraph {
1290        assertions,
1291        entities,
1292        relations,
1293        topics,
1294        index,
1295        errors,
1296    }
1297}
1298
1299fn validate_knowledge_assertion(
1300    assertion: &KnowledgeAssertion,
1301    policy: &KnowledgeGraphPolicy,
1302) -> Vec<String> {
1303    let mut errors = Vec::new();
1304    if assertion.id.is_empty() {
1305        errors.push("id must be a non-empty string".to_owned());
1306    }
1307    if assertion.subject_id.is_empty() {
1308        errors.push("subject_id must be a non-empty string".to_owned());
1309    }
1310    if assertion.predicate.is_empty() {
1311        errors.push("predicate must be a non-empty string".to_owned());
1312    }
1313    if !policy.allowed_predicates.is_empty()
1314        && !policy.allowed_predicates.contains(&assertion.predicate)
1315    {
1316        errors.push(format!(
1317            "predicate '{}' is rejected by policy",
1318            assertion.predicate
1319        ));
1320    }
1321    if let KnowledgeAssertionObject::Entity { entity_id } = &assertion.object {
1322        if entity_id.is_empty() {
1323            errors.push("object entity_id must be a non-empty string".to_owned());
1324        }
1325    }
1326    if !assertion.confidence.is_finite() || !(0.0..=1.0).contains(&assertion.confidence) {
1327        errors.push("confidence must be finite in [0, 1]".to_owned());
1328    }
1329    errors
1330}
1331
1332fn materialize_knowledge_graph(
1333    assertions: &[KnowledgeAssertion],
1334) -> (
1335    Vec<KnowledgeGraphEntity>,
1336    Vec<KnowledgeGraphRelation>,
1337    Vec<KnowledgeGraphTopic>,
1338    KnowledgeGraphIndex,
1339) {
1340    type EntityBuckets = (HashSet<FactId>, HashSet<FactId>, HashSet<FactId>);
1341    type TopicBuckets = (HashSet<FactId>, HashSet<FactId>);
1342
1343    let mut entity_map: BTreeMap<FactId, EntityBuckets> = BTreeMap::new();
1344    let mut topic_map: BTreeMap<String, TopicBuckets> = BTreeMap::new();
1345    let mut relations = Vec::new();
1346    for assertion in assertions {
1347        let subject_entry = entity_map.entry(assertion.subject_id.clone()).or_default();
1348        subject_entry.0.insert(assertion.id.clone());
1349        subject_entry.1.insert(assertion.id.clone());
1350        let topic_entry = topic_map.entry(assertion.predicate.clone()).or_default();
1351        topic_entry.0.insert(assertion.id.clone());
1352        topic_entry.1.insert(assertion.subject_id.clone());
1353        if let KnowledgeAssertionObject::Entity { entity_id } = &assertion.object {
1354            let object_entry = entity_map.entry(entity_id.clone()).or_default();
1355            object_entry.0.insert(assertion.id.clone());
1356            object_entry.2.insert(assertion.id.clone());
1357            topic_entry.1.insert(entity_id.clone());
1358        }
1359        relations.push(KnowledgeGraphRelation {
1360            assertion_id: assertion.id.clone(),
1361            subject_id: assertion.subject_id.clone(),
1362            predicate: assertion.predicate.clone(),
1363            object: assertion.object.clone(),
1364            sources: assertion.sources.clone(),
1365            confidence: assertion.confidence,
1366        });
1367    }
1368    relations.sort_by(|a, b| a.assertion_id.cmp(&b.assertion_id));
1369    let entities = entity_map
1370        .into_iter()
1371        .map(
1372            |(id, (assertions, subjects, objects))| KnowledgeGraphEntity {
1373                id,
1374                assertion_ids: sorted_set(assertions),
1375                subject_assertion_ids: sorted_set(subjects),
1376                object_assertion_ids: sorted_set(objects),
1377            },
1378        )
1379        .collect::<Vec<_>>();
1380    let topics = topic_map
1381        .into_iter()
1382        .map(|(predicate, (assertions, entities))| KnowledgeGraphTopic {
1383            predicate,
1384            assertion_ids: sorted_set(assertions),
1385            entity_ids: sorted_set(entities),
1386        })
1387        .collect::<Vec<_>>();
1388    let index = KnowledgeGraphIndex {
1389        assertion_ids: {
1390            let mut ids = assertions
1391                .iter()
1392                .map(|assertion| assertion.id.clone())
1393                .collect::<Vec<_>>();
1394            ids.sort();
1395            ids
1396        },
1397        entity_ids: entities.iter().map(|entity| entity.id.clone()).collect(),
1398        relation_ids: relations
1399            .iter()
1400            .map(|relation| relation.assertion_id.clone())
1401            .collect(),
1402        predicates: topics.iter().map(|topic| topic.predicate.clone()).collect(),
1403    };
1404    (entities, relations, topics, index)
1405}
1406
1407fn sorted_set(set: HashSet<FactId>) -> Vec<FactId> {
1408    let mut out = set.into_iter().collect::<Vec<_>>();
1409    out.sort();
1410    out
1411}
1412
1413fn retrieval_projection<T, U, F>(
1414    graph: &Graph,
1415    snapshot: &Node<MemoryRetrievalSnapshot<T>>,
1416    name: String,
1417    factory: &'static str,
1418    select: F,
1419) -> Node<U>
1420where
1421    T: Clone + 'static,
1422    U: 'static,
1423    F: Fn(&MemoryRetrievalSnapshot<T>) -> U + 'static,
1424{
1425    graph.init_node(
1426        Operator::with_opts(factory, pattern_node_config(), move |ctx| {
1427            for snapshot in ctx.batch::<MemoryRetrievalSnapshot<T>>(0) {
1428                ctx.emit(select(snapshot.as_ref()));
1429            }
1430        }),
1431        vec![snapshot.erased()],
1432        named_graph_node_opts(name),
1433    )
1434}
1435
1436fn pattern_node_config() -> NodeOpts {
1437    NodeOpts {
1438        complete_when_deps_complete: false,
1439        error_when_deps_error: false,
1440        ..NodeOpts::default()
1441    }
1442}
1443
1444fn named_graph_node_opts(name: String) -> GraphNodeOpts {
1445    GraphNodeOpts {
1446        name: Some(name),
1447        ..GraphNodeOpts::default()
1448    }
1449}
1450
1451fn validate_query(query: &MemoryRetrievalQuery) -> Vec<PendingMemoryRetrievalError> {
1452    let mut errors = Vec::new();
1453    if let Some(min_confidence) = query.min_confidence {
1454        if !min_confidence.is_finite() || !(0.0..=1.0).contains(&min_confidence) {
1455            errors.push(PendingMemoryRetrievalError {
1456                code: MemoryRetrievalErrorCode::InvalidQuery,
1457                message: "memory_retrieval_bundle: query.min_confidence must be finite in [0, 1]"
1458                    .to_owned(),
1459                index: None,
1460                fragment_id: None,
1461                validation_errors: vec!["min_confidence must be finite in [0, 1]".to_owned()],
1462            });
1463        }
1464    }
1465    if let Some(vector) = &query.vector {
1466        if vector.iter().any(|component| !component.is_finite()) {
1467            errors.push(PendingMemoryRetrievalError {
1468                code: MemoryRetrievalErrorCode::InvalidQueryVector,
1469                message: "memory_retrieval_bundle: query.vector must be a finite number array"
1470                    .to_owned(),
1471                index: None,
1472                fragment_id: None,
1473                validation_errors: vec!["vector must be a finite number array".to_owned()],
1474            });
1475        }
1476    }
1477    errors
1478}
1479
1480fn validate_fragment<T>(fragment: &MemoryFragment<T>) -> Vec<String> {
1481    let mut errors = Vec::new();
1482    if fragment.id.is_empty() {
1483        errors.push("id must be a non-empty string".to_owned());
1484    }
1485    if !fragment.confidence.is_finite() || !(0.0..=1.0).contains(&fragment.confidence) {
1486        errors.push("confidence must be finite in [0, 1]".to_owned());
1487    }
1488    if let (Some(valid_from), Some(valid_to)) = (fragment.valid_from, fragment.valid_to) {
1489        if valid_from >= valid_to {
1490            errors.push("valid_from must be earlier than valid_to".to_owned());
1491        }
1492    }
1493    if fragment
1494        .embedding
1495        .as_ref()
1496        .is_some_and(|embedding| embedding.iter().any(|component| !component.is_finite()))
1497    {
1498        errors.push("embedding must be a finite number array when present".to_owned());
1499    }
1500    errors
1501}
1502
1503fn is_recoverable_fragment_error(error: &PendingMemoryRetrievalError) -> bool {
1504    matches!(
1505        error.code,
1506        MemoryRetrievalErrorCode::InvalidFragment | MemoryRetrievalErrorCode::DuplicateFragmentId
1507    )
1508}
1509
1510fn status_state(
1511    errors: &[PendingMemoryRetrievalError],
1512    result_count: usize,
1513) -> MemoryRetrievalStatusState {
1514    if errors
1515        .iter()
1516        .any(|error| !is_recoverable_fragment_error(error))
1517    {
1518        MemoryRetrievalStatusState::Error
1519    } else if !errors.is_empty() {
1520        MemoryRetrievalStatusState::Partial
1521    } else if result_count > 0 {
1522        MemoryRetrievalStatusState::Ready
1523    } else {
1524        MemoryRetrievalStatusState::Empty
1525    }
1526}
1527
1528fn rank_fragments<T: Clone>(
1529    fragments: &[MemoryFragment<T>],
1530    query: &MemoryRetrievalQuery,
1531) -> Vec<MemoryFragment<T>> {
1532    let mut ranked: Vec<_> = fragments
1533        .iter()
1534        .filter(|fragment| memory_fragment_matches_query(fragment, &query.memory_query()))
1535        .cloned()
1536        .collect();
1537    if query.vector.is_some() {
1538        ranked.sort_by(|a, b| {
1539            vector_score(b, query)
1540                .total_cmp(&vector_score(a, query))
1541                .then_with(|| b.confidence.total_cmp(&a.confidence))
1542                .then_with(|| b.t_ns.cmp(&a.t_ns))
1543        });
1544    } else {
1545        ranked.sort_by(|a, b| {
1546            b.confidence
1547                .total_cmp(&a.confidence)
1548                .then_with(|| b.t_ns.cmp(&a.t_ns))
1549        });
1550    }
1551    if let Some(limit) = query.limit {
1552        ranked.truncate(limit);
1553    }
1554    ranked
1555}
1556
1557fn vector_score<T>(fragment: &MemoryFragment<T>, query: &MemoryRetrievalQuery) -> f64 {
1558    match (&query.vector, &fragment.embedding) {
1559        (Some(query), Some(embedding)) => cosine_similarity(query, embedding),
1560        _ => 0.0,
1561    }
1562}