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}