From 8ad16be0ab8cf59ec953b85cceeeeccbdc4b86a4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 14 Aug 2026 12:22:20 +0200 Subject: [PATCH] Seed fresh RepoModel as keyed Mind --- .../src/bin/epiphany-repository-body.rs | 87 ++-- epiphany-core/src/repo_model_documents.rs | 373 ++++++++++++++++++ 2 files changed, 418 insertions(+), 42 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-repository-body.rs b/epiphany-core/src/bin/epiphany-repository-body.rs index 12891919..0b53b23d 100644 --- a/epiphany-core/src/bin/epiphany-repository-body.rs +++ b/epiphany-core/src/bin/epiphany-repository-body.rs @@ -1,13 +1,12 @@ -use anyhow::{Result, bail}; +use anyhow::{bail, Result}; use chrono::Utc; use epiphany_core::{ - EpiphanyMemoryAnchor, EpiphanyMemoryDomain, EpiphanyMemoryGraphSnapshot, - EpiphanyMemoryLifecycle, EpiphanyMemoryNode, EpiphanyMemoryNodeKind, EpiphanyMemoryProfile, - MEMORY_GRAPH_SCHEMA_VERSION, ObserveOutcome, RuntimeSpineInitOptions, admit_legacy_agent_memory_generation, bind_repository_body, bind_runtime_to_agent_memory_swarm, bind_runtime_workspace_coverage_store, ensure_agent_memory_swarm_identity, - ensure_runtime_repo_model, initialize_runtime_spine, load_repository_body_status, - observe_repository_body, + initialize_keyed_repo_model, initialize_runtime_spine, load_repository_body_status, + observe_repository_body, EpiphanyMemoryAnchor, EpiphanyMemoryDomain, EpiphanyMemoryLifecycle, + EpiphanyMemoryNode, EpiphanyMemoryNodeKind, EpiphanyMemoryProfile, EpiphanyRepoModelSeed, + EpiphanyRepoModelSeedDocuments, ObserveOutcome, RuntimeSpineInitOptions, }; use std::path::PathBuf; @@ -42,10 +41,14 @@ fn main() -> Result<()> { let binding = bind_repository_body(&repo, &store, &runtime_store, workspace_id)?; bind_runtime_workspace_coverage_store(&runtime_store, &workspace_coverage_store, &at)?; observe_repository_body(&repo, &store, &runtime_store)?; - let bootstrap = EpiphanyMemoryGraphSnapshot { - schema_version: Some(MEMORY_GRAPH_SCHEMA_VERSION.to_string()), - graph_id: format!("{}-repo-model", binding.runtime_id), - domains: vec![EpiphanyMemoryDomain { + let seed = EpiphanyRepoModelSeed::new( + format!("repo-model-seed-{}", binding.runtime_id), + format!("{}-repo-model", binding.runtime_id), + binding.swarm_id.clone(), + binding.workspace_id.clone(), + binding.source_identity_sha256.clone(), + EpiphanyRepoModelSeedDocuments { + domains: vec![EpiphanyMemoryDomain { id: "repository-body".to_string(), profile: EpiphanyMemoryProfile::RepoArchitecture, title: "Deployed repository Body".to_string(), @@ -54,42 +57,42 @@ fn main() -> Result<()> { .to_string(), ), lifecycle: EpiphanyMemoryLifecycle::Accepted, - }], - nodes: vec![EpiphanyMemoryNode { - id: "claim-deployed-repository-body".to_string(), - domain_id: "repository-body".to_string(), - profile: EpiphanyMemoryProfile::RepoArchitecture, - kind: EpiphanyMemoryNodeKind::RuntimeContract, - title: "Runtime is bound to the deployed repository Body".to_string(), - claim: format!( - "Runtime {} models workspace {} at its authenticated Git Body.", - binding.runtime_id, binding.workspace_id - ), - question: "What architecture does live Modeling admit from this Body?" - .to_string(), - action_implication: "Expand only through Body-grounded Modeling admission." - .to_string(), - anchors: vec![EpiphanyMemoryAnchor { - id: "anchor-deployed-repository-body".to_string(), - kind: "repository_body_binding".to_string(), - target: binding.git_top_level.clone(), - source_hash: Some(binding.source_identity_sha256.clone()), - note: Some( - "Cold-start anchor to the authenticated deployed Git Body.".to_string(), + }], + nodes: vec![EpiphanyMemoryNode { + id: "claim-deployed-repository-body".to_string(), + domain_id: "repository-body".to_string(), + profile: EpiphanyMemoryProfile::RepoArchitecture, + kind: EpiphanyMemoryNodeKind::RuntimeContract, + title: "Runtime is bound to the deployed repository Body".to_string(), + claim: format!( + "Runtime {} models workspace {} at its authenticated Git Body.", + binding.runtime_id, binding.workspace_id ), + question: "What architecture does live Modeling admit from this Body?" + .to_string(), + action_implication: "Expand only through Body-grounded Modeling admission." + .to_string(), + anchors: vec![EpiphanyMemoryAnchor { + id: "anchor-deployed-repository-body".to_string(), + kind: "repository_body_binding".to_string(), + target: binding.git_top_level.clone(), + source_hash: Some(binding.source_identity_sha256.clone()), + note: Some( + "Cold-start anchor to the authenticated deployed Git Body." + .to_string(), + ), + ..Default::default() + }], + lifecycle: EpiphanyMemoryLifecycle::Accepted, ..Default::default() }], - lifecycle: EpiphanyMemoryLifecycle::Accepted, - ..Default::default() - }], - ..Default::default() - }; - ensure_runtime_repo_model( - &runtime_store, - runtime_store.with_extension("absent-legacy-repo-model"), - &bootstrap, - &at, + edges: Vec::new(), + summaries: Vec::new(), + frontier: Vec::new(), + lifecycle_receipts: Vec::new(), + }, )?; + initialize_keyed_repo_model(&runtime_store, &seed, &at)?; println!( "bootstrapped workspace={} swarm={} runtime={} scope={} root={}", binding.workspace_id, diff --git a/epiphany-core/src/repo_model_documents.rs b/epiphany-core/src/repo_model_documents.rs index 541d10e8..d6bad631 100644 --- a/epiphany-core/src/repo_model_documents.rs +++ b/epiphany-core/src/repo_model_documents.rs @@ -124,6 +124,119 @@ pub struct EpiphanyRepoModelView { pub const REPO_MODEL_MUTATION_PROPOSAL_SCHEMA_VERSION: &str = "epiphany.repo_model.mutation_proposal.v1"; +pub const REPO_MODEL_SEED_SCHEMA_VERSION: &str = "epiphany.repo_model.seed.v1"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EpiphanyRepoModelSeedDocuments { + pub domains: Vec, + pub nodes: Vec, + pub edges: Vec, + pub summaries: Vec, + pub frontier: Vec, + pub lifecycle_receipts: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.body.repo_model_seed.v1", + schema = "EpiphanyRepoModelSeed" +)] +pub struct EpiphanyRepoModelSeed { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub seed_id: String, + #[cultcache(key = 2)] + pub graph_id: String, + #[cultcache(key = 3)] + pub swarm_id: String, + #[cultcache(key = 4)] + pub workspace_id: String, + #[cultcache(key = 5)] + pub body_binding_sha256: String, + #[cultcache(key = 6)] + pub documents_msgpack: Vec, +} + +impl EpiphanyRepoModelSeed { + pub fn new( + seed_id: impl Into, + graph_id: impl Into, + swarm_id: impl Into, + workspace_id: impl Into, + body_binding_sha256: impl Into, + mut documents: EpiphanyRepoModelSeedDocuments, + ) -> Result { + documents + .domains + .sort_by(|left, right| left.id.cmp(&right.id)); + documents + .nodes + .sort_by(|left, right| left.id.cmp(&right.id)); + documents + .edges + .sort_by(|left, right| left.id.cmp(&right.id)); + documents + .summaries + .sort_by(|left, right| left.id.cmp(&right.id)); + documents + .frontier + .sort_by(|left, right| left.id.cmp(&right.id)); + documents + .lifecycle_receipts + .sort_by(|left, right| left.id.cmp(&right.id)); + let seed = Self { + schema_version: REPO_MODEL_SEED_SCHEMA_VERSION.into(), + seed_id: seed_id.into(), + graph_id: graph_id.into(), + swarm_id: swarm_id.into(), + workspace_id: workspace_id.into(), + body_binding_sha256: body_binding_sha256.into(), + documents_msgpack: rmp_serde::to_vec_named(&documents)?, + }; + seed.validate()?; + Ok(seed) + } + + pub fn documents(&self) -> Result { + rmp_serde::from_slice(&self.documents_msgpack) + .map_err(|error| anyhow!("invalid RepoModel seed documents: {error}")) + } + + pub fn validate(&self) -> Result<()> { + if self.schema_version != REPO_MODEL_SEED_SCHEMA_VERSION { + return Err(anyhow!("unsupported RepoModel seed schema")); + } + for (value, label) in [ + (&self.seed_id, "seed"), + (&self.graph_id, "graph"), + (&self.swarm_id, "swarm"), + (&self.workspace_id, "workspace"), + (&self.body_binding_sha256, "Body binding"), + ] { + require_semantic_id(value, label)?; + } + let documents = self.documents()?; + let obligations = claim_obligations_for_frontier(&documents.frontier); + validate_repo_model_parts( + &EpiphanyRepoModelIdentityDocument { + schema_epoch: REPO_MODEL_SCHEMA_EPOCH.into(), + graph_id: self.graph_id.clone(), + runtime_id: "seed-validation".into(), + swarm_id: self.swarm_id.clone(), + workspace_id: self.workspace_id.clone(), + body_binding_sha256: self.body_binding_sha256.clone(), + }, + documents.domains, + documents.nodes, + documents.edges, + documents.summaries, + documents.frontier, + documents.lifecycle_receipts, + obligations, + ) + } +} #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "operation", rename_all = "snake_case")] @@ -198,9 +311,151 @@ pub(crate) fn register_repo_model_document_types(cache: &mut CultCache) -> Resul cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; + cache.register_entry_type::()?; Ok(()) } +pub fn initialize_keyed_repo_model( + store_path: impl AsRef, + seed: &EpiphanyRepoModelSeed, + seeded_at: &str, +) -> Result { + seed.validate()?; + chrono::DateTime::parse_from_rfc3339(seeded_at) + .map_err(|error| anyhow!("RepoModel seed time is invalid: {error}"))?; + let store_path = store_path.as_ref(); + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + if cache.snapshot_envelopes().into_iter().any(|envelope| { + envelope.r#type == crate::MEMORY_GRAPH_TYPE && envelope.key == crate::MEMORY_GRAPH_KEY + }) { + return Err(anyhow!( + "keyed RepoModel initialization refuses an aggregate RepoModel store" + )); + } + let runtime = cache + .get::(crate::RUNTIME_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("RepoModel seed has no runtime identity"))?; + let mind_identity = cache + .get::(crate::MIND_SCHEMA_EPOCH)? + .ok_or_else(|| anyhow!("RepoModel seed has no Mind schema identity"))?; + if mind_identity.schema_epoch != crate::MIND_SCHEMA_EPOCH + || mind_identity.runtime_id != runtime.runtime_id + { + return Err(anyhow!("RepoModel seed crosses the Mind schema epoch")); + } + let identity = EpiphanyRepoModelIdentityDocument { + schema_epoch: REPO_MODEL_SCHEMA_EPOCH.into(), + graph_id: seed.graph_id.clone(), + runtime_id: runtime.runtime_id, + swarm_id: seed.swarm_id.clone(), + workspace_id: seed.workspace_id.clone(), + body_binding_sha256: seed.body_binding_sha256.clone(), + }; + if let Some(existing) = + cache.get::(REPO_MODEL_IDENTITY_KEY)? + { + if existing != identity + || cache.get::(&seed.seed_id)?.as_ref() != Some(seed) + { + return Err(anyhow!("RepoModel seed identity collision")); + } + let view = assemble_repo_model_view(store_path)?; + if !repo_model_view_matches_seed(&view, seed)? { + return Err(anyhow!("RepoModel seed replay found divergent keyed state")); + } + return Ok(view); + } + + let documents = seed.documents()?; + let obligations = claim_obligations_for_frontier(&documents.frontier); + let mut writes = Vec::new(); + writes.push(cache.prepare_entry(REPO_MODEL_IDENTITY_KEY, &identity)?.0); + for value in &documents.domains { + writes.push( + cache + .prepare_entry(&value.id, &EpiphanyRepoModelDomainDocument::new(value)?)? + .0, + ); + } + for value in &documents.nodes { + writes.push( + cache + .prepare_entry(&value.id, &EpiphanyRepoModelNodeDocument::new(value)?)? + .0, + ); + } + for value in &documents.edges { + writes.push( + cache + .prepare_entry(&value.id, &EpiphanyRepoModelEdgeDocument::new(value)?)? + .0, + ); + } + for value in &documents.summaries { + writes.push( + cache + .prepare_entry(&value.id, &EpiphanyRepoModelSummaryDocument::new(value)?)? + .0, + ); + } + for value in &documents.frontier { + writes.push( + cache + .prepare_entry(&value.id, &EpiphanyRepoModelFrontierDocument::new(value)?)? + .0, + ); + } + for value in &documents.lifecycle_receipts { + writes.push( + cache + .prepare_entry( + &value.id, + &EpiphanyRepoModelLifecycleReceiptDocument::new(value)?, + )? + .0, + ); + } + for obligation in obligations { + writes.push(cache.prepare_entry(&obligation.node_id, &obligation)?.0); + } + let mind_envelope = cache + .get_envelope::(crate::MIND_SCHEMA_EPOCH)? + .ok_or_else(|| anyhow!("RepoModel seed lost its Mind identity envelope"))?; + let provenance = cache.prepare_entry(&seed.seed_id, seed)?.0; + match crate::commit_operator_mind_mutation( + store_path, + provenance, + "Modeling.repo_model_seed", + vec![mind_envelope], + writes, + seeded_at, + )? { + crate::EpiphanyMindCommitOutcome::Committed(_) => assemble_repo_model_view(store_path), + crate::EpiphanyMindCommitOutcome::Conflict { .. } => { + Err(anyhow!("RepoModel seed lost its exact-envelope commit")) + } + } +} + +fn repo_model_view_matches_seed( + view: &EpiphanyRepoModelView, + seed: &EpiphanyRepoModelSeed, +) -> Result { + let documents = seed.documents()?; + Ok(view.identity.graph_id == seed.graph_id + && view.identity.swarm_id == seed.swarm_id + && view.identity.workspace_id == seed.workspace_id + && view.identity.body_binding_sha256 == seed.body_binding_sha256 + && view.domains == documents.domains + && view.nodes == documents.nodes + && view.edges == documents.edges + && view.summaries == documents.summaries + && view.frontier == documents.frontier + && view.lifecycle_receipts == documents.lifecycle_receipts + && view.claim_obligations == claim_obligations_for_frontier(&documents.frontier)) +} + 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)?; @@ -676,6 +931,29 @@ fn frontier_is_unresolved(item: &RepoFrontierItem) -> bool { ) } +fn claim_obligations_for_frontier( + frontier: &[RepoFrontierItem], +) -> Vec { + let mut obligations = BTreeMap::>::new(); + for item in frontier.iter().filter(|item| frontier_is_unresolved(item)) { + for node_id in &item.target_claim_ids { + obligations + .entry(node_id.clone()) + .or_default() + .insert(item.id.clone()); + } + } + obligations + .into_iter() + .map( + |(node_id, unresolved_frontier_ids)| EpiphanyRepoModelClaimObligationsDocument { + node_id, + unresolved_frontier_ids: unresolved_frontier_ids.into_iter().collect(), + }, + ) + .collect() +} + 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()?; @@ -835,6 +1113,101 @@ mod tests { EpiphanyMemoryProfile, RepoFrontierStatus, }; + #[test] + fn fresh_repo_model_seed_commits_keyed_documents_and_refuses_divergent_replay() -> Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("repo-model-seed.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "repo-model-seed".into(), + display_name: "RepoModel seed".into(), + created_at: "2026-08-14T00:00:00Z".into(), + }, + )?; + let domain = EpiphanyMemoryDomain { + id: "domain-seed".into(), + profile: EpiphanyMemoryProfile::RepoArchitecture, + title: "Seed domain".into(), + lifecycle: EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }; + let node = EpiphanyMemoryNode { + id: "node-seed".into(), + domain_id: domain.id.clone(), + profile: EpiphanyMemoryProfile::RepoArchitecture, + kind: EpiphanyMemoryNodeKind::Module, + title: "Seed node".into(), + claim: "Bootstrap is keyed".into(), + question: "Can the aggregate enter?".into(), + action_implication: "Refuse it".into(), + source_hashes: vec!["anchor:missing".into()], + lifecycle: EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }; + let frontier = RepoFrontierItem { + id: "frontier-seed".into(), + migration_body: "repo".into(), + question: "What remains?".into(), + target_claim_ids: vec![node.id.clone()], + source_scope: vec!["epiphany-core".into()], + recommended_next_organ: "Modeling".into(), + status: RepoFrontierStatus::Active, + ..Default::default() + }; + let seed = EpiphanyRepoModelSeed::new( + "seed-1", + "graph-seed", + "swarm-seed", + "workspace-seed", + "sha256:body-seed", + EpiphanyRepoModelSeedDocuments { + domains: vec![domain], + nodes: vec![node], + edges: Vec::new(), + summaries: Vec::new(), + frontier: vec![frontier], + lifecycle_receipts: Vec::new(), + }, + )?; + let first = initialize_keyed_repo_model(&store, &seed, "2026-08-14T00:00:01Z")?; + let replay = initialize_keyed_repo_model(&store, &seed, "2026-08-14T00:00:02Z")?; + assert_eq!(first, replay); + assert_eq!(first.claim_obligations[0].node_id, "node-seed"); + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + assert_eq!( + cache.get::("seed-1")?, + Some(seed.clone()) + ); + assert!(cache + .get_all::()? + .iter() + .any(|receipt| receipt.invariant_owner == "Modeling.repo_model_seed")); + assert!(cache + .snapshot_envelopes() + .iter() + .all(|envelope| envelope.r#type != crate::MEMORY_GRAPH_TYPE)); + + let divergent = EpiphanyRepoModelSeed::new( + "seed-1", + "other-graph", + "swarm-seed", + "workspace-seed", + "sha256:body-seed", + EpiphanyRepoModelSeedDocuments { + domains: Vec::new(), + nodes: Vec::new(), + edges: Vec::new(), + summaries: Vec::new(), + frontier: Vec::new(), + lifecycle_receipts: Vec::new(), + }, + )?; + assert!(initialize_keyed_repo_model(&store, &divergent, "2026-08-14T00:00:03Z").is_err()); + Ok(()) + } + #[test] fn keyed_repo_model_view_is_deterministic_and_has_no_global_revision() -> Result<()> { let temp = tempfile::tempdir()?;