Skip to main content

writerslogic_warm/core/engine/
governance.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::{BTreeMap, BTreeSet};
5
6use anyhow::{ensure, Context, Result};
7
8use super::{Mutation, WarmCore};
9use crate::core::atom_memory::AtomMemory;
10use crate::core::cognition::governor::{
11    GovernanceConflict, GovernanceReport, GovernorConfig, MemoryGovernor,
12};
13use crate::core::composite_memory::CompositeMemory;
14use crate::core::entangled::EntangledHVec;
15use crate::core::query_limits::{QueryBudget, QueryLimits};
16use crate::core::triple_store::{TripleRecord, TripleStore};
17
18const MAX_OPERATIONS: usize = 4096;
19const MAX_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
20
21struct Snapshot {
22    revision: u64,
23    atoms: BTreeMap<String, EntangledHVec>,
24    composites: BTreeMap<String, EntangledHVec>,
25    triples: Vec<TripleRecord>,
26    mirrored: BTreeSet<String>,
27}
28
29struct Plan {
30    revision: u64,
31    mutations: Vec<Mutation>,
32    payload: Vec<u8>,
33    report: GovernanceReport,
34}
35
36fn same(left: &EntangledHVec, right: &EntangledHVec) -> bool {
37    left.dim == right.dim && left.indices() == right.indices()
38}
39
40fn append(
41    mutations: &mut Vec<Mutation>,
42    budget: &mut QueryBudget,
43    mutation: Mutation,
44) -> Result<()> {
45    ensure!(
46        mutations.len() < MAX_OPERATIONS,
47        "governance exceeds the 4096-operation atomic transaction limit"
48    );
49    budget.frontier(mutations.len() + 1)?;
50    mutations.push(mutation);
51    Ok(())
52}
53
54impl WarmCore {
55    /// Plans governance on an isolated snapshot, then commits one bounded transaction if
56    /// its revision is still current. A conflict changes no governance state and may be retried.
57    pub fn govern_memory_with_limits(&self, limits: QueryLimits) -> Result<GovernanceReport> {
58        self.govern_with_hook(limits, || {})
59    }
60
61    fn govern_with_hook(
62        &self,
63        limits: QueryLimits,
64        after_plan: impl FnOnce(),
65    ) -> Result<GovernanceReport> {
66        if !self.meaning_enabled() {
67            return Ok(GovernanceReport::default());
68        }
69        ensure!(
70            self.schema >= crate::core::format::MEANING_MAINTENANCE_SCHEMA_VERSION,
71            "durable governance requires store schema 5; use warm-format upgrade to create an upgraded store"
72        );
73        let mut budget = QueryBudget::new(limits);
74        // Two shadow indexes, rebuild overlap and IDF scratch scale with the ambient dimension.
75        budget.reserve(self.dimensions.saturating_mul(96))?;
76        budget.work(self.dimensions.saturating_mul(12))?;
77        let snapshot = self.governance_snapshot(&mut budget)?;
78        let plan = self.governance_plan(snapshot, &mut budget)?;
79        after_plan();
80        ensure!(
81            self.commit_serialized_if_revision(&plan.mutations, &plan.payload, plan.revision)?,
82            GovernanceConflict
83        );
84        Ok(plan.report)
85    }
86
87    fn governance_snapshot(&self, budget: &mut QueryBudget) -> Result<Snapshot> {
88        let _transaction = self.mutation_gate.read();
89        let revision = self.revision();
90        let atoms = self
91            .atom_memory
92            .as_ref()
93            .context("meaning atom store is missing")?
94            .inner()
95            .bounded_snapshot(budget)?
96            .into_iter()
97            .collect();
98        let composites: BTreeMap<_, _> = self
99            .composite_memory
100            .as_ref()
101            .context("meaning composite store is missing")?
102            .inner()
103            .bounded_snapshot(budget)?
104            .into_iter()
105            .collect();
106        let triples = self
107            .triple_store
108            .as_ref()
109            .context("meaning triple store is missing")?
110            .bounded_snapshot(budget)?;
111        let mut mirrored = BTreeSet::new();
112        self.shards.read().try_for_each_shard(|shard| {
113            for (id, vector) in shard.vectors.read().iter() {
114                budget.work(1)?;
115                if composites
116                    .get(id)
117                    .is_some_and(|composite| same(composite, vector))
118                {
119                    budget.reserve(id.len().saturating_mul(2).saturating_add(64))?;
120                    mirrored.insert(id.clone());
121                }
122            }
123            Ok(())
124        })?;
125        Ok(Snapshot {
126            revision,
127            atoms,
128            composites,
129            triples,
130            mirrored,
131        })
132    }
133
134    fn governance_plan(&self, snapshot: Snapshot, budget: &mut QueryBudget) -> Result<Plan> {
135        let config = GovernorConfig {
136            duplicate_threshold: self.config.cognition.governor_duplicate_threshold,
137            max_scan_size: self.config.cognition.governor_max_scan_size,
138            forget_unreferenced_atoms: self.config.cognition.governor_forget_unreferenced,
139            refine_atoms: self.config.cognition.refine_atoms,
140            ..Default::default()
141        };
142        let scan = config.max_scan_size.min(snapshot.composites.len());
143        let owned: BTreeSet<_> = snapshot
144            .triples
145            .iter()
146            .map(|triple| triple.composite_id.as_str())
147            .collect();
148        let eligible = snapshot
149            .composites
150            .keys()
151            .take(scan)
152            .filter(|id| !owned.contains(id.as_str()))
153            .count();
154        let composite_width = snapshot
155            .composites
156            .values()
157            .take(scan)
158            .map(|vector| vector.indices().len())
159            .max()
160            .unwrap_or(0)
161            .max(1);
162        let activations = snapshot
163            .atoms
164            .values()
165            .chain(snapshot.composites.values())
166            .fold(0usize, |total, vector| {
167                total.saturating_add(vector.indices().len())
168            });
169        let refinement = if config.refine_atoms {
170            let width = snapshot
171                .atoms
172                .values()
173                .map(|vector| vector.indices().len())
174                .max()
175                .unwrap_or(0)
176                .max((self.dimensions / 256).max(1));
177            snapshot
178                .atoms
179                .len()
180                .saturating_mul(snapshot.triples.len())
181                .saturating_mul(width)
182                .saturating_mul(3)
183        } else {
184            0
185        };
186        budget.work(
187            eligible
188                .saturating_mul(scan)
189                .saturating_mul(composite_width)
190                .saturating_mul(3)
191                .saturating_add(activations.saturating_mul(4))
192                .saturating_add(snapshot.atoms.len())
193                .saturating_add(snapshot.composites.len())
194                .saturating_add(snapshot.triples.len().saturating_mul(4))
195                .saturating_add(refinement),
196        )?;
197        let atoms = AtomMemory::new(self.dimensions, self.config.meaning.idf_clip_factor);
198        let composites = CompositeMemory::new(self.dimensions, self.config.meaning.idf_clip_factor);
199        let triples = TripleStore::new();
200        for (id, vector) in &snapshot.atoms {
201            atoms.insert_with_vec(id, vector);
202        }
203        for (id, vector) in &snapshot.composites {
204            composites.insert(id.clone(), vector.clone());
205        }
206        for triple in &snapshot.triples {
207            ensure!(
208                snapshot.composites.contains_key(&triple.composite_id),
209                "governance found a live triple without its composite; repair the store first"
210            );
211            triples.load_triple(triple.clone());
212        }
213        let report = MemoryGovernor::govern(&atoms, &composites, &triples, &config);
214        let mut mutations = Vec::new();
215        let mut changed = BTreeSet::new();
216        for (id, old) in &snapshot.atoms {
217            match atoms.get(id) {
218                None => append(
219                    &mut mutations,
220                    budget,
221                    Mutation::DeleteAtom { id: id.clone() },
222                )?,
223                Some(vector) if !same(old, &vector) => {
224                    changed.insert(id.as_str());
225                    append(
226                        &mut mutations,
227                        budget,
228                        Mutation::Atom {
229                            id: id.clone(),
230                            vector,
231                        },
232                    )?;
233                }
234                Some(_) => {}
235            }
236        }
237        for id in snapshot.composites.keys() {
238            if composites.inner().get(id).is_none() {
239                ensure!(
240                    !triples.has_composite(id),
241                    "governance cannot discard an owned composite"
242                );
243                append(
244                    &mut mutations,
245                    budget,
246                    Mutation::DeleteComposite { id: id.clone() },
247                )?;
248            }
249        }
250        let mut owners: BTreeMap<&str, Vec<&TripleRecord>> = BTreeMap::new();
251        for triple in &snapshot.triples {
252            owners.entry(&triple.composite_id).or_default().push(triple);
253        }
254        let roles = self
255            .role_registry
256            .as_ref()
257            .context("meaning roles are missing")?;
258        for (id, owners) in owners {
259            if !owners.iter().any(|owner| {
260                [&owner.subject_id, &owner.relation_id, &owner.object_id]
261                    .iter()
262                    .any(|atom| changed.contains(atom.as_str()))
263            }) {
264                continue;
265            }
266            ensure!(
267                owners.len() == 1 && snapshot.mirrored.contains(id),
268                "governance cannot refine an ambiguous composite/vector owner"
269            );
270            budget.work(1)?;
271            let owner = owners[0];
272            let subject = atoms
273                .get(&owner.subject_id)
274                .context("refinement lost a referenced subject")?;
275            let relation = atoms
276                .get(&owner.relation_id)
277                .context("refinement lost a referenced relation")?;
278            let object = atoms
279                .get(&owner.object_id)
280                .context("refinement lost a referenced object")?;
281            let vector = roles.compose_triple(&subject, &relation, &object);
282            append(
283                &mut mutations,
284                budget,
285                Mutation::Composite {
286                    id: id.to_owned(),
287                    vector,
288                },
289            )?;
290        }
291        append(&mut mutations, budget, Mutation::RefreshMeaningIndices {})?;
292        let json_bytes = crate::core::documents::serialized_size(&mutations, MAX_PAYLOAD_BYTES)
293            .context("governance transaction exceeds 32 MiB")?;
294        budget.reserve(
295            json_bytes
296                .saturating_add(mutations.len().saturating_mul(8))
297                .saturating_add(64),
298        )?;
299        let payload = self.transaction_payload(&mutations)?;
300        ensure!(
301            payload.len() <= MAX_PAYLOAD_BYTES,
302            "governance transaction exceeds 32 MiB"
303        );
304        Ok(Plan {
305            revision: snapshot.revision,
306            mutations,
307            payload,
308            report,
309        })
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::core::{entangled::EntangledHVec, WarmConfig};
317
318    fn config() -> WarmConfig {
319        let mut config = WarmConfig::default();
320        config.meaning.enabled = true;
321        config.cognition.governor_forget_unreferenced = true;
322        config
323    }
324
325    #[test]
326    fn governance_keeps_distinct_live_triple_owners() {
327        let directory = tempfile::tempdir().unwrap();
328        let core = WarmCore::new(
329            16384,
330            Some(directory.path().display().to_string()),
331            Some(config()),
332        )
333        .unwrap();
334        for index in 0..512 {
335            core.memorize_triplet(
336                format!("triple-{index:03}"),
337                "alice".into(),
338                "knows".into(),
339                "bob".into(),
340            )
341            .unwrap();
342        }
343        core.govern_memory().unwrap();
344        assert_eq!(core.meaning_composite_count(), 512);
345        assert_eq!(core.meaning_triple_count(), 512);
346        for triple in core.triple_store.as_ref().unwrap().snapshot() {
347            assert!(core
348                .composite_memory
349                .as_ref()
350                .unwrap()
351                .inner()
352                .get(&triple.composite_id)
353                .is_some());
354        }
355    }
356
357    #[test]
358    fn governance_atom_alias_deletion_is_durable_without_deleting_the_raw_vector() {
359        let directory = tempfile::tempdir().unwrap();
360        let path = directory.path().display().to_string();
361        let core = WarmCore::new(1024, Some(path.clone()), Some(config())).unwrap();
362        let vector = EntangledHVec::from_indices(vec![7, 9], 1024);
363        core.memorize("alias".into(), vector.clone()).unwrap();
364        core.govern_memory().unwrap();
365        assert!(core.atom_memory.as_ref().unwrap().get("alias").is_none());
366        assert_eq!(
367            core.shards.read().get_vector("alias").unwrap().indices(),
368            vector.indices()
369        );
370        drop(core);
371        let core = WarmCore::new(1024, Some(path.clone()), Some(config())).unwrap();
372        assert!(core.atom_memory.as_ref().unwrap().get("alias").is_none());
373        assert_eq!(
374            core.shards.read().get_vector("alias").unwrap().indices(),
375            vector.indices()
376        );
377        core.compact().unwrap();
378        drop(core);
379        let core = WarmCore::new(1024, Some(path), Some(config())).unwrap();
380        assert!(core.atom_memory.as_ref().unwrap().get("alias").is_none());
381        assert_eq!(
382            core.shards.read().get_vector("alias").unwrap().indices(),
383            vector.indices()
384        );
385    }
386
387    fn fingerprint(core: &WarmCore) -> serde_json::Value {
388        let entries = |memory: &crate::core::indexed_memory::IndexedMemory| {
389            let mut entries: Vec<_> = memory
390                .all_vectors()
391                .into_iter()
392                .map(|(_, id, vector)| (id, vector.indices().to_vec()))
393                .collect();
394            entries.sort_unstable();
395            entries
396        };
397        let mut vectors: Vec<_> = core
398            .shards
399            .read()
400            .collect_all_patterns()
401            .into_iter()
402            .map(|(id, vector)| (id, vector.indices().to_vec()))
403            .collect();
404        vectors.sort_unstable();
405        let mut triples: Vec<_> = core
406            .triple_store
407            .as_ref()
408            .unwrap()
409            .snapshot()
410            .into_iter()
411            .map(|t| (t.subject_id, t.relation_id, t.object_id, t.composite_id))
412            .collect();
413        triples.sort_unstable();
414        serde_json::json!({"atoms": entries(core.atom_memory.as_ref().unwrap().inner()), "composites": entries(core.composite_memory.as_ref().unwrap().inner()), "vectors": vectors, "triples": triples})
415    }
416
417    #[test]
418    fn governance_refinement_rebinds_owned_composites_and_survives_replay_and_compaction() {
419        let directory = tempfile::tempdir().unwrap();
420        let path = directory.path().display().to_string();
421        let mut config = config();
422        config.cognition.refine_atoms = true;
423        let core = WarmCore::new(16384, Some(path.clone()), Some(config.clone())).unwrap();
424        for (city, country) in [
425            ("paris", "france"),
426            ("berlin", "germany"),
427            ("tokyo", "japan"),
428        ] {
429            core.memorize_triplet(
430                format!("{city}:capital"),
431                city.into(),
432                "capital_of".into(),
433                country.into(),
434            )
435            .unwrap();
436            core.memorize_triplet(
437                format!("{city}:located"),
438                city.into(),
439                "located_in".into(),
440                "continent".into(),
441            )
442            .unwrap();
443        }
444        let before = fingerprint(&core);
445        let report = core.govern_memory().unwrap();
446        assert!(report.refinement.atoms_refined > 0);
447        let after = fingerprint(&core);
448        assert_ne!(before["atoms"], after["atoms"]);
449        for triple in core.triple_store.as_ref().unwrap().snapshot() {
450            let atoms = core.atom_memory.as_ref().unwrap();
451            let vector = core.role_registry.as_ref().unwrap().compose_triple(
452                &atoms.get(&triple.subject_id).unwrap(),
453                &atoms.get(&triple.relation_id).unwrap(),
454                &atoms.get(&triple.object_id).unwrap(),
455            );
456            assert!(same(
457                &vector,
458                &core
459                    .composite_memory
460                    .as_ref()
461                    .unwrap()
462                    .inner()
463                    .get(&triple.composite_id)
464                    .unwrap()
465            ));
466            assert!(same(
467                &vector,
468                &core.shards.read().get_vector(&triple.composite_id).unwrap()
469            ));
470        }
471        drop(core);
472        let core = WarmCore::new(16384, Some(path.clone()), Some(config.clone())).unwrap();
473        assert_eq!(fingerprint(&core), after);
474        core.compact().unwrap();
475        drop(core);
476        let core = WarmCore::new(16384, Some(path), Some(config)).unwrap();
477        assert_eq!(fingerprint(&core), after);
478    }
479
480    #[test]
481    fn governance_cas_rejects_a_concurrent_writer_without_publishing_the_plan() {
482        let directory = tempfile::tempdir().unwrap();
483        let core = std::sync::Arc::new(
484            WarmCore::new(
485                1024,
486                Some(directory.path().display().to_string()),
487                Some(config()),
488            )
489            .unwrap(),
490        );
491        core.memorize("alias".into(), EntangledHVec::from_indices(vec![1], 1024))
492            .unwrap();
493        let gate = std::sync::Arc::new(std::sync::Barrier::new(2));
494        let (writer_core, writer_gate) = (core.clone(), gate.clone());
495        let writer = std::thread::spawn(move || {
496            writer_gate.wait();
497            writer_core
498                .memorize("writer".into(), EntangledHVec::from_indices(vec![2], 1024))
499                .unwrap();
500            writer_gate.wait();
501        });
502        let error = core
503            .govern_with_hook(QueryLimits::default(), || {
504                gate.wait();
505                gate.wait();
506            })
507            .unwrap_err();
508        writer.join().unwrap();
509        assert!(error.downcast_ref::<GovernanceConflict>().is_some());
510        assert!(core.atom_memory.as_ref().unwrap().get("alias").is_some());
511        assert!(core.shards.read().get_vector("writer").is_some());
512        core.govern_memory().unwrap();
513        assert!(core.atom_memory.as_ref().unwrap().get("alias").is_none());
514    }
515
516    #[test]
517    fn governance_transaction_limit_is_atomic_at_4096_operations() {
518        for atoms in [4094, 4095, 4096] {
519            let directory = tempfile::tempdir().unwrap();
520            let core = WarmCore::new(
521                1024,
522                Some(directory.path().display().to_string()),
523                Some(config()),
524            )
525            .unwrap();
526            let mutations: Vec<_> = (0..atoms)
527                .map(|index| Mutation::Atom {
528                    id: format!("a{index:04}"),
529                    vector: EntangledHVec::from_indices(vec![1], 1024),
530                })
531                .collect();
532            core.commit(&mutations).unwrap();
533            let revision = core.revision();
534            let result = core.govern_memory();
535            if atoms == 4096 {
536                assert!(result.unwrap_err().to_string().contains("4096-operation"));
537                assert_eq!(core.meaning_atom_count(), atoms);
538                assert_eq!(core.revision(), revision);
539            } else {
540                result.unwrap();
541                assert_eq!(core.meaning_atom_count(), 0);
542            }
543        }
544    }
545
546    #[test]
547    fn governance_budget_errors_leave_authoritative_state_unchanged() {
548        let directory = tempfile::tempdir().unwrap();
549        let core = WarmCore::new(
550            1024,
551            Some(directory.path().display().to_string()),
552            Some(config()),
553        )
554        .unwrap();
555        core.memorize("alias".into(), EntangledHVec::from_indices(vec![1], 1024))
556            .unwrap();
557        let before = fingerprint(&core);
558        for limits in [
559            QueryLimits {
560                max_work: 0,
561                ..Default::default()
562            },
563            QueryLimits {
564                max_bytes: 0,
565                ..Default::default()
566            },
567            QueryLimits {
568                max_bytes: 4096,
569                ..Default::default()
570            },
571        ] {
572            assert!(core.govern_memory_with_limits(limits).is_err());
573            assert_eq!(fingerprint(&core), before);
574        }
575    }
576}