Skip to main content

core_api/
memory_schema.rs

1//! The schema a memory store is created with.
2//!
3//! `recall` searches the pairs `fulltext_pairs()` reports, and until 0.7 the
4//! only pair any memory store ever had was `Note.text`, self-declared by
5//! `remember`. So `recall` could return notes and never the entities they were
6//! about. This module is the answer: one `Schema` value, applied once at
7//! creation, expressed entirely in types that already exist.
8//!
9//! It is deliberately *not* applied to an existing store on open. Declaring
10//! full-text rebuilds the index at open — measured at 227 ms against 3.8 ms
11//! with none (ledger row 36) — and creating a rule fires a backfill across
12//! every node. An existing store is upgraded only by an explicit
13//! `mushroomdb schema apply --memory-defaults`.
14
15use crate::memory::identity::{
16    ALIASES_FIELD, ALIAS_KEYS_FIELD, SAME_AS_EDGE, SAME_AS_FLOOR, SAME_AS_WEIGHT,
17};
18use crate::schema::Schema;
19use core_rules::{Predicate, RuleDef};
20
21/// Labels the memory surface treats as entities.
22pub const MEMORY_ENTITY_LABELS: &[&str] = &["Person", "Org", "Project", "Concept", "Event"];
23
24/// The label a provisional subject is created under by `remember`.
25pub const PROVISIONAL_LABEL: &str = "Entity";
26
27/// The property marking a node as provisional — a subject named before it was
28/// described. Never removed silently; see the spec's O-4.
29pub const PROVISIONAL_PROP: &str = "provisional";
30
31/// The field each entity label carries its human-readable name in.
32pub const NAME_FIELD: &str = "name";
33
34/// The default schema a memory store is created with.
35pub fn memory_defaults() -> Schema {
36    let mut fulltext: Vec<(String, String)> = MEMORY_ENTITY_LABELS
37        .iter()
38        .map(|l| ((*l).to_string(), NAME_FIELD.to_string()))
39        .collect();
40    fulltext.push(("Note".to_string(), "text".to_string()));
41    fulltext.push(("Concept".to_string(), "summary".to_string()));
42    fulltext.push((PROVISIONAL_LABEL.to_string(), NAME_FIELD.to_string()));
43
44    let indexes: Vec<(String, String)> = MEMORY_ENTITY_LABELS
45        .iter()
46        .map(|l| ((*l).to_string(), NAME_FIELD.to_string()))
47        .collect();
48
49    Schema {
50        fulltext,
51        indexes,
52        // No rules: creating one stays an explicit, approved act
53        // (SKILL.md:66). The identity rules are `memory_identity()`, opt-in.
54        rules: Vec::new(),
55        views: Vec::new(),
56        roles: Vec::new(),
57    }
58}
59
60/// The identity preset: `SAME_AS` rules over the `aliases` list, and over the
61/// declared `alias_keys` list.
62///
63/// Opt-in, never part of [`memory_defaults`]: a store gains rules only when
64/// someone asks (`SKILL.md`: never create a rule silently), so this is applied
65/// by `mushroomdb schema apply <db> --memory-identity` and nothing else.
66///
67/// Sixteen rules, all global (`namespace: None`):
68///
69/// - one same-label rule per entity label and for the provisional label —
70///   `Person→Person`, …, `Entity→Entity` — `Overlap` on `aliases` at
71///   [`SAME_AS_FLOOR`]. A same-label rule derives both directions, which
72///   [`crate::memory::identity::same_as_pairs`] reads as one claim;
73/// - one directed rule from the provisional label to each entity label —
74///   `Entity→Person`, … — the same `Overlap`. A stub `remember` made from an
75///   `about` key is an `Entity` for life, and a `Person→Person` rule never
76///   sees it, so without these the case identity exists for — a subject named
77///   before it was described — could never link;
78/// - one directed rule from each entity label to the provisional label —
79///   `Person→Entity`, … — `KeyMatch` on `alias_keys`: an entity that declares
80///   an alias equal to a stub's key links that stub, at 1.0. `Overlap` cannot:
81///   a stub keyed `matt` holds one alias, and a declared alias is not among
82///   the entity's own — those are its key, its name and the name's words. It
83///   reads `alias_keys`, not `aliases`, because `aliases` holds derived name
84///   words, and an entity merely named "Alex" must not claim a stub keyed
85///   `alex`. The rules key on labels, and a node `about` created keeps
86///   `Entity` for life: a stub that was later described can still be claimed,
87///   and cannot itself claim.
88///
89/// The `KeyMatch` rules stand alone rather than under an `Any` beside the
90/// `Overlap`: `Any` is never `KeyMatch`-rooted, so it would lose the engine's
91/// foreign-key fast path (`core_rules::is_keymatch_rooted`). A pair both kinds
92/// link carries one edge per direction, each owned by its own rule, and is
93/// still one claim, at the higher score.
94///
95/// `weight_prop` and `max_edges` are set explicitly: `apply_schema` takes a
96/// `RuleDef` as given, without the defaults MCP `create_rule` fills in.
97pub fn memory_identity() -> Schema {
98    let overlap = Predicate::Overlap {
99        field: ALIASES_FIELD.to_string(),
100        min: SAME_AS_FLOOR,
101    };
102    let claim = Predicate::KeyMatch {
103        field: ALIAS_KEYS_FIELD.to_string(),
104    };
105    let rule = |name: String, src: &str, dst: &str, predicate: &Predicate| RuleDef {
106        name,
107        src_label: src.to_string(),
108        dst_label: dst.to_string(),
109        predicate: predicate.clone(),
110        edge_type: SAME_AS_EDGE.to_string(),
111        weight_prop: Some(SAME_AS_WEIGHT.to_string()),
112        max_edges: Some(core_rules::default_max_edges(predicate)),
113        approximate: false,
114        via_label: None,
115        via_edge: None,
116        via_dir: None,
117        namespace: None,
118    };
119    let mut rules: Vec<RuleDef> = MEMORY_ENTITY_LABELS
120        .iter()
121        .chain(std::iter::once(&PROVISIONAL_LABEL))
122        .map(|l| rule(format!("same_as_{}", l.to_lowercase()), l, l, &overlap))
123        .collect();
124    rules.extend(MEMORY_ENTITY_LABELS.iter().map(|l| {
125        rule(
126            format!("same_as_entity_{}", l.to_lowercase()),
127            PROVISIONAL_LABEL,
128            l,
129            &overlap,
130        )
131    }));
132    rules.extend(MEMORY_ENTITY_LABELS.iter().map(|l| {
133        rule(
134            format!("same_as_claim_{}", l.to_lowercase()),
135            l,
136            PROVISIONAL_LABEL,
137            &claim,
138        )
139    }));
140    Schema {
141        fulltext: Vec::new(),
142        indexes: Vec::new(),
143        rules,
144        views: Vec::new(),
145        roles: Vec::new(),
146    }
147}