Seed fresh RepoModel as keyed Mind

This commit is contained in:
Meta Vens 2026-08-14 12:22:20 +02:00
commit 8ad16be0ab
2 changed files with 418 additions and 42 deletions

View file

@ -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,

View file

@ -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<EpiphanyMemoryDomain>,
pub nodes: Vec<EpiphanyMemoryNode>,
pub edges: Vec<EpiphanyMemoryEdge>,
pub summaries: Vec<EpiphanyMemorySummary>,
pub frontier: Vec<RepoFrontierItem>,
pub lifecycle_receipts: Vec<EpiphanyMemoryLifecycleReceipt>,
}
#[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<u8>,
}
impl EpiphanyRepoModelSeed {
pub fn new(
seed_id: impl Into<String>,
graph_id: impl Into<String>,
swarm_id: impl Into<String>,
workspace_id: impl Into<String>,
body_binding_sha256: impl Into<String>,
mut documents: EpiphanyRepoModelSeedDocuments,
) -> Result<Self> {
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<EpiphanyRepoModelSeedDocuments> {
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::<EpiphanyRepoModelLifecycleReceiptDocument>()?;
cache.register_entry_type::<EpiphanyRepoModelClaimObligationsDocument>()?;
cache.register_entry_type::<EpiphanyRepoModelMutationProposal>()?;
cache.register_entry_type::<EpiphanyRepoModelSeed>()?;
Ok(())
}
pub fn initialize_keyed_repo_model(
store_path: impl AsRef<Path>,
seed: &EpiphanyRepoModelSeed,
seeded_at: &str,
) -> Result<EpiphanyRepoModelView> {
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::EpiphanyRuntimeIdentity>(crate::RUNTIME_IDENTITY_KEY)?
.ok_or_else(|| anyhow!("RepoModel seed has no runtime identity"))?;
let mind_identity = cache
.get::<crate::EpiphanyMindIdentity>(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::<EpiphanyRepoModelIdentityDocument>(REPO_MODEL_IDENTITY_KEY)?
{
if existing != identity
|| cache.get::<EpiphanyRepoModelSeed>(&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::EpiphanyMindIdentity>(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<bool> {
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<Option<String>> {
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<EpiphanyRepoModelClaimObligationsDocument> {
let mut obligations = BTreeMap::<String, BTreeSet<String>>::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<Path>) -> Result<EpiphanyRepoModelView> {
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::<EpiphanyRepoModelSeed>("seed-1")?,
Some(seed.clone())
);
assert!(cache
.get_all::<crate::EpiphanyMindCommitReceipt>()?
.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()?;