1use std::collections::{BTreeMap, BTreeSet};
8
9use rusqlite::OptionalExtension;
10use serde::{Deserialize, Serialize};
11
12use crate::{Pool, StoreError, StoreResult};
13
14#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(default)]
17pub struct SemanticSnapshot {
18 pub snapshot_id: Option<String>,
20 pub schema_version: Option<u16>,
22 pub active_principles: BTreeMap<String, PrincipleState>,
24 pub active_doctrine: BTreeMap<String, DoctrineState>,
26 pub active_memories: BTreeMap<String, MemoryState>,
28 pub salience_distribution: SalienceDistribution,
30 pub unresolved_contradictions: BTreeMap<String, ContradictionState>,
32 pub trust_state: TrustKeyState,
34 pub truth_ceiling: TruthCeilingState,
36}
37
38impl SemanticSnapshot {
39 pub fn diff_against_restore(&self, restored: &Self) -> SemanticDiff {
44 SemanticDiff::between(self, restored)
45 }
46}
47
48pub fn semantic_snapshot_from_store(pool: &Pool) -> StoreResult<SemanticSnapshot> {
54 let schema_version = current_schema_version(pool)?;
55 let mut active_memories = BTreeMap::new();
56 let mut salience_distribution = SalienceDistribution::default();
57
58 let mut memory_stmt = pool.prepare(
59 "SELECT id, memory_type, claim, salience_json, authority
60 FROM memories
61 WHERE status = 'active'
62 ORDER BY id;",
63 )?;
64 let memory_rows = memory_stmt.query_map([], |row| {
65 Ok((
66 row.get::<_, String>(0)?,
67 row.get::<_, String>(1)?,
68 row.get::<_, String>(2)?,
69 row.get::<_, String>(3)?,
70 row.get::<_, String>(4)?,
71 ))
72 })?;
73 for row in memory_rows {
74 let (id, memory_type, claim, salience_json, authority) = row?;
75 let salience = parse_salience_score(&salience_json)?;
76 salience_distribution.add(salience);
77 active_memories.insert(
78 id,
79 MemoryState {
80 claim,
81 memory_type: parse_memory_kind(&memory_type),
82 claim_key: None,
83 truth_state: truth_state_from_authority(&authority),
84 salience,
85 },
86 );
87 }
88
89 Ok(SemanticSnapshot {
90 snapshot_id: Some("store-current".into()),
91 schema_version,
92 active_principles: active_principles_from_store(pool)?,
93 active_doctrine: active_doctrine_from_store(pool)?,
94 active_memories,
95 salience_distribution,
96 unresolved_contradictions: unresolved_contradictions_from_store(pool)?,
97 trust_state: trust_state_from_store(pool)?,
98 truth_ceiling: TruthCeilingState::default(),
99 })
100}
101
102#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
104pub struct PrincipleState {
105 pub title: String,
107 pub truth_state: TruthState,
109 pub supporting_memory_ids: BTreeSet<String>,
111}
112
113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
115pub struct DoctrineState {
116 pub rule: String,
118 pub source_principle_id: String,
120 pub force: DoctrineForce,
122 pub truth_state: TruthState,
124}
125
126#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
128#[serde(rename_all = "snake_case")]
129pub enum DoctrineForce {
130 Advisory,
132 Conditioning,
134 Gate,
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
140pub struct MemoryState {
141 pub claim: String,
143 pub memory_type: MemoryKind,
145 pub claim_key: Option<String>,
147 pub truth_state: TruthState,
149 pub salience: SalienceScore,
151}
152
153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
155#[serde(rename_all = "snake_case")]
156pub enum MemoryKind {
157 Episodic,
159 Semantic,
161 Procedural,
163 Strategic,
165 Affective,
167}
168
169#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
171pub struct SalienceScore(pub u32);
172
173#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
175#[serde(default)]
176pub struct SalienceDistribution {
177 pub low: u32,
179 pub medium: u32,
181 pub high: u32,
183 pub critical: u32,
185}
186
187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
189pub struct ContradictionState {
190 pub claim_key: String,
192 pub memory_ids: BTreeSet<String>,
194 pub reason: String,
196}
197
198#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
200#[serde(default)]
201pub struct TrustKeyState {
202 pub principals: BTreeMap<String, PrincipalTrustState>,
204 pub keys: BTreeMap<String, KeyLifecycleState>,
206}
207
208#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
210pub struct PrincipalTrustState {
211 pub trust_tier: TrustTier,
213 pub key_ids: BTreeSet<String>,
215}
216
217#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
219#[serde(rename_all = "snake_case")]
220pub enum TrustTier {
221 Untrusted,
223 Observed,
225 Verified,
227 Operator,
229}
230
231#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
233pub struct KeyLifecycleState {
234 pub principal_id: String,
236 pub key_state: KeyState,
238}
239
240#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
242#[serde(rename_all = "snake_case")]
243pub enum KeyState {
244 Revoked,
246 Retired,
248 Active,
250}
251
252impl KeyState {
253 fn permits_new_signing(&self) -> bool {
254 matches!(self, Self::Active)
255 }
256}
257
258#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
260#[serde(rename_all = "snake_case")]
261pub enum TruthState {
262 Unknown,
264 Candidate,
266 Active,
268 Validated,
270 FullChainVerified,
272 AuthorityGrade,
274}
275
276#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
278#[serde(default)]
279pub struct TruthCeilingState {
280 pub runtime_mode: RuntimeMode,
282 pub proof_state: ProofState,
284 pub claim_ceiling: TruthCeiling,
286 pub per_claim: BTreeMap<String, TruthCeiling>,
288}
289
290impl Default for TruthCeilingState {
291 fn default() -> Self {
292 Self {
293 runtime_mode: RuntimeMode::Unknown,
294 proof_state: ProofState::Unknown,
295 claim_ceiling: TruthCeiling::Unknown,
296 per_claim: BTreeMap::new(),
297 }
298 }
299}
300
301#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
303#[serde(rename_all = "snake_case")]
304pub enum RuntimeMode {
305 Unknown,
307 Dev,
309 LocalUnsigned,
311 SignedLocalLedger,
313 ExternallyAnchored,
315 AuthorityGrade,
317}
318
319#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
321#[serde(rename_all = "snake_case")]
322pub enum ProofState {
323 Broken,
325 Unknown,
327 Partial,
329 FullChainVerified,
331}
332
333#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
335#[serde(rename_all = "snake_case")]
336pub enum TruthCeiling {
337 None,
339 Unknown,
341 Advisory,
343 ObservedLocal,
345 TrustedLocalLedger,
347 ExternallyAnchored,
349 AuthorityGrade,
351}
352
353#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
355pub struct SemanticDiff {
356 pub current_snapshot_id: Option<String>,
358 pub restored_snapshot_id: Option<String>,
360 pub changes: Vec<SemanticChange>,
362}
363
364impl SemanticDiff {
365 pub fn between(current: &SemanticSnapshot, restored: &SemanticSnapshot) -> Self {
367 let mut diff = Self {
368 current_snapshot_id: current.snapshot_id.clone(),
369 restored_snapshot_id: restored.snapshot_id.clone(),
370 changes: Vec::new(),
371 };
372
373 compare_named_map(
374 &mut diff,
375 ¤t.active_principles,
376 &restored.active_principles,
377 SemanticChange::active_principle_missing,
378 SemanticChange::active_principle_added,
379 |id, before, after| {
380 if after.truth_state < before.truth_state {
381 SemanticChange::truth_state_downgraded(
382 ArtifactKind::ActivePrinciple,
383 id,
384 before.truth_state.clone(),
385 after.truth_state.clone(),
386 )
387 } else {
388 SemanticChange::active_principle_changed(id)
389 }
390 },
391 );
392
393 compare_named_map(
394 &mut diff,
395 ¤t.active_doctrine,
396 &restored.active_doctrine,
397 SemanticChange::active_doctrine_missing,
398 SemanticChange::active_doctrine_added,
399 |id, before, after| {
400 if after.truth_state < before.truth_state {
401 SemanticChange::truth_state_downgraded(
402 ArtifactKind::ActiveDoctrine,
403 id,
404 before.truth_state.clone(),
405 after.truth_state.clone(),
406 )
407 } else {
408 SemanticChange::active_doctrine_changed(id)
409 }
410 },
411 );
412
413 compare_named_map(
414 &mut diff,
415 ¤t.active_memories,
416 &restored.active_memories,
417 SemanticChange::active_memory_missing,
418 SemanticChange::active_memory_added,
419 |id, before, after| {
420 if after.truth_state < before.truth_state {
421 SemanticChange::truth_state_downgraded(
422 ArtifactKind::ActiveMemory,
423 id,
424 before.truth_state.clone(),
425 after.truth_state.clone(),
426 )
427 } else {
428 SemanticChange::active_memory_changed(id)
429 }
430 },
431 );
432
433 if current.salience_distribution != restored.salience_distribution {
434 diff.changes.push(SemanticChange {
435 severity: SemanticSeverity::Warning,
436 kind: SemanticChangeKind::SalienceDistributionChanged {
437 current: current.salience_distribution.clone(),
438 restored: restored.salience_distribution.clone(),
439 },
440 });
441 }
442
443 compare_named_map(
444 &mut diff,
445 ¤t.unresolved_contradictions,
446 &restored.unresolved_contradictions,
447 |id| SemanticChange {
448 severity: SemanticSeverity::PreconditionUnmet,
449 kind: SemanticChangeKind::UnresolvedContradictionMissing { id },
450 },
451 |id| SemanticChange {
452 severity: SemanticSeverity::Warning,
453 kind: SemanticChangeKind::UnresolvedContradictionAdded { id },
454 },
455 |id, _, _| SemanticChange {
456 severity: SemanticSeverity::PreconditionUnmet,
457 kind: SemanticChangeKind::UnresolvedContradictionChanged { id },
458 },
459 );
460
461 compare_principals(&mut diff, ¤t.trust_state, &restored.trust_state);
462 compare_keys(&mut diff, ¤t.trust_state, &restored.trust_state);
463 compare_truth_ceiling(&mut diff, ¤t.truth_ceiling, &restored.truth_ceiling);
464
465 diff
466 }
467
468 pub fn severity(&self) -> SemanticSeverity {
470 self.changes
471 .iter()
472 .map(|change| change.severity)
473 .max()
474 .unwrap_or(SemanticSeverity::Clean)
475 }
476
477 pub fn is_clean(&self) -> bool {
479 self.changes.is_empty()
480 }
481
482 pub fn restore_decision(&self, recovery_acknowledged: bool) -> RestoreDecision {
484 RestoreDecision::from_diff(self, recovery_acknowledged)
485 }
486}
487
488fn current_schema_version(pool: &Pool) -> StoreResult<Option<u16>> {
489 let version = pool
490 .query_row(
491 "SELECT schema_version FROM events
492 UNION ALL
493 SELECT schema_version FROM traces
494 ORDER BY schema_version DESC
495 LIMIT 1;",
496 [],
497 |row| row.get::<_, u16>(0),
498 )
499 .optional()?;
500 Ok(version)
501}
502
503fn active_principles_from_store(pool: &Pool) -> StoreResult<BTreeMap<String, PrincipleState>> {
504 let mut stmt = pool.prepare(
505 "SELECT id, statement, status, supporting_memories_json
506 FROM principles
507 WHERE status IN ('active', 'promoted_to_doctrine')
508 ORDER BY id;",
509 )?;
510 let rows = stmt.query_map([], |row| {
511 Ok((
512 row.get::<_, String>(0)?,
513 row.get::<_, String>(1)?,
514 row.get::<_, String>(2)?,
515 row.get::<_, String>(3)?,
516 ))
517 })?;
518
519 let mut principles = BTreeMap::new();
520 for row in rows {
521 let (id, title, status, supporting_memories_json) = row?;
522 let supporting_memory_ids = string_set_from_json(&supporting_memories_json)?;
523 principles.insert(
524 id,
525 PrincipleState {
526 title,
527 truth_state: truth_state_from_status(&status),
528 supporting_memory_ids,
529 },
530 );
531 }
532 Ok(principles)
533}
534
535fn active_doctrine_from_store(pool: &Pool) -> StoreResult<BTreeMap<String, DoctrineState>> {
536 let mut stmt = pool.prepare(
537 "SELECT id, source_principle, rule, force
538 FROM doctrine
539 ORDER BY id;",
540 )?;
541 let rows = stmt.query_map([], |row| {
542 Ok((
543 row.get::<_, String>(0)?,
544 row.get::<_, String>(1)?,
545 row.get::<_, String>(2)?,
546 row.get::<_, String>(3)?,
547 ))
548 })?;
549
550 let mut doctrine = BTreeMap::new();
551 for row in rows {
552 let (id, source_principle_id, rule, force) = row?;
553 doctrine.insert(
554 id,
555 DoctrineState {
556 rule,
557 source_principle_id,
558 force: parse_doctrine_force(&force)?,
559 truth_state: TruthState::Active,
560 },
561 );
562 }
563 Ok(doctrine)
564}
565
566fn unresolved_contradictions_from_store(
567 pool: &Pool,
568) -> StoreResult<BTreeMap<String, ContradictionState>> {
569 let mut stmt = pool.prepare(
570 "SELECT id, left_ref, right_ref, contradiction_type
571 FROM contradictions
572 WHERE status = 'unresolved'
573 ORDER BY id;",
574 )?;
575 let rows = stmt.query_map([], |row| {
576 Ok((
577 row.get::<_, String>(0)?,
578 row.get::<_, String>(1)?,
579 row.get::<_, String>(2)?,
580 row.get::<_, String>(3)?,
581 ))
582 })?;
583
584 let mut contradictions = BTreeMap::new();
585 for row in rows {
586 let (id, left_ref, right_ref, reason) = row?;
587 contradictions.insert(
588 id,
589 ContradictionState {
590 claim_key: format!("{left_ref}|{right_ref}"),
591 memory_ids: BTreeSet::from([left_ref, right_ref]),
592 reason,
593 },
594 );
595 }
596 Ok(contradictions)
597}
598
599fn trust_state_from_store(pool: &Pool) -> StoreResult<TrustKeyState> {
600 let mut trust_state = TrustKeyState::default();
601
602 let mut principal_stmt = pool.prepare(
603 "SELECT p.principal_id, p.trust_tier
604 FROM authority_principal_timeline p
605 WHERE NOT EXISTS (
606 SELECT 1 FROM authority_principal_timeline later
607 WHERE later.principal_id = p.principal_id
608 AND later.effective_at > p.effective_at
609 )
610 ORDER BY p.principal_id;",
611 )?;
612 let principal_rows = principal_stmt.query_map([], |row| {
613 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
614 })?;
615 for row in principal_rows {
616 let (principal_id, trust_tier) = row?;
617 trust_state.principals.insert(
618 principal_id,
619 PrincipalTrustState {
620 trust_tier: parse_trust_tier(&trust_tier)?,
621 key_ids: BTreeSet::new(),
622 },
623 );
624 }
625
626 let mut key_stmt = pool.prepare(
627 "SELECT k.key_id, k.principal_id, k.state
628 FROM authority_key_timeline k
629 WHERE NOT EXISTS (
630 SELECT 1 FROM authority_key_timeline later
631 WHERE later.key_id = k.key_id
632 AND later.effective_at > k.effective_at
633 )
634 ORDER BY k.key_id, k.state;",
635 )?;
636 let key_rows = key_stmt.query_map([], |row| {
637 Ok((
638 row.get::<_, String>(0)?,
639 row.get::<_, String>(1)?,
640 row.get::<_, String>(2)?,
641 ))
642 })?;
643 for row in key_rows {
644 let (key_id, principal_id, key_state) = row?;
645 trust_state
646 .principals
647 .entry(principal_id.clone())
648 .or_insert_with(|| PrincipalTrustState {
649 trust_tier: TrustTier::Untrusted,
650 key_ids: BTreeSet::new(),
651 })
652 .key_ids
653 .insert(key_id.clone());
654 trust_state.keys.insert(
655 key_id,
656 KeyLifecycleState {
657 principal_id,
658 key_state: parse_key_state(&key_state)?,
659 },
660 );
661 }
662
663 Ok(trust_state)
664}
665
666fn string_set_from_json(raw: &str) -> StoreResult<BTreeSet<String>> {
667 let value: serde_json::Value = serde_json::from_str(raw)?;
668 let Some(values) = value.as_array() else {
669 return Err(StoreError::Validation(
670 "semantic snapshot expected JSON array".into(),
671 ));
672 };
673 Ok(values
674 .iter()
675 .filter_map(|value| value.as_str().map(ToOwned::to_owned))
676 .collect())
677}
678
679fn parse_salience_score(raw: &str) -> StoreResult<SalienceScore> {
680 let value: serde_json::Value = serde_json::from_str(raw)?;
681 let score = value
682 .get("score")
683 .and_then(serde_json::Value::as_f64)
684 .unwrap_or_default()
685 .clamp(0.0, 1.0);
686 Ok(SalienceScore((score * 100.0).round() as u32))
687}
688
689impl SalienceDistribution {
690 fn add(&mut self, score: SalienceScore) {
691 match score.0 {
692 0..=24 => self.low += 1,
693 25..=74 => self.medium += 1,
694 75..=89 => self.high += 1,
695 _ => self.critical += 1,
696 }
697 }
698}
699
700fn parse_memory_kind(value: &str) -> MemoryKind {
701 match value {
702 "episodic" => MemoryKind::Episodic,
703 "procedural" => MemoryKind::Procedural,
704 "strategic" => MemoryKind::Strategic,
705 "affective" => MemoryKind::Affective,
706 _ => MemoryKind::Semantic,
707 }
708}
709
710fn parse_doctrine_force(value: &str) -> StoreResult<DoctrineForce> {
711 match value {
712 "Advisory" | "advisory" => Ok(DoctrineForce::Advisory),
713 "Conditioning" | "conditioning" => Ok(DoctrineForce::Conditioning),
714 "Gate" | "gate" => Ok(DoctrineForce::Gate),
715 other => Err(StoreError::Validation(format!(
716 "unknown doctrine force `{other}`"
717 ))),
718 }
719}
720
721fn parse_trust_tier(value: &str) -> StoreResult<TrustTier> {
722 match value {
723 "untrusted" => Ok(TrustTier::Untrusted),
724 "observed" => Ok(TrustTier::Observed),
725 "verified" => Ok(TrustTier::Verified),
726 "operator" => Ok(TrustTier::Operator),
727 other => Err(StoreError::Validation(format!(
728 "unknown trust tier `{other}`"
729 ))),
730 }
731}
732
733fn parse_key_state(value: &str) -> StoreResult<KeyState> {
734 match value {
735 "active" => Ok(KeyState::Active),
736 "retired" => Ok(KeyState::Retired),
737 "revoked" => Ok(KeyState::Revoked),
738 other => Err(StoreError::Validation(format!(
739 "unknown key state `{other}`"
740 ))),
741 }
742}
743
744fn truth_state_from_status(status: &str) -> TruthState {
745 match status {
746 "candidate" => TruthState::Candidate,
747 "promoted_to_doctrine" | "active" => TruthState::Active,
748 _ => TruthState::Unknown,
749 }
750}
751
752fn truth_state_from_authority(authority: &str) -> TruthState {
753 match authority {
754 "verified" | "operator" => TruthState::Validated,
755 "candidate" | "runtime" | "user" | "derived" => TruthState::Active,
756 _ => TruthState::Unknown,
757 }
758}
759
760fn compare_named_map<T, Missing, Added, Changed>(
761 diff: &mut SemanticDiff,
762 current: &BTreeMap<String, T>,
763 restored: &BTreeMap<String, T>,
764 missing: Missing,
765 added: Added,
766 changed: Changed,
767) where
768 T: Eq,
769 Missing: Fn(String) -> SemanticChange,
770 Added: Fn(String) -> SemanticChange,
771 Changed: Fn(String, &T, &T) -> SemanticChange,
772{
773 for (id, current_value) in current {
774 match restored.get(id) {
775 Some(restored_value) if restored_value == current_value => {}
776 Some(restored_value) => {
777 diff.changes
778 .push(changed(id.clone(), current_value, restored_value));
779 }
780 None => diff.changes.push(missing(id.clone())),
781 }
782 }
783
784 for id in restored.keys() {
785 if !current.contains_key(id) {
786 diff.changes.push(added(id.clone()));
787 }
788 }
789}
790
791fn compare_principals(diff: &mut SemanticDiff, current: &TrustKeyState, restored: &TrustKeyState) {
792 compare_named_map(
793 diff,
794 ¤t.principals,
795 &restored.principals,
796 |id| SemanticChange {
797 severity: SemanticSeverity::PreconditionUnmet,
798 kind: SemanticChangeKind::PrincipalMissing { id },
799 },
800 |id| SemanticChange {
801 severity: SemanticSeverity::Warning,
802 kind: SemanticChangeKind::PrincipalAdded { id },
803 },
804 |id, before, after| {
805 if after.trust_tier < before.trust_tier {
806 SemanticChange {
807 severity: SemanticSeverity::PreconditionUnmet,
808 kind: SemanticChangeKind::TrustTierDowngraded {
809 id,
810 current: before.trust_tier.clone(),
811 restored: after.trust_tier.clone(),
812 },
813 }
814 } else if after.trust_tier != before.trust_tier {
815 SemanticChange {
816 severity: SemanticSeverity::Warning,
817 kind: SemanticChangeKind::TrustTierChanged {
818 id,
819 current: before.trust_tier.clone(),
820 restored: after.trust_tier.clone(),
821 },
822 }
823 } else {
824 SemanticChange {
825 severity: SemanticSeverity::Warning,
826 kind: SemanticChangeKind::PrincipalChanged { id },
827 }
828 }
829 },
830 );
831}
832
833fn compare_keys(diff: &mut SemanticDiff, current: &TrustKeyState, restored: &TrustKeyState) {
834 compare_named_map(
835 diff,
836 ¤t.keys,
837 &restored.keys,
838 |id| SemanticChange {
839 severity: SemanticSeverity::PreconditionUnmet,
840 kind: SemanticChangeKind::KeyMissing { id },
841 },
842 |id| SemanticChange {
843 severity: SemanticSeverity::Warning,
844 kind: SemanticChangeKind::KeyAdded { id },
845 },
846 |id, before, after| {
847 if after.principal_id != before.principal_id {
848 SemanticChange {
849 severity: SemanticSeverity::PreconditionUnmet,
850 kind: SemanticChangeKind::KeyPrincipalChanged {
851 id,
852 current: before.principal_id.clone(),
853 restored: after.principal_id.clone(),
854 },
855 }
856 } else if after.key_state.permits_new_signing()
857 && !before.key_state.permits_new_signing()
858 {
859 SemanticChange {
860 severity: SemanticSeverity::PreconditionUnmet,
861 kind: SemanticChangeKind::KeyReactivated {
862 id,
863 current: before.key_state.clone(),
864 restored: after.key_state.clone(),
865 },
866 }
867 } else if after.key_state < before.key_state {
868 SemanticChange {
869 severity: SemanticSeverity::PreconditionUnmet,
870 kind: SemanticChangeKind::KeyStateDowngraded {
871 id,
872 current: before.key_state.clone(),
873 restored: after.key_state.clone(),
874 },
875 }
876 } else {
877 SemanticChange {
878 severity: SemanticSeverity::Warning,
879 kind: SemanticChangeKind::KeyChanged { id },
880 }
881 }
882 },
883 );
884}
885
886fn compare_truth_ceiling(
887 diff: &mut SemanticDiff,
888 current: &TruthCeilingState,
889 restored: &TruthCeilingState,
890) {
891 if restored.runtime_mode < current.runtime_mode {
892 diff.changes.push(SemanticChange {
893 severity: SemanticSeverity::PreconditionUnmet,
894 kind: SemanticChangeKind::RuntimeModeDowngraded {
895 current: current.runtime_mode.clone(),
896 restored: restored.runtime_mode.clone(),
897 },
898 });
899 }
900
901 if restored.proof_state < current.proof_state {
902 diff.changes.push(SemanticChange {
903 severity: SemanticSeverity::PreconditionUnmet,
904 kind: SemanticChangeKind::ProofStateDowngraded {
905 current: current.proof_state.clone(),
906 restored: restored.proof_state.clone(),
907 },
908 });
909 }
910
911 if restored.claim_ceiling < current.claim_ceiling {
912 diff.changes.push(SemanticChange {
913 severity: SemanticSeverity::PreconditionUnmet,
914 kind: SemanticChangeKind::TruthCeilingDowngraded {
915 current: current.claim_ceiling.clone(),
916 restored: restored.claim_ceiling.clone(),
917 },
918 });
919 } else if restored.claim_ceiling != current.claim_ceiling {
920 diff.changes.push(SemanticChange {
921 severity: SemanticSeverity::Warning,
922 kind: SemanticChangeKind::TruthCeilingChanged {
923 current: current.claim_ceiling.clone(),
924 restored: restored.claim_ceiling.clone(),
925 },
926 });
927 }
928
929 for (claim, current_ceiling) in ¤t.per_claim {
930 match restored.per_claim.get(claim) {
931 Some(restored_ceiling) if restored_ceiling < current_ceiling => {
932 diff.changes.push(SemanticChange {
933 severity: SemanticSeverity::PreconditionUnmet,
934 kind: SemanticChangeKind::ClaimTruthCeilingDowngraded {
935 claim: claim.clone(),
936 current: current_ceiling.clone(),
937 restored: restored_ceiling.clone(),
938 },
939 });
940 }
941 Some(restored_ceiling) if restored_ceiling != current_ceiling => {
942 diff.changes.push(SemanticChange {
943 severity: SemanticSeverity::Warning,
944 kind: SemanticChangeKind::ClaimTruthCeilingChanged {
945 claim: claim.clone(),
946 current: current_ceiling.clone(),
947 restored: restored_ceiling.clone(),
948 },
949 });
950 }
951 Some(_) => {}
952 None => diff.changes.push(SemanticChange {
953 severity: SemanticSeverity::PreconditionUnmet,
954 kind: SemanticChangeKind::ClaimTruthCeilingMissing {
955 claim: claim.clone(),
956 },
957 }),
958 }
959 }
960
961 for claim in restored.per_claim.keys() {
962 if !current.per_claim.contains_key(claim) {
963 diff.changes.push(SemanticChange {
964 severity: SemanticSeverity::Warning,
965 kind: SemanticChangeKind::ClaimTruthCeilingAdded {
966 claim: claim.clone(),
967 },
968 });
969 }
970 }
971}
972
973#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
975pub struct SemanticChange {
976 pub severity: SemanticSeverity,
978 pub kind: SemanticChangeKind,
980}
981
982impl SemanticChange {
983 fn active_principle_missing(id: String) -> Self {
984 Self {
985 severity: SemanticSeverity::PreconditionUnmet,
986 kind: SemanticChangeKind::ActivePrincipleMissing { id },
987 }
988 }
989
990 fn active_principle_added(id: String) -> Self {
991 Self {
992 severity: SemanticSeverity::Warning,
993 kind: SemanticChangeKind::ActivePrincipleAdded { id },
994 }
995 }
996
997 fn active_principle_changed(id: String) -> Self {
998 Self {
999 severity: SemanticSeverity::Warning,
1000 kind: SemanticChangeKind::ActivePrincipleChanged { id },
1001 }
1002 }
1003
1004 fn active_doctrine_missing(id: String) -> Self {
1005 Self {
1006 severity: SemanticSeverity::PreconditionUnmet,
1007 kind: SemanticChangeKind::ActiveDoctrineMissing { id },
1008 }
1009 }
1010
1011 fn active_doctrine_added(id: String) -> Self {
1012 Self {
1013 severity: SemanticSeverity::Warning,
1014 kind: SemanticChangeKind::ActiveDoctrineAdded { id },
1015 }
1016 }
1017
1018 fn active_doctrine_changed(id: String) -> Self {
1019 Self {
1020 severity: SemanticSeverity::Warning,
1021 kind: SemanticChangeKind::ActiveDoctrineChanged { id },
1022 }
1023 }
1024
1025 fn active_memory_missing(id: String) -> Self {
1026 Self {
1027 severity: SemanticSeverity::PreconditionUnmet,
1028 kind: SemanticChangeKind::ActiveMemoryMissing { id },
1029 }
1030 }
1031
1032 fn active_memory_added(id: String) -> Self {
1033 Self {
1034 severity: SemanticSeverity::Warning,
1035 kind: SemanticChangeKind::ActiveMemoryAdded { id },
1036 }
1037 }
1038
1039 fn active_memory_changed(id: String) -> Self {
1040 Self {
1041 severity: SemanticSeverity::Warning,
1042 kind: SemanticChangeKind::ActiveMemoryChanged { id },
1043 }
1044 }
1045
1046 fn truth_state_downgraded(
1047 artifact: ArtifactKind,
1048 id: String,
1049 current: TruthState,
1050 restored: TruthState,
1051 ) -> Self {
1052 Self {
1053 severity: SemanticSeverity::PreconditionUnmet,
1054 kind: SemanticChangeKind::TruthStateDowngraded {
1055 artifact,
1056 id,
1057 current,
1058 restored,
1059 },
1060 }
1061 }
1062}
1063
1064#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
1066#[serde(rename_all = "snake_case")]
1067pub enum SemanticSeverity {
1068 Clean,
1070 Warning,
1072 PreconditionUnmet,
1074}
1075
1076#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1078#[serde(rename_all = "snake_case")]
1079pub enum RestoreDecision {
1080 Clean,
1082 #[allow(missing_docs)]
1084 Warning {
1085 recovery_acknowledged: bool,
1086 change_count: usize,
1087 blocked_change_count: usize,
1088 },
1089 #[allow(missing_docs)]
1091 PreconditionUnmet { blocked_change_count: usize },
1092}
1093
1094impl RestoreDecision {
1095 pub fn from_diff(diff: &SemanticDiff, recovery_acknowledged: bool) -> Self {
1097 let blocked_change_count = diff
1098 .changes
1099 .iter()
1100 .filter(|change| change.severity == SemanticSeverity::PreconditionUnmet)
1101 .count();
1102
1103 if diff.is_clean() {
1104 Self::Clean
1105 } else if blocked_change_count == 0 || recovery_acknowledged {
1106 Self::Warning {
1107 recovery_acknowledged,
1108 change_count: diff.changes.len(),
1109 blocked_change_count,
1110 }
1111 } else {
1112 Self::PreconditionUnmet {
1113 blocked_change_count,
1114 }
1115 }
1116 }
1117
1118 pub fn clean() -> Self {
1120 Self::Clean
1121 }
1122
1123 pub fn warning(change_count: usize) -> Self {
1125 Self::Warning {
1126 recovery_acknowledged: false,
1127 change_count,
1128 blocked_change_count: 0,
1129 }
1130 }
1131
1132 pub fn precondition_unmet(blocked_change_count: usize) -> Self {
1134 Self::PreconditionUnmet {
1135 blocked_change_count,
1136 }
1137 }
1138
1139 pub fn allows_restore(&self) -> bool {
1141 matches!(self, Self::Clean | Self::Warning { .. })
1142 }
1143}
1144
1145#[allow(missing_docs)]
1147#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1148#[serde(rename_all = "snake_case")]
1149pub enum SemanticChangeKind {
1150 ActivePrincipleMissing {
1151 id: String,
1152 },
1153 ActivePrincipleAdded {
1154 id: String,
1155 },
1156 ActivePrincipleChanged {
1157 id: String,
1158 },
1159 ActiveDoctrineMissing {
1160 id: String,
1161 },
1162 ActiveDoctrineAdded {
1163 id: String,
1164 },
1165 ActiveDoctrineChanged {
1166 id: String,
1167 },
1168 ActiveMemoryMissing {
1169 id: String,
1170 },
1171 ActiveMemoryAdded {
1172 id: String,
1173 },
1174 ActiveMemoryChanged {
1175 id: String,
1176 },
1177 SalienceDistributionChanged {
1178 current: SalienceDistribution,
1179 restored: SalienceDistribution,
1180 },
1181 UnresolvedContradictionMissing {
1182 id: String,
1183 },
1184 UnresolvedContradictionAdded {
1185 id: String,
1186 },
1187 UnresolvedContradictionChanged {
1188 id: String,
1189 },
1190 PrincipalMissing {
1191 id: String,
1192 },
1193 PrincipalAdded {
1194 id: String,
1195 },
1196 PrincipalChanged {
1197 id: String,
1198 },
1199 TrustTierDowngraded {
1200 id: String,
1201 current: TrustTier,
1202 restored: TrustTier,
1203 },
1204 TrustTierChanged {
1205 id: String,
1206 current: TrustTier,
1207 restored: TrustTier,
1208 },
1209 KeyMissing {
1210 id: String,
1211 },
1212 KeyAdded {
1213 id: String,
1214 },
1215 KeyChanged {
1216 id: String,
1217 },
1218 KeyPrincipalChanged {
1219 id: String,
1220 current: String,
1221 restored: String,
1222 },
1223 KeyStateDowngraded {
1224 id: String,
1225 current: KeyState,
1226 restored: KeyState,
1227 },
1228 KeyReactivated {
1229 id: String,
1230 current: KeyState,
1231 restored: KeyState,
1232 },
1233 TruthStateDowngraded {
1234 artifact: ArtifactKind,
1235 id: String,
1236 current: TruthState,
1237 restored: TruthState,
1238 },
1239 RuntimeModeDowngraded {
1240 current: RuntimeMode,
1241 restored: RuntimeMode,
1242 },
1243 ProofStateDowngraded {
1244 current: ProofState,
1245 restored: ProofState,
1246 },
1247 TruthCeilingDowngraded {
1248 current: TruthCeiling,
1249 restored: TruthCeiling,
1250 },
1251 TruthCeilingChanged {
1252 current: TruthCeiling,
1253 restored: TruthCeiling,
1254 },
1255 ClaimTruthCeilingMissing {
1256 claim: String,
1257 },
1258 ClaimTruthCeilingAdded {
1259 claim: String,
1260 },
1261 ClaimTruthCeilingDowngraded {
1262 claim: String,
1263 current: TruthCeiling,
1264 restored: TruthCeiling,
1265 },
1266 ClaimTruthCeilingChanged {
1267 claim: String,
1268 current: TruthCeiling,
1269 restored: TruthCeiling,
1270 },
1271}
1272
1273#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1275#[serde(rename_all = "snake_case")]
1276pub enum ArtifactKind {
1277 ActivePrinciple,
1279 ActiveDoctrine,
1281 ActiveMemory,
1283}
1284
1285#[cfg(test)]
1286mod tests {
1287 use super::*;
1288
1289 #[test]
1290 fn restore_semantic_diff_detects_older_principle_set() {
1291 let mut current = base_snapshot();
1292 current.active_principles.insert(
1293 "prn_current".to_string(),
1294 PrincipleState {
1295 title: "Current principle".to_string(),
1296 truth_state: TruthState::FullChainVerified,
1297 supporting_memory_ids: BTreeSet::from(["mem_current".to_string()]),
1298 },
1299 );
1300 current.active_doctrine.insert(
1301 "doc_current".to_string(),
1302 DoctrineState {
1303 rule: "Current doctrine".to_string(),
1304 source_principle_id: "prn_current".to_string(),
1305 force: DoctrineForce::Gate,
1306 truth_state: TruthState::FullChainVerified,
1307 },
1308 );
1309
1310 let restored = base_snapshot();
1311 let diff = SemanticDiff::between(¤t, &restored);
1312
1313 assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
1314 assert!(diff.changes.iter().any(|change| matches!(
1315 &change.kind,
1316 SemanticChangeKind::ActivePrincipleMissing { id } if id == "prn_current"
1317 )));
1318 assert!(diff.changes.iter().any(|change| matches!(
1319 &change.kind,
1320 SemanticChangeKind::ActiveDoctrineMissing { id } if id == "doc_current"
1321 )));
1322 assert_eq!(
1323 diff.restore_decision(false),
1324 RestoreDecision::PreconditionUnmet {
1325 blocked_change_count: 2
1326 }
1327 );
1328 }
1329
1330 #[test]
1331 fn restore_semantic_diff_blocks_active_truth_state_downgrade_unless_acknowledged() {
1332 let mut current = base_snapshot();
1333 current.active_memories.insert(
1334 "mem_truth".to_string(),
1335 MemoryState {
1336 claim: "Restore candidate must not lower current truth".to_string(),
1337 memory_type: MemoryKind::Semantic,
1338 claim_key: Some("truth-slot".to_string()),
1339 truth_state: TruthState::FullChainVerified,
1340 salience: SalienceScore(90),
1341 },
1342 );
1343 current.truth_ceiling.claim_ceiling = TruthCeiling::TrustedLocalLedger;
1344
1345 let mut restored = current.clone();
1346 restored.active_memories.insert(
1347 "mem_truth".to_string(),
1348 MemoryState {
1349 claim: "Restore candidate must not lower current truth".to_string(),
1350 memory_type: MemoryKind::Semantic,
1351 claim_key: Some("truth-slot".to_string()),
1352 truth_state: TruthState::Active,
1353 salience: SalienceScore(90),
1354 },
1355 );
1356 restored.truth_ceiling.claim_ceiling = TruthCeiling::Advisory;
1357
1358 let diff = current.diff_against_restore(&restored);
1359
1360 assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
1361 assert!(diff.changes.iter().any(|change| matches!(
1362 &change.kind,
1363 SemanticChangeKind::TruthStateDowngraded {
1364 artifact: ArtifactKind::ActiveMemory,
1365 id,
1366 current: TruthState::FullChainVerified,
1367 restored: TruthState::Active,
1368 } if id == "mem_truth"
1369 )));
1370 assert!(diff.changes.iter().any(|change| matches!(
1371 &change.kind,
1372 SemanticChangeKind::TruthCeilingDowngraded {
1373 current: TruthCeiling::TrustedLocalLedger,
1374 restored: TruthCeiling::Advisory,
1375 }
1376 )));
1377 assert!(!diff.restore_decision(false).allows_restore());
1378 assert!(diff.restore_decision(true).allows_restore());
1379 assert_eq!(
1380 diff.restore_decision(true),
1381 RestoreDecision::Warning {
1382 recovery_acknowledged: true,
1383 change_count: 2,
1384 blocked_change_count: 2
1385 }
1386 );
1387 }
1388
1389 #[test]
1390 fn restore_semantic_diff_blocks_key_principal_rebinding() {
1391 let mut current = base_snapshot();
1392 current.trust_state.principals.insert(
1393 "principal_current".to_string(),
1394 PrincipalTrustState {
1395 trust_tier: TrustTier::Operator,
1396 key_ids: BTreeSet::from(["key_primary".to_string()]),
1397 },
1398 );
1399 current.trust_state.keys.insert(
1400 "key_primary".to_string(),
1401 KeyLifecycleState {
1402 principal_id: "principal_current".to_string(),
1403 key_state: KeyState::Active,
1404 },
1405 );
1406
1407 let mut restored = current.clone();
1408 restored.trust_state.principals.insert(
1409 "principal_restored".to_string(),
1410 PrincipalTrustState {
1411 trust_tier: TrustTier::Operator,
1412 key_ids: BTreeSet::from(["key_primary".to_string()]),
1413 },
1414 );
1415 restored.trust_state.keys.insert(
1416 "key_primary".to_string(),
1417 KeyLifecycleState {
1418 principal_id: "principal_restored".to_string(),
1419 key_state: KeyState::Active,
1420 },
1421 );
1422
1423 let diff = current.diff_against_restore(&restored);
1424
1425 assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
1426 assert!(diff.changes.iter().any(|change| matches!(
1427 &change.kind,
1428 SemanticChangeKind::KeyPrincipalChanged {
1429 id,
1430 current,
1431 restored,
1432 } if id == "key_primary"
1433 && current == "principal_current"
1434 && restored == "principal_restored"
1435 )));
1436 assert_eq!(
1437 diff.restore_decision(false),
1438 RestoreDecision::PreconditionUnmet {
1439 blocked_change_count: 1
1440 }
1441 );
1442 }
1443
1444 #[test]
1445 fn restore_semantic_diff_warns_on_salience_distribution_drift() {
1446 let current = base_snapshot();
1447 let mut restored = current.clone();
1448 restored.salience_distribution = SalienceDistribution {
1449 low: 0,
1450 medium: 1,
1451 high: 1,
1452 critical: 1,
1453 };
1454
1455 let diff = current.diff_against_restore(&restored);
1456
1457 assert_eq!(diff.severity(), SemanticSeverity::Warning);
1458 assert_eq!(diff.changes.len(), 1);
1459 assert!(matches!(
1460 &diff.changes[0],
1461 SemanticChange {
1462 severity: SemanticSeverity::Warning,
1463 kind: SemanticChangeKind::SalienceDistributionChanged {
1464 current: SalienceDistribution {
1465 low: 1,
1466 medium: 1,
1467 high: 0,
1468 critical: 0,
1469 },
1470 restored: SalienceDistribution {
1471 low: 0,
1472 medium: 1,
1473 high: 1,
1474 critical: 1,
1475 },
1476 },
1477 }
1478 ));
1479 assert_eq!(diff.restore_decision(false), RestoreDecision::warning(1));
1480 }
1481
1482 #[test]
1483 fn restore_semantic_diff_blocks_unresolved_contradiction_change() {
1484 let mut current = base_snapshot();
1485 current.unresolved_contradictions.insert(
1486 "ctr_current".to_string(),
1487 ContradictionState {
1488 claim_key: "claim://operator-intent".to_string(),
1489 memory_ids: BTreeSet::from(["mem_a".to_string(), "mem_b".to_string()]),
1490 reason: "current unresolved tension".to_string(),
1491 },
1492 );
1493
1494 let mut restored = current.clone();
1495 restored.unresolved_contradictions.insert(
1496 "ctr_current".to_string(),
1497 ContradictionState {
1498 claim_key: "claim://operator-intent".to_string(),
1499 memory_ids: BTreeSet::from(["mem_a".to_string(), "mem_c".to_string()]),
1500 reason: "restored conflict points at different evidence".to_string(),
1501 },
1502 );
1503
1504 let diff = current.diff_against_restore(&restored);
1505
1506 assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
1507 assert!(diff.changes.iter().any(|change| matches!(
1508 &change.kind,
1509 SemanticChangeKind::UnresolvedContradictionChanged { id } if id == "ctr_current"
1510 )));
1511 assert_eq!(
1512 diff.restore_decision(false),
1513 RestoreDecision::PreconditionUnmet {
1514 blocked_change_count: 1
1515 }
1516 );
1517 }
1518
1519 #[test]
1520 fn restore_semantic_diff_reports_trust_tier_drift() {
1521 let mut current = base_snapshot();
1522 current.trust_state.principals.insert(
1523 "principal_downgraded".to_string(),
1524 PrincipalTrustState {
1525 trust_tier: TrustTier::Operator,
1526 key_ids: BTreeSet::from(["key_operator".to_string()]),
1527 },
1528 );
1529 current.trust_state.principals.insert(
1530 "principal_upgraded".to_string(),
1531 PrincipalTrustState {
1532 trust_tier: TrustTier::Observed,
1533 key_ids: BTreeSet::from(["key_observed".to_string()]),
1534 },
1535 );
1536
1537 let mut restored = current.clone();
1538 restored.trust_state.principals.insert(
1539 "principal_downgraded".to_string(),
1540 PrincipalTrustState {
1541 trust_tier: TrustTier::Verified,
1542 key_ids: BTreeSet::from(["key_operator".to_string()]),
1543 },
1544 );
1545 restored.trust_state.principals.insert(
1546 "principal_upgraded".to_string(),
1547 PrincipalTrustState {
1548 trust_tier: TrustTier::Verified,
1549 key_ids: BTreeSet::from(["key_observed".to_string()]),
1550 },
1551 );
1552
1553 let diff = current.diff_against_restore(&restored);
1554
1555 assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
1556 assert_eq!(diff.changes.len(), 2);
1557 assert!(diff.changes.iter().any(|change| matches!(
1558 &change.kind,
1559 SemanticChangeKind::TrustTierDowngraded {
1560 id,
1561 current: TrustTier::Operator,
1562 restored: TrustTier::Verified,
1563 } if id == "principal_downgraded"
1564 )));
1565 assert!(diff.changes.iter().any(|change| matches!(
1566 &change.kind,
1567 SemanticChangeKind::TrustTierChanged {
1568 id,
1569 current: TrustTier::Observed,
1570 restored: TrustTier::Verified,
1571 } if id == "principal_upgraded"
1572 )));
1573 assert_eq!(
1574 diff.restore_decision(false),
1575 RestoreDecision::PreconditionUnmet {
1576 blocked_change_count: 1
1577 }
1578 );
1579 }
1580
1581 #[test]
1582 fn semantic_snapshot_extracts_active_store_state() {
1583 let pool = rusqlite::Connection::open_in_memory().expect("open sqlite");
1584 crate::migrate::apply_pending(&pool).expect("migrate");
1585 pool.execute(
1586 "INSERT INTO memories (
1587 id, memory_type, status, claim, source_episodes_json, source_events_json,
1588 domains_json, salience_json, confidence, authority, applies_when_json,
1589 does_not_apply_when_json, created_at, updated_at
1590 ) VALUES (
1591 'mem_snapshot', 'semantic', 'active', 'Snapshot extraction is read-only.',
1592 '[]', '[\"evt_snapshot\"]', '[]', '{\"score\":0.8}', 0.7, 'verified',
1593 '[]', '[]', '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
1594 );",
1595 [],
1596 )
1597 .expect("insert memory");
1598 pool.execute(
1599 "INSERT INTO principles (
1600 id, statement, status, supporting_memories_json, contradicting_memories_json,
1601 domains_observed_json, applies_when_json, does_not_apply_when_json,
1602 confidence, validation, brightness, created_by_json, created_at, updated_at
1603 ) VALUES (
1604 'prn_snapshot', 'Snapshot extraction must not mutate restore state.',
1605 'promoted_to_doctrine', '[\"mem_snapshot\"]', '[]', '[]', '[]', '[]',
1606 0.8, 0.8, 0.8, '{}', '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
1607 );",
1608 [],
1609 )
1610 .expect("insert principle");
1611 pool.execute(
1612 "INSERT INTO doctrine (
1613 id, source_principle, rule, force, promotion_reason, promoted_by_json, created_at
1614 ) VALUES (
1615 'doc_snapshot', 'prn_snapshot', 'Snapshot extraction must not mutate restore state.',
1616 'Gate', 'test', '{}', '2026-05-04T12:00:00Z'
1617 );",
1618 [],
1619 )
1620 .expect("insert doctrine");
1621 pool.execute(
1622 "INSERT INTO contradictions (
1623 id, left_ref, right_ref, contradiction_type, status, interpretation, created_at, updated_at
1624 ) VALUES (
1625 'ctr_snapshot', 'mem:a', 'mem:b', 'conflicting_causal_claim',
1626 'unresolved', NULL, '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
1627 );",
1628 [],
1629 )
1630 .expect("insert contradiction");
1631 pool.execute(
1632 "INSERT INTO authority_principal_timeline (
1633 principal_id, trust_tier, effective_at, trust_review_due_at, removed_at, audit_ref
1634 ) VALUES (
1635 'operator', 'operator', '2026-05-04T12:00:00Z', NULL, NULL, NULL
1636 );",
1637 [],
1638 )
1639 .expect("insert principal");
1640 pool.execute(
1641 "INSERT INTO authority_key_timeline (
1642 key_id, principal_id, state, effective_at, reason, audit_ref
1643 ) VALUES (
1644 'key_operator', 'operator', 'active', '2026-05-04T12:00:00Z', NULL, NULL
1645 );",
1646 [],
1647 )
1648 .expect("insert key");
1649
1650 let snapshot = semantic_snapshot_from_store(&pool).expect("extract snapshot");
1651
1652 assert_eq!(snapshot.snapshot_id.as_deref(), Some("store-current"));
1653 assert_eq!(
1654 snapshot.active_memories["mem_snapshot"].truth_state,
1655 TruthState::Validated
1656 );
1657 assert_eq!(
1658 snapshot.active_memories["mem_snapshot"].salience,
1659 SalienceScore(80)
1660 );
1661 assert_eq!(snapshot.salience_distribution.high, 1);
1662 assert_eq!(
1663 snapshot.active_principles["prn_snapshot"].supporting_memory_ids,
1664 BTreeSet::from(["mem_snapshot".to_string()])
1665 );
1666 assert_eq!(
1667 snapshot.active_doctrine["doc_snapshot"].force,
1668 DoctrineForce::Gate
1669 );
1670 assert!(snapshot
1671 .unresolved_contradictions
1672 .contains_key("ctr_snapshot"));
1673 assert_eq!(
1674 snapshot.trust_state.principals["operator"].trust_tier,
1675 TrustTier::Operator
1676 );
1677 assert_eq!(
1678 snapshot.trust_state.keys["key_operator"].key_state,
1679 KeyState::Active
1680 );
1681 }
1682
1683 fn base_snapshot() -> SemanticSnapshot {
1684 SemanticSnapshot {
1685 snapshot_id: Some("snap".to_string()),
1686 schema_version: Some(2),
1687 active_principles: BTreeMap::new(),
1688 active_doctrine: BTreeMap::new(),
1689 active_memories: BTreeMap::new(),
1690 salience_distribution: SalienceDistribution {
1691 low: 1,
1692 medium: 1,
1693 high: 0,
1694 critical: 0,
1695 },
1696 unresolved_contradictions: BTreeMap::new(),
1697 trust_state: TrustKeyState::default(),
1698 truth_ceiling: TruthCeilingState {
1699 runtime_mode: RuntimeMode::SignedLocalLedger,
1700 proof_state: ProofState::FullChainVerified,
1701 claim_ceiling: TruthCeiling::ObservedLocal,
1702 per_claim: BTreeMap::new(),
1703 },
1704 }
1705 }
1706}