Bind Modeling memory to live source truth
This commit is contained in:
parent
af34ef6b9a
commit
fcbab316e6
19 changed files with 583 additions and 76 deletions
|
|
@ -39,7 +39,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
|
|||
wiremock = "0.6"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading"] }
|
||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use epiphany_core::memory_graph_from_epiphany_graphs;
|
|||
use epiphany_core::memory_graph_from_heartbeat_cognition;
|
||||
use epiphany_core::memory_graph_node_id;
|
||||
use epiphany_core::plan_memory_graph_context_cut;
|
||||
use epiphany_core::refresh_or_validate_repo_memory_graph;
|
||||
use epiphany_core::validate_memory_graph_snapshot;
|
||||
use epiphany_core::write_memory_graph_snapshot;
|
||||
use std::env;
|
||||
|
|
@ -291,6 +292,41 @@ fn read_refresh_args(args: impl Iterator<Item = String>) -> Result<RefreshArgs>
|
|||
}
|
||||
|
||||
fn refresh_memory_graph_store(args: RefreshArgs) -> Result<serde_json::Value> {
|
||||
if args.sources.is_empty()
|
||||
&& args.agent_store.is_none()
|
||||
&& args.heartbeat_store.is_none()
|
||||
&& let Some(thread_state_store) = &args.thread_state_store
|
||||
{
|
||||
let state = load_thread_state(thread_state_store)?.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"thread state store {} is missing",
|
||||
thread_state_store.display()
|
||||
)
|
||||
})?;
|
||||
let repo_root = thread_state_store
|
||||
.parent()
|
||||
.and_then(std::path::Path::parent)
|
||||
.unwrap_or_else(|| std::path::Path::new("."));
|
||||
let source_identity = thread_state_store
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| thread_state_store.clone())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let (snapshot, refresh) = refresh_or_validate_repo_memory_graph(
|
||||
&args.store,
|
||||
&source_identity,
|
||||
&state,
|
||||
repo_root,
|
||||
args.graph_id,
|
||||
)?;
|
||||
return Ok(serde_json::json!({
|
||||
"ok": true, "store": args.store, "graphId": snapshot.graph_id,
|
||||
"source": snapshot.source, "refresh": format!("{refresh:?}").to_lowercase(),
|
||||
"domains": snapshot.domains.len(), "nodes": snapshot.nodes.len(),
|
||||
"edges": snapshot.edges.len(), "summaries": snapshot.summaries.len(),
|
||||
"freshness": snapshot.freshness,
|
||||
}));
|
||||
}
|
||||
let mut snapshots = Vec::new();
|
||||
let mut source_status = Vec::new();
|
||||
|
||||
|
|
@ -341,7 +377,22 @@ fn refresh_memory_graph_store(args: RefreshArgs) -> Result<serde_json::Value> {
|
|||
thread_state_store.display()
|
||||
)
|
||||
})?;
|
||||
let snapshot = memory_graph_from_epiphany_graphs("repo-profile", &state.graphs);
|
||||
let repo_root = thread_state_store
|
||||
.parent()
|
||||
.and_then(std::path::Path::parent)
|
||||
.unwrap_or_else(|| std::path::Path::new("."));
|
||||
let source_identity = thread_state_store
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| thread_state_store.clone())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let snapshot = memory_graph_from_epiphany_graphs(
|
||||
"repo-profile",
|
||||
&state.graphs,
|
||||
source_identity,
|
||||
state.revision,
|
||||
repo_root,
|
||||
)?;
|
||||
source_status.push(serde_json::json!({
|
||||
"kind": "thread_state_store",
|
||||
"source": thread_state_store,
|
||||
|
|
|
|||
|
|
@ -5,21 +5,20 @@ use crate::EpiphanyMemoryContextPacket;
|
|||
use crate::EpiphanyMemoryContextQuery;
|
||||
use crate::EpiphanyMemoryProfile;
|
||||
use crate::EpiphanyPromptContextInput;
|
||||
use crate::RepoMemoryGraphRefresh;
|
||||
use crate::RuntimeHandsReceiptChainSummary;
|
||||
use crate::load_epiphany_cultmesh_cluster_topology;
|
||||
use crate::load_epiphany_cultmesh_status;
|
||||
use crate::load_latest_epiphany_cultmesh_work_loop_telemetry;
|
||||
use crate::load_memory_graph_snapshot;
|
||||
use crate::memory_graph_from_epiphany_graphs;
|
||||
use crate::plan_memory_graph_context_cut;
|
||||
use crate::query_epiphany_local_verse_context;
|
||||
use crate::refresh_or_validate_repo_memory_graph;
|
||||
use crate::render_epiphany_prompt_context;
|
||||
use crate::runtime_hands_command_receipt;
|
||||
use crate::runtime_hands_commit_receipt;
|
||||
use crate::runtime_hands_patch_receipt;
|
||||
use crate::runtime_latest_hands_receipt_chain_after;
|
||||
use crate::write_epiphany_cultmesh_work_loop_telemetry;
|
||||
use crate::write_memory_graph_snapshot;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use epiphany_state_model::EpiphanyAcceptanceReceipt;
|
||||
|
|
@ -632,28 +631,27 @@ fn launch_memory_context(
|
|||
focus: &str,
|
||||
) -> Result<EpiphanyMemoryContextPacket, String> {
|
||||
let memory_graph_store = memory_graph_store_path(runtime_store_path);
|
||||
let (snapshot, refreshed_from_state) = match load_memory_graph_snapshot(&memory_graph_store)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to load memory graph store {}: {error}",
|
||||
memory_graph_store.display()
|
||||
)
|
||||
})? {
|
||||
Some(snapshot) => (snapshot, false),
|
||||
None => {
|
||||
let snapshot = memory_graph_from_epiphany_graphs(
|
||||
format!("bridge-launch-state-rev-{}", state.revision),
|
||||
&state.graphs,
|
||||
);
|
||||
write_memory_graph_snapshot(&memory_graph_store, &snapshot).map_err(|error| {
|
||||
format!(
|
||||
"failed to write memory graph store {} from thread state: {error}",
|
||||
memory_graph_store.display()
|
||||
)
|
||||
})?;
|
||||
(snapshot, true)
|
||||
}
|
||||
};
|
||||
let repo_root = runtime_store_path
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
let source_identity = fs::canonicalize(runtime_store_path)
|
||||
.unwrap_or_else(|_| runtime_store_path.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let (snapshot, refresh) = refresh_or_validate_repo_memory_graph(
|
||||
&memory_graph_store,
|
||||
&source_identity,
|
||||
state,
|
||||
repo_root,
|
||||
format!("bridge-launch-state-rev-{}", state.revision),
|
||||
)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to refresh memory graph store {}: {error}",
|
||||
memory_graph_store.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut packet = plan_memory_graph_context_cut(
|
||||
&snapshot,
|
||||
|
|
@ -665,7 +663,7 @@ fn launch_memory_context(
|
|||
..Default::default()
|
||||
},
|
||||
);
|
||||
if refreshed_from_state {
|
||||
if refresh == RepoMemoryGraphRefresh::Refreshed {
|
||||
packet.warnings.push(format!(
|
||||
"Memory graph store was refreshed from current thread-state repo graph at {}.",
|
||||
memory_graph_store.display()
|
||||
|
|
|
|||
|
|
@ -617,6 +617,7 @@ pub use memory_graph::EpiphanyMemoryPatchCandidate;
|
|||
pub use memory_graph::MEMORY_GRAPH_KEY;
|
||||
pub use memory_graph::MEMORY_GRAPH_SCHEMA_VERSION;
|
||||
pub use memory_graph::MEMORY_GRAPH_TYPE;
|
||||
pub use memory_graph::RepoMemoryGraphRefresh;
|
||||
pub use memory_graph::compose_memory_graph_snapshots;
|
||||
pub use memory_graph::derive_memory_graph_freshness;
|
||||
pub use memory_graph::lifecycle_allowed_for_profile;
|
||||
|
|
@ -630,6 +631,7 @@ pub use memory_graph::memory_graph_from_epiphany_graphs;
|
|||
pub use memory_graph::memory_graph_from_heartbeat_cognition;
|
||||
pub use memory_graph::memory_graph_node_id;
|
||||
pub use memory_graph::plan_memory_graph_context_cut;
|
||||
pub use memory_graph::refresh_or_validate_repo_memory_graph;
|
||||
pub use memory_graph::validate_memory_graph_entry;
|
||||
pub use memory_graph::validate_memory_graph_snapshot;
|
||||
pub use memory_graph::write_memory_graph_entry;
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ pub fn plan_memory_graph_context_cut(
|
|||
query: &EpiphanyMemoryContextQuery,
|
||||
) -> EpiphanyMemoryContextPacket {
|
||||
let budget = query.budget.unwrap_or(12).clamp(1, 64) as usize;
|
||||
let freshness = snapshot
|
||||
let dirty_source_hashes = snapshot
|
||||
.freshness
|
||||
.clone()
|
||||
.unwrap_or_else(|| derive_memory_graph_freshness(snapshot, &[]));
|
||||
.as_ref()
|
||||
.map(|freshness| freshness.dirty_source_hashes.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let freshness = derive_memory_graph_freshness(snapshot, dirty_source_hashes);
|
||||
let stale_nodes: HashSet<&str> = freshness
|
||||
.stale_node_ids
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub use epiphany_state_model::EpiphanyMemoryEmbeddingManifest;
|
|||
pub use epiphany_state_model::EpiphanyMemoryFreshness;
|
||||
pub use epiphany_state_model::EpiphanyMemoryFreshnessStatus;
|
||||
pub use epiphany_state_model::EpiphanyMemoryGraphSnapshot;
|
||||
pub use epiphany_state_model::EpiphanyMemoryGraphSource;
|
||||
pub use epiphany_state_model::EpiphanyMemoryLifecycle;
|
||||
pub use epiphany_state_model::EpiphanyMemoryLifecycleReceipt;
|
||||
pub use epiphany_state_model::EpiphanyMemoryNode;
|
||||
|
|
@ -30,9 +31,11 @@ pub use freshness::derive_memory_graph_freshness;
|
|||
pub use ids::memory_graph_domain_id;
|
||||
pub use ids::memory_graph_edge_id;
|
||||
pub use ids::memory_graph_node_id;
|
||||
pub use profiles::RepoMemoryGraphRefresh;
|
||||
pub use profiles::memory_graph_from_agent_memories;
|
||||
pub use profiles::memory_graph_from_epiphany_graphs;
|
||||
pub use profiles::memory_graph_from_heartbeat_cognition;
|
||||
pub use profiles::refresh_or_validate_repo_memory_graph;
|
||||
pub use store::EpiphanyMemoryGraphEntry;
|
||||
pub use store::MEMORY_GRAPH_KEY;
|
||||
pub use store::MEMORY_GRAPH_SCHEMA_VERSION;
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ pub fn memory_graph_from_agent_memories(
|
|||
}
|
||||
|
||||
EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some("epiphany.memory_graph.v0".to_string()),
|
||||
schema_version: Some("epiphany.memory_graph.v1".to_string()),
|
||||
graph_id: graph_id.into(),
|
||||
domains,
|
||||
nodes,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub fn memory_graph_from_heartbeat_cognition(
|
|||
import_bridge_pressure(cognition, &mut domains, &mut nodes, &mut summaries);
|
||||
|
||||
EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some("epiphany.memory_graph.v0".to_string()),
|
||||
schema_version: Some("epiphany.memory_graph.v1".to_string()),
|
||||
graph_id: graph_id.into(),
|
||||
domains,
|
||||
nodes,
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ pub mod repo;
|
|||
|
||||
pub use agent::memory_graph_from_agent_memories;
|
||||
pub use heartbeat::memory_graph_from_heartbeat_cognition;
|
||||
pub use repo::RepoMemoryGraphRefresh;
|
||||
pub use repo::memory_graph_from_epiphany_graphs;
|
||||
pub use repo::refresh_or_validate_repo_memory_graph;
|
||||
|
|
|
|||
|
|
@ -13,18 +13,128 @@ use crate::memory_graph::EpiphanyMemorySummary;
|
|||
use crate::memory_graph::memory_graph_domain_id;
|
||||
use crate::memory_graph::memory_graph_edge_id;
|
||||
use crate::memory_graph::memory_graph_node_id;
|
||||
use anyhow::{Context, Result};
|
||||
use epiphany_state_model::EpiphanyCodeRef;
|
||||
use epiphany_state_model::EpiphanyGraph;
|
||||
use epiphany_state_model::EpiphanyGraphEdge;
|
||||
use epiphany_state_model::EpiphanyGraphLink;
|
||||
use epiphany_state_model::EpiphanyGraphNode;
|
||||
use epiphany_state_model::EpiphanyGraphs;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RepoMemoryGraphRefresh {
|
||||
Reused,
|
||||
Refreshed,
|
||||
}
|
||||
|
||||
pub fn refresh_or_validate_repo_memory_graph(
|
||||
store_path: &Path,
|
||||
source_identity: &str,
|
||||
state: &EpiphanyThreadState,
|
||||
repo_root: &Path,
|
||||
graph_id: impl Into<String>,
|
||||
) -> Result<(EpiphanyMemoryGraphSnapshot, RepoMemoryGraphRefresh)> {
|
||||
let current_entry = crate::memory_graph::load_memory_graph_entry(store_path)?;
|
||||
if let Some(snapshot) = current_entry
|
||||
.filter(|entry| entry.schema_version == crate::memory_graph::MEMORY_GRAPH_SCHEMA_VERSION)
|
||||
.and_then(|entry| entry.snapshot().ok())
|
||||
.filter(|snapshot| {
|
||||
snapshot.schema_version.as_deref()
|
||||
== Some(crate::memory_graph::MEMORY_GRAPH_SCHEMA_VERSION)
|
||||
})
|
||||
{
|
||||
let provenance_matches = snapshot.source.as_ref().is_some_and(|source| {
|
||||
source.kind == "thread_state_repo_graph"
|
||||
&& source.identity == source_identity
|
||||
&& source.revision == state.revision
|
||||
});
|
||||
if provenance_matches && snapshot_anchor_bytes_match(&snapshot, repo_root) {
|
||||
return Ok((snapshot, RepoMemoryGraphRefresh::Reused));
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = memory_graph_from_epiphany_graphs(
|
||||
graph_id,
|
||||
&state.graphs,
|
||||
source_identity,
|
||||
state.revision,
|
||||
repo_root,
|
||||
)?;
|
||||
let temporary = store_path.with_extension(format!("refresh-{}.tmp", Uuid::new_v4()));
|
||||
crate::memory_graph::write_memory_graph_snapshot(&temporary, &snapshot)?;
|
||||
atomic_replace_file(&temporary, store_path).with_context(|| {
|
||||
format!(
|
||||
"atomically replace memory graph store {}",
|
||||
store_path.display()
|
||||
)
|
||||
})?;
|
||||
Ok((snapshot, RepoMemoryGraphRefresh::Refreshed))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn atomic_replace_file(source: &Path, destination: &Path) -> Result<()> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW,
|
||||
};
|
||||
let source = source
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect::<Vec<_>>();
|
||||
let destination = destination
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect::<Vec<_>>();
|
||||
let moved = unsafe {
|
||||
MoveFileExW(
|
||||
source.as_ptr(),
|
||||
destination.as_ptr(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
|
||||
)
|
||||
};
|
||||
if moved == 0 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn atomic_replace_file(source: &Path, destination: &Path) -> Result<()> {
|
||||
fs::rename(source, destination)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_anchor_bytes_match(snapshot: &EpiphanyMemoryGraphSnapshot, repo_root: &Path) -> bool {
|
||||
snapshot
|
||||
.nodes
|
||||
.iter()
|
||||
.flat_map(|node| &node.anchors)
|
||||
.chain(snapshot.edges.iter().flat_map(|edge| &edge.anchors))
|
||||
.all(|anchor| {
|
||||
let (Some(code_ref), Some(expected)) = (&anchor.code_ref, &anchor.source_hash) else {
|
||||
return false;
|
||||
};
|
||||
fs::read(repo_root.join(&code_ref.path))
|
||||
.ok()
|
||||
.is_some_and(|bytes| format!("sha256:{:x}", Sha256::digest(bytes)) == *expected)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn memory_graph_from_epiphany_graphs(
|
||||
graph_id: impl Into<String>,
|
||||
graphs: &EpiphanyGraphs,
|
||||
) -> EpiphanyMemoryGraphSnapshot {
|
||||
source_identity: impl Into<String>,
|
||||
source_revision: u64,
|
||||
repo_root: &Path,
|
||||
) -> Result<EpiphanyMemoryGraphSnapshot> {
|
||||
let architecture_domain = memory_graph_domain_id(
|
||||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
"accepted_graph",
|
||||
|
|
@ -54,12 +164,14 @@ pub fn memory_graph_from_epiphany_graphs(
|
|||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
&mut node_map,
|
||||
&mut nodes,
|
||||
repo_root,
|
||||
);
|
||||
import_graph_edges(
|
||||
&graphs.architecture,
|
||||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
&node_map,
|
||||
&mut edges,
|
||||
repo_root,
|
||||
);
|
||||
summaries.push(repo_summary(
|
||||
"summary-accepted-architecture-graph",
|
||||
|
|
@ -84,12 +196,14 @@ pub fn memory_graph_from_epiphany_graphs(
|
|||
EpiphanyMemoryProfile::RepoDataflow,
|
||||
&mut node_map,
|
||||
&mut nodes,
|
||||
repo_root,
|
||||
);
|
||||
import_graph_edges(
|
||||
&graphs.dataflow,
|
||||
EpiphanyMemoryProfile::RepoDataflow,
|
||||
&node_map,
|
||||
&mut edges,
|
||||
repo_root,
|
||||
);
|
||||
summaries.push(repo_summary(
|
||||
"summary-accepted-dataflow-graph",
|
||||
|
|
@ -102,22 +216,56 @@ pub fn memory_graph_from_epiphany_graphs(
|
|||
));
|
||||
}
|
||||
|
||||
import_graph_links(graphs.links.as_slice(), &node_map, &mut edges);
|
||||
import_graph_links(graphs.links.as_slice(), &node_map, &mut edges, repo_root);
|
||||
|
||||
EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some("epiphany.memory_graph.v0".to_string()),
|
||||
let stale_node_ids = nodes
|
||||
.iter()
|
||||
.filter(|node| node.lifecycle == EpiphanyMemoryLifecycle::Stale)
|
||||
.map(|node| node.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let stale_edge_ids = edges
|
||||
.iter()
|
||||
.filter(|edge| edge.lifecycle == EpiphanyMemoryLifecycle::Stale)
|
||||
.map(|edge| edge.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let stale = !stale_node_ids.is_empty() || !stale_edge_ids.is_empty();
|
||||
if stale {
|
||||
for summary in &mut summaries {
|
||||
summary.freshness = EpiphanyMemoryFreshnessStatus::Stale;
|
||||
}
|
||||
}
|
||||
Ok(EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some("epiphany.memory_graph.v1".to_string()),
|
||||
graph_id: graph_id.into(),
|
||||
source: Some(crate::memory_graph::EpiphanyMemoryGraphSource {
|
||||
kind: "thread_state_repo_graph".to_string(),
|
||||
identity: source_identity.into(),
|
||||
revision: source_revision,
|
||||
}),
|
||||
domains,
|
||||
nodes,
|
||||
edges,
|
||||
summaries,
|
||||
freshness: Some(EpiphanyMemoryFreshness {
|
||||
status: EpiphanyMemoryFreshnessStatus::Ready,
|
||||
note: Some("Imported from accepted Epiphany graph state.".to_string()),
|
||||
status: if stale {
|
||||
EpiphanyMemoryFreshnessStatus::Stale
|
||||
} else {
|
||||
EpiphanyMemoryFreshnessStatus::Ready
|
||||
},
|
||||
stale_node_ids,
|
||||
stale_edge_ids,
|
||||
note: Some(
|
||||
if stale {
|
||||
"One or more accepted graph anchors could not be verified against source bytes."
|
||||
} else {
|
||||
"Imported from accepted Epiphany graph state."
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn repo_domain(id: String, profile: EpiphanyMemoryProfile, title: &str) -> EpiphanyMemoryDomain {
|
||||
|
|
@ -136,13 +284,15 @@ fn import_graph_nodes(
|
|||
profile: EpiphanyMemoryProfile,
|
||||
node_map: &mut HashMap<String, String>,
|
||||
nodes: &mut Vec<EpiphanyMemoryNode>,
|
||||
repo_root: &Path,
|
||||
) {
|
||||
for node in &graph.nodes {
|
||||
let memory_id = memory_graph_node_id(domain_id, "accepted_graph_node", &node.id, None);
|
||||
node_map.insert(node.id.clone(), memory_id.clone());
|
||||
nodes.push(memory_node_from_graph_node(
|
||||
memory_id, domain_id, profile, node,
|
||||
));
|
||||
nodes.push(
|
||||
memory_node_from_graph_node(memory_id.clone(), domain_id, profile, node, repo_root)
|
||||
.unwrap_or_else(|_| stale_memory_node(memory_id, domain_id, profile, node)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +301,19 @@ fn memory_node_from_graph_node(
|
|||
domain_id: &str,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
node: &EpiphanyGraphNode,
|
||||
) -> EpiphanyMemoryNode {
|
||||
let anchors = anchors_from_code_refs(&id, &node.code_refs);
|
||||
repo_root: &Path,
|
||||
) -> Result<EpiphanyMemoryNode> {
|
||||
let anchors = anchors_from_code_refs(&id, &node.code_refs, repo_root)?;
|
||||
let source_hashes = if anchors.is_empty() {
|
||||
vec!["anchor:missing".to_string()]
|
||||
} else {
|
||||
anchors.iter().map(|anchor| anchor.id.clone()).collect()
|
||||
anchors
|
||||
.iter()
|
||||
.filter_map(|anchor| anchor.source_hash.clone())
|
||||
.collect()
|
||||
};
|
||||
EpiphanyMemoryNode {
|
||||
let grounded = !anchors.is_empty();
|
||||
Ok(EpiphanyMemoryNode {
|
||||
id,
|
||||
domain_id: domain_id.to_string(),
|
||||
profile,
|
||||
|
|
@ -173,9 +328,37 @@ fn memory_node_from_graph_node(
|
|||
),
|
||||
anchors,
|
||||
source_hashes,
|
||||
lifecycle: graph_status_lifecycle(node.status.as_deref()),
|
||||
lifecycle: if grounded {
|
||||
graph_status_lifecycle(node.status.as_deref())
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Stale
|
||||
},
|
||||
salience: 70,
|
||||
confidence: 80,
|
||||
confidence: if grounded { 80 } else { 0 },
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn stale_memory_node(
|
||||
id: String,
|
||||
domain_id: &str,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
node: &EpiphanyGraphNode,
|
||||
) -> EpiphanyMemoryNode {
|
||||
EpiphanyMemoryNode {
|
||||
id,
|
||||
domain_id: domain_id.to_string(),
|
||||
profile,
|
||||
kind: memory_node_kind(node),
|
||||
title: node.title.clone(),
|
||||
claim: graph_node_claim(node),
|
||||
question: "Source anchor is unavailable; refresh after restoring it.".to_string(),
|
||||
tension: "Accepted anatomy cannot be verified against source bytes.".to_string(),
|
||||
action_implication: "Do not use this claim as fresh compressed anatomy.".to_string(),
|
||||
source_hashes: vec!["anchor:missing".to_string()],
|
||||
lifecycle: EpiphanyMemoryLifecycle::Stale,
|
||||
salience: 70,
|
||||
confidence: 0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -185,6 +368,7 @@ fn import_graph_edges(
|
|||
profile: EpiphanyMemoryProfile,
|
||||
node_map: &HashMap<String, String>,
|
||||
edges: &mut Vec<EpiphanyMemoryEdge>,
|
||||
repo_root: &Path,
|
||||
) {
|
||||
for edge in &graph.edges {
|
||||
let source_id = node_map
|
||||
|
|
@ -196,7 +380,7 @@ fn import_graph_edges(
|
|||
.cloned()
|
||||
.unwrap_or_else(|| edge.target_id.clone());
|
||||
edges.push(memory_edge_from_graph_edge(
|
||||
source_id, target_id, profile, edge,
|
||||
source_id, target_id, profile, edge, repo_root,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -206,6 +390,7 @@ fn memory_edge_from_graph_edge(
|
|||
target_id: String,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
edge: &EpiphanyGraphEdge,
|
||||
repo_root: &Path,
|
||||
) -> EpiphanyMemoryEdge {
|
||||
let id = edge.id.clone().unwrap_or_else(|| {
|
||||
memory_graph_edge_id(
|
||||
|
|
@ -215,6 +400,12 @@ fn memory_edge_from_graph_edge(
|
|||
edge.code_refs.iter().map(code_ref_key),
|
||||
)
|
||||
});
|
||||
let anchors = anchors_from_code_refs(
|
||||
edge.id.as_deref().unwrap_or("edge"),
|
||||
&edge.code_refs,
|
||||
repo_root,
|
||||
);
|
||||
let grounded = edge.code_refs.is_empty() || anchors.is_ok();
|
||||
EpiphanyMemoryEdge {
|
||||
id,
|
||||
source_id,
|
||||
|
|
@ -222,9 +413,13 @@ fn memory_edge_from_graph_edge(
|
|||
kind: memory_edge_kind(edge.kind.as_str()),
|
||||
profile,
|
||||
claim: graph_edge_claim(edge),
|
||||
anchors: anchors_from_code_refs(edge.id.as_deref().unwrap_or("edge"), &edge.code_refs),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
confidence: 80,
|
||||
anchors: anchors.unwrap_or_default(),
|
||||
lifecycle: if grounded {
|
||||
EpiphanyMemoryLifecycle::Accepted
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Stale
|
||||
},
|
||||
confidence: if grounded { 80 } else { 0 },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +427,7 @@ fn import_graph_links(
|
|||
links: &[EpiphanyGraphLink],
|
||||
node_map: &HashMap<String, String>,
|
||||
edges: &mut Vec<EpiphanyMemoryEdge>,
|
||||
repo_root: &Path,
|
||||
) {
|
||||
for link in links {
|
||||
let Some(source_id) = node_map.get(&link.dataflow_node_id).cloned() else {
|
||||
|
|
@ -240,6 +436,8 @@ fn import_graph_links(
|
|||
let Some(target_id) = node_map.get(&link.architecture_node_id).cloned() else {
|
||||
continue;
|
||||
};
|
||||
let anchors = anchors_from_code_refs("link", &link.code_refs, repo_root);
|
||||
let grounded = link.code_refs.is_empty() || anchors.is_ok();
|
||||
edges.push(EpiphanyMemoryEdge {
|
||||
id: memory_graph_edge_id(
|
||||
&source_id,
|
||||
|
|
@ -255,9 +453,13 @@ fn import_graph_links(
|
|||
.relationship
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Dataflow node is linked to architecture node.".to_string()),
|
||||
anchors: anchors_from_code_refs("link", &link.code_refs),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
confidence: 75,
|
||||
anchors: anchors.unwrap_or_default(),
|
||||
lifecycle: if grounded {
|
||||
EpiphanyMemoryLifecycle::Accepted
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Stale
|
||||
},
|
||||
confidence: if grounded { 75 } else { 0 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -306,17 +508,23 @@ fn repo_summary(
|
|||
fn anchors_from_code_refs(
|
||||
prefix: &str,
|
||||
code_refs: &[EpiphanyCodeRef],
|
||||
) -> Vec<EpiphanyMemoryAnchor> {
|
||||
repo_root: &Path,
|
||||
) -> Result<Vec<EpiphanyMemoryAnchor>> {
|
||||
code_refs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, code_ref)| EpiphanyMemoryAnchor {
|
||||
id: format!("anchor-{prefix}-{index}"),
|
||||
kind: "code_ref".to_string(),
|
||||
target: code_ref_key(code_ref),
|
||||
code_ref: Some(code_ref.clone()),
|
||||
source_hash: Some(code_ref_key(code_ref)),
|
||||
..Default::default()
|
||||
.map(|(index, code_ref)| {
|
||||
let bytes = fs::read(repo_root.join(&code_ref.path))
|
||||
.with_context(|| format!("missing source anchor {}", code_ref.path.display()))?;
|
||||
let digest = format!("sha256:{:x}", Sha256::digest(&bytes));
|
||||
Ok(EpiphanyMemoryAnchor {
|
||||
id: format!("anchor-{prefix}-{index}"),
|
||||
kind: "code_ref".to_string(),
|
||||
target: code_ref_key(code_ref),
|
||||
code_ref: Some(code_ref.clone()),
|
||||
source_hash: Some(digest),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -429,7 +637,14 @@ mod tests {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let snapshot = memory_graph_from_epiphany_graphs("repo-profile", &graphs);
|
||||
let snapshot = memory_graph_from_epiphany_graphs(
|
||||
"repo-profile",
|
||||
&graphs,
|
||||
"test-state",
|
||||
1,
|
||||
Path::new("."),
|
||||
)
|
||||
.unwrap();
|
||||
let errors = validate_memory_graph_snapshot(&snapshot);
|
||||
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
|
|
@ -458,7 +673,14 @@ mod tests {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let snapshot = memory_graph_from_epiphany_graphs("repo-profile", &graphs);
|
||||
let snapshot = memory_graph_from_epiphany_graphs(
|
||||
"repo-profile",
|
||||
&graphs,
|
||||
"test-state",
|
||||
1,
|
||||
Path::new("."),
|
||||
)
|
||||
.unwrap();
|
||||
let errors = validate_memory_graph_snapshot(&snapshot);
|
||||
|
||||
assert!(
|
||||
|
|
@ -472,4 +694,109 @@ mod tests {
|
|||
.any(|error| error.message.contains("missing target node"))
|
||||
);
|
||||
}
|
||||
|
||||
fn anchored_state(path: &str, revision: u64) -> EpiphanyThreadState {
|
||||
EpiphanyThreadState {
|
||||
revision,
|
||||
graphs: EpiphanyGraphs {
|
||||
architecture: EpiphanyGraph {
|
||||
nodes: vec![EpiphanyGraphNode {
|
||||
id: "body".to_string(),
|
||||
title: "Body".to_string(),
|
||||
purpose: "Grounded body anatomy.".to_string(),
|
||||
code_refs: vec![EpiphanyCodeRef {
|
||||
path: PathBuf::from(path),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_cache_reuse_requires_revision_and_anchor_bytes() -> anyhow::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
std::fs::write(temp.path().join("body.rs"), "one")?;
|
||||
let store = temp.path().join("memory-graph.msgpack");
|
||||
let state = anchored_state("body.rs", 1);
|
||||
|
||||
let (first, first_refresh) =
|
||||
refresh_or_validate_repo_memory_graph(&store, "thread", &state, temp.path(), "repo")?;
|
||||
assert_eq!(first_refresh, RepoMemoryGraphRefresh::Refreshed);
|
||||
assert!(first.nodes[0].source_hashes[0].starts_with("sha256:"));
|
||||
let (same, same_refresh) =
|
||||
refresh_or_validate_repo_memory_graph(&store, "thread", &state, temp.path(), "repo")?;
|
||||
assert_eq!(same_refresh, RepoMemoryGraphRefresh::Reused);
|
||||
assert_eq!(first.source, same.source);
|
||||
|
||||
std::fs::write(temp.path().join("body.rs"), "two")?;
|
||||
let (changed, bytes_refresh) =
|
||||
refresh_or_validate_repo_memory_graph(&store, "thread", &state, temp.path(), "repo")?;
|
||||
assert_eq!(bytes_refresh, RepoMemoryGraphRefresh::Refreshed);
|
||||
assert_ne!(first.nodes[0].source_hashes, changed.nodes[0].source_hashes);
|
||||
|
||||
let next = anchored_state("body.rs", 2);
|
||||
let (revised, revision_refresh) =
|
||||
refresh_or_validate_repo_memory_graph(&store, "thread", &next, temp.path(), "repo")?;
|
||||
assert_eq!(revision_refresh, RepoMemoryGraphRefresh::Refreshed);
|
||||
assert_eq!(revised.source.unwrap().revision, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_anchor_cannot_emit_ready_summary() -> anyhow::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let snapshot = memory_graph_from_epiphany_graphs(
|
||||
"repo",
|
||||
&anchored_state("gone.rs", 1).graphs,
|
||||
"thread",
|
||||
1,
|
||||
temp.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
snapshot.freshness.unwrap().status,
|
||||
EpiphanyMemoryFreshnessStatus::Stale
|
||||
);
|
||||
assert!(
|
||||
snapshot
|
||||
.summaries
|
||||
.iter()
|
||||
.all(|summary| summary.freshness == EpiphanyMemoryFreshnessStatus::Stale)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_edge_anchor_stales_edge_summary_and_snapshot() -> anyhow::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
std::fs::write(temp.path().join("body.rs"), "body")?;
|
||||
let mut state = anchored_state("body.rs", 1);
|
||||
state.graphs.architecture.edges.push(EpiphanyGraphEdge {
|
||||
id: Some("edge-body".to_string()),
|
||||
source_id: "body".to_string(),
|
||||
target_id: "body".to_string(),
|
||||
kind: "owns".to_string(),
|
||||
code_refs: vec![EpiphanyCodeRef {
|
||||
path: PathBuf::from("missing-edge.rs"),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
let snapshot =
|
||||
memory_graph_from_epiphany_graphs("repo", &state.graphs, "thread", 1, temp.path())?;
|
||||
assert_eq!(snapshot.edges[0].lifecycle, EpiphanyMemoryLifecycle::Stale);
|
||||
assert_eq!(
|
||||
snapshot.summaries[0].freshness,
|
||||
EpiphanyMemoryFreshnessStatus::Stale
|
||||
);
|
||||
let freshness = snapshot.freshness.expect("freshness");
|
||||
assert_eq!(freshness.status, EpiphanyMemoryFreshnessStatus::Stale);
|
||||
assert_eq!(freshness.stale_edge_ids, vec!["edge-body".to_string()]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use super::EpiphanyMemoryFreshnessStatus;
|
||||
use super::EpiphanyMemoryGraphSnapshot;
|
||||
use super::derive_memory_graph_freshness;
|
||||
use super::validate_memory_graph_snapshot;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
|
|
@ -9,7 +11,7 @@ use std::path::Path;
|
|||
|
||||
pub const MEMORY_GRAPH_TYPE: &str = "epiphany.memory_graph";
|
||||
pub const MEMORY_GRAPH_KEY: &str = "default";
|
||||
pub const MEMORY_GRAPH_SCHEMA_VERSION: &str = "epiphany.memory_graph.v0";
|
||||
pub const MEMORY_GRAPH_SCHEMA_VERSION: &str = "epiphany.memory_graph.v1";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.memory_graph", schema = "EpiphanyMemoryGraphEntry")]
|
||||
|
|
@ -89,6 +91,13 @@ pub fn validate_memory_graph_entry(entry: &EpiphanyMemoryGraphEntry) -> Result<(
|
|||
));
|
||||
}
|
||||
let snapshot = entry.snapshot()?;
|
||||
if snapshot.schema_version.as_deref() != Some(MEMORY_GRAPH_SCHEMA_VERSION) {
|
||||
return Err(anyhow!(
|
||||
"memory graph snapshot schema_version is {:?}, expected {:?}",
|
||||
snapshot.schema_version,
|
||||
MEMORY_GRAPH_SCHEMA_VERSION
|
||||
));
|
||||
}
|
||||
if entry.graph_id != snapshot.graph_id {
|
||||
return Err(anyhow!(
|
||||
"memory graph entry graph_id {:?} does not match snapshot graph_id {:?}",
|
||||
|
|
@ -105,5 +114,29 @@ pub fn validate_memory_graph_entry(entry: &EpiphanyMemoryGraphEntry) -> Result<(
|
|||
.join("; ");
|
||||
return Err(anyhow!("memory graph validation failed: {message}"));
|
||||
}
|
||||
let dirty = snapshot
|
||||
.freshness
|
||||
.as_ref()
|
||||
.map(|freshness| freshness.dirty_source_hashes.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let derived = derive_memory_graph_freshness(&snapshot, dirty);
|
||||
if snapshot.freshness.as_ref().is_some_and(|stored| {
|
||||
stored.status != derived.status
|
||||
|| stored.stale_node_ids != derived.stale_node_ids
|
||||
|| stored.stale_edge_ids != derived.stale_edge_ids
|
||||
|| stored.stale_summary_ids != derived.stale_summary_ids
|
||||
}) {
|
||||
return Err(anyhow!(
|
||||
"memory graph stored freshness disagrees with derived lifecycle/source freshness"
|
||||
));
|
||||
}
|
||||
if snapshot.summaries.iter().any(|summary| {
|
||||
derived.stale_summary_ids.contains(&summary.id)
|
||||
&& summary.freshness != EpiphanyMemoryFreshnessStatus::Stale
|
||||
}) {
|
||||
return Err(anyhow!(
|
||||
"memory graph has a ready summary covering stale anatomy"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ fn fixture_snapshot() -> EpiphanyMemoryGraphSnapshot {
|
|||
let node_b = memory_graph_node_id(&domain_id, "module", "src/memory_graph.rs", Some("b"));
|
||||
let edge = memory_graph_edge_id(&node_a, &node_b, "owns", ["src/memory_graph.rs"]);
|
||||
EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some(MEMORY_GRAPH_SCHEMA_VERSION.to_string()),
|
||||
graph_id: "graph".to_string(),
|
||||
domains: vec![EpiphanyMemoryDomain {
|
||||
id: domain_id.clone(),
|
||||
|
|
@ -197,6 +198,15 @@ fn memory_graph_entry_validation_rejects_mismatched_identity() {
|
|||
assert!(error.to_string().contains("does not match snapshot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_graph_entry_rejects_embedded_legacy_schema() {
|
||||
let mut snapshot = fixture_snapshot();
|
||||
snapshot.schema_version = Some("epiphany.memory_graph.v0".to_string());
|
||||
let entry = EpiphanyMemoryGraphEntry::from_snapshot(&snapshot).expect("snapshot entry");
|
||||
let error = validate_memory_graph_entry(&entry).expect_err("embedded v0 is invalid");
|
||||
assert!(error.to_string().contains("snapshot schema_version"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_graph_profile_law_keeps_role_memory_from_repo_lifecycle() {
|
||||
assert!(lifecycle_allowed_for_profile(
|
||||
|
|
@ -362,6 +372,33 @@ fn memory_graph_context_cut_descends_when_summary_is_stale() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_graph_context_cut_rederives_forged_ready_freshness() {
|
||||
let mut snapshot = fixture_snapshot();
|
||||
snapshot.nodes[0].lifecycle = EpiphanyMemoryLifecycle::Stale;
|
||||
snapshot.freshness = Some(EpiphanyMemoryFreshness {
|
||||
status: EpiphanyMemoryFreshnessStatus::Ready,
|
||||
note: Some("forged cache claim".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
let packet = plan_memory_graph_context_cut(
|
||||
&snapshot,
|
||||
&EpiphanyMemoryContextQuery {
|
||||
id: "query".to_string(),
|
||||
profile: Some(EpiphanyMemoryProfile::RepoArchitecture),
|
||||
text: Some("shared graph law".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(packet.summaries.is_empty());
|
||||
assert!(
|
||||
packet
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("descended"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_graph_context_cut_reports_missing_explicit_ids() {
|
||||
let snapshot = fixture_snapshot();
|
||||
|
|
|
|||
|
|
@ -728,6 +728,9 @@ pub struct EpiphanyMemoryGraphSnapshot {
|
|||
#[ts(type = "string | null")]
|
||||
pub schema_version: Option<String>,
|
||||
pub graph_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyMemoryGraphSource | null")]
|
||||
pub source: Option<EpiphanyMemoryGraphSource>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyMemoryDomain>")]
|
||||
pub domains: Vec<EpiphanyMemoryDomain>,
|
||||
|
|
@ -751,6 +754,13 @@ pub struct EpiphanyMemoryGraphSnapshot {
|
|||
pub lifecycle_receipts: Vec<EpiphanyMemoryLifecycleReceipt>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct EpiphanyMemoryGraphSource {
|
||||
pub kind: String,
|
||||
pub identity: String,
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyMemoryDomain {
|
||||
pub id: String,
|
||||
|
|
|
|||
|
|
@ -1618,6 +1618,29 @@ local Verse, result/checked ref, watched ref, source commit, runbook commit, and
|
|||
current HEAD. Readiness re-resolves the underlying deployment receipt; absence
|
||||
or any mismatch remains incomplete.
|
||||
|
||||
## Cache existence is not fresh Modeling anatomy
|
||||
|
||||
The persistent repo memory graph was reused indefinitely whenever
|
||||
`memory-graph.msgpack` existed. Repo import stamped anatomy `Ready`, while its
|
||||
so-called source hashes were only `path#symbol` locators. Launch context could
|
||||
therefore inject stale compressed summaries after accepted state revisions or
|
||||
source bytes changed.
|
||||
|
||||
`epiphany.memory_graph.v1` now carries typed source identity and accepted state
|
||||
revision. Repo anchors use SHA-256 source-byte digests. Launch and explicit
|
||||
thread-state refresh share one refresh/validate primitive; reuse requires the
|
||||
same state-store identity, revision, schema, and every anchored source digest.
|
||||
Missing or changed node, edge, or link anchors become stale, and context cuts
|
||||
derive freshness from lifecycle/source evidence rather than trusting a cached
|
||||
freshness claim. Replacement is atomic on Windows and Unix. The graph embedding
|
||||
manifest still has no production indexer; Modeling semantic-index availability
|
||||
is therefore `unavailable`, not borrowed from generic workspace retrieval.
|
||||
|
||||
The independent Eyes pass found the next P0 frontier: repo-work `run_adopt`
|
||||
locally authors a document claiming Mind review and then approves Hands from
|
||||
field predicates. The existing typed Mind gateway must own an immutable review
|
||||
bound to the exact plan; Self/scheduler may route it but may not impersonate it.
|
||||
|
||||
## Ambient command identity is not a typed crossing command
|
||||
|
||||
Bifrost's source establishes one typed command -> one crossing attempt -> one
|
||||
|
|
|
|||
|
|
@ -55,6 +55,19 @@ readiness no longer launders its cached projection. Typed Idunn deployment and
|
|||
aftercare receipts must resolve and bind across identity, runtime, Verse,
|
||||
result/ref, runbook, source commit, and current HEAD.
|
||||
|
||||
The next Modeling pass repaired the targeting organ itself. Memory-graph v1
|
||||
binds repo anatomy to the actual state-store identity, accepted revision, and
|
||||
SHA-256 source bytes. Cache presence, path/symbol locators, and serialized
|
||||
freshness no longer make anatomy Ready. Launch and explicit refresh share one
|
||||
validator; missing/changed node, edge, or link anchors stale their summaries.
|
||||
There is still no production semantic index for graph claims, so Modeling index
|
||||
availability is honestly unavailable rather than inferred from code search.
|
||||
|
||||
Next P0 cut: `epiphany-work run_adopt` currently synthesizes Mind ownership and
|
||||
an approved Hands review from local JSON predicates. Route adoption through the
|
||||
existing typed Mind gateway, immutably bound to the exact plan, and delete the
|
||||
scheduler's ability to author Mind/Hands consequence.
|
||||
|
||||
Epiphany is in an authority-provenance purification pass. The live question is not whether a command can produce a plausible document; it is whether the subsystem writing that document owns the fact it asserts.
|
||||
|
||||
The confirmed conceptual substitutions have been cut:
|
||||
|
|
|
|||
|
|
@ -4,17 +4,22 @@
|
|||
"title": "Epiphany Memory Graph",
|
||||
"description": "Unified typed memory graph for repo architecture/dataflow, role memory, heartbeat pressure, incubation, agency, candidate interventions, and evidence. Embeddings are cache metadata, not canonical truth.",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"schemaVersion": { "const": "epiphany.memory_graph.v1" },
|
||||
"graphId": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "identity", "revision"],
|
||||
"properties": {
|
||||
"kind": { "type": "string" },
|
||||
"identity": { "type": "string" },
|
||||
"revision": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -66,6 +71,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"graphId"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@
|
|||
"wireContracts": [
|
||||
"cultnet.schema.v0"
|
||||
],
|
||||
"schemaVersion": "epiphany.memory_graph.v0",
|
||||
"schemaVersion": "epiphany.memory_graph.v1",
|
||||
"documentType": "epiphany.memory_graph",
|
||||
"title": "Epiphany Memory Graph",
|
||||
"path": "epiphany.memory-graph.schema.json"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -661,4 +661,4 @@ current_status:
|
|||
- Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass.
|
||||
- `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure.
|
||||
last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
|
||||
next_action: Continue the observation-to-consequence audit using live organ-shaped prompt probes. Preserve the Aetheria dogfood lesson: Modeling's typed persistent anatomy, freshness, frontier, and semantic-index knowledgebase is the targeting organ for Hands, not a transient prelude. Idunn managed-child sight now feeds swarm overview as typed renderer-neutral rows, with executable identity required before PID liveness can satisfy recovery. Deployment/aftercare completion now accepts only a conjunctively bound typed Idunn CultMesh chain; raw response files and cached projection laundering are deleted. Model the next live frontier before cutting it. The intentionally absent Other-world surface action still requires a typed intent contract and real command owner; never advertise a wish. Keep closure's zero-substring invariant.
|
||||
next_action: Cut repo-work Mind impersonation. `epiphany-work run_adopt` currently manufactures a Mind-owned adoption document and approved Hands review from local JSON predicates. Self/scheduler may route a plan, but only a resolved typed Mind gateway review immutably bound to the exact plan may authorize the admission primitive that creates Hands review. Delete the synthetic owner/interpreter path and prove that a structurally plausible plan without Mind receipt creates neither adoption nor Hands authority. Preserve the Aetheria lesson: memory-graph v1 now binds Modeling anatomy to state-store identity, accepted revision, and SHA-256 source bytes; launch and explicit refresh share validation; missing/changed anchors stale summaries. Modeling semantic graph indexing remains honestly unavailable until a real graph-claim indexer exists. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant.
|
||||
|
|
|
|||
Reference in a new issue