use std::collections::{BTreeMap, BTreeSet};
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use crate::{Pool, StoreError, StoreResult};
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct SemanticSnapshot {
pub snapshot_id: Option<String>,
pub schema_version: Option<u16>,
pub active_principles: BTreeMap<String, PrincipleState>,
pub active_doctrine: BTreeMap<String, DoctrineState>,
pub active_memories: BTreeMap<String, MemoryState>,
pub salience_distribution: SalienceDistribution,
pub unresolved_contradictions: BTreeMap<String, ContradictionState>,
pub trust_state: TrustKeyState,
pub truth_ceiling: TruthCeilingState,
}
impl SemanticSnapshot {
pub fn diff_against_restore(&self, restored: &Self) -> SemanticDiff {
SemanticDiff::between(self, restored)
}
}
pub fn semantic_snapshot_from_store(pool: &Pool) -> StoreResult<SemanticSnapshot> {
let schema_version = current_schema_version(pool)?;
let mut active_memories = BTreeMap::new();
let mut salience_distribution = SalienceDistribution::default();
let mut memory_stmt = pool.prepare(
"SELECT id, memory_type, claim, salience_json, authority
FROM memories
WHERE status = 'active'
ORDER BY id;",
)?;
let memory_rows = memory_stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, String>(4)?,
))
})?;
for row in memory_rows {
let (id, memory_type, claim, salience_json, authority) = row?;
let salience = parse_salience_score(&salience_json)?;
salience_distribution.add(salience);
active_memories.insert(
id,
MemoryState {
claim,
memory_type: parse_memory_kind(&memory_type),
claim_key: None,
truth_state: truth_state_from_authority(&authority),
salience,
},
);
}
Ok(SemanticSnapshot {
snapshot_id: Some("store-current".into()),
schema_version,
active_principles: active_principles_from_store(pool)?,
active_doctrine: active_doctrine_from_store(pool)?,
active_memories,
salience_distribution,
unresolved_contradictions: unresolved_contradictions_from_store(pool)?,
trust_state: trust_state_from_store(pool)?,
truth_ceiling: TruthCeilingState::default(),
})
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct PrincipleState {
pub title: String,
pub truth_state: TruthState,
pub supporting_memory_ids: BTreeSet<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct DoctrineState {
pub rule: String,
pub source_principle_id: String,
pub force: DoctrineForce,
pub truth_state: TruthState,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DoctrineForce {
Advisory,
Conditioning,
Gate,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct MemoryState {
pub claim: String,
pub memory_type: MemoryKind,
pub claim_key: Option<String>,
pub truth_state: TruthState,
pub salience: SalienceScore,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryKind {
Episodic,
Semantic,
Procedural,
Strategic,
Affective,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct SalienceScore(pub u32);
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(default)]
pub struct SalienceDistribution {
pub low: u32,
pub medium: u32,
pub high: u32,
pub critical: u32,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct ContradictionState {
pub claim_key: String,
pub memory_ids: BTreeSet<String>,
pub reason: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct TrustKeyState {
pub principals: BTreeMap<String, PrincipalTrustState>,
pub keys: BTreeMap<String, KeyLifecycleState>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct PrincipalTrustState {
pub trust_tier: TrustTier,
pub key_ids: BTreeSet<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
Untrusted,
Observed,
Verified,
Operator,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct KeyLifecycleState {
pub principal_id: String,
pub key_state: KeyState,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyState {
Revoked,
Retired,
Active,
}
impl KeyState {
fn permits_new_signing(&self) -> bool {
matches!(self, Self::Active)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TruthState {
Unknown,
Candidate,
Active,
Validated,
FullChainVerified,
AuthorityGrade,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(default)]
pub struct TruthCeilingState {
pub runtime_mode: RuntimeMode,
pub proof_state: ProofState,
pub claim_ceiling: TruthCeiling,
pub per_claim: BTreeMap<String, TruthCeiling>,
}
impl Default for TruthCeilingState {
fn default() -> Self {
Self {
runtime_mode: RuntimeMode::Unknown,
proof_state: ProofState::Unknown,
claim_ceiling: TruthCeiling::Unknown,
per_claim: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMode {
Unknown,
Dev,
LocalUnsigned,
SignedLocalLedger,
ExternallyAnchored,
AuthorityGrade,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProofState {
Broken,
Unknown,
Partial,
FullChainVerified,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TruthCeiling {
None,
Unknown,
Advisory,
ObservedLocal,
TrustedLocalLedger,
ExternallyAnchored,
AuthorityGrade,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct SemanticDiff {
pub current_snapshot_id: Option<String>,
pub restored_snapshot_id: Option<String>,
pub changes: Vec<SemanticChange>,
}
impl SemanticDiff {
pub fn between(current: &SemanticSnapshot, restored: &SemanticSnapshot) -> Self {
let mut diff = Self {
current_snapshot_id: current.snapshot_id.clone(),
restored_snapshot_id: restored.snapshot_id.clone(),
changes: Vec::new(),
};
compare_named_map(
&mut diff,
¤t.active_principles,
&restored.active_principles,
SemanticChange::active_principle_missing,
SemanticChange::active_principle_added,
|id, before, after| {
if after.truth_state < before.truth_state {
SemanticChange::truth_state_downgraded(
ArtifactKind::ActivePrinciple,
id,
before.truth_state.clone(),
after.truth_state.clone(),
)
} else {
SemanticChange::active_principle_changed(id)
}
},
);
compare_named_map(
&mut diff,
¤t.active_doctrine,
&restored.active_doctrine,
SemanticChange::active_doctrine_missing,
SemanticChange::active_doctrine_added,
|id, before, after| {
if after.truth_state < before.truth_state {
SemanticChange::truth_state_downgraded(
ArtifactKind::ActiveDoctrine,
id,
before.truth_state.clone(),
after.truth_state.clone(),
)
} else {
SemanticChange::active_doctrine_changed(id)
}
},
);
compare_named_map(
&mut diff,
¤t.active_memories,
&restored.active_memories,
SemanticChange::active_memory_missing,
SemanticChange::active_memory_added,
|id, before, after| {
if after.truth_state < before.truth_state {
SemanticChange::truth_state_downgraded(
ArtifactKind::ActiveMemory,
id,
before.truth_state.clone(),
after.truth_state.clone(),
)
} else {
SemanticChange::active_memory_changed(id)
}
},
);
if current.salience_distribution != restored.salience_distribution {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::SalienceDistributionChanged {
current: current.salience_distribution.clone(),
restored: restored.salience_distribution.clone(),
},
});
}
compare_named_map(
&mut diff,
¤t.unresolved_contradictions,
&restored.unresolved_contradictions,
|id| SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::UnresolvedContradictionMissing { id },
},
|id| SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::UnresolvedContradictionAdded { id },
},
|id, _, _| SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::UnresolvedContradictionChanged { id },
},
);
compare_principals(&mut diff, ¤t.trust_state, &restored.trust_state);
compare_keys(&mut diff, ¤t.trust_state, &restored.trust_state);
compare_truth_ceiling(&mut diff, ¤t.truth_ceiling, &restored.truth_ceiling);
diff
}
pub fn severity(&self) -> SemanticSeverity {
self.changes
.iter()
.map(|change| change.severity)
.max()
.unwrap_or(SemanticSeverity::Clean)
}
pub fn is_clean(&self) -> bool {
self.changes.is_empty()
}
pub fn restore_decision(&self, recovery_acknowledged: bool) -> RestoreDecision {
RestoreDecision::from_diff(self, recovery_acknowledged)
}
}
fn current_schema_version(pool: &Pool) -> StoreResult<Option<u16>> {
let version = pool
.query_row(
"SELECT schema_version FROM events
UNION ALL
SELECT schema_version FROM traces
ORDER BY schema_version DESC
LIMIT 1;",
[],
|row| row.get::<_, u16>(0),
)
.optional()?;
Ok(version)
}
fn active_principles_from_store(pool: &Pool) -> StoreResult<BTreeMap<String, PrincipleState>> {
let mut stmt = pool.prepare(
"SELECT id, statement, status, supporting_memories_json
FROM principles
WHERE status IN ('active', 'promoted_to_doctrine')
ORDER BY id;",
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})?;
let mut principles = BTreeMap::new();
for row in rows {
let (id, title, status, supporting_memories_json) = row?;
let supporting_memory_ids = string_set_from_json(&supporting_memories_json)?;
principles.insert(
id,
PrincipleState {
title,
truth_state: truth_state_from_status(&status),
supporting_memory_ids,
},
);
}
Ok(principles)
}
fn active_doctrine_from_store(pool: &Pool) -> StoreResult<BTreeMap<String, DoctrineState>> {
let mut stmt = pool.prepare(
"SELECT id, source_principle, rule, force
FROM doctrine
ORDER BY id;",
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})?;
let mut doctrine = BTreeMap::new();
for row in rows {
let (id, source_principle_id, rule, force) = row?;
doctrine.insert(
id,
DoctrineState {
rule,
source_principle_id,
force: parse_doctrine_force(&force)?,
truth_state: TruthState::Active,
},
);
}
Ok(doctrine)
}
fn unresolved_contradictions_from_store(
pool: &Pool,
) -> StoreResult<BTreeMap<String, ContradictionState>> {
let mut stmt = pool.prepare(
"SELECT id, left_ref, right_ref, contradiction_type
FROM contradictions
WHERE status = 'unresolved'
ORDER BY id;",
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})?;
let mut contradictions = BTreeMap::new();
for row in rows {
let (id, left_ref, right_ref, reason) = row?;
contradictions.insert(
id,
ContradictionState {
claim_key: format!("{left_ref}|{right_ref}"),
memory_ids: BTreeSet::from([left_ref, right_ref]),
reason,
},
);
}
Ok(contradictions)
}
fn trust_state_from_store(pool: &Pool) -> StoreResult<TrustKeyState> {
let mut trust_state = TrustKeyState::default();
let mut principal_stmt = pool.prepare(
"SELECT p.principal_id, p.trust_tier
FROM authority_principal_timeline p
WHERE NOT EXISTS (
SELECT 1 FROM authority_principal_timeline later
WHERE later.principal_id = p.principal_id
AND later.effective_at > p.effective_at
)
ORDER BY p.principal_id;",
)?;
let principal_rows = principal_stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
for row in principal_rows {
let (principal_id, trust_tier) = row?;
trust_state.principals.insert(
principal_id,
PrincipalTrustState {
trust_tier: parse_trust_tier(&trust_tier)?,
key_ids: BTreeSet::new(),
},
);
}
let mut key_stmt = pool.prepare(
"SELECT k.key_id, k.principal_id, k.state
FROM authority_key_timeline k
WHERE NOT EXISTS (
SELECT 1 FROM authority_key_timeline later
WHERE later.key_id = k.key_id
AND later.effective_at > k.effective_at
)
ORDER BY k.key_id, k.state;",
)?;
let key_rows = key_stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
for row in key_rows {
let (key_id, principal_id, key_state) = row?;
trust_state
.principals
.entry(principal_id.clone())
.or_insert_with(|| PrincipalTrustState {
trust_tier: TrustTier::Untrusted,
key_ids: BTreeSet::new(),
})
.key_ids
.insert(key_id.clone());
trust_state.keys.insert(
key_id,
KeyLifecycleState {
principal_id,
key_state: parse_key_state(&key_state)?,
},
);
}
Ok(trust_state)
}
fn string_set_from_json(raw: &str) -> StoreResult<BTreeSet<String>> {
let value: serde_json::Value = serde_json::from_str(raw)?;
let Some(values) = value.as_array() else {
return Err(StoreError::Validation(
"semantic snapshot expected JSON array".into(),
));
};
Ok(values
.iter()
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
.collect())
}
fn parse_salience_score(raw: &str) -> StoreResult<SalienceScore> {
let value: serde_json::Value = serde_json::from_str(raw)?;
let score = value
.get("score")
.and_then(serde_json::Value::as_f64)
.unwrap_or_default()
.clamp(0.0, 1.0);
Ok(SalienceScore((score * 100.0).round() as u32))
}
impl SalienceDistribution {
fn add(&mut self, score: SalienceScore) {
match score.0 {
0..=24 => self.low += 1,
25..=74 => self.medium += 1,
75..=89 => self.high += 1,
_ => self.critical += 1,
}
}
}
fn parse_memory_kind(value: &str) -> MemoryKind {
match value {
"episodic" => MemoryKind::Episodic,
"procedural" => MemoryKind::Procedural,
"strategic" => MemoryKind::Strategic,
"affective" => MemoryKind::Affective,
_ => MemoryKind::Semantic,
}
}
fn parse_doctrine_force(value: &str) -> StoreResult<DoctrineForce> {
match value {
"Advisory" | "advisory" => Ok(DoctrineForce::Advisory),
"Conditioning" | "conditioning" => Ok(DoctrineForce::Conditioning),
"Gate" | "gate" => Ok(DoctrineForce::Gate),
other => Err(StoreError::Validation(format!(
"unknown doctrine force `{other}`"
))),
}
}
fn parse_trust_tier(value: &str) -> StoreResult<TrustTier> {
match value {
"untrusted" => Ok(TrustTier::Untrusted),
"observed" => Ok(TrustTier::Observed),
"verified" => Ok(TrustTier::Verified),
"operator" => Ok(TrustTier::Operator),
other => Err(StoreError::Validation(format!(
"unknown trust tier `{other}`"
))),
}
}
fn parse_key_state(value: &str) -> StoreResult<KeyState> {
match value {
"active" => Ok(KeyState::Active),
"retired" => Ok(KeyState::Retired),
"revoked" => Ok(KeyState::Revoked),
other => Err(StoreError::Validation(format!(
"unknown key state `{other}`"
))),
}
}
fn truth_state_from_status(status: &str) -> TruthState {
match status {
"candidate" => TruthState::Candidate,
"promoted_to_doctrine" | "active" => TruthState::Active,
_ => TruthState::Unknown,
}
}
fn truth_state_from_authority(authority: &str) -> TruthState {
match authority {
"verified" | "operator" => TruthState::Validated,
"candidate" | "runtime" | "user" | "derived" => TruthState::Active,
_ => TruthState::Unknown,
}
}
fn compare_named_map<T, Missing, Added, Changed>(
diff: &mut SemanticDiff,
current: &BTreeMap<String, T>,
restored: &BTreeMap<String, T>,
missing: Missing,
added: Added,
changed: Changed,
) where
T: Eq,
Missing: Fn(String) -> SemanticChange,
Added: Fn(String) -> SemanticChange,
Changed: Fn(String, &T, &T) -> SemanticChange,
{
for (id, current_value) in current {
match restored.get(id) {
Some(restored_value) if restored_value == current_value => {}
Some(restored_value) => {
diff.changes
.push(changed(id.clone(), current_value, restored_value));
}
None => diff.changes.push(missing(id.clone())),
}
}
for id in restored.keys() {
if !current.contains_key(id) {
diff.changes.push(added(id.clone()));
}
}
}
fn compare_principals(diff: &mut SemanticDiff, current: &TrustKeyState, restored: &TrustKeyState) {
compare_named_map(
diff,
¤t.principals,
&restored.principals,
|id| SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::PrincipalMissing { id },
},
|id| SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::PrincipalAdded { id },
},
|id, before, after| {
if after.trust_tier < before.trust_tier {
SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::TrustTierDowngraded {
id,
current: before.trust_tier.clone(),
restored: after.trust_tier.clone(),
},
}
} else if after.trust_tier != before.trust_tier {
SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::TrustTierChanged {
id,
current: before.trust_tier.clone(),
restored: after.trust_tier.clone(),
},
}
} else {
SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::PrincipalChanged { id },
}
}
},
);
}
fn compare_keys(diff: &mut SemanticDiff, current: &TrustKeyState, restored: &TrustKeyState) {
compare_named_map(
diff,
¤t.keys,
&restored.keys,
|id| SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::KeyMissing { id },
},
|id| SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::KeyAdded { id },
},
|id, before, after| {
if after.principal_id != before.principal_id {
SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::KeyPrincipalChanged {
id,
current: before.principal_id.clone(),
restored: after.principal_id.clone(),
},
}
} else if after.key_state.permits_new_signing()
&& !before.key_state.permits_new_signing()
{
SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::KeyReactivated {
id,
current: before.key_state.clone(),
restored: after.key_state.clone(),
},
}
} else if after.key_state < before.key_state {
SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::KeyStateDowngraded {
id,
current: before.key_state.clone(),
restored: after.key_state.clone(),
},
}
} else {
SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::KeyChanged { id },
}
}
},
);
}
fn compare_truth_ceiling(
diff: &mut SemanticDiff,
current: &TruthCeilingState,
restored: &TruthCeilingState,
) {
if restored.runtime_mode < current.runtime_mode {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::RuntimeModeDowngraded {
current: current.runtime_mode.clone(),
restored: restored.runtime_mode.clone(),
},
});
}
if restored.proof_state < current.proof_state {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::ProofStateDowngraded {
current: current.proof_state.clone(),
restored: restored.proof_state.clone(),
},
});
}
if restored.claim_ceiling < current.claim_ceiling {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::TruthCeilingDowngraded {
current: current.claim_ceiling.clone(),
restored: restored.claim_ceiling.clone(),
},
});
} else if restored.claim_ceiling != current.claim_ceiling {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::TruthCeilingChanged {
current: current.claim_ceiling.clone(),
restored: restored.claim_ceiling.clone(),
},
});
}
for (claim, current_ceiling) in ¤t.per_claim {
match restored.per_claim.get(claim) {
Some(restored_ceiling) if restored_ceiling < current_ceiling => {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::ClaimTruthCeilingDowngraded {
claim: claim.clone(),
current: current_ceiling.clone(),
restored: restored_ceiling.clone(),
},
});
}
Some(restored_ceiling) if restored_ceiling != current_ceiling => {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ClaimTruthCeilingChanged {
claim: claim.clone(),
current: current_ceiling.clone(),
restored: restored_ceiling.clone(),
},
});
}
Some(_) => {}
None => diff.changes.push(SemanticChange {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::ClaimTruthCeilingMissing {
claim: claim.clone(),
},
}),
}
}
for claim in restored.per_claim.keys() {
if !current.per_claim.contains_key(claim) {
diff.changes.push(SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ClaimTruthCeilingAdded {
claim: claim.clone(),
},
});
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SemanticChange {
pub severity: SemanticSeverity,
pub kind: SemanticChangeKind,
}
impl SemanticChange {
fn active_principle_missing(id: String) -> Self {
Self {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::ActivePrincipleMissing { id },
}
}
fn active_principle_added(id: String) -> Self {
Self {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ActivePrincipleAdded { id },
}
}
fn active_principle_changed(id: String) -> Self {
Self {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ActivePrincipleChanged { id },
}
}
fn active_doctrine_missing(id: String) -> Self {
Self {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::ActiveDoctrineMissing { id },
}
}
fn active_doctrine_added(id: String) -> Self {
Self {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ActiveDoctrineAdded { id },
}
}
fn active_doctrine_changed(id: String) -> Self {
Self {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ActiveDoctrineChanged { id },
}
}
fn active_memory_missing(id: String) -> Self {
Self {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::ActiveMemoryMissing { id },
}
}
fn active_memory_added(id: String) -> Self {
Self {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ActiveMemoryAdded { id },
}
}
fn active_memory_changed(id: String) -> Self {
Self {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::ActiveMemoryChanged { id },
}
}
fn truth_state_downgraded(
artifact: ArtifactKind,
id: String,
current: TruthState,
restored: TruthState,
) -> Self {
Self {
severity: SemanticSeverity::PreconditionUnmet,
kind: SemanticChangeKind::TruthStateDowngraded {
artifact,
id,
current,
restored,
},
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticSeverity {
Clean,
Warning,
PreconditionUnmet,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RestoreDecision {
Clean,
#[allow(missing_docs)]
Warning {
recovery_acknowledged: bool,
change_count: usize,
blocked_change_count: usize,
},
#[allow(missing_docs)]
PreconditionUnmet { blocked_change_count: usize },
}
impl RestoreDecision {
pub fn from_diff(diff: &SemanticDiff, recovery_acknowledged: bool) -> Self {
let blocked_change_count = diff
.changes
.iter()
.filter(|change| change.severity == SemanticSeverity::PreconditionUnmet)
.count();
if diff.is_clean() {
Self::Clean
} else if blocked_change_count == 0 || recovery_acknowledged {
Self::Warning {
recovery_acknowledged,
change_count: diff.changes.len(),
blocked_change_count,
}
} else {
Self::PreconditionUnmet {
blocked_change_count,
}
}
}
pub fn clean() -> Self {
Self::Clean
}
pub fn warning(change_count: usize) -> Self {
Self::Warning {
recovery_acknowledged: false,
change_count,
blocked_change_count: 0,
}
}
pub fn precondition_unmet(blocked_change_count: usize) -> Self {
Self::PreconditionUnmet {
blocked_change_count,
}
}
pub fn allows_restore(&self) -> bool {
matches!(self, Self::Clean | Self::Warning { .. })
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticChangeKind {
ActivePrincipleMissing {
id: String,
},
ActivePrincipleAdded {
id: String,
},
ActivePrincipleChanged {
id: String,
},
ActiveDoctrineMissing {
id: String,
},
ActiveDoctrineAdded {
id: String,
},
ActiveDoctrineChanged {
id: String,
},
ActiveMemoryMissing {
id: String,
},
ActiveMemoryAdded {
id: String,
},
ActiveMemoryChanged {
id: String,
},
SalienceDistributionChanged {
current: SalienceDistribution,
restored: SalienceDistribution,
},
UnresolvedContradictionMissing {
id: String,
},
UnresolvedContradictionAdded {
id: String,
},
UnresolvedContradictionChanged {
id: String,
},
PrincipalMissing {
id: String,
},
PrincipalAdded {
id: String,
},
PrincipalChanged {
id: String,
},
TrustTierDowngraded {
id: String,
current: TrustTier,
restored: TrustTier,
},
TrustTierChanged {
id: String,
current: TrustTier,
restored: TrustTier,
},
KeyMissing {
id: String,
},
KeyAdded {
id: String,
},
KeyChanged {
id: String,
},
KeyPrincipalChanged {
id: String,
current: String,
restored: String,
},
KeyStateDowngraded {
id: String,
current: KeyState,
restored: KeyState,
},
KeyReactivated {
id: String,
current: KeyState,
restored: KeyState,
},
TruthStateDowngraded {
artifact: ArtifactKind,
id: String,
current: TruthState,
restored: TruthState,
},
RuntimeModeDowngraded {
current: RuntimeMode,
restored: RuntimeMode,
},
ProofStateDowngraded {
current: ProofState,
restored: ProofState,
},
TruthCeilingDowngraded {
current: TruthCeiling,
restored: TruthCeiling,
},
TruthCeilingChanged {
current: TruthCeiling,
restored: TruthCeiling,
},
ClaimTruthCeilingMissing {
claim: String,
},
ClaimTruthCeilingAdded {
claim: String,
},
ClaimTruthCeilingDowngraded {
claim: String,
current: TruthCeiling,
restored: TruthCeiling,
},
ClaimTruthCeilingChanged {
claim: String,
current: TruthCeiling,
restored: TruthCeiling,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactKind {
ActivePrinciple,
ActiveDoctrine,
ActiveMemory,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restore_semantic_diff_detects_older_principle_set() {
let mut current = base_snapshot();
current.active_principles.insert(
"prn_current".to_string(),
PrincipleState {
title: "Current principle".to_string(),
truth_state: TruthState::FullChainVerified,
supporting_memory_ids: BTreeSet::from(["mem_current".to_string()]),
},
);
current.active_doctrine.insert(
"doc_current".to_string(),
DoctrineState {
rule: "Current doctrine".to_string(),
source_principle_id: "prn_current".to_string(),
force: DoctrineForce::Gate,
truth_state: TruthState::FullChainVerified,
},
);
let restored = base_snapshot();
let diff = SemanticDiff::between(¤t, &restored);
assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::ActivePrincipleMissing { id } if id == "prn_current"
)));
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::ActiveDoctrineMissing { id } if id == "doc_current"
)));
assert_eq!(
diff.restore_decision(false),
RestoreDecision::PreconditionUnmet {
blocked_change_count: 2
}
);
}
#[test]
fn restore_semantic_diff_blocks_active_truth_state_downgrade_unless_acknowledged() {
let mut current = base_snapshot();
current.active_memories.insert(
"mem_truth".to_string(),
MemoryState {
claim: "Restore candidate must not lower current truth".to_string(),
memory_type: MemoryKind::Semantic,
claim_key: Some("truth-slot".to_string()),
truth_state: TruthState::FullChainVerified,
salience: SalienceScore(90),
},
);
current.truth_ceiling.claim_ceiling = TruthCeiling::TrustedLocalLedger;
let mut restored = current.clone();
restored.active_memories.insert(
"mem_truth".to_string(),
MemoryState {
claim: "Restore candidate must not lower current truth".to_string(),
memory_type: MemoryKind::Semantic,
claim_key: Some("truth-slot".to_string()),
truth_state: TruthState::Active,
salience: SalienceScore(90),
},
);
restored.truth_ceiling.claim_ceiling = TruthCeiling::Advisory;
let diff = current.diff_against_restore(&restored);
assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::TruthStateDowngraded {
artifact: ArtifactKind::ActiveMemory,
id,
current: TruthState::FullChainVerified,
restored: TruthState::Active,
} if id == "mem_truth"
)));
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::TruthCeilingDowngraded {
current: TruthCeiling::TrustedLocalLedger,
restored: TruthCeiling::Advisory,
}
)));
assert!(!diff.restore_decision(false).allows_restore());
assert!(diff.restore_decision(true).allows_restore());
assert_eq!(
diff.restore_decision(true),
RestoreDecision::Warning {
recovery_acknowledged: true,
change_count: 2,
blocked_change_count: 2
}
);
}
#[test]
fn restore_semantic_diff_blocks_key_principal_rebinding() {
let mut current = base_snapshot();
current.trust_state.principals.insert(
"principal_current".to_string(),
PrincipalTrustState {
trust_tier: TrustTier::Operator,
key_ids: BTreeSet::from(["key_primary".to_string()]),
},
);
current.trust_state.keys.insert(
"key_primary".to_string(),
KeyLifecycleState {
principal_id: "principal_current".to_string(),
key_state: KeyState::Active,
},
);
let mut restored = current.clone();
restored.trust_state.principals.insert(
"principal_restored".to_string(),
PrincipalTrustState {
trust_tier: TrustTier::Operator,
key_ids: BTreeSet::from(["key_primary".to_string()]),
},
);
restored.trust_state.keys.insert(
"key_primary".to_string(),
KeyLifecycleState {
principal_id: "principal_restored".to_string(),
key_state: KeyState::Active,
},
);
let diff = current.diff_against_restore(&restored);
assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::KeyPrincipalChanged {
id,
current,
restored,
} if id == "key_primary"
&& current == "principal_current"
&& restored == "principal_restored"
)));
assert_eq!(
diff.restore_decision(false),
RestoreDecision::PreconditionUnmet {
blocked_change_count: 1
}
);
}
#[test]
fn restore_semantic_diff_warns_on_salience_distribution_drift() {
let current = base_snapshot();
let mut restored = current.clone();
restored.salience_distribution = SalienceDistribution {
low: 0,
medium: 1,
high: 1,
critical: 1,
};
let diff = current.diff_against_restore(&restored);
assert_eq!(diff.severity(), SemanticSeverity::Warning);
assert_eq!(diff.changes.len(), 1);
assert!(matches!(
&diff.changes[0],
SemanticChange {
severity: SemanticSeverity::Warning,
kind: SemanticChangeKind::SalienceDistributionChanged {
current: SalienceDistribution {
low: 1,
medium: 1,
high: 0,
critical: 0,
},
restored: SalienceDistribution {
low: 0,
medium: 1,
high: 1,
critical: 1,
},
},
}
));
assert_eq!(diff.restore_decision(false), RestoreDecision::warning(1));
}
#[test]
fn restore_semantic_diff_blocks_unresolved_contradiction_change() {
let mut current = base_snapshot();
current.unresolved_contradictions.insert(
"ctr_current".to_string(),
ContradictionState {
claim_key: "claim://operator-intent".to_string(),
memory_ids: BTreeSet::from(["mem_a".to_string(), "mem_b".to_string()]),
reason: "current unresolved tension".to_string(),
},
);
let mut restored = current.clone();
restored.unresolved_contradictions.insert(
"ctr_current".to_string(),
ContradictionState {
claim_key: "claim://operator-intent".to_string(),
memory_ids: BTreeSet::from(["mem_a".to_string(), "mem_c".to_string()]),
reason: "restored conflict points at different evidence".to_string(),
},
);
let diff = current.diff_against_restore(&restored);
assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::UnresolvedContradictionChanged { id } if id == "ctr_current"
)));
assert_eq!(
diff.restore_decision(false),
RestoreDecision::PreconditionUnmet {
blocked_change_count: 1
}
);
}
#[test]
fn restore_semantic_diff_reports_trust_tier_drift() {
let mut current = base_snapshot();
current.trust_state.principals.insert(
"principal_downgraded".to_string(),
PrincipalTrustState {
trust_tier: TrustTier::Operator,
key_ids: BTreeSet::from(["key_operator".to_string()]),
},
);
current.trust_state.principals.insert(
"principal_upgraded".to_string(),
PrincipalTrustState {
trust_tier: TrustTier::Observed,
key_ids: BTreeSet::from(["key_observed".to_string()]),
},
);
let mut restored = current.clone();
restored.trust_state.principals.insert(
"principal_downgraded".to_string(),
PrincipalTrustState {
trust_tier: TrustTier::Verified,
key_ids: BTreeSet::from(["key_operator".to_string()]),
},
);
restored.trust_state.principals.insert(
"principal_upgraded".to_string(),
PrincipalTrustState {
trust_tier: TrustTier::Verified,
key_ids: BTreeSet::from(["key_observed".to_string()]),
},
);
let diff = current.diff_against_restore(&restored);
assert_eq!(diff.severity(), SemanticSeverity::PreconditionUnmet);
assert_eq!(diff.changes.len(), 2);
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::TrustTierDowngraded {
id,
current: TrustTier::Operator,
restored: TrustTier::Verified,
} if id == "principal_downgraded"
)));
assert!(diff.changes.iter().any(|change| matches!(
&change.kind,
SemanticChangeKind::TrustTierChanged {
id,
current: TrustTier::Observed,
restored: TrustTier::Verified,
} if id == "principal_upgraded"
)));
assert_eq!(
diff.restore_decision(false),
RestoreDecision::PreconditionUnmet {
blocked_change_count: 1
}
);
}
#[test]
fn semantic_snapshot_extracts_active_store_state() {
let pool = rusqlite::Connection::open_in_memory().expect("open sqlite");
crate::migrate::apply_pending(&pool).expect("migrate");
pool.execute(
"INSERT INTO memories (
id, memory_type, status, claim, source_episodes_json, source_events_json,
domains_json, salience_json, confidence, authority, applies_when_json,
does_not_apply_when_json, created_at, updated_at
) VALUES (
'mem_snapshot', 'semantic', 'active', 'Snapshot extraction is read-only.',
'[]', '[\"evt_snapshot\"]', '[]', '{\"score\":0.8}', 0.7, 'verified',
'[]', '[]', '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
);",
[],
)
.expect("insert memory");
pool.execute(
"INSERT INTO principles (
id, statement, status, supporting_memories_json, contradicting_memories_json,
domains_observed_json, applies_when_json, does_not_apply_when_json,
confidence, validation, brightness, created_by_json, created_at, updated_at
) VALUES (
'prn_snapshot', 'Snapshot extraction must not mutate restore state.',
'promoted_to_doctrine', '[\"mem_snapshot\"]', '[]', '[]', '[]', '[]',
0.8, 0.8, 0.8, '{}', '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
);",
[],
)
.expect("insert principle");
pool.execute(
"INSERT INTO doctrine (
id, source_principle, rule, force, promotion_reason, promoted_by_json, created_at
) VALUES (
'doc_snapshot', 'prn_snapshot', 'Snapshot extraction must not mutate restore state.',
'Gate', 'test', '{}', '2026-05-04T12:00:00Z'
);",
[],
)
.expect("insert doctrine");
pool.execute(
"INSERT INTO contradictions (
id, left_ref, right_ref, contradiction_type, status, interpretation, created_at, updated_at
) VALUES (
'ctr_snapshot', 'mem:a', 'mem:b', 'conflicting_causal_claim',
'unresolved', NULL, '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
);",
[],
)
.expect("insert contradiction");
pool.execute(
"INSERT INTO authority_principal_timeline (
principal_id, trust_tier, effective_at, trust_review_due_at, removed_at, audit_ref
) VALUES (
'operator', 'operator', '2026-05-04T12:00:00Z', NULL, NULL, NULL
);",
[],
)
.expect("insert principal");
pool.execute(
"INSERT INTO authority_key_timeline (
key_id, principal_id, state, effective_at, reason, audit_ref
) VALUES (
'key_operator', 'operator', 'active', '2026-05-04T12:00:00Z', NULL, NULL
);",
[],
)
.expect("insert key");
let snapshot = semantic_snapshot_from_store(&pool).expect("extract snapshot");
assert_eq!(snapshot.snapshot_id.as_deref(), Some("store-current"));
assert_eq!(
snapshot.active_memories["mem_snapshot"].truth_state,
TruthState::Validated
);
assert_eq!(
snapshot.active_memories["mem_snapshot"].salience,
SalienceScore(80)
);
assert_eq!(snapshot.salience_distribution.high, 1);
assert_eq!(
snapshot.active_principles["prn_snapshot"].supporting_memory_ids,
BTreeSet::from(["mem_snapshot".to_string()])
);
assert_eq!(
snapshot.active_doctrine["doc_snapshot"].force,
DoctrineForce::Gate
);
assert!(snapshot
.unresolved_contradictions
.contains_key("ctr_snapshot"));
assert_eq!(
snapshot.trust_state.principals["operator"].trust_tier,
TrustTier::Operator
);
assert_eq!(
snapshot.trust_state.keys["key_operator"].key_state,
KeyState::Active
);
}
fn base_snapshot() -> SemanticSnapshot {
SemanticSnapshot {
snapshot_id: Some("snap".to_string()),
schema_version: Some(2),
active_principles: BTreeMap::new(),
active_doctrine: BTreeMap::new(),
active_memories: BTreeMap::new(),
salience_distribution: SalienceDistribution {
low: 1,
medium: 1,
high: 0,
critical: 0,
},
unresolved_contradictions: BTreeMap::new(),
trust_state: TrustKeyState::default(),
truth_ceiling: TruthCeilingState {
runtime_mode: RuntimeMode::SignedLocalLedger,
proof_state: ProofState::FullChainVerified,
claim_ceiling: TruthCeiling::ObservedLocal,
per_claim: BTreeMap::new(),
},
}
}
}