From 0fbf862cb39f171c39b8eae89925548c4bee1373 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 14 Aug 2026 11:52:11 +0200 Subject: [PATCH] Add keyed RepoModel document foundation --- epiphany-core/src/lib.rs | 2 + epiphany-core/src/mind_documents.rs | 119 +++++-- epiphany-core/src/reasoning_context.rs | 60 +++- epiphany-core/src/repo_model_documents.rs | 410 ++++++++++++++++++++++ notes/epiphany-current-algorithmic-map.md | 26 ++ notes/fresh-workspace-handoff.md | 15 +- state/map.yaml | 2 +- 7 files changed, 596 insertions(+), 38 deletions(-) create mode 100644 epiphany-core/src/repo_model_documents.rs diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 9a12ab1f..a0f5b6f0 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -40,6 +40,7 @@ mod prompt_context; mod proposal; mod repo_model_gateway; mod repository_body_observer; +mod repo_model_documents; mod repository_readiness; mod resident_readiness; mod resident_self; @@ -984,6 +985,7 @@ pub use proposal::EpiphanyMapProposalInput; pub use proposal::propose_map_update; pub use repo_model_gateway::*; pub use repository_body_observer::*; +pub use repo_model_documents::*; pub use repository_readiness::*; pub use resident_readiness::*; pub use resident_self::*; diff --git a/epiphany-core/src/mind_documents.rs b/epiphany-core/src/mind_documents.rs index acfb0871..573cb082 100644 --- a/epiphany-core/src/mind_documents.rs +++ b/epiphany-core/src/mind_documents.rs @@ -3,9 +3,10 @@ use std::path::Path; use anyhow::{Result, anyhow}; use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry}; use epiphany_state_model::{ - EpiphanyBacklogItem, EpiphanyEvidenceRecord, EpiphanyInvestigationCheckpoint, - EpiphanyInvariant, EpiphanyModeState, EpiphanyObjectiveDraft, EpiphanyObservation, - EpiphanyPlanningCapture, EpiphanyPlanningState, EpiphanyRoadmapStream, EpiphanySubgoal, + EpiphanyBacklogItem, EpiphanyEvidenceRecord, EpiphanyInvariant, + EpiphanyInvestigationCheckpoint, EpiphanyModeState, EpiphanyObjectiveDraft, + EpiphanyObservation, EpiphanyPlanningCapture, EpiphanyPlanningState, EpiphanyRoadmapStream, + EpiphanySubgoal, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -27,7 +28,10 @@ pub struct EpiphanyMindIdentity { } #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.objective.v1", schema = "EpiphanyMindObjectiveDocument")] +#[cultcache( + type = "epiphany.mind.objective.v1", + schema = "EpiphanyMindObjectiveDocument" +)] pub struct EpiphanyMindObjectiveDocument { #[cultcache(key = 0)] pub objective: String, @@ -50,28 +54,40 @@ pub struct EpiphanyMindModeDocument { } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.subgoal.v1", schema = "EpiphanyMindSubgoalDocument")] +#[cultcache( + type = "epiphany.mind.subgoal.v1", + schema = "EpiphanyMindSubgoalDocument" +)] pub struct EpiphanyMindSubgoalDocument { #[cultcache(key = 0)] pub value: EpiphanySubgoal, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.invariant.v1", schema = "EpiphanyMindInvariantDocument")] +#[cultcache( + type = "epiphany.mind.invariant.v1", + schema = "EpiphanyMindInvariantDocument" +)] pub struct EpiphanyMindInvariantDocument { #[cultcache(key = 0)] pub value: EpiphanyInvariant, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.observation.v1", schema = "EpiphanyMindObservationDocument")] +#[cultcache( + type = "epiphany.mind.observation.v1", + schema = "EpiphanyMindObservationDocument" +)] pub struct EpiphanyMindObservationDocument { #[cultcache(key = 0)] pub value: EpiphanyObservation, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.evidence.v1", schema = "EpiphanyMindEvidenceDocument")] +#[cultcache( + type = "epiphany.mind.evidence.v1", + schema = "EpiphanyMindEvidenceDocument" +)] pub struct EpiphanyMindEvidenceDocument { #[cultcache(key = 0)] pub value: EpiphanyEvidenceRecord, @@ -88,28 +104,40 @@ pub struct EpiphanyMindInvestigationCheckpointDocument { } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.planning_capture.v1", schema = "EpiphanyMindPlanningCaptureDocument")] +#[cultcache( + type = "epiphany.mind.planning_capture.v1", + schema = "EpiphanyMindPlanningCaptureDocument" +)] pub struct EpiphanyMindPlanningCaptureDocument { #[cultcache(key = 0)] pub value: EpiphanyPlanningCapture, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.backlog_item.v1", schema = "EpiphanyMindBacklogItemDocument")] +#[cultcache( + type = "epiphany.mind.backlog_item.v1", + schema = "EpiphanyMindBacklogItemDocument" +)] pub struct EpiphanyMindBacklogItemDocument { #[cultcache(key = 0)] pub value: EpiphanyBacklogItem, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.roadmap_stream.v1", schema = "EpiphanyMindRoadmapStreamDocument")] +#[cultcache( + type = "epiphany.mind.roadmap_stream.v1", + schema = "EpiphanyMindRoadmapStreamDocument" +)] pub struct EpiphanyMindRoadmapStreamDocument { #[cultcache(key = 0)] pub value: EpiphanyRoadmapStream, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache(type = "epiphany.mind.objective_draft.v1", schema = "EpiphanyMindObjectiveDraftDocument")] +#[cultcache( + type = "epiphany.mind.objective_draft.v1", + schema = "EpiphanyMindObjectiveDraftDocument" +)] pub struct EpiphanyMindObjectiveDraftDocument { #[cultcache(key = 0)] pub value: EpiphanyObjectiveDraft, @@ -128,6 +156,7 @@ pub struct EpiphanyMindView { pub investigation_checkpoint: Option, pub mode: Option, pub planning: EpiphanyPlanningState, + pub repo_model: Option, } pub(crate) fn register_mind_document_types(cache: &mut CultCache) -> Result<()> { @@ -144,10 +173,19 @@ pub(crate) fn register_mind_document_types(cache: &mut CultCache) -> Result<()> cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; + crate::repo_model_documents::register_repo_model_document_types(cache)?; Ok(()) } pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Result<()> { + if let Some(expected_key) = crate::repo_model_documents::repo_model_write_key(envelope)? { + if envelope.key != expected_key { + return Err(anyhow!( + "RepoModel document semantic identity does not match its envelope key" + )); + } + return Ok(()); + } let expected_key = if envelope.r#type == EpiphanyMindIdentity::TYPE { let value: EpiphanyMindIdentity = rmp_serde::from_slice(&envelope.payload)?; if value.schema_epoch != MIND_SCHEMA_EPOCH || value.runtime_id.trim().is_empty() { @@ -167,25 +205,41 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu let _: EpiphanyMindModeDocument = rmp_serde::from_slice(&envelope.payload)?; MIND_MODE_KEY.to_string() } else if envelope.r#type == EpiphanyMindSubgoalDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindInvariantDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindObservationDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindEvidenceDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindInvestigationCheckpointDocument::TYPE { rmp_serde::from_slice::(&envelope.payload)? .value .checkpoint_id } else if envelope.r#type == EpiphanyMindPlanningCaptureDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindBacklogItemDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindRoadmapStreamDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else if envelope.r#type == EpiphanyMindObjectiveDraftDocument::TYPE { - rmp_serde::from_slice::(&envelope.payload)?.value.id + rmp_serde::from_slice::(&envelope.payload)? + .value + .id } else { return Err(anyhow!( "Mind mutation cannot write non-canonical document type {:?}", @@ -207,7 +261,9 @@ pub fn assemble_mind_view(store_path: impl AsRef) -> Result(MIND_SCHEMA_EPOCH)? .ok_or_else(|| anyhow!("writable Mind store has no v1 identity"))?; if identity.schema_epoch != MIND_SCHEMA_EPOCH { - return Err(anyhow!("writable Mind store has an unsupported schema epoch")); + return Err(anyhow!( + "writable Mind store has an unsupported schema epoch" + )); } let objective = cache @@ -222,10 +278,8 @@ pub fn assemble_mind_view(store_path: impl AsRef) -> Result(&cache, |value| value.value)?; let mut evidence = values::(&cache, |value| value.value)?; - let checkpoints = values::( - &cache, - |value| value.value, - )?; + let checkpoints = + values::(&cache, |value| value.value)?; let mut captures = values::(&cache, |value| value.value)?; let mut backlog_items = @@ -234,6 +288,14 @@ pub fn assemble_mind_view(store_path: impl AsRef) -> Result(&cache, |value| value.value)?; let mut objective_drafts = values::(&cache, |value| value.value)?; + let repo_model = if cache + .get::(crate::REPO_MODEL_IDENTITY_KEY)? + .is_some() + { + Some(crate::assemble_repo_model_view(store_path.as_ref())?) + } else { + None + }; subgoals.sort_by(|left, right| left.id.cmp(&right.id)); invariants.sort_by(|left, right| left.id.cmp(&right.id)); observations.sort_by(|left, right| left.id.cmp(&right.id)); @@ -256,10 +318,7 @@ pub fn assemble_mind_view(store_path: impl AsRef) -> Result) -> Result>(); let write_ids = writes .iter() - .map(|entry| (entry.r#type.as_str(), entry.key.as_str())) + .map(|entry| (entry.r#type.clone(), entry.key.clone())) .collect::>(); - if !expected_ids.is_subset(&write_ids) { - return Err(anyhow!( - "Mind mutation must replace every strongly read identity" - )); - } let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; let mut companion_expected = Vec::new(); @@ -701,6 +696,12 @@ fn commit_authorized_mind_mutation( return Ok(EpiphanyMindCommitOutcome::Committed(existing)); } let mut replacements = writes; + replacements.extend( + strong_reads + .iter() + .filter(|entry| !write_ids.contains(&(entry.r#type.clone(), entry.key.clone()))) + .cloned(), + ); replacements.extend(companion_replacements); replacements.push(cache.prepare_entry(&receipt_id, &receipt)?.0); let mut expected = strong_reads.clone(); @@ -1117,6 +1118,53 @@ mod tests { )?, EpiphanyMindCommitOutcome::Conflict { .. } )); + let hands = backing + .pull_all()? + .into_iter() + .find(|entry| { + entry.r#type == crate::EpiphanyMindObservationDocument::TYPE && entry.key == "hands" + }) + .unwrap(); + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + vec![hands.clone()], + vec![make("modeling", "read-only dependency")?], + "2026-08-14T00:00:06Z" + )?, + EpiphanyMindCommitOutcome::Committed(_) + )); + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + vec![hands.clone()], + vec![make("hands", "dependency changed")?], + "2026-08-14T00:00:07Z" + )?, + EpiphanyMindCommitOutcome::Committed(_) + )); + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + vec![hands], + vec![make("verification", "must not partially appear")?], + "2026-08-14T00:00:08Z" + )?, + EpiphanyMindCommitOutcome::Conflict { .. } + )); + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + assert!( + cache + .get::("verification")? + .is_none() + ); let _ = EpiphanyRuntimeJobStatus::Completed; Ok(()) } diff --git a/epiphany-core/src/repo_model_documents.rs b/epiphany-core/src/repo_model_documents.rs new file mode 100644 index 00000000..4a30d105 --- /dev/null +++ b/epiphany-core/src/repo_model_documents.rs @@ -0,0 +1,410 @@ +use std::path::Path; + +use anyhow::{Result, anyhow}; +use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry}; +use epiphany_state_model::{ + EpiphanyMemoryDomain, EpiphanyMemoryEdge, EpiphanyMemoryGraphSnapshot, + EpiphanyMemoryLifecycleReceipt, EpiphanyMemoryNode, EpiphanyMemorySummary, RepoFrontierItem, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{EpiphanyMindDocumentVersion, runtime_spine_cache, validate_memory_graph_snapshot}; + +pub const REPO_MODEL_SCHEMA_EPOCH: &str = "epiphany.repo_model.epoch.v1"; +pub const REPO_MODEL_IDENTITY_KEY: &str = "repo-model"; + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.repo_model.identity.v1", + schema = "EpiphanyRepoModelIdentityDocument" +)] +pub struct EpiphanyRepoModelIdentityDocument { + #[cultcache(key = 0)] + pub schema_epoch: String, + #[cultcache(key = 1)] + pub graph_id: String, + #[cultcache(key = 2)] + pub runtime_id: String, + #[cultcache(key = 3)] + pub swarm_id: String, + #[cultcache(key = 4)] + pub workspace_id: String, + #[cultcache(key = 5)] + pub body_binding_sha256: String, +} + +macro_rules! repo_document { + ($name:ident, $type:literal, $schema:literal, $value:ty) => { + #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] + #[cultcache(type = $type, schema = $schema)] + pub struct $name { + #[cultcache(key = 0)] + pub payload_msgpack: Vec, + } + + impl $name { + pub fn new(value: &$value) -> Result { + Ok(Self { + payload_msgpack: rmp_serde::to_vec_named(value)?, + }) + } + + pub fn value(&self) -> Result<$value> { + rmp_serde::from_slice(&self.payload_msgpack) + .map_err(|error| anyhow!("invalid {} payload: {error}", $schema)) + } + } + }; +} + +repo_document!( + EpiphanyRepoModelDomainDocument, + "epiphany.mind.repo_model.domain.v1", + "EpiphanyRepoModelDomainDocument", + EpiphanyMemoryDomain +); +repo_document!( + EpiphanyRepoModelNodeDocument, + "epiphany.mind.repo_model.node.v1", + "EpiphanyRepoModelNodeDocument", + EpiphanyMemoryNode +); +repo_document!( + EpiphanyRepoModelEdgeDocument, + "epiphany.mind.repo_model.edge.v1", + "EpiphanyRepoModelEdgeDocument", + EpiphanyMemoryEdge +); +repo_document!( + EpiphanyRepoModelSummaryDocument, + "epiphany.mind.repo_model.summary.v1", + "EpiphanyRepoModelSummaryDocument", + EpiphanyMemorySummary +); +repo_document!( + EpiphanyRepoModelFrontierDocument, + "epiphany.mind.repo_model.frontier.v1", + "EpiphanyRepoModelFrontierDocument", + RepoFrontierItem +); +repo_document!( + EpiphanyRepoModelLifecycleReceiptDocument, + "epiphany.mind.repo_model.lifecycle_receipt.v1", + "EpiphanyRepoModelLifecycleReceiptDocument", + EpiphanyMemoryLifecycleReceipt +); + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.repo_model.claim_obligations.v1", + schema = "EpiphanyRepoModelClaimObligationsDocument" +)] +pub struct EpiphanyRepoModelClaimObligationsDocument { + #[cultcache(key = 0)] + pub node_id: String, + #[cultcache(key = 1)] + pub unresolved_frontier_ids: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EpiphanyRepoModelView { + pub identity: EpiphanyRepoModelIdentityDocument, + pub projection_digest: String, + pub source_documents: Vec, + pub domains: Vec, + pub nodes: Vec, + pub edges: Vec, + pub summaries: Vec, + pub frontier: Vec, + pub lifecycle_receipts: Vec, + pub claim_obligations: Vec, +} + +pub(crate) fn register_repo_model_document_types(cache: &mut CultCache) -> Result<()> { + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + Ok(()) +} + +pub(crate) fn repo_model_write_key(envelope: &CultCacheEnvelope) -> Result> { + let key = if envelope.r#type == EpiphanyRepoModelIdentityDocument::TYPE { + let value: EpiphanyRepoModelIdentityDocument = rmp_serde::from_slice(&envelope.payload)?; + if value.schema_epoch != REPO_MODEL_SCHEMA_EPOCH + || [ + value.graph_id.as_str(), + value.runtime_id.as_str(), + value.swarm_id.as_str(), + value.workspace_id.as_str(), + value.body_binding_sha256.as_str(), + ] + .into_iter() + .any(|value| value.trim().is_empty()) + { + return Err(anyhow!("RepoModel identity write is invalid")); + } + REPO_MODEL_IDENTITY_KEY.to_string() + } else if envelope.r#type == EpiphanyRepoModelDomainDocument::TYPE { + rmp_serde::from_slice::(&envelope.payload)? + .value()? + .id + } else if envelope.r#type == EpiphanyRepoModelNodeDocument::TYPE { + rmp_serde::from_slice::(&envelope.payload)? + .value()? + .id + } else if envelope.r#type == EpiphanyRepoModelEdgeDocument::TYPE { + rmp_serde::from_slice::(&envelope.payload)? + .value()? + .id + } else if envelope.r#type == EpiphanyRepoModelSummaryDocument::TYPE { + rmp_serde::from_slice::(&envelope.payload)? + .value()? + .id + } else if envelope.r#type == EpiphanyRepoModelFrontierDocument::TYPE { + rmp_serde::from_slice::(&envelope.payload)? + .value()? + .id + } else if envelope.r#type == EpiphanyRepoModelLifecycleReceiptDocument::TYPE { + rmp_serde::from_slice::(&envelope.payload)? + .value()? + .id + } else if envelope.r#type == EpiphanyRepoModelClaimObligationsDocument::TYPE { + let value: EpiphanyRepoModelClaimObligationsDocument = + rmp_serde::from_slice(&envelope.payload)?; + if value + .unresolved_frontier_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(anyhow!("RepoModel claim obligations are not canonical")); + } + value.node_id + } else { + return Ok(None); + }; + if key.trim().is_empty() { + return Err(anyhow!("RepoModel semantic identity cannot be empty")); + } + Ok(Some(key)) +} + +pub fn assemble_repo_model_view(store_path: impl AsRef) -> Result { + let mut cache = runtime_spine_cache(store_path.as_ref())?; + cache.pull_all_backing_stores()?; + let identity = cache + .get::(REPO_MODEL_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("writable Mind store has no keyed RepoModel identity"))?; + let mut domains = values::(&cache, |entry| entry.value())?; + let mut nodes = values::(&cache, |entry| entry.value())?; + let mut edges = values::(&cache, |entry| entry.value())?; + let mut summaries = + values::(&cache, |entry| entry.value())?; + let mut frontier = + values::(&cache, |entry| entry.value())?; + let mut lifecycle_receipts = + values::(&cache, |entry| entry.value())?; + let mut claim_obligations = cache.get_all::()?; + domains.sort_by(|left, right| left.id.cmp(&right.id)); + nodes.sort_by(|left, right| left.id.cmp(&right.id)); + edges.sort_by(|left, right| left.id.cmp(&right.id)); + summaries.sort_by(|left, right| left.id.cmp(&right.id)); + frontier.sort_by(|left, right| left.id.cmp(&right.id)); + lifecycle_receipts.sort_by(|left, right| left.id.cmp(&right.id)); + claim_obligations.sort_by(|left, right| left.node_id.cmp(&right.node_id)); + + let validation = EpiphanyMemoryGraphSnapshot { + graph_id: identity.graph_id.clone(), + domains: domains.clone(), + nodes: nodes.clone(), + edges: edges.clone(), + summaries: summaries.clone(), + lifecycle_receipts: lifecycle_receipts.clone(), + frontier: frontier.clone(), + ..Default::default() + }; + let errors = validate_memory_graph_snapshot(&validation); + if !errors.is_empty() { + return Err(anyhow!( + "keyed RepoModel view is invalid: {}", + errors + .iter() + .map(|error| format!("{}: {}", error.path, error.message)) + .collect::>() + .join("; ") + )); + } + validate_claim_obligations(&nodes, &frontier, &claim_obligations)?; + + let mut source_documents = cache + .snapshot_envelopes() + .into_iter() + .filter(|entry| entry.r#type.starts_with("epiphany.mind.repo_model.")) + .map(|entry| EpiphanyMindDocumentVersion::from_envelope("epiphany-mind", &entry)) + .collect::>>()?; + source_documents.sort_by(|left, right| { + (&left.document_type, &left.document_key).cmp(&(&right.document_type, &right.document_key)) + }); + let projection_digest = format!( + "sha256:{:x}", + Sha256::digest(rmp_serde::to_vec_named(&source_documents)?) + ); + Ok(EpiphanyRepoModelView { + identity, + projection_digest, + source_documents, + domains, + nodes, + edges, + summaries, + frontier, + lifecycle_receipts, + claim_obligations, + }) +} + +fn values(cache: &CultCache, take: impl Fn(T) -> Result) -> Result> +where + T: DatabaseEntry, +{ + cache.get_all::()?.into_iter().map(take).collect() +} + +fn validate_claim_obligations( + nodes: &[EpiphanyMemoryNode], + frontier: &[RepoFrontierItem], + obligations: &[EpiphanyRepoModelClaimObligationsDocument], +) -> Result<()> { + for obligation in obligations { + if !nodes.iter().any(|node| node.id == obligation.node_id) { + return Err(anyhow!("RepoModel claim obligation names a missing node")); + } + for frontier_id in &obligation.unresolved_frontier_ids { + let item = frontier + .iter() + .find(|item| item.id == *frontier_id) + .ok_or_else(|| anyhow!("RepoModel claim obligation names a missing frontier"))?; + if !item.target_claim_ids.contains(&obligation.node_id) + || !matches!( + item.status, + epiphany_state_model::RepoFrontierStatus::Proposed + | epiphany_state_model::RepoFrontierStatus::Active + | epiphany_state_model::RepoFrontierStatus::Blocked + ) + { + return Err(anyhow!("RepoModel claim obligation is not live")); + } + } + } + for item in frontier.iter().filter(|item| { + matches!( + item.status, + epiphany_state_model::RepoFrontierStatus::Proposed + | epiphany_state_model::RepoFrontierStatus::Active + | epiphany_state_model::RepoFrontierStatus::Blocked + ) + }) { + for node_id in &item.target_claim_ids { + if !obligations.iter().any(|obligation| { + obligation.node_id == *node_id + && obligation.unresolved_frontier_ids.contains(&item.id) + }) { + return Err(anyhow!("unresolved frontier lost its claim obligation")); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{RuntimeSpineInitOptions, initialize_runtime_spine}; + use epiphany_state_model::{ + EpiphanyMemoryLifecycle, EpiphanyMemoryNodeKind, EpiphanyMemoryProfile, RepoFrontierStatus, + }; + + #[test] + fn keyed_repo_model_view_is_deterministic_and_has_no_global_revision() -> Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("repo-model.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "repo-model-view".into(), + display_name: "RepoModel view".into(), + created_at: "2026-08-14T00:00:00Z".into(), + }, + )?; + let mut cache = runtime_spine_cache(&store)?; + cache.put( + REPO_MODEL_IDENTITY_KEY, + &EpiphanyRepoModelIdentityDocument { + schema_epoch: REPO_MODEL_SCHEMA_EPOCH.into(), + graph_id: "graph-1".into(), + runtime_id: "repo-model-view".into(), + swarm_id: "swarm-1".into(), + workspace_id: "workspace-1".into(), + body_binding_sha256: "sha256:body".into(), + }, + )?; + cache.put( + "domain-1", + &EpiphanyRepoModelDomainDocument::new(&EpiphanyMemoryDomain { + id: "domain-1".into(), + profile: EpiphanyMemoryProfile::RepoArchitecture, + title: "Architecture".into(), + lifecycle: EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + })?, + )?; + cache.put( + "node-1", + &EpiphanyRepoModelNodeDocument::new(&EpiphanyMemoryNode { + id: "node-1".into(), + domain_id: "domain-1".into(), + profile: EpiphanyMemoryProfile::RepoArchitecture, + kind: EpiphanyMemoryNodeKind::Module, + title: "Mind".into(), + claim: "Mind state is keyed".into(), + question: "Which owner writes next?".into(), + action_implication: "Keep mutations disjoint".into(), + source_hashes: vec!["anchor:missing".into()], + lifecycle: EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + })?, + )?; + cache.put( + "frontier-1", + &EpiphanyRepoModelFrontierDocument::new(&RepoFrontierItem { + id: "frontier-1".into(), + migration_body: "repo".into(), + question: "Cut the aggregate?".into(), + target_claim_ids: vec!["node-1".into()], + recommended_next_organ: "Modeling".into(), + status: RepoFrontierStatus::Active, + ..Default::default() + })?, + )?; + cache.put( + "node-1", + &EpiphanyRepoModelClaimObligationsDocument { + node_id: "node-1".into(), + unresolved_frontier_ids: vec!["frontier-1".into()], + }, + )?; + let first = assemble_repo_model_view(&store)?; + let second = assemble_repo_model_view(&store)?; + assert_eq!(first, second); + assert_eq!(first.nodes[0].id, "node-1"); + assert!(first.projection_digest.starts_with("sha256:")); + let mind = crate::assemble_mind_view(&store)?; + assert_eq!(mind.repo_model.as_ref(), Some(&first)); + Ok(()) + } +} diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index ec3ad612..e0cbee02 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -4600,3 +4600,29 @@ than translated. It existed to repair receipt-shaped routing authority; unresolved typed state must own routing instead. Its commands and exported writer are gone. The remaining frontier execution inspector/amendment tool is named `epiphany-frontier-execution`, matching its actual authority. + +### Keyed RepoModel foundation + +RepoModel identity, domains, nodes, edges, summaries, frontier items, lifecycle +receipts, and per-node unresolved-claim obligations are distinct Mind document +identities. `EpiphanyRepoModelView` deterministically sorts and validates those +documents and is included in `EpiphanyMindView`; it has a projection digest but +no global revision or model hash. It never reads or translates the aggregate. + +Each semantic document owns canonical named MessagePack behind a typed +constructor and decoder. Directly nesting the existing optional-rich graph +structs exposed compact MessagePack field shifting when a skipped middle field +was absent; canonical named payloads make that corruption structurally +unavailable while keeping the `.cc` contract typed. + +Per-node claim obligations are the narrow concurrency index for the phantom +invariant: unresolved frontier targeting and node retirement must contend on +the same semantic node identity. The view validates both directions so neither +a missing membership nor a dead frontier can masquerade as a live obligation. + +Strong reads in `commit_mind_mutation` are logical fences, not compulsory +writes. CultCache's physical CAS requires expected envelopes in its replacement +batch, so read-only dependencies are re-emitted byte-identically; the Mind +receipt still lists only actual writes. Tests prove a disjoint insert can depend +on an unchanged document and that a stale dependency refuses the insert with no +partial state. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 053c24d4..43542ec0 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -41,6 +41,18 @@ so operator decisions do not counterfeit model reasoning. The aggregate-only Modeling acceptance repair writer and commands are deleted; frontier execution inspection/amendment remains under a truthfully named binary. +The keyed RepoModel foundation now gives identity, domain, node, edge, summary, +frontier, lifecycle receipt, and per-node unresolved-claim obligations separate +CultCache identities. Its deterministic validated view is part of +`EpiphanyMindView` and has no revision/hash head. Documents store canonical +named MessagePack behind typed constructors because direct compact nesting let +skipped middle fields shift into the wrong schema positions. Mind CAS strong +reads are now genuine read fences: CultCache receives byte-identical expected +envelopes as required by its physical primitive, while the commit receipt lists +only semantic writes. A stale read-only dependency blocks the whole mutation +without partial insertion. Full core remains 690 passing with one ignored +cross-process helper. + Operational correction: the original c011 resident and Heartbeat containers are not alive; both exited 255 on 2026-08-11. Their named volumes are quiescent and remain untouched. Historical c011 zero-zombie and organ-circuit evidence is @@ -154,7 +166,8 @@ unchanged; the lifecycle authorities above remain separate. ## Immediate next action -Migrate concrete focus/mode, subgoal/invariant, evidence/observation, +Build the concrete keyed RepoModel mutation planner and cut its aggregate +admission writers. In parallel sequence, migrate focus/mode, subgoal/invariant, evidence/observation, checkpoint, and planning admission owners from `EpiphanyThreadStateEntry` to their keyed documents. Delete the aggregate transaction, reader, and schema before enabling old writable-store refusal; do not translate or dual-read it. diff --git a/state/map.yaml b/state/map.yaml index 82b8082e..5033738d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -70,7 +70,7 @@ current_status: yggdrasil_epiphany_service_contract: repaired_in_gamecult_ops_812b427_not_installed resident_physiology: historical_c011_exact_465af24d_zero_zombie_proof_accepted_current_containers_exited_255 unresolved_authority: Coordinator receipt, Heartbeat pulse-artifact, genuine-idle and active resource plateaus, exact-grant terminality, restart-persistent cooldown refusal, schedule/action issuance cardinality, runtime-owned typed fulfillment, all three packaged fulfilled-before-receipt lanes, genuine packaged Imagination option generation, narrow model-generation retention, published model-turn JSON ingress, semantic logical/physical retention, Persona conversation retention, stable Starfire firewall ownership, completed model-adapter session archival, normal coordinator lifecycle opening/error closure/legacy repair/archival, co-resident resident lifecycle retention, authenticated outer-worker attempt retention, deployed supervisor/projector journald ownership, Docker named-volume atomic snapshot visibility, deterministic incremental release construction, and canonical-brake refusal of direct daemon lifecycle actuation are accepted. Starfire's Docker Desktop F: bind is rejected for mutable authority after an exact packaged lockless reader observed an empty published snapshot during writer interruption. Historical migrated semantic attempts without recorded physical namespace remain durable and non-retirable. One completed Discord consequence remains blocked by the absent Yggdrasil credential. Exact aa841873 also makes legacy coordinator repair one full-snapshot transaction so a later hostile family cannot leave an earlier partial repair. - next_action: Complete the hard Decision-Auditable Concurrent Mind cut. Exact bd5034cd moves first operator objective intake onto one keyed Mind objective plus an immutable typed operator-provenance commit receipt; thread identity is provenance, the path writes no EpiphanyThreadStateEntry, and hostile replacement is byte-identical refusal. Model and operator commits share the exact-envelope CAS primitive without operator actions impersonating model reasoning. The aggregate transaction remains live for launch, acceptance, interrupt, and legacy generic updates. The aggregate-only Modeling acceptance repair command and writer are deleted; its remaining frontier execution functions are named by their actual authority. Next migrate focus/mode and collection admission owners, then launch and acceptance, before deleting the aggregate reader/writer and enabling old-store refusal. RepoModel split follows. No migrator or dual-read path; historical stores remain read-only. + next_action: Complete the hard Decision-Auditable Concurrent Mind cut. Exact bd5034cd moves operator objective intake onto keyed Mind and exact a786273d deletes aggregate acceptance-repair authority. The current keyed RepoModel foundation defines separate identity, domain, node, edge, summary, frontier, lifecycle-receipt, and per-node claim-obligation documents plus a deterministic validated view embedded in EpiphanyMindView. It has no revision/hash head and does not read the aggregate. Canonical named MessagePack payloads preserve optional typed fields without compact-sequence drift. Mind CAS now permits read-only strong dependencies by re-emitting their envelopes byte-identically for CultCache while excluding them from the receipt write set; stale dependencies refuse without partial insertion. Full core passes 690 with one ignored helper. Next add the concrete keyed RepoModel mutation planner and cut aggregate admission writers, then finish keyed coordinator admission and delete the aggregate reader/writer before enabling old-store refusal. No migrator or dual-read path; historical stores remain read-only. active_subgoals: - Prove Persona, Self, Eyes, Modeling, Imagination, Hands, Soul, Mind, Continuity, and nervous-system behavior individually.