Remove global thread state model
This commit is contained in:
parent
07d891ba6c
commit
ec1431fff8
23 changed files with 139 additions and 4547 deletions
|
|
@ -174,10 +174,6 @@ path = "src/bin/epiphany-persona-reddit.rs"
|
|||
name = "epiphany-prepare-compaction"
|
||||
path = "src/bin/epiphany-prepare-compaction.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-prompt-context-smoke"
|
||||
path = "src/bin/epiphany-prompt-context-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-repo"
|
||||
path = "src/bin/epiphany-repo.rs"
|
||||
|
|
|
|||
|
|
@ -4,16 +4,10 @@ use crate::EpiphanyCoordinatorAction as CoreEpiphanyCoordinatorAction;
|
|||
use crate::EpiphanyCoordinatorRoleResultStatus as CoreEpiphanyCoordinatorRoleResultStatus;
|
||||
use crate::EpiphanyCrrcAction as CoreEpiphanyCrrcAction;
|
||||
use crate::EpiphanyCrrcResultStatus as CoreEpiphanyCrrcResultStatus;
|
||||
use crate::EpiphanyJobLaunchRequest;
|
||||
use crate::EpiphanyPressure;
|
||||
use crate::EpiphanyPressureLevel as CoreEpiphanyPressureLevel;
|
||||
use crate::EpiphanyReorientAction as CoreEpiphanyReorientAction;
|
||||
use crate::EpiphanyRoleResultRoleId;
|
||||
use crate::EpiphanyRoleWorkerLaunchDocument;
|
||||
use crate::EpiphanyWorkerLaunchDocument;
|
||||
use crate::default_launch_organ_contract;
|
||||
use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
|
||||
pub const EPIPHANY_IMAGINATION_ROLE_BINDING_ID: &str = "planning-synthesis-worker";
|
||||
pub const EPIPHANY_IMAGINATION_OWNER_ROLE: &str = "epiphany-imagination";
|
||||
|
|
@ -1045,181 +1039,7 @@ pub fn epiphany_worker_prompt(body: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn build_epiphany_role_launch_request(
|
||||
thread_id: &str,
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
expected_revision: Option<u64>,
|
||||
max_runtime_seconds: Option<u64>,
|
||||
state: &EpiphanyThreadState,
|
||||
) -> Result<EpiphanyJobLaunchRequest, String> {
|
||||
build_epiphany_role_launch_request_with_dynamic_context(
|
||||
thread_id,
|
||||
role_id,
|
||||
expected_revision,
|
||||
max_runtime_seconds,
|
||||
state,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_epiphany_role_launch_request_with_dynamic_context(
|
||||
thread_id: &str,
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
expected_revision: Option<u64>,
|
||||
max_runtime_seconds: Option<u64>,
|
||||
state: &EpiphanyThreadState,
|
||||
dynamic_prompt_context: Option<String>,
|
||||
) -> Result<EpiphanyJobLaunchRequest, String> {
|
||||
let binding_id = epiphany_role_binding_id(role_id)?;
|
||||
let owner_role = epiphany_role_owner(role_id)?;
|
||||
let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state));
|
||||
let linked_graph_node_ids = epiphany_active_graph_node_ids(Some(state));
|
||||
let (scope, authority_scope, instruction) = match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => (
|
||||
"role-scoped planning synthesis",
|
||||
"epiphany.role.imagination",
|
||||
build_epiphany_role_launch_instruction(role_id),
|
||||
),
|
||||
EpiphanyRoleResultRoleId::Research => (
|
||||
"role-scoped source gathering",
|
||||
"epiphany.role.research",
|
||||
build_epiphany_role_launch_instruction(role_id),
|
||||
),
|
||||
EpiphanyRoleResultRoleId::Modeling => (
|
||||
"role-scoped modeling/checkpoint maintenance",
|
||||
"epiphany.role.modeling",
|
||||
build_epiphany_role_launch_instruction(role_id),
|
||||
),
|
||||
EpiphanyRoleResultRoleId::Verification => (
|
||||
"role-scoped verification/review",
|
||||
"epiphany.role.verification",
|
||||
build_epiphany_role_launch_instruction(role_id),
|
||||
),
|
||||
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
|
||||
return Err(epiphany_role_binding_id(role_id).unwrap_err());
|
||||
}
|
||||
};
|
||||
let launch_document = EpiphanyWorkerLaunchDocument::Role(EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: thread_id.to_string(),
|
||||
role_id: epiphany_role_label(role_id).to_string(),
|
||||
state_revision: state.revision,
|
||||
objective: state.objective.clone(),
|
||||
dynamic_prompt_context,
|
||||
repository_body_observation_basis: None,
|
||||
proposal_modeling_context: None,
|
||||
frontier_verdict_modeling_context: None,
|
||||
frontier_planning_context: None,
|
||||
frontier_research_context: None,
|
||||
frontier_verification_context: None,
|
||||
frontier_plan_mind_context: None,
|
||||
imagination_consideration_context: None,
|
||||
admitted_model_direction_consideration_context: None,
|
||||
active_subgoal_id: state.active_subgoal_id.clone(),
|
||||
active_subgoals: state
|
||||
.subgoals
|
||||
.iter()
|
||||
.filter(|subgoal| Some(subgoal.id.as_str()) == state.active_subgoal_id.as_deref())
|
||||
.cloned()
|
||||
.collect(),
|
||||
active_graph_node_ids: linked_graph_node_ids.clone(),
|
||||
investigation_checkpoint: state.investigation_checkpoint.clone(),
|
||||
scratch: state.scratch.clone(),
|
||||
invariants: state.invariants.clone(),
|
||||
graphs: Some(state.graphs.clone()),
|
||||
recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(),
|
||||
recent_observations: state.observations.iter().take(8).cloned().collect(),
|
||||
graph_frontier: state.graph_frontier.clone(),
|
||||
graph_checkpoint: state.graph_checkpoint.clone(),
|
||||
planning: Some(state.planning.clone()),
|
||||
churn: state.churn.clone(),
|
||||
});
|
||||
let output_contract_id = launch_document.output_contract_id().to_string();
|
||||
let organ_launch_contract = default_launch_organ_contract(
|
||||
authority_scope,
|
||||
launch_document.document_kind(),
|
||||
&output_contract_id,
|
||||
);
|
||||
|
||||
Ok(EpiphanyJobLaunchRequest {
|
||||
expected_revision,
|
||||
binding_id: binding_id.to_string(),
|
||||
kind: CoreEpiphanyJobKind::Specialist,
|
||||
scope: scope.to_string(),
|
||||
owner_role: owner_role.to_string(),
|
||||
authority_scope: authority_scope.to_string(),
|
||||
linked_subgoal_ids,
|
||||
linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)),
|
||||
instruction,
|
||||
launch_document,
|
||||
output_contract_id,
|
||||
organ_launch_contract,
|
||||
max_runtime_seconds,
|
||||
proposal_modeling_request_id: None,
|
||||
frontier_planning_request_id: None,
|
||||
frontier_plan_mind_request_id: None,
|
||||
imagination_consideration_request_id: None,
|
||||
admitted_model_direction_consideration_request_id: None,
|
||||
repo_frontier_modeling_request_id: None,
|
||||
repo_frontier_research_request_id: None,
|
||||
repo_frontier_verification_request_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_epiphany_imagination_consideration_launch_request(
|
||||
thread_id: &str,
|
||||
expected_revision: Option<u64>,
|
||||
max_runtime_seconds: Option<u64>,
|
||||
state: &EpiphanyThreadState,
|
||||
request_id: String,
|
||||
) -> Result<EpiphanyJobLaunchRequest, String> {
|
||||
let mut launch = build_epiphany_role_launch_request_with_dynamic_context(
|
||||
thread_id,
|
||||
EpiphanyRoleResultRoleId::Imagination,
|
||||
expected_revision,
|
||||
max_runtime_seconds,
|
||||
state,
|
||||
None,
|
||||
)?;
|
||||
launch.scope = "role-scoped organizational feedback consideration".into();
|
||||
launch.authority_scope = "epiphany.imagination.consideration.proposal_only".into();
|
||||
launch.instruction = "Await coordinator-owned typed consideration context.".into();
|
||||
launch.organ_launch_contract = default_launch_organ_contract(
|
||||
&launch.authority_scope,
|
||||
launch.launch_document.document_kind(),
|
||||
&launch.output_contract_id,
|
||||
);
|
||||
launch.imagination_consideration_request_id = Some(request_id);
|
||||
Ok(launch)
|
||||
}
|
||||
|
||||
pub fn build_epiphany_admitted_model_direction_consideration_launch_request(
|
||||
thread_id: &str,
|
||||
expected_revision: Option<u64>,
|
||||
max_runtime_seconds: Option<u64>,
|
||||
state: &EpiphanyThreadState,
|
||||
request_id: String,
|
||||
) -> Result<EpiphanyJobLaunchRequest, String> {
|
||||
let mut launch = build_epiphany_role_launch_request_with_dynamic_context(
|
||||
thread_id,
|
||||
EpiphanyRoleResultRoleId::Imagination,
|
||||
expected_revision,
|
||||
max_runtime_seconds,
|
||||
state,
|
||||
None,
|
||||
)?;
|
||||
launch.scope = "role-scoped admitted Modeling-map direction consideration".into();
|
||||
launch.authority_scope =
|
||||
"epiphany.imagination.admitted_model_direction_consideration.proposal_only".into();
|
||||
launch.instruction = "Await coordinator-owned typed admitted model context.".into();
|
||||
launch.organ_launch_contract = default_launch_organ_contract(
|
||||
&launch.authority_scope,
|
||||
launch.launch_document.document_kind(),
|
||||
&launch.output_contract_id,
|
||||
);
|
||||
launch.admitted_model_direction_consideration_request_id = Some(request_id);
|
||||
Ok(launch)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn build_epiphany_role_launch_instruction(role_id: EpiphanyRoleResultRoleId) -> String {
|
||||
let prompts = &epiphany_specialist_prompt_config().roles;
|
||||
let body = match role_id {
|
||||
|
|
@ -1243,50 +1063,6 @@ pub fn build_epiphany_reorient_launch_instruction(action: CoreEpiphanyReorientAc
|
|||
epiphany_worker_prompt(body)
|
||||
}
|
||||
|
||||
pub fn build_epiphany_job_launch_request(
|
||||
expected_revision: Option<u64>,
|
||||
binding_id: String,
|
||||
kind: CoreEpiphanyJobKind,
|
||||
scope: String,
|
||||
owner_role: String,
|
||||
authority_scope: String,
|
||||
linked_subgoal_ids: Vec<String>,
|
||||
linked_graph_node_ids: Vec<String>,
|
||||
instruction: String,
|
||||
launch_document: EpiphanyWorkerLaunchDocument,
|
||||
output_contract_id: String,
|
||||
max_runtime_seconds: Option<u64>,
|
||||
) -> EpiphanyJobLaunchRequest {
|
||||
let organ_launch_contract = default_launch_organ_contract(
|
||||
&authority_scope,
|
||||
launch_document.document_kind(),
|
||||
&output_contract_id,
|
||||
);
|
||||
EpiphanyJobLaunchRequest {
|
||||
expected_revision,
|
||||
binding_id,
|
||||
kind,
|
||||
scope,
|
||||
owner_role,
|
||||
authority_scope,
|
||||
linked_subgoal_ids,
|
||||
linked_graph_node_ids,
|
||||
instruction,
|
||||
launch_document,
|
||||
output_contract_id,
|
||||
organ_launch_contract,
|
||||
max_runtime_seconds,
|
||||
proposal_modeling_request_id: None,
|
||||
frontier_planning_request_id: None,
|
||||
frontier_plan_mind_request_id: None,
|
||||
imagination_consideration_request_id: None,
|
||||
admitted_model_direction_consideration_request_id: None,
|
||||
repo_frontier_modeling_request_id: None,
|
||||
repo_frontier_research_request_id: None,
|
||||
repo_frontier_verification_request_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_epiphany_coordinator_note(
|
||||
crrc_action: CoreEpiphanyCrrcAction,
|
||||
pressure_level: CoreEpiphanyPressureLevel,
|
||||
|
|
@ -1373,20 +1149,6 @@ fn extend_unique_strings(target: &mut Vec<String>, values: impl IntoIterator<Ite
|
|||
}
|
||||
}
|
||||
|
||||
fn epiphany_active_subgoal_ids(state: Option<&EpiphanyThreadState>) -> Vec<String> {
|
||||
state
|
||||
.and_then(|state| state.active_subgoal_id.clone())
|
||||
.map(|id| vec![id])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn epiphany_active_graph_node_ids(state: Option<&EpiphanyThreadState>) -> Vec<String> {
|
||||
state
|
||||
.and_then(|state| state.graph_frontier.as_ref())
|
||||
.map(|frontier| frontier.active_node_ids.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,183 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use epiphany_core::EpiphanyMemoryContextQuery;
|
||||
use epiphany_core::EpiphanyMemoryDomain;
|
||||
use epiphany_core::EpiphanyMemoryFreshnessStatus;
|
||||
use epiphany_core::EpiphanyMemoryGraphSnapshot;
|
||||
use epiphany_core::EpiphanyMemoryLifecycle;
|
||||
use epiphany_core::EpiphanyMemoryNode;
|
||||
use epiphany_core::EpiphanyMemoryNodeKind;
|
||||
use epiphany_core::EpiphanyMemoryProfile;
|
||||
use epiphany_core::EpiphanyMemorySummary;
|
||||
use epiphany_core::EpiphanyPromptContextInput;
|
||||
use epiphany_core::EpiphanyRoleResultRoleId;
|
||||
use epiphany_core::EpiphanyThreadState;
|
||||
use epiphany_core::EpiphanyWorkerLaunchDocument;
|
||||
use epiphany_core::build_epiphany_role_launch_request_with_dynamic_context;
|
||||
use epiphany_core::memory_graph_domain_id;
|
||||
use epiphany_core::memory_graph_node_id;
|
||||
use epiphany_core::plan_memory_graph_context_cut;
|
||||
use epiphany_core::query_epiphany_local_verse_context;
|
||||
use epiphany_core::render_epiphany_prompt_context;
|
||||
use epiphany_core::seed_epiphany_local_verse_context;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if std::env::args().nth(1).is_some() {
|
||||
anyhow::bail!(
|
||||
"prompt-context smoke accepts no arguments and writes only beneath .epiphany-smoke"
|
||||
);
|
||||
}
|
||||
let store = PathBuf::from(".epiphany-smoke")
|
||||
.join("cultmesh")
|
||||
.join("epiphany-prompt-context.ccmp");
|
||||
if let Some(parent) = store.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
seed_epiphany_local_verse_context(
|
||||
&store,
|
||||
"epiphany-prompt-context-smoke",
|
||||
"2026-06-02T00:00:00Z",
|
||||
"repo:C:/fixture/Epiphany",
|
||||
)?;
|
||||
let local_verse = query_epiphany_local_verse_context(&store, "epiphany-prompt-context-smoke")?;
|
||||
let memory_context = plan_memory_graph_context_cut(
|
||||
&smoke_snapshot(),
|
||||
&EpiphanyMemoryContextQuery {
|
||||
id: "prompt-context-smoke-query".to_string(),
|
||||
profile: Some(EpiphanyMemoryProfile::RepoArchitecture),
|
||||
text: Some("Verse prompt assembly".to_string()),
|
||||
budget: Some(4),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let rendered = render_epiphany_prompt_context(&EpiphanyPromptContextInput {
|
||||
focus: "Build a bounded worker prompt from local Verse and memory context.".to_string(),
|
||||
local_verse,
|
||||
memory_context,
|
||||
});
|
||||
|
||||
let checks = [
|
||||
rendered.contains("<epiphany_dynamic_context>"),
|
||||
rendered.contains("Odin"),
|
||||
rendered.contains("Yggdrasil"),
|
||||
rendered.contains("Bifrost"),
|
||||
rendered.contains("gamecult-local"),
|
||||
rendered.contains("Verse-backed prompt assembly"),
|
||||
rendered.contains("Mind reviews durable state effects"),
|
||||
!rendered.contains("raw private worker transcript should never render"),
|
||||
];
|
||||
if checks.iter().any(|passed| !passed) {
|
||||
anyhow::bail!("prompt context smoke failed rendered packet checks:\n{rendered}");
|
||||
}
|
||||
|
||||
let launch_request = build_epiphany_role_launch_request_with_dynamic_context(
|
||||
"thread-prompt-context-smoke",
|
||||
EpiphanyRoleResultRoleId::Modeling,
|
||||
Some(1),
|
||||
Some(60),
|
||||
&EpiphanyThreadState {
|
||||
revision: 1,
|
||||
objective: Some("Launch a worker with bounded dynamic context.".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
Some(rendered.clone()),
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let EpiphanyWorkerLaunchDocument::Role(role_document) = &launch_request.launch_document else {
|
||||
anyhow::bail!("prompt context smoke expected a role launch document");
|
||||
};
|
||||
if role_document.dynamic_prompt_context.as_deref() != Some(rendered.as_str()) {
|
||||
anyhow::bail!("prompt context smoke failed to preserve dynamic launch context");
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"status": "ok",
|
||||
"store": store,
|
||||
"renderedBytes": rendered.len(),
|
||||
"containsVerseContext": true,
|
||||
"containsMemoryContext": true,
|
||||
"launchDocumentCarriesContext": true,
|
||||
"omitsUnselectedPrivateText": true,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn smoke_snapshot() -> EpiphanyMemoryGraphSnapshot {
|
||||
let domain_id = memory_graph_domain_id(
|
||||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
"crate",
|
||||
"epiphany-core",
|
||||
);
|
||||
let selected_node = memory_graph_node_id(
|
||||
&domain_id,
|
||||
"module",
|
||||
"epiphany-core/src/prompt_context.rs",
|
||||
Some("prompt_context"),
|
||||
);
|
||||
let unselected_node = memory_graph_node_id(
|
||||
&domain_id,
|
||||
"module",
|
||||
"sealed/raw_worker_transcript.rs",
|
||||
Some("sealed_private_text"),
|
||||
);
|
||||
|
||||
EpiphanyMemoryGraphSnapshot {
|
||||
graph_id: "prompt-context-smoke".to_string(),
|
||||
domains: vec![EpiphanyMemoryDomain {
|
||||
id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
title: "epiphany-core".to_string(),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
..Default::default()
|
||||
}],
|
||||
nodes: vec![
|
||||
EpiphanyMemoryNode {
|
||||
id: selected_node.clone(),
|
||||
domain_id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
kind: EpiphanyMemoryNodeKind::Module,
|
||||
title: "prompt_context".to_string(),
|
||||
claim: "Verse-backed prompt assembly injects bounded local Verse and semantic memory packets.".to_string(),
|
||||
action_implication:
|
||||
"Use compact dynamic context before launching broad swarm turns."
|
||||
.to_string(),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
confidence: 90,
|
||||
salience: 90,
|
||||
..Default::default()
|
||||
},
|
||||
EpiphanyMemoryNode {
|
||||
id: unselected_node,
|
||||
domain_id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
kind: EpiphanyMemoryNodeKind::Module,
|
||||
title: "sealed_private_text".to_string(),
|
||||
claim: "raw private worker transcript should never render".to_string(),
|
||||
action_implication: "This text must stay absent from the smoke output.".to_string(),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
confidence: 90,
|
||||
salience: 10,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
summaries: vec![EpiphanyMemorySummary {
|
||||
id: "summary-prompt-context".to_string(),
|
||||
domain_id,
|
||||
covers_node_ids: vec![selected_node],
|
||||
target: "prompt_context".to_string(),
|
||||
claim: "Verse-backed prompt assembly can ask the local Verse and memory graph for compact context.".to_string(),
|
||||
action_implication:
|
||||
"Render this packet into worker prompts, then keep Mind as the durable-state gate."
|
||||
.to_string(),
|
||||
freshness: EpiphanyMemoryFreshnessStatus::Ready,
|
||||
confidence: 95,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +81,6 @@ fn prepare_authority_family(store: &Path) -> Result<EpiphanyToolInvocationIntent
|
|||
let launch_document = EpiphanyWorkerLaunchDocument::Role(EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "public-source-no-grant-thread".into(),
|
||||
role_id: "research".into(),
|
||||
state_revision: 0,
|
||||
objective: Some("Prove the exact public-source grant boundary.".into()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
|
|||
|
|
@ -1,790 +0,0 @@
|
|||
use crate::EpiphanyMemoryContextPacket;
|
||||
use crate::EpiphanyMemoryContextQuery;
|
||||
use crate::EpiphanyMemoryProfile;
|
||||
use crate::EpiphanyPromptContextInput;
|
||||
use crate::EpiphanyRepoModelView;
|
||||
use crate::assemble_repo_model_view;
|
||||
use crate::load_epiphany_cultmesh_cluster_topology;
|
||||
use crate::load_epiphany_cultmesh_status;
|
||||
use crate::plan_memory_graph_context_cut;
|
||||
use crate::query_epiphany_local_verse_context;
|
||||
use crate::render_epiphany_prompt_context;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use sha2::Digest;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const EPIPHANY_LOCAL_VERSE_RUNTIME_ID: &str = "epiphany-local";
|
||||
|
||||
pub fn local_verse_store_path(runtime_store_path: &Path) -> PathBuf {
|
||||
sibling_state_store_path(runtime_store_path, "local-verse.ccmp")
|
||||
}
|
||||
|
||||
pub fn memory_graph_store_path(runtime_store_path: &Path) -> PathBuf {
|
||||
sibling_state_store_path(runtime_store_path, "memory-graph.msgpack")
|
||||
}
|
||||
|
||||
fn sibling_state_store_path(runtime_store_path: &Path, filename: &str) -> PathBuf {
|
||||
runtime_store_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.join(filename)
|
||||
}
|
||||
|
||||
pub fn role_launch_context_focus(state: &EpiphanyThreadState, role_label: &str) -> String {
|
||||
let objective = state
|
||||
.objective
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|objective| !objective.is_empty())
|
||||
.unwrap_or("Epiphany worker launch");
|
||||
format!("Launch `{role_label}` worker for: {objective}")
|
||||
}
|
||||
|
||||
pub fn reorient_launch_context_focus(state: &EpiphanyThreadState, next_action: &str) -> String {
|
||||
let objective = state
|
||||
.objective
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|objective| !objective.is_empty())
|
||||
.unwrap_or("Epiphany reorientation");
|
||||
format!("Launch reorientation worker for: {objective}. Next action: {next_action}")
|
||||
}
|
||||
|
||||
pub fn render_launch_dynamic_prompt_context(
|
||||
runtime_store_path: &Path,
|
||||
local_verse_store: &Path,
|
||||
state: &EpiphanyThreadState,
|
||||
focus: String,
|
||||
) -> Result<String, String> {
|
||||
render_launch_dynamic_prompt_context_with_snapshot(
|
||||
runtime_store_path,
|
||||
local_verse_store,
|
||||
state,
|
||||
focus,
|
||||
)
|
||||
.map(|(context, _)| context)
|
||||
}
|
||||
|
||||
pub fn render_modeling_launch_dynamic_prompt_context(
|
||||
runtime_store_path: &Path,
|
||||
local_verse_store: &Path,
|
||||
state: &EpiphanyThreadState,
|
||||
focus: String,
|
||||
) -> Result<String, String> {
|
||||
let (context, snapshot) = render_launch_dynamic_prompt_context_with_snapshot(
|
||||
runtime_store_path,
|
||||
local_verse_store,
|
||||
state,
|
||||
focus,
|
||||
)?;
|
||||
Ok(append_modeling_repo_model_shape_snapshot(
|
||||
context, &snapshot,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_launch_dynamic_prompt_context_with_snapshot(
|
||||
runtime_store_path: &Path,
|
||||
local_verse_store: &Path,
|
||||
state: &EpiphanyThreadState,
|
||||
focus: String,
|
||||
) -> Result<(String, EpiphanyRepoModelView), String> {
|
||||
load_epiphany_cultmesh_status(local_verse_store, EPIPHANY_LOCAL_VERSE_RUNTIME_ID)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to inspect local Verse context store {}: {error}",
|
||||
local_verse_store.display()
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"local Verse is not bootstrapped at {}; initialize it before building worker launch context",
|
||||
local_verse_store.display()
|
||||
)
|
||||
})?;
|
||||
let topology =
|
||||
load_epiphany_cultmesh_cluster_topology(local_verse_store, EPIPHANY_LOCAL_VERSE_RUNTIME_ID)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to inspect local Verse topology store {}: {error}",
|
||||
local_verse_store.display()
|
||||
)
|
||||
})?;
|
||||
if topology.is_empty() {
|
||||
return Err(format!(
|
||||
"local Verse has no persisted cluster topology at {}; initialize it before building worker launch context",
|
||||
local_verse_store.display()
|
||||
));
|
||||
}
|
||||
let local_verse =
|
||||
query_epiphany_local_verse_context(local_verse_store, EPIPHANY_LOCAL_VERSE_RUNTIME_ID)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"failed to query local Verse context store {}: {error}",
|
||||
local_verse_store.display()
|
||||
)
|
||||
})?;
|
||||
let (memory_context, snapshot) =
|
||||
launch_memory_context(runtime_store_path, state, focus.as_str()).map_err(|error| {
|
||||
format!(
|
||||
"failed to build launch memory context beside {}: {error}",
|
||||
runtime_store_path.display()
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
render_epiphany_prompt_context(&EpiphanyPromptContextInput {
|
||||
focus,
|
||||
local_verse,
|
||||
memory_context,
|
||||
}),
|
||||
snapshot,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn validate_frontier_plan_decision_chain(
|
||||
decision: &crate::RepoFrontierPlanDecisionReceipt,
|
||||
route: &crate::RepoFrontierRoute,
|
||||
current_model: &EpiphanyRepoModelView,
|
||||
) -> Result<(), String> {
|
||||
validate_current_frontier_route(route, current_model, None)?;
|
||||
let plan = route
|
||||
.adopted_plan
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Soul frontier plan decision has no adopted plan".to_string())?;
|
||||
let source_is_complete = match decision.decision_source.as_ref() {
|
||||
Some(crate::RepoFrontierPlanDecisionSource::MindWorker { result_id, job_id }) => {
|
||||
!result_id.trim().is_empty() && !job_id.trim().is_empty()
|
||||
}
|
||||
Some(crate::RepoFrontierPlanDecisionSource::AuthenticatedOperatorReview {
|
||||
command_id,
|
||||
admission_id,
|
||||
packet_sha256,
|
||||
source_actor_id,
|
||||
}) => [command_id, admission_id, packet_sha256, source_actor_id]
|
||||
.into_iter()
|
||||
.all(|value| !value.trim().is_empty()),
|
||||
None => false,
|
||||
};
|
||||
let decision_basis = crate::EpiphanyRepoModelBasis {
|
||||
projection_digest: decision.model_projection_digest.clone(),
|
||||
source_documents: decision.model_source_documents.clone(),
|
||||
};
|
||||
decision_basis.validate().map_err(|error| {
|
||||
format!("Soul frontier plan decision has an invalid keyed basis: {error}")
|
||||
})?;
|
||||
if decision.schema_version != crate::REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION
|
||||
|| decision.contract != crate::REPO_FRONTIER_PLAN_DECISION_CONTRACT
|
||||
|| decision.decision != crate::RepoFrontierPlanDecision::Adopt
|
||||
|| decision.legacy_mind_worker_result_id.is_some()
|
||||
|| decision.legacy_mind_worker_job_id.is_some()
|
||||
|| decision.planning_request_id != plan.planning_request_id
|
||||
|| decision.candidate_id != plan.candidate_id
|
||||
|| decision.candidate_sha256 != plan.candidate_sha256
|
||||
|| decision_basis != current_model.reasoning_basis()
|
||||
|| decision.frontier_item_id != route.frontier_item_id
|
||||
|| decision.frontier_item_hash != route.frontier_item_hash
|
||||
|| !source_is_complete
|
||||
{
|
||||
return Err(
|
||||
"Soul frontier plan decision does not exactly bind its keyed basis, adopted plan, route, and decision source"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_current_frontier_route<'a>(
|
||||
route: &crate::RepoFrontierRoute,
|
||||
current_model: &'a EpiphanyRepoModelView,
|
||||
verification_request: Option<&crate::RepoFrontierVerificationRequest>,
|
||||
) -> Result<&'a crate::RepoFrontierItem, String> {
|
||||
let route_basis = crate::EpiphanyRepoModelBasis {
|
||||
projection_digest: route.model_projection_digest.clone(),
|
||||
source_documents: route.model_source_documents.clone(),
|
||||
};
|
||||
route_basis
|
||||
.validate()
|
||||
.map_err(|error| format!("frontier route has an invalid keyed basis: {error}"))?;
|
||||
if route_basis != current_model.reasoning_basis() {
|
||||
return Err("frontier route does not bind the current keyed RepoModel view".to_string());
|
||||
}
|
||||
let current_item = current_model
|
||||
.frontier
|
||||
.iter()
|
||||
.find(|item| item.id == route.frontier_item_id)
|
||||
.ok_or_else(|| {
|
||||
"routed frontier item disappeared from the current keyed view".to_string()
|
||||
})?;
|
||||
let current_item_hash = format!(
|
||||
"{:x}",
|
||||
sha2::Sha256::digest(
|
||||
rmp_serde::to_vec_named(current_item)
|
||||
.map_err(|error| format!("failed to hash current frontier item: {error}"))?
|
||||
)
|
||||
);
|
||||
let expected_source_scope = current_item
|
||||
.adopted_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.safe_paths.as_slice())
|
||||
.unwrap_or(current_item.source_scope.as_slice());
|
||||
if route.schema_version != crate::REPO_FRONTIER_ROUTE_SCHEMA_VERSION
|
||||
|| route.contract != crate::REPO_FRONTIER_ROUTE_CONTRACT
|
||||
|| route.next_organ != crate::RepoFrontierNextOrgan::Hands
|
||||
|| route.frontier_item_hash != current_item_hash
|
||||
|| route.migration_body != current_item.migration_body
|
||||
|| route.question != current_item.question
|
||||
|| route.gap != current_item.gap
|
||||
|| route.target_claim_ids != current_item.target_claim_ids
|
||||
|| route.source_scope != expected_source_scope
|
||||
|| route.adopted_plan != current_item.adopted_plan
|
||||
|| current_item.status != crate::RepoFrontierStatus::Active
|
||||
|| current_item.recommended_next_organ != "Hands"
|
||||
{
|
||||
return Err(
|
||||
"frontier route does not exactly bind its current keyed frontier item".to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(request) = verification_request {
|
||||
if request.schema_version != crate::REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION
|
||||
|| request.contract != crate::REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT
|
||||
|| request.route_id != route.route_id
|
||||
|| request.model_projection_digest != route.model_projection_digest
|
||||
|| request.model_source_documents != route.model_source_documents
|
||||
|| request.frontier_item_id != route.frontier_item_id
|
||||
|| request.frontier_item_hash != route.frontier_item_hash
|
||||
{
|
||||
return Err(
|
||||
"Soul verification request does not exactly bind the current keyed route"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(current_item)
|
||||
}
|
||||
|
||||
pub fn append_modeling_repo_model_shape_context(
|
||||
context: String,
|
||||
runtime_store_path: &Path,
|
||||
) -> Result<String, String> {
|
||||
let view = assemble_repo_model_view(runtime_store_path)
|
||||
.map_err(|error| format!("failed to load canonical RepoModel shape: {error}"))?;
|
||||
Ok(append_modeling_repo_model_shape_snapshot(context, &view))
|
||||
}
|
||||
|
||||
fn append_modeling_repo_model_shape_snapshot(
|
||||
mut context: String,
|
||||
view: &EpiphanyRepoModelView,
|
||||
) -> String {
|
||||
context.push_str("\n\n<canonical_repo_model_shape>\n");
|
||||
context.push_str(&format!("projectionDigest: {}\n", view.projection_digest));
|
||||
context.push_str("existingDomains:\n");
|
||||
for domain in &view.domains {
|
||||
let profile = debug_variant_snake_case(domain.profile);
|
||||
let lifecycle = debug_variant_snake_case(domain.lifecycle);
|
||||
context.push_str(&format!(
|
||||
"- id={} profile={} lifecycle={} title={}\n",
|
||||
domain.id, profile, lifecycle, domain.title
|
||||
));
|
||||
}
|
||||
context.push_str("existingClaims:\n");
|
||||
for node in &view.nodes {
|
||||
context.push_str(&format!(
|
||||
"- id={} domain={} title={}\n",
|
||||
node.id, node.domain_id, node.title
|
||||
));
|
||||
}
|
||||
context.push_str("existingFrontier:\n");
|
||||
for item in &view.frontier {
|
||||
let status = debug_variant_snake_case(item.status);
|
||||
context.push_str(&format!(
|
||||
"- id={} status={} recommendedNextOrgan={} targetClaims={} sourceScope={}\n",
|
||||
item.id,
|
||||
status,
|
||||
item.recommended_next_organ,
|
||||
item.target_claim_ids.join(" | "),
|
||||
item.source_scope.join(" | "),
|
||||
));
|
||||
}
|
||||
context.push_str("existingSurfaceOffers:\n");
|
||||
for offer in &view.surface_offers {
|
||||
context.push_str(&format!(
|
||||
"- surfaceId={} label={} contract={} lifecycle={} sourceRefs={}\n",
|
||||
offer.surface_id,
|
||||
offer.label,
|
||||
render_atlas_contract_descriptor(&offer.contract),
|
||||
render_atlas_offer_lifecycle(&offer.lifecycle),
|
||||
offer
|
||||
.body_evidence
|
||||
.iter()
|
||||
.map(|source| format!("{}@{}", source.path, source.raw_sha256))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | "),
|
||||
));
|
||||
}
|
||||
context.push_str("existingDependencyClaims:\n");
|
||||
for claim in &view.dependency_claims {
|
||||
context.push_str(&format!(
|
||||
"- claimId={} label={} target={} kind={} failureSemantics={} impactScope={} lifecycle={} sourceRefs={}\n",
|
||||
claim.claim_id,
|
||||
claim.label,
|
||||
render_atlas_dependency_target(&claim.target),
|
||||
debug_variant_snake_case(claim.entanglement_kind),
|
||||
debug_variant_snake_case(claim.failure_semantics),
|
||||
render_atlas_impact_scope(&claim.impact_scope),
|
||||
debug_variant_snake_case(claim.lifecycle),
|
||||
claim
|
||||
.body_evidence
|
||||
.iter()
|
||||
.map(|source| format!("{}@{}", source.path, source.raw_sha256))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | "),
|
||||
));
|
||||
}
|
||||
context.push_str("currentDependencyVerifications:\n");
|
||||
for verification in &view.dependency_verifications {
|
||||
context.push_str(&format!(
|
||||
"- claimId={} verdict={} claimPublication={} offerPublication={}\n",
|
||||
verification.claim_id,
|
||||
debug_variant_snake_case(verification.verdict),
|
||||
verification.claim_publication_id,
|
||||
verification.offer_publication_id,
|
||||
));
|
||||
}
|
||||
context.push_str("currentDependencyImpacts:\n");
|
||||
for impact in &view.dependency_impacts {
|
||||
context.push_str(&format!(
|
||||
"- impactId={} claimId={} criticality={} projection={}\n",
|
||||
impact.impact_id,
|
||||
impact.claim_id,
|
||||
debug_variant_snake_case(impact.criticality),
|
||||
impact.projection_sha256,
|
||||
));
|
||||
}
|
||||
context.push_str("RepoModel mutations address semantic document identities directly. New nodes must reference one exact existing domain id or a domain created by the same mutation. Every unresolved frontier item must target at least one exact existing claim id or a claim created by the same mutation. Every frontier source_scope must be non-empty and contain safe relative paths in strict lexicographic ascending order without duplicates. Recommended next organ must use its exact canonical spelling. RepoArchitecture and RepoDataflow nodes/edges may use only observed, proposed, accepted, stale, or retired lifecycle. Atlas create operations do not accept caller-authored offer or claim ids: runtime derives opaque UUIDs from the admitted proposal. Lifecycle operations may name only ids listed above. Exact dependency targets must name a provider repository and surface; unresolved targets remain unresolved. Local-surface impact scopes may name only locally owned surface ids listed above. The projection digest is display and audit identity, never a global stale-write fence.\n");
|
||||
context.push_str("</canonical_repo_model_shape>");
|
||||
context
|
||||
}
|
||||
|
||||
fn render_atlas_contract_descriptor(contract: &crate::AtlasContractDescriptor) -> String {
|
||||
match contract {
|
||||
crate::AtlasContractDescriptor::Semver {
|
||||
contract_id,
|
||||
version,
|
||||
} => format!("semver:{contract_id}@{version}"),
|
||||
crate::AtlasContractDescriptor::ExactSchema {
|
||||
contract_id,
|
||||
schema_id,
|
||||
} => format!("exact_schema:{contract_id}@{schema_id}"),
|
||||
crate::AtlasContractDescriptor::ExactDigest {
|
||||
contract_id,
|
||||
sha256,
|
||||
} => format!("exact_digest:{contract_id}@{sha256}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_atlas_contract_requirement(requirement: &crate::AtlasContractRequirement) -> String {
|
||||
match requirement {
|
||||
crate::AtlasContractRequirement::Semver {
|
||||
contract_id,
|
||||
requirement,
|
||||
} => format!("semver:{contract_id}@{requirement}"),
|
||||
crate::AtlasContractRequirement::ExactSchema {
|
||||
contract_id,
|
||||
schema_id,
|
||||
} => format!("exact_schema:{contract_id}@{schema_id}"),
|
||||
crate::AtlasContractRequirement::ExactDigest {
|
||||
contract_id,
|
||||
sha256,
|
||||
} => format!("exact_digest:{contract_id}@{sha256}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_atlas_offer_lifecycle(lifecycle: &crate::AtlasOfferLifecycle) -> String {
|
||||
match lifecycle {
|
||||
crate::AtlasOfferLifecycle::Active => "active".to_string(),
|
||||
crate::AtlasOfferLifecycle::Deprecated {
|
||||
replacement_surface_id: Some(replacement_surface_id),
|
||||
} => format!("deprecated:replacement={replacement_surface_id}"),
|
||||
crate::AtlasOfferLifecycle::Deprecated {
|
||||
replacement_surface_id: None,
|
||||
} => "deprecated".to_string(),
|
||||
crate::AtlasOfferLifecycle::Withdrawn => "withdrawn".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_atlas_dependency_target(target: &crate::AtlasDependencyTarget) -> String {
|
||||
match target {
|
||||
crate::AtlasDependencyTarget::Exact {
|
||||
provider,
|
||||
surface_id,
|
||||
requirement,
|
||||
} => format!(
|
||||
"exact:{}#{} requires {}",
|
||||
provider.repository_uri,
|
||||
surface_id,
|
||||
render_atlas_contract_requirement(requirement)
|
||||
),
|
||||
crate::AtlasDependencyTarget::Unresolved { requirement } => format!(
|
||||
"unresolved:{}",
|
||||
render_atlas_contract_requirement(requirement)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_atlas_impact_scope(scope: &crate::AtlasImpactScope) -> String {
|
||||
match scope {
|
||||
crate::AtlasImpactScope::WholeRepository => "whole_repository".to_string(),
|
||||
crate::AtlasImpactScope::LocalSurfaces { surface_ids } => format!(
|
||||
"local_surfaces:{}",
|
||||
surface_ids
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_variant_snake_case(value: impl std::fmt::Debug) -> String {
|
||||
let name = format!("{value:?}");
|
||||
let mut output = String::with_capacity(name.len() + 4);
|
||||
for (index, character) in name.chars().enumerate() {
|
||||
if character.is_ascii_uppercase() && index > 0 {
|
||||
output.push('_');
|
||||
}
|
||||
output.push(character.to_ascii_lowercase());
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn launch_memory_context(
|
||||
runtime_store_path: &Path,
|
||||
_state: &EpiphanyThreadState,
|
||||
focus: &str,
|
||||
) -> Result<(EpiphanyMemoryContextPacket, EpiphanyRepoModelView), String> {
|
||||
let view = assemble_repo_model_view(runtime_store_path)
|
||||
.map_err(|error| format!("failed to load keyed RepoModel view: {error}"))?;
|
||||
let projection = view.memory_context_projection();
|
||||
|
||||
let mut packet = plan_memory_graph_context_cut(
|
||||
&projection,
|
||||
&EpiphanyMemoryContextQuery {
|
||||
id: format!("launch-context-query-{}", view.projection_digest),
|
||||
profile: Some(EpiphanyMemoryProfile::RepoArchitecture),
|
||||
text: Some(focus.to_string()),
|
||||
budget: Some(5),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
packet.warnings.push(format!(
|
||||
"RepoModel context is assembled from exact keyed Mind documents at projection digest {}.",
|
||||
view.projection_digest
|
||||
));
|
||||
if packet.nodes.is_empty() && packet.summaries.is_empty() {
|
||||
packet.warnings.push(
|
||||
"Memory graph context is empty for this launch focus; the accepted repo graph may be thin or stale.".to_string(),
|
||||
);
|
||||
}
|
||||
Ok((packet, view))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::EpiphanyMemoryDomain;
|
||||
use crate::EpiphanyMemoryLifecycle;
|
||||
use crate::EpiphanyMemoryNode;
|
||||
use crate::EpiphanyMemoryNodeKind;
|
||||
use crate::EpiphanyRoleResultRoleId;
|
||||
use crate::RepoFrontierItem;
|
||||
use crate::RepoFrontierStatus;
|
||||
use crate::RuntimeSpineHeartbeatJobOptions;
|
||||
use crate::RuntimeSpineInitOptions;
|
||||
use crate::build_epiphany_role_launch_request_with_dynamic_context;
|
||||
use crate::initialize_runtime_spine;
|
||||
use crate::open_runtime_spine_heartbeat_job;
|
||||
use crate::runtime_worker_launch_request;
|
||||
use crate::seed_epiphany_local_verse_context;
|
||||
use std::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn native_launch_context_has_no_bridge_or_host_dependency() {
|
||||
let source = include_str!("coordinator_launch_context.rs");
|
||||
let production = source.split("#[cfg(test)]").next().unwrap_or(source);
|
||||
for forbidden in [
|
||||
"epiphany_codex_bridge",
|
||||
"epiphany_core::",
|
||||
"AppServerClient",
|
||||
"serde_json",
|
||||
] {
|
||||
assert!(
|
||||
!production.contains(forbidden),
|
||||
"native launch context contains host marker {forbidden:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_model_frontier_survives_newer_thread_state_and_guides_launch() -> anyhow::Result<()>
|
||||
{
|
||||
let temp = std::env::temp_dir().join(format!(
|
||||
"epiphany-canonical-model-launch-{}",
|
||||
Uuid::new_v4()
|
||||
));
|
||||
fs::create_dir(&temp)?;
|
||||
let runtime_store = temp.join("runtime-spine.msgpack");
|
||||
initialize_runtime_spine(
|
||||
&runtime_store,
|
||||
RuntimeSpineInitOptions {
|
||||
runtime_id: "canonical-model-launch-test".to_string(),
|
||||
display_name: "Canonical Model Launch Test".to_string(),
|
||||
created_at: "2026-06-12T00:00:00Z".to_string(),
|
||||
},
|
||||
)?;
|
||||
crate::runtime_spine::tests::bind_test_runtime_swarm(
|
||||
&runtime_store,
|
||||
"canonical-model-launch-swarm",
|
||||
)?;
|
||||
let documents = crate::EpiphanyRepoModelSeedDocuments {
|
||||
domains: vec![EpiphanyMemoryDomain {
|
||||
id: "repo".to_string(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
title: "Canonical repository model".to_string(),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
..Default::default()
|
||||
}],
|
||||
nodes: vec![EpiphanyMemoryNode {
|
||||
id: "claim-modeling-authority".to_string(),
|
||||
domain_id: "repo".to_string(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
kind: EpiphanyMemoryNodeKind::RuntimeContract,
|
||||
title: "Modeling authority".to_string(),
|
||||
claim: "Canonical Modeling state survives transcript revision churn.".to_string(),
|
||||
question: "Which downstream organ consumes it?".to_string(),
|
||||
action_implication: "Route the exact claim into launch context.".to_string(),
|
||||
source_hashes: vec!["anchor:missing".to_string()],
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
..Default::default()
|
||||
}],
|
||||
frontier: vec![RepoFrontierItem {
|
||||
id: "frontier-modeling-handoff".to_string(),
|
||||
migration_body: "Carry the canonical repository frontier into organ prompts."
|
||||
.to_string(),
|
||||
question: "Can Hands see the exact target claim?".to_string(),
|
||||
gap: "Launch assembly previously saw only semantically similar prose.".to_string(),
|
||||
target_claim_ids: vec!["claim-modeling-authority".to_string()],
|
||||
source_scope: vec!["epiphany-core/src".to_string()],
|
||||
recommended_next_organ: "Hands".to_string(),
|
||||
status: RepoFrontierStatus::Active,
|
||||
..Default::default()
|
||||
}],
|
||||
edges: Vec::new(),
|
||||
summaries: Vec::new(),
|
||||
lifecycle_receipts: Vec::new(),
|
||||
};
|
||||
let seed = crate::EpiphanyRepoModelSeed::new(
|
||||
"canonical-model-seed",
|
||||
"canonical-model",
|
||||
"canonical-model-launch-swarm",
|
||||
"canonical-model-workspace",
|
||||
"sha256:canonical-model-body",
|
||||
documents.clone(),
|
||||
)?;
|
||||
crate::initialize_keyed_repo_model(&runtime_store, &seed, "2026-06-12T00:00:01Z")?;
|
||||
let newer_thread_state = EpiphanyThreadState {
|
||||
revision: 999,
|
||||
objective: Some("Discuss irrelevant weather bananas.".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (packet, _) = launch_memory_context(
|
||||
&runtime_store,
|
||||
&newer_thread_state,
|
||||
"irrelevant weather bananas",
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(packet.frontier[0].id, "frontier-modeling-handoff");
|
||||
assert!(
|
||||
packet
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|node| node.id == "claim-modeling-authority")
|
||||
);
|
||||
let preserved = crate::assemble_repo_model_view(&runtime_store)?;
|
||||
assert_eq!(preserved.frontier, documents.frontier);
|
||||
let shape = append_modeling_repo_model_shape_context("base".to_string(), &runtime_store)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
assert_eq!(
|
||||
shape,
|
||||
append_modeling_repo_model_shape_snapshot("base".to_string(), &preserved)
|
||||
);
|
||||
assert!(shape.contains("<canonical_repo_model_shape>"));
|
||||
assert!(shape.contains("id=repo"));
|
||||
assert!(shape.contains("existingClaims:"));
|
||||
assert!(shape.contains("id=claim-modeling-authority domain=repo"));
|
||||
assert!(shape.contains("existingFrontier:"));
|
||||
assert!(shape.contains("id=frontier-modeling-handoff status=active"));
|
||||
assert!(shape.contains("projectionDigest: sha256:"));
|
||||
assert!(shape.contains("semantic document identities directly"));
|
||||
assert!(shape.contains("strict lexicographic ascending order without duplicates"));
|
||||
assert!(shape.contains("projection digest is display and audit identity"));
|
||||
fs::remove_dir_all(&temp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_context_persists_on_runtime_worker_request() -> anyhow::Result<()> {
|
||||
let temp =
|
||||
std::env::temp_dir().join(format!("epiphany-bridge-launch-context-{}", Uuid::new_v4()));
|
||||
fs::create_dir(&temp)?;
|
||||
let runtime_store = temp.join("runtime-spine.msgpack");
|
||||
crate::initialize_runtime_spine(
|
||||
&runtime_store,
|
||||
crate::RuntimeSpineInitOptions {
|
||||
runtime_id: "launch-context-runtime".to_string(),
|
||||
display_name: "Launch context test".to_string(),
|
||||
created_at: "2026-07-12T00:00:00Z".to_string(),
|
||||
},
|
||||
)?;
|
||||
let agent_store = temp.join("agents.msgpack");
|
||||
crate::ensure_agent_memory_swarm_identity(&agent_store, "launch-context-swarm")?;
|
||||
crate::bind_runtime_to_agent_memory_swarm(
|
||||
&runtime_store,
|
||||
&agent_store,
|
||||
"2026-07-12T00:00:01Z",
|
||||
)?;
|
||||
crate::runtime_spine::tests::bind_test_repository_body(
|
||||
&runtime_store,
|
||||
"launch-context-workspace",
|
||||
)?;
|
||||
let body_basis = crate::observe_runtime_repository_body_basis(&runtime_store)?;
|
||||
let seed = crate::EpiphanyRepoModelSeed::new(
|
||||
"launch-context-seed",
|
||||
"launch-context-graph",
|
||||
body_basis.swarm_id.clone(),
|
||||
body_basis.workspace_id.clone(),
|
||||
body_basis.body_binding_sha256.clone(),
|
||||
crate::EpiphanyRepoModelSeedDocuments {
|
||||
domains: Vec::new(),
|
||||
nodes: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
summaries: Vec::new(),
|
||||
frontier: Vec::new(),
|
||||
lifecycle_receipts: Vec::new(),
|
||||
},
|
||||
)?;
|
||||
crate::initialize_keyed_repo_model(&runtime_store, &seed, "2026-07-12T00:00:02Z")?;
|
||||
let state = EpiphanyThreadState {
|
||||
revision: 7,
|
||||
objective: Some("Test launch context.".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
seed_epiphany_local_verse_context(
|
||||
local_verse_store_path(&runtime_store),
|
||||
EPIPHANY_LOCAL_VERSE_RUNTIME_ID,
|
||||
"2026-07-12T00:00:00Z",
|
||||
"repo:C:/fixture/Epiphany",
|
||||
)?;
|
||||
|
||||
let rendered = render_launch_dynamic_prompt_context(
|
||||
&runtime_store,
|
||||
&local_verse_store_path(&runtime_store),
|
||||
&state,
|
||||
role_launch_context_focus(&state, "modeling"),
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert!(rendered.contains("<epiphany_dynamic_context>"));
|
||||
assert!(rendered.contains("Test launch context."));
|
||||
assert!(rendered.contains("Odin"));
|
||||
assert!(rendered.contains("Yggdrasil"));
|
||||
assert!(rendered.contains("Memory graph"));
|
||||
assert!(local_verse_store_path(&runtime_store).exists());
|
||||
assert!(runtime_store.exists());
|
||||
|
||||
let mut launch_request = build_epiphany_role_launch_request_with_dynamic_context(
|
||||
"thread-1",
|
||||
EpiphanyRoleResultRoleId::Modeling,
|
||||
Some(state.revision),
|
||||
Some(60),
|
||||
&state,
|
||||
Some(rendered.clone()),
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let crate::EpiphanyWorkerLaunchDocument::Role(role_document) =
|
||||
&mut launch_request.launch_document
|
||||
else {
|
||||
unreachable!("role launch builder returned reorient document")
|
||||
};
|
||||
role_document.repository_body_observation_basis = Some(body_basis);
|
||||
open_runtime_spine_heartbeat_job(
|
||||
&runtime_store,
|
||||
RuntimeSpineHeartbeatJobOptions {
|
||||
runtime_id: "launch-context-runtime".to_string(),
|
||||
display_name: "Launch context test".to_string(),
|
||||
session_id: "epiphany-main".to_string(),
|
||||
objective: "Test persisted launch context.".to_string(),
|
||||
coordinator_note: "Bridge launch-context smoke.".to_string(),
|
||||
job_id: "job-launch-context".to_string(),
|
||||
role: launch_request.owner_role.clone(),
|
||||
binding_id: launch_request.binding_id.clone(),
|
||||
authority_scope: launch_request.authority_scope.clone(),
|
||||
instruction: launch_request.instruction.clone(),
|
||||
launch_document: launch_request.launch_document.clone(),
|
||||
output_contract_id: launch_request.output_contract_id.clone(),
|
||||
organ_launch_contract: launch_request.organ_launch_contract.clone(),
|
||||
proposal_modeling_request_id: None,
|
||||
frontier_planning_request_id: None,
|
||||
frontier_plan_mind_request_id: None,
|
||||
imagination_consideration_request_id: None,
|
||||
admitted_model_direction_consideration_request_id: None,
|
||||
repo_frontier_modeling_request_id: None,
|
||||
repo_frontier_research_request_id: None,
|
||||
repo_frontier_verification_request_id: None,
|
||||
created_at: "2026-06-02T00:00:00Z".to_string(),
|
||||
},
|
||||
)?;
|
||||
let stored = runtime_worker_launch_request(&runtime_store, "job-launch-context")?
|
||||
.expect("runtime worker launch request should be persisted");
|
||||
let stored_document = stored.launch_document()?;
|
||||
let stored_context = stored_document
|
||||
.dynamic_prompt_context()
|
||||
.expect("stored launch document should carry dynamic context");
|
||||
assert!(stored_context.contains("Odin"));
|
||||
assert!(stored_context.contains("Memory graph"));
|
||||
assert!(stored_context.contains("Test launch context."));
|
||||
|
||||
fs::remove_dir_all(&temp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_context_refuses_to_bootstrap_shared_state() -> anyhow::Result<()> {
|
||||
let temp = std::env::temp_dir().join(format!(
|
||||
"epiphany-launch-context-unbootstrapped-{}",
|
||||
Uuid::new_v4()
|
||||
));
|
||||
fs::create_dir(&temp)?;
|
||||
let runtime_store = temp.join("runtime-spine.msgpack");
|
||||
let local_verse_store = local_verse_store_path(&runtime_store);
|
||||
let state = EpiphanyThreadState {
|
||||
revision: 1,
|
||||
objective: Some("Prove launch assembly cannot initialize shared state.".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let error = render_launch_dynamic_prompt_context(
|
||||
&runtime_store,
|
||||
&local_verse_store,
|
||||
&state,
|
||||
role_launch_context_focus(&state, "modeling"),
|
||||
)
|
||||
.expect_err("unbootstrapped launch context must fail closed");
|
||||
|
||||
assert!(error.contains("local Verse is not bootstrapped"));
|
||||
assert!(!local_verse_store.exists());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -954,7 +954,6 @@ pub fn launch_current_frontier_planning_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: job_id.clone(),
|
||||
role_id: "imagination".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -1147,7 +1146,6 @@ pub fn launch_current_frontier_plan_mind_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: job_id.clone(),
|
||||
role_id: "mindAdmissionReview".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -1336,7 +1334,6 @@ pub fn launch_current_frontier_verification_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: job_id.clone(),
|
||||
role_id: "verification".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -1515,7 +1512,6 @@ pub fn launch_current_frontier_research_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: job_id.clone(),
|
||||
role_id: "research".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -2443,7 +2439,6 @@ fn launch_consideration_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: job_id.clone(),
|
||||
role_id: "imagination".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: Some(dynamic_prompt_context),
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -2679,7 +2674,6 @@ pub fn launch_current_body_modeling_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: binding_record_id.clone(),
|
||||
role_id: "modeling".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: Some(work.body_basis.clone()),
|
||||
|
|
@ -2850,7 +2844,6 @@ pub fn launch_current_proposal_modeling_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: request.thread_id.clone(),
|
||||
role_id: "modeling".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: Some(body_basis),
|
||||
|
|
@ -3023,7 +3016,6 @@ pub fn launch_current_frontier_verdict_modeling_work(
|
|||
crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: job_id.clone(),
|
||||
role_id: "modeling".into(),
|
||||
state_revision: 0,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: Some(body_basis),
|
||||
|
|
@ -3627,7 +3619,6 @@ mod tests {
|
|||
let crate::EpiphanyWorkerLaunchDocument::Role(proposal_document) = proposal_document else {
|
||||
panic!("proposal Modeling must be a role pass")
|
||||
};
|
||||
assert_eq!(proposal_document.state_revision, 0);
|
||||
assert!(proposal_document.proposal_modeling_context.is_some());
|
||||
assert!(
|
||||
proposal_document
|
||||
|
|
|
|||
123
epiphany-core/src/frontier_plan_chain.rs
Normal file
123
epiphany-core/src/frontier_plan_chain.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use crate::EpiphanyRepoModelView;
|
||||
use sha2::Digest;
|
||||
|
||||
pub fn validate_frontier_plan_decision_chain(
|
||||
decision: &crate::RepoFrontierPlanDecisionReceipt,
|
||||
route: &crate::RepoFrontierRoute,
|
||||
current_model: &EpiphanyRepoModelView,
|
||||
) -> Result<(), String> {
|
||||
validate_current_frontier_route(route, current_model, None)?;
|
||||
let plan = route
|
||||
.adopted_plan
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Soul frontier plan decision has no adopted plan".to_string())?;
|
||||
let source_is_complete = match decision.decision_source.as_ref() {
|
||||
Some(crate::RepoFrontierPlanDecisionSource::MindWorker { result_id, job_id }) => {
|
||||
!result_id.trim().is_empty() && !job_id.trim().is_empty()
|
||||
}
|
||||
Some(crate::RepoFrontierPlanDecisionSource::AuthenticatedOperatorReview {
|
||||
command_id,
|
||||
admission_id,
|
||||
packet_sha256,
|
||||
source_actor_id,
|
||||
}) => [command_id, admission_id, packet_sha256, source_actor_id]
|
||||
.into_iter()
|
||||
.all(|value| !value.trim().is_empty()),
|
||||
None => false,
|
||||
};
|
||||
let decision_basis = crate::EpiphanyRepoModelBasis {
|
||||
projection_digest: decision.model_projection_digest.clone(),
|
||||
source_documents: decision.model_source_documents.clone(),
|
||||
};
|
||||
decision_basis.validate().map_err(|error| {
|
||||
format!("Soul frontier plan decision has an invalid keyed basis: {error}")
|
||||
})?;
|
||||
if decision.schema_version != crate::REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION
|
||||
|| decision.contract != crate::REPO_FRONTIER_PLAN_DECISION_CONTRACT
|
||||
|| decision.decision != crate::RepoFrontierPlanDecision::Adopt
|
||||
|| decision.legacy_mind_worker_result_id.is_some()
|
||||
|| decision.legacy_mind_worker_job_id.is_some()
|
||||
|| decision.planning_request_id != plan.planning_request_id
|
||||
|| decision.candidate_id != plan.candidate_id
|
||||
|| decision.candidate_sha256 != plan.candidate_sha256
|
||||
|| decision_basis != current_model.reasoning_basis()
|
||||
|| decision.frontier_item_id != route.frontier_item_id
|
||||
|| decision.frontier_item_hash != route.frontier_item_hash
|
||||
|| !source_is_complete
|
||||
{
|
||||
return Err(
|
||||
"Soul frontier plan decision does not exactly bind its keyed basis, adopted plan, route, and decision source"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_current_frontier_route<'a>(
|
||||
route: &crate::RepoFrontierRoute,
|
||||
current_model: &'a EpiphanyRepoModelView,
|
||||
verification_request: Option<&crate::RepoFrontierVerificationRequest>,
|
||||
) -> Result<&'a crate::RepoFrontierItem, String> {
|
||||
let route_basis = crate::EpiphanyRepoModelBasis {
|
||||
projection_digest: route.model_projection_digest.clone(),
|
||||
source_documents: route.model_source_documents.clone(),
|
||||
};
|
||||
route_basis
|
||||
.validate()
|
||||
.map_err(|error| format!("frontier route has an invalid keyed basis: {error}"))?;
|
||||
if route_basis != current_model.reasoning_basis() {
|
||||
return Err("frontier route does not bind the current keyed RepoModel view".to_string());
|
||||
}
|
||||
let current_item = current_model
|
||||
.frontier
|
||||
.iter()
|
||||
.find(|item| item.id == route.frontier_item_id)
|
||||
.ok_or_else(|| {
|
||||
"routed frontier item disappeared from the current keyed view".to_string()
|
||||
})?;
|
||||
let current_item_hash = format!(
|
||||
"{:x}",
|
||||
sha2::Sha256::digest(
|
||||
rmp_serde::to_vec_named(current_item)
|
||||
.map_err(|error| format!("failed to hash current frontier item: {error}"))?
|
||||
)
|
||||
);
|
||||
let expected_source_scope = current_item
|
||||
.adopted_plan
|
||||
.as_ref()
|
||||
.map(|plan| plan.safe_paths.as_slice())
|
||||
.unwrap_or(current_item.source_scope.as_slice());
|
||||
if route.schema_version != crate::REPO_FRONTIER_ROUTE_SCHEMA_VERSION
|
||||
|| route.contract != crate::REPO_FRONTIER_ROUTE_CONTRACT
|
||||
|| route.next_organ != crate::RepoFrontierNextOrgan::Hands
|
||||
|| route.frontier_item_hash != current_item_hash
|
||||
|| route.migration_body != current_item.migration_body
|
||||
|| route.question != current_item.question
|
||||
|| route.gap != current_item.gap
|
||||
|| route.target_claim_ids != current_item.target_claim_ids
|
||||
|| route.source_scope != expected_source_scope
|
||||
|| route.adopted_plan != current_item.adopted_plan
|
||||
|| current_item.status != crate::RepoFrontierStatus::Active
|
||||
|| current_item.recommended_next_organ != "Hands"
|
||||
{
|
||||
return Err(
|
||||
"frontier route does not exactly bind its current keyed frontier item".to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(request) = verification_request {
|
||||
if request.schema_version != crate::REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION
|
||||
|| request.contract != crate::REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT
|
||||
|| request.route_id != route.route_id
|
||||
|| request.model_projection_digest != route.model_projection_digest
|
||||
|| request.model_source_documents != route.model_source_documents
|
||||
|| request.frontier_item_id != route.frontier_item_id
|
||||
|| request.frontier_item_hash != route.frontier_item_hash
|
||||
{
|
||||
return Err(
|
||||
"Soul verification request does not exactly bind the current keyed route"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(current_item)
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@ fn participant_constraints(role_id: &str) -> Vec<&'static str> {
|
|||
vec![
|
||||
"Runs Ghostlight-shaped persistent role memory.",
|
||||
"May improve lane memory when awake and idle.",
|
||||
"Project truth belongs in EpiphanyThreadState, not role memory.",
|
||||
"Project truth belongs in keyed Mind documents, not role memory.",
|
||||
role_specific,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ mod agent_utterance_state;
|
|||
pub mod atlas;
|
||||
mod causal_work_identity;
|
||||
mod continuity_gateway;
|
||||
mod coordinator_launch_context;
|
||||
mod coordinator_objective_intake;
|
||||
mod coordinator_results;
|
||||
mod cultmesh_integration;
|
||||
mod current_work;
|
||||
mod distillation;
|
||||
mod eyes_gateway;
|
||||
mod frontier_plan_chain;
|
||||
mod hands_gateway;
|
||||
mod heartbeat_state;
|
||||
mod host_identity;
|
||||
|
|
@ -94,12 +94,7 @@ pub use agent_launch::EpiphanyReorientationPromptConfig;
|
|||
pub use agent_launch::EpiphanyRolePromptConfig;
|
||||
pub use agent_launch::EpiphanySharedPromptConfig;
|
||||
pub use agent_launch::EpiphanySpecialistPromptConfig;
|
||||
pub use agent_launch::build_epiphany_admitted_model_direction_consideration_launch_request;
|
||||
pub use agent_launch::build_epiphany_imagination_consideration_launch_request;
|
||||
pub use agent_launch::build_epiphany_job_launch_request;
|
||||
pub use agent_launch::build_epiphany_reorient_launch_instruction;
|
||||
pub use agent_launch::build_epiphany_role_launch_request;
|
||||
pub use agent_launch::build_epiphany_role_launch_request_with_dynamic_context;
|
||||
pub use agent_launch::epiphany_admitted_model_direction_consideration_output_schema;
|
||||
pub use agent_launch::epiphany_agent_prompt_with_memory;
|
||||
pub use agent_launch::epiphany_frontier_plan_mind_output_schema;
|
||||
|
|
@ -191,15 +186,7 @@ pub use agent_utterance_state::derive_agent_utterance_state;
|
|||
pub use atlas::*;
|
||||
pub use causal_work_identity::*;
|
||||
pub use continuity_gateway::*;
|
||||
pub use coordinator_launch_context::EPIPHANY_LOCAL_VERSE_RUNTIME_ID;
|
||||
pub use coordinator_launch_context::append_modeling_repo_model_shape_context;
|
||||
pub use coordinator_launch_context::local_verse_store_path;
|
||||
pub use coordinator_launch_context::memory_graph_store_path;
|
||||
pub use coordinator_launch_context::render_launch_dynamic_prompt_context;
|
||||
pub use coordinator_launch_context::render_modeling_launch_dynamic_prompt_context;
|
||||
pub use coordinator_launch_context::reorient_launch_context_focus;
|
||||
pub use coordinator_launch_context::role_launch_context_focus;
|
||||
pub use coordinator_launch_context::validate_frontier_plan_decision_chain;
|
||||
pub use frontier_plan_chain::validate_frontier_plan_decision_chain;
|
||||
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_CONTRACT;
|
||||
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_SCHEMA_VERSION;
|
||||
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_TYPE;
|
||||
|
|
@ -566,8 +553,6 @@ pub use epiphany_state_model::EpiphanyMemoryLifecycle;
|
|||
pub use epiphany_state_model::EpiphanyMemoryNode;
|
||||
pub use epiphany_state_model::EpiphanyMemoryProfile;
|
||||
pub use epiphany_state_model::EpiphanyMemorySummary;
|
||||
pub use epiphany_state_model::EpiphanyThreadState;
|
||||
pub use epiphany_state_model::render_epiphany_state;
|
||||
pub use eyes_gateway::EYES_EVIDENCE_PACKET_SCHEMA_VERSION;
|
||||
pub use eyes_gateway::EYES_EVIDENCE_PACKET_TYPE;
|
||||
pub use eyes_gateway::EYES_EVIDENCE_REFUSAL_RECEIPT_SCHEMA_VERSION;
|
||||
|
|
@ -742,7 +727,6 @@ pub use memory_graph::RepoFrontierAdoptedPlan;
|
|||
pub use memory_graph::RepoFrontierExecutionAmendment;
|
||||
pub use memory_graph::RepoFrontierItem;
|
||||
pub use memory_graph::RepoFrontierStatus;
|
||||
pub use memory_graph::RepoMemoryGraphRefresh;
|
||||
pub use memory_graph::SEMANTIC_PROJECTION_SCHEMA_VERSION;
|
||||
pub use memory_graph::SemanticCanonicalLocator;
|
||||
pub use memory_graph::SemanticDocumentKind;
|
||||
|
|
@ -771,7 +755,6 @@ pub use memory_graph::load_memory_semantic_projection_success;
|
|||
pub use memory_graph::memory_graph_domain_id;
|
||||
pub use memory_graph::memory_graph_edge_id;
|
||||
pub use memory_graph::memory_graph_from_agent_memories;
|
||||
pub use memory_graph::memory_graph_from_epiphany_graphs;
|
||||
pub use memory_graph::memory_graph_from_heartbeat_cognition;
|
||||
pub use memory_graph::memory_graph_model_hash;
|
||||
pub use memory_graph::memory_graph_node_id;
|
||||
|
|
@ -780,7 +763,6 @@ pub use memory_graph::migrate_memory_semantic_projection_attempts_v0;
|
|||
pub use memory_graph::observe_memory_semantic_projection;
|
||||
pub use memory_graph::plan_memory_graph_context_cut;
|
||||
pub use memory_graph::plan_memory_graph_context_cut_with_ranked_ids;
|
||||
pub use memory_graph::refresh_or_validate_repo_memory_graph;
|
||||
pub use memory_graph::resolve_semantic_candidate;
|
||||
pub use memory_graph::retain_memory_semantic_projection_lifecycles;
|
||||
pub use memory_graph::retire_memory_semantic_projection_claims_v0;
|
||||
|
|
@ -948,10 +930,6 @@ pub use runtime_spine::EpiphanyArchivedRuntimeWorkerAttempt;
|
|||
pub use runtime_spine::EpiphanyCoordinatorDeathRecovery;
|
||||
pub use runtime_spine::EpiphanyCoordinatorRunReceipt;
|
||||
pub use runtime_spine::EpiphanyCoordinatorRunReceiptRetentionHead;
|
||||
pub use runtime_spine::EpiphanyJobInterruptRequest;
|
||||
pub use runtime_spine::EpiphanyJobInterruptResult;
|
||||
pub use runtime_spine::EpiphanyJobLaunchRequest;
|
||||
pub use runtime_spine::EpiphanyJobLaunchResult;
|
||||
pub use runtime_spine::EpiphanyRuntimeEvent;
|
||||
pub use runtime_spine::EpiphanyRuntimeIdentity;
|
||||
pub use runtime_spine::EpiphanyRuntimeJob;
|
||||
|
|
@ -998,8 +976,6 @@ pub use runtime_spine::RepoFrontierVerdictModelingLaunchAuthority;
|
|||
pub use runtime_spine::RuntimeHandsReceiptChainSummary;
|
||||
pub use runtime_spine::RuntimeSpineEventOptions;
|
||||
pub use runtime_spine::RuntimeSpineHeartbeatJobOptions;
|
||||
pub use runtime_spine::RuntimeSpineHeartbeatLaunchPlan;
|
||||
pub use runtime_spine::RuntimeSpineHeartbeatLaunchPlanOptions;
|
||||
pub use runtime_spine::RuntimeSpineInitOptions;
|
||||
pub use runtime_spine::RuntimeSpineJobOptions;
|
||||
pub use runtime_spine::RuntimeSpineJobResultOptions;
|
||||
|
|
@ -1018,7 +994,6 @@ pub use runtime_spine::bind_runtime_repository_domain;
|
|||
pub use runtime_spine::bind_runtime_to_agent_memory_swarm;
|
||||
pub use runtime_spine::canonical_repo_frontier_plan_candidate_id;
|
||||
pub use runtime_spine::claim_runtime_worker_process;
|
||||
pub use runtime_spine::clear_epiphany_job_binding_backend;
|
||||
pub use runtime_spine::close_runtime_session;
|
||||
pub use runtime_spine::commit_operator_repo_frontier_plan_review;
|
||||
pub use runtime_spine::commit_repo_frontier_modeling_request;
|
||||
|
|
@ -1040,7 +1015,6 @@ pub use runtime_spine::open_runtime_model_execution;
|
|||
pub use runtime_spine::open_runtime_spine_heartbeat_job;
|
||||
pub use runtime_spine::operator_repo_frontier_plan_review_is_current;
|
||||
pub use runtime_spine::pending_repo_frontier_plan_reviews;
|
||||
pub use runtime_spine::plan_runtime_spine_heartbeat_launch;
|
||||
pub use runtime_spine::prepare_runtime_spine_heartbeat_job;
|
||||
pub use runtime_spine::promote_autonomous_direction_options_for_modeling;
|
||||
pub use runtime_spine::put_coordinator_run_receipt;
|
||||
|
|
@ -1063,7 +1037,6 @@ pub use runtime_spine::put_substrate_gate_repo_access_grant_receipt;
|
|||
pub use runtime_spine::relinquish_repo_frontier_hands_route;
|
||||
pub use runtime_spine::repair_legacy_terminal_coordinator_sessions;
|
||||
pub use runtime_spine::repair_runtime_root_session_after_invalid_completion;
|
||||
pub use runtime_spine::replace_or_append_epiphany_job_binding;
|
||||
pub use runtime_spine::require_runtime_tool_execution_binding;
|
||||
pub use runtime_spine::retain_completed_runtime_sessions;
|
||||
pub use runtime_spine::retain_coordinator_run_receipts;
|
||||
|
|
@ -1160,15 +1133,10 @@ pub use substrate_gate::SubstrateGateRepoAccessGrantReceipt;
|
|||
pub use substrate_gate::default_substrate_gate_cultnet_contracts;
|
||||
pub use substrate_gate::substrate_gate_coordinator_implementation_grant;
|
||||
pub use substrate_gate::substrate_gate_operation_for_governed_tool;
|
||||
pub use substrate_gate::substrate_gate_repo_access_grant_for_launch;
|
||||
pub use substrate_gate::substrate_gate_repo_access_grant_for_worker;
|
||||
pub use substrate_gate::substrate_gate_repo_work_planning_grant;
|
||||
pub use surfaces::AdmittedModelDirectionConsiderationContextProjection;
|
||||
pub use surfaces::EpiphanyAcceptanceBundle;
|
||||
pub use surfaces::EpiphanyContext;
|
||||
pub use surfaces::EpiphanyContextMissing;
|
||||
pub use surfaces::EpiphanyContextParams;
|
||||
pub use surfaces::EpiphanyContextStateStatus;
|
||||
pub use surfaces::EpiphanyContextView;
|
||||
pub use surfaces::EpiphanyCoordinatorAction;
|
||||
pub use surfaces::EpiphanyCoordinatorAutomationAction;
|
||||
pub use surfaces::EpiphanyCoordinatorCrrcRecommendation;
|
||||
|
|
@ -1188,20 +1156,6 @@ pub use surfaces::EpiphanyCrrcReorientAction;
|
|||
pub use surfaces::EpiphanyCrrcResultStatus;
|
||||
pub use surfaces::EpiphanyCrrcSceneAction;
|
||||
pub use surfaces::EpiphanyCrrcStateStatus;
|
||||
pub use surfaces::EpiphanyFreshnessInput;
|
||||
pub use surfaces::EpiphanyFreshnessView;
|
||||
pub use surfaces::EpiphanyFreshnessWatcherInput;
|
||||
pub use surfaces::EpiphanyGraphContext;
|
||||
pub use surfaces::EpiphanyGraphFreshness;
|
||||
pub use surfaces::EpiphanyGraphFreshnessStatus;
|
||||
pub use surfaces::EpiphanyGraphQuery;
|
||||
pub use surfaces::EpiphanyGraphQueryDirection;
|
||||
pub use surfaces::EpiphanyGraphQueryKind;
|
||||
pub use surfaces::EpiphanyGraphQueryMatched;
|
||||
pub use surfaces::EpiphanyGraphQueryMissing;
|
||||
pub use surfaces::EpiphanyGraphQueryView;
|
||||
pub use surfaces::EpiphanyInvalidationInput;
|
||||
pub use surfaces::EpiphanyInvalidationStatus;
|
||||
pub use surfaces::EpiphanyJobStatus;
|
||||
pub use surfaces::EpiphanyJobView;
|
||||
pub use surfaces::EpiphanyJobsInput;
|
||||
|
|
@ -1217,8 +1171,6 @@ pub use surfaces::EpiphanyReorientAcceptanceFinding;
|
|||
pub use surfaces::EpiphanyReorientAction;
|
||||
pub use surfaces::EpiphanyReorientFindingInterpretation;
|
||||
pub use surfaces::EpiphanyReorientWorkerLaunchDocument;
|
||||
pub use surfaces::EpiphanyRetrievalFreshness;
|
||||
pub use surfaces::EpiphanyRetrievalFreshnessStatus;
|
||||
pub use surfaces::EpiphanyRoleAcceptanceFinding;
|
||||
pub use surfaces::EpiphanyRoleBoardInput;
|
||||
pub use surfaces::EpiphanyRoleBoardJob;
|
||||
|
|
@ -1267,9 +1219,6 @@ pub use surfaces::RepoFrontierVerificationContextProjection;
|
|||
pub use surfaces::build_reorient_acceptance_bundle;
|
||||
pub use surfaces::build_role_acceptance_bundle;
|
||||
pub use surfaces::default_epiphany_view_lenses;
|
||||
pub use surfaces::derive_context;
|
||||
pub use surfaces::derive_freshness;
|
||||
pub use surfaces::derive_graph_query;
|
||||
pub use surfaces::derive_jobs;
|
||||
pub use surfaces::derive_planning_view;
|
||||
pub use surfaces::derive_pressure_view;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ 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;
|
||||
|
|
@ -40,11 +39,8 @@ 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 semantic_index::*;
|
||||
pub use semantic_projection::*;
|
||||
pub use semantic_projector::*;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
pub mod agent;
|
||||
pub mod heartbeat;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,502 +0,0 @@
|
|||
use crate::memory_graph::EpiphanyMemoryAnchor;
|
||||
use crate::memory_graph::EpiphanyMemoryDomain;
|
||||
use crate::memory_graph::EpiphanyMemoryEdge;
|
||||
use crate::memory_graph::EpiphanyMemoryEdgeKind;
|
||||
use crate::memory_graph::EpiphanyMemoryFreshnessStatus;
|
||||
use crate::memory_graph::EpiphanyMemoryGraphSnapshot;
|
||||
use crate::memory_graph::EpiphanyMemoryLifecycle;
|
||||
use crate::memory_graph::EpiphanyMemoryNode;
|
||||
use crate::memory_graph::EpiphanyMemoryNodeKind;
|
||||
use crate::memory_graph::EpiphanyMemoryProfile;
|
||||
use crate::memory_graph::EpiphanyMemorySummary;
|
||||
use crate::memory_graph::derive_memory_graph_freshness;
|
||||
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;
|
||||
|
||||
#[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)> {
|
||||
Ok((
|
||||
memory_graph_from_epiphany_graphs(
|
||||
graph_id,
|
||||
&state.graphs,
|
||||
source_identity,
|
||||
state.revision,
|
||||
repo_root,
|
||||
)?,
|
||||
RepoMemoryGraphRefresh::Refreshed,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn memory_graph_from_epiphany_graphs(
|
||||
graph_id: impl Into<String>,
|
||||
graphs: &EpiphanyGraphs,
|
||||
source_identity: impl Into<String>,
|
||||
source_revision: u64,
|
||||
repo_root: &Path,
|
||||
) -> Result<EpiphanyMemoryGraphSnapshot> {
|
||||
let architecture_domain = memory_graph_domain_id(
|
||||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
"accepted_graph",
|
||||
"architecture",
|
||||
);
|
||||
let dataflow_domain = memory_graph_domain_id(
|
||||
EpiphanyMemoryProfile::RepoDataflow,
|
||||
"accepted_graph",
|
||||
"dataflow",
|
||||
);
|
||||
|
||||
let mut domains = Vec::new();
|
||||
let mut nodes = Vec::new();
|
||||
let mut edges = Vec::new();
|
||||
let mut summaries = Vec::new();
|
||||
let mut node_map = HashMap::new();
|
||||
|
||||
if !graphs.architecture.is_empty() {
|
||||
domains.push(repo_domain(
|
||||
architecture_domain.clone(),
|
||||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
"Accepted architecture graph",
|
||||
));
|
||||
import_graph_nodes(
|
||||
&graphs.architecture,
|
||||
&architecture_domain,
|
||||
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",
|
||||
&architecture_domain,
|
||||
"accepted architecture graph",
|
||||
"Accepted architecture graph nodes are available as memory graph repo-profile claims.",
|
||||
&nodes,
|
||||
&edges,
|
||||
EpiphanyMemoryProfile::RepoArchitecture,
|
||||
));
|
||||
}
|
||||
|
||||
if !graphs.dataflow.is_empty() {
|
||||
domains.push(repo_domain(
|
||||
dataflow_domain.clone(),
|
||||
EpiphanyMemoryProfile::RepoDataflow,
|
||||
"Accepted dataflow graph",
|
||||
));
|
||||
import_graph_nodes(
|
||||
&graphs.dataflow,
|
||||
&dataflow_domain,
|
||||
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",
|
||||
&dataflow_domain,
|
||||
"accepted dataflow graph",
|
||||
"Accepted dataflow graph nodes are available as memory graph repo-profile claims.",
|
||||
&nodes,
|
||||
&edges,
|
||||
EpiphanyMemoryProfile::RepoDataflow,
|
||||
));
|
||||
}
|
||||
|
||||
import_graph_links(graphs.links.as_slice(), &node_map, &mut edges, repo_root);
|
||||
|
||||
let stale = nodes
|
||||
.iter()
|
||||
.any(|node| node.lifecycle == EpiphanyMemoryLifecycle::Stale)
|
||||
|| edges
|
||||
.iter()
|
||||
.any(|edge| edge.lifecycle == EpiphanyMemoryLifecycle::Stale);
|
||||
if stale {
|
||||
for summary in &mut summaries {
|
||||
summary.freshness = EpiphanyMemoryFreshnessStatus::Stale;
|
||||
}
|
||||
}
|
||||
let mut snapshot = 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,
|
||||
..Default::default()
|
||||
};
|
||||
snapshot.freshness = Some(derive_memory_graph_freshness(&snapshot, &[]));
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn repo_domain(id: String, profile: EpiphanyMemoryProfile, title: &str) -> EpiphanyMemoryDomain {
|
||||
EpiphanyMemoryDomain {
|
||||
id,
|
||||
profile,
|
||||
title: title.to_string(),
|
||||
description: Some("Repo profile imported from accepted Epiphany graph state.".to_string()),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_graph_nodes(
|
||||
graph: &EpiphanyGraph,
|
||||
domain_id: &str,
|
||||
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.clone(), domain_id, profile, node, repo_root)
|
||||
.unwrap_or_else(|_| stale_memory_node(memory_id, domain_id, profile, node)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_node_from_graph_node(
|
||||
id: String,
|
||||
domain_id: &str,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
node: &EpiphanyGraphNode,
|
||||
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()
|
||||
.filter_map(|anchor| anchor.source_hash.clone())
|
||||
.collect()
|
||||
};
|
||||
let grounded = !anchors.is_empty();
|
||||
Ok(EpiphanyMemoryNode {
|
||||
id,
|
||||
domain_id: domain_id.to_string(),
|
||||
profile,
|
||||
kind: memory_node_kind(node),
|
||||
title: node.title.clone(),
|
||||
claim: graph_node_claim(node),
|
||||
question: "What changes if this accepted graph node is stale?".to_string(),
|
||||
tension: node.mechanism.clone().unwrap_or_default(),
|
||||
action_implication: format!(
|
||||
"Use accepted graph node {} when selecting repo context.",
|
||||
node.id
|
||||
),
|
||||
anchors,
|
||||
source_hashes,
|
||||
lifecycle: if grounded {
|
||||
graph_status_lifecycle(node.status.as_deref())
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Stale
|
||||
},
|
||||
salience: 70,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
fn import_graph_edges(
|
||||
graph: &EpiphanyGraph,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
node_map: &HashMap<String, String>,
|
||||
edges: &mut Vec<EpiphanyMemoryEdge>,
|
||||
repo_root: &Path,
|
||||
) {
|
||||
for edge in &graph.edges {
|
||||
let source_id = node_map
|
||||
.get(&edge.source_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| edge.source_id.clone());
|
||||
let target_id = node_map
|
||||
.get(&edge.target_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| edge.target_id.clone());
|
||||
edges.push(memory_edge_from_graph_edge(
|
||||
source_id, target_id, profile, edge, repo_root,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_edge_from_graph_edge(
|
||||
source_id: String,
|
||||
target_id: String,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
edge: &EpiphanyGraphEdge,
|
||||
repo_root: &Path,
|
||||
) -> EpiphanyMemoryEdge {
|
||||
let id = edge.id.clone().unwrap_or_else(|| {
|
||||
memory_graph_edge_id(
|
||||
&source_id,
|
||||
&target_id,
|
||||
edge.kind.as_str(),
|
||||
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,
|
||||
target_id,
|
||||
kind: memory_edge_kind(edge.kind.as_str()),
|
||||
profile,
|
||||
claim: graph_edge_claim(edge),
|
||||
anchors: anchors.unwrap_or_default(),
|
||||
lifecycle: if grounded {
|
||||
EpiphanyMemoryLifecycle::Accepted
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Stale
|
||||
},
|
||||
confidence: if grounded { 80 } else { 0 },
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
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,
|
||||
&target_id,
|
||||
"grounds",
|
||||
link.code_refs.iter().map(code_ref_key),
|
||||
),
|
||||
source_id,
|
||||
target_id,
|
||||
kind: EpiphanyMemoryEdgeKind::Grounds,
|
||||
profile: EpiphanyMemoryProfile::RepoDataflow,
|
||||
claim: link
|
||||
.relationship
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Dataflow node is linked to architecture node.".to_string()),
|
||||
anchors: anchors.unwrap_or_default(),
|
||||
lifecycle: if grounded {
|
||||
EpiphanyMemoryLifecycle::Accepted
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Stale
|
||||
},
|
||||
confidence: if grounded { 75 } else { 0 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_summary(
|
||||
id: &str,
|
||||
domain_id: &str,
|
||||
target: &str,
|
||||
claim: &str,
|
||||
nodes: &[EpiphanyMemoryNode],
|
||||
edges: &[EpiphanyMemoryEdge],
|
||||
profile: EpiphanyMemoryProfile,
|
||||
) -> EpiphanyMemorySummary {
|
||||
let covers_node_ids = nodes
|
||||
.iter()
|
||||
.filter(|node| node.profile == profile)
|
||||
.map(|node| node.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let covers_edge_ids = edges
|
||||
.iter()
|
||||
.filter(|edge| edge.profile == profile)
|
||||
.map(|edge| edge.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let anchor_count = nodes
|
||||
.iter()
|
||||
.filter(|node| node.profile == profile)
|
||||
.map(|node| node.anchors.len() as u32)
|
||||
.sum();
|
||||
EpiphanyMemorySummary {
|
||||
id: id.to_string(),
|
||||
domain_id: domain_id.to_string(),
|
||||
covers_node_ids,
|
||||
covers_edge_ids,
|
||||
target: target.to_string(),
|
||||
claim: claim.to_string(),
|
||||
question: "Which profile producer should refresh this accepted graph next?".to_string(),
|
||||
tension: String::new(),
|
||||
action_implication: "Use this summary for broad repo context; descend when exact nodes are stale or relevant.".to_string(),
|
||||
anchor_count,
|
||||
freshness: EpiphanyMemoryFreshnessStatus::Ready,
|
||||
confidence: 80,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn anchors_from_code_refs(
|
||||
prefix: &str,
|
||||
code_refs: &[EpiphanyCodeRef],
|
||||
repo_root: &Path,
|
||||
) -> Result<Vec<EpiphanyMemoryAnchor>> {
|
||||
code_refs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.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()
|
||||
}
|
||||
|
||||
fn code_ref_key(code_ref: &EpiphanyCodeRef) -> String {
|
||||
let mut key = code_ref.path.to_string_lossy().replace('\\', "/");
|
||||
if let Some(symbol) = code_ref.symbol.as_deref() {
|
||||
key.push('#');
|
||||
key.push_str(symbol);
|
||||
}
|
||||
key
|
||||
}
|
||||
|
||||
fn graph_node_claim(node: &EpiphanyGraphNode) -> String {
|
||||
if !node.purpose.trim().is_empty() {
|
||||
return node.purpose.clone();
|
||||
}
|
||||
format!("Accepted graph node {} exists.", node.id)
|
||||
}
|
||||
|
||||
fn graph_edge_claim(edge: &EpiphanyGraphEdge) -> String {
|
||||
edge.mechanism
|
||||
.clone()
|
||||
.or_else(|| edge.label.clone())
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"Accepted graph edge {} connects {} to {}.",
|
||||
edge.kind, edge.source_id, edge.target_id
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn graph_status_lifecycle(status: Option<&str>) -> EpiphanyMemoryLifecycle {
|
||||
match status.unwrap_or_default() {
|
||||
"observed" => EpiphanyMemoryLifecycle::Observed,
|
||||
"proposed" => EpiphanyMemoryLifecycle::Proposed,
|
||||
"stale" => EpiphanyMemoryLifecycle::Stale,
|
||||
"retired" => EpiphanyMemoryLifecycle::Retired,
|
||||
_ => EpiphanyMemoryLifecycle::Accepted,
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_node_kind(node: &EpiphanyGraphNode) -> EpiphanyMemoryNodeKind {
|
||||
let text = format!("{} {}", node.title, node.purpose).to_lowercase();
|
||||
if text.contains("schema") {
|
||||
EpiphanyMemoryNodeKind::Schema
|
||||
} else if text.contains("runtime") || text.contains("contract") {
|
||||
EpiphanyMemoryNodeKind::RuntimeContract
|
||||
} else if text.contains("adapter") || text.contains("bridge") {
|
||||
EpiphanyMemoryNodeKind::Adapter
|
||||
} else if text.contains("test") || text.contains("smoke") {
|
||||
EpiphanyMemoryNodeKind::TestSeam
|
||||
} else if text.contains("state") || text.contains("store") {
|
||||
EpiphanyMemoryNodeKind::StateStore
|
||||
} else {
|
||||
EpiphanyMemoryNodeKind::Module
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_edge_kind(kind: &str) -> EpiphanyMemoryEdgeKind {
|
||||
match kind {
|
||||
"owns" => EpiphanyMemoryEdgeKind::Owns,
|
||||
"reads" => EpiphanyMemoryEdgeKind::Reads,
|
||||
"writes" => EpiphanyMemoryEdgeKind::Writes,
|
||||
"derives" => EpiphanyMemoryEdgeKind::Derives,
|
||||
"adapts" => EpiphanyMemoryEdgeKind::Adapts,
|
||||
"persists" => EpiphanyMemoryEdgeKind::Persists,
|
||||
"launches" => EpiphanyMemoryEdgeKind::Launches,
|
||||
"verifies" | "tests" => EpiphanyMemoryEdgeKind::Verifies,
|
||||
"depends_on" | "depends" => EpiphanyMemoryEdgeKind::DependsOn,
|
||||
_ => EpiphanyMemoryEdgeKind::Other,
|
||||
}
|
||||
}
|
||||
|
|
@ -1293,7 +1293,6 @@ mod tests {
|
|||
EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "creation-thread".into(),
|
||||
role_id: "Modeling".into(),
|
||||
state_revision: 7,
|
||||
objective: Some("Map the Body".into()),
|
||||
dynamic_prompt_context: Some("typed projection".into()),
|
||||
repository_body_observation_basis: None,
|
||||
|
|
|
|||
|
|
@ -134,10 +134,6 @@ use epiphany_openai_adapter::EpiphanyOpenAiModelReceipt;
|
|||
use epiphany_openai_adapter::EpiphanyOpenAiModelRequest;
|
||||
use epiphany_openai_adapter::EpiphanyOpenAiStreamEvent;
|
||||
use epiphany_openai_adapter::EpiphanyOpenAiStreamPayload;
|
||||
use epiphany_state_model::EpiphanyJobBinding;
|
||||
use epiphany_state_model::EpiphanyJobKind;
|
||||
use epiphany_state_model::EpiphanyRuntimeLink;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use epiphany_tool_adapter::EpiphanyToolCapability;
|
||||
use epiphany_tool_adapter::EpiphanyToolInvocationIntent;
|
||||
use epiphany_tool_adapter::EpiphanyToolInvocationReceipt;
|
||||
|
|
@ -1023,77 +1019,6 @@ pub struct PreparedRuntimeSpineHeartbeatJob {
|
|||
pub envelopes: Vec<CultCacheEnvelope>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeSpineHeartbeatLaunchPlanOptions {
|
||||
pub binding_id: String,
|
||||
pub kind: EpiphanyJobKind,
|
||||
pub scope: String,
|
||||
pub owner_role: String,
|
||||
pub authority_scope: String,
|
||||
pub linked_subgoal_ids: Vec<String>,
|
||||
pub linked_graph_node_ids: Vec<String>,
|
||||
pub instruction: String,
|
||||
pub launch_document: EpiphanyWorkerLaunchDocument,
|
||||
pub output_contract_id: String,
|
||||
pub organ_launch_contract: EpiphanyLaunchOrganContract,
|
||||
pub max_runtime_seconds: Option<u64>,
|
||||
pub runtime_job_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeSpineHeartbeatLaunchPlan {
|
||||
pub binding: EpiphanyJobBinding,
|
||||
pub runtime_link: EpiphanyRuntimeLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EpiphanyJobLaunchRequest {
|
||||
pub expected_revision: Option<u64>,
|
||||
pub binding_id: String,
|
||||
pub kind: EpiphanyJobKind,
|
||||
pub scope: String,
|
||||
pub owner_role: String,
|
||||
pub authority_scope: String,
|
||||
pub linked_subgoal_ids: Vec<String>,
|
||||
pub linked_graph_node_ids: Vec<String>,
|
||||
pub instruction: String,
|
||||
pub launch_document: EpiphanyWorkerLaunchDocument,
|
||||
pub output_contract_id: String,
|
||||
pub organ_launch_contract: EpiphanyLaunchOrganContract,
|
||||
pub max_runtime_seconds: Option<u64>,
|
||||
pub proposal_modeling_request_id: Option<String>,
|
||||
pub frontier_planning_request_id: Option<String>,
|
||||
pub frontier_plan_mind_request_id: Option<String>,
|
||||
pub imagination_consideration_request_id: Option<String>,
|
||||
pub admitted_model_direction_consideration_request_id: Option<String>,
|
||||
pub repo_frontier_modeling_request_id: Option<String>,
|
||||
pub repo_frontier_research_request_id: Option<String>,
|
||||
pub repo_frontier_verification_request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EpiphanyJobLaunchResult {
|
||||
pub epiphany_state: EpiphanyThreadState,
|
||||
pub binding_id: String,
|
||||
pub launcher_job_id: String,
|
||||
pub backend_job_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EpiphanyJobInterruptRequest {
|
||||
pub expected_revision: Option<u64>,
|
||||
pub binding_id: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EpiphanyJobInterruptResult {
|
||||
pub epiphany_state: EpiphanyThreadState,
|
||||
pub binding_id: String,
|
||||
pub cancel_requested: bool,
|
||||
pub interrupted_thread_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EpiphanyRuntimeJobSnapshot {
|
||||
pub job: EpiphanyRuntimeJob,
|
||||
|
|
@ -3055,64 +2980,6 @@ where
|
|||
Ok(archive)
|
||||
}
|
||||
|
||||
pub fn plan_runtime_spine_heartbeat_launch(
|
||||
state: &EpiphanyThreadState,
|
||||
options: RuntimeSpineHeartbeatLaunchPlanOptions,
|
||||
) -> Result<RuntimeSpineHeartbeatLaunchPlan> {
|
||||
validate_heartbeat_launch_options(state, &options)?;
|
||||
Ok(RuntimeSpineHeartbeatLaunchPlan {
|
||||
binding: EpiphanyJobBinding {
|
||||
id: options.binding_id.clone(),
|
||||
kind: options.kind,
|
||||
scope: options.scope.clone(),
|
||||
owner_role: options.owner_role.clone(),
|
||||
authority_scope: Some(options.authority_scope.clone()),
|
||||
linked_subgoal_ids: options.linked_subgoal_ids.clone(),
|
||||
linked_graph_node_ids: options.linked_graph_node_ids.clone(),
|
||||
blocking_reason: None,
|
||||
},
|
||||
runtime_link: EpiphanyRuntimeLink {
|
||||
id: format!(
|
||||
"runtime-link-{}-{}",
|
||||
options.binding_id, options.runtime_job_id
|
||||
),
|
||||
binding_id: options.binding_id,
|
||||
surface: "jobLaunch".to_string(),
|
||||
role_id: options.owner_role,
|
||||
authority_scope: options.authority_scope,
|
||||
runtime_job_id: options.runtime_job_id,
|
||||
runtime_result_id: None,
|
||||
linked_subgoal_ids: options.linked_subgoal_ids,
|
||||
linked_graph_node_ids: options.linked_graph_node_ids,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_or_append_epiphany_job_binding(
|
||||
mut bindings: Vec<EpiphanyJobBinding>,
|
||||
replacement: EpiphanyJobBinding,
|
||||
) -> Vec<EpiphanyJobBinding> {
|
||||
if let Some(existing) = bindings
|
||||
.iter_mut()
|
||||
.find(|binding| binding.id == replacement.id)
|
||||
{
|
||||
*existing = replacement;
|
||||
return bindings;
|
||||
}
|
||||
bindings.push(replacement);
|
||||
bindings
|
||||
}
|
||||
|
||||
pub fn clear_epiphany_job_binding_backend(
|
||||
mut bindings: Vec<EpiphanyJobBinding>,
|
||||
binding_index: usize,
|
||||
blocking_reason: &str,
|
||||
) -> Vec<EpiphanyJobBinding> {
|
||||
let binding = &mut bindings[binding_index];
|
||||
binding.blocking_reason = Some(blocking_reason.to_string());
|
||||
bindings
|
||||
}
|
||||
|
||||
pub fn open_runtime_spine_heartbeat_job(
|
||||
store_path: impl AsRef<Path>,
|
||||
options: RuntimeSpineHeartbeatJobOptions,
|
||||
|
|
@ -13856,76 +13723,6 @@ where
|
|||
.transpose()
|
||||
}
|
||||
|
||||
fn validate_heartbeat_launch_options(
|
||||
state: &EpiphanyThreadState,
|
||||
options: &RuntimeSpineHeartbeatLaunchPlanOptions,
|
||||
) -> Result<()> {
|
||||
validate_non_empty(&options.binding_id, "epiphany job launch binding_id")?;
|
||||
if matches!(
|
||||
options.binding_id.as_str(),
|
||||
"retrieval-index" | "graph-remap" | "verification"
|
||||
) {
|
||||
return Err(anyhow!(
|
||||
"epiphany job launch binding_id {:?} is reserved for a derived built-in slot",
|
||||
options.binding_id
|
||||
));
|
||||
}
|
||||
if options.kind != EpiphanyJobKind::Specialist {
|
||||
return Err(anyhow!(
|
||||
"epiphany job launch currently supports only specialist heartbeat turns"
|
||||
));
|
||||
}
|
||||
validate_non_empty(&options.scope, "epiphany job launch scope")?;
|
||||
validate_non_empty(&options.owner_role, "epiphany job launch owner_role")?;
|
||||
validate_non_empty(
|
||||
&options.authority_scope,
|
||||
"epiphany job launch authority_scope",
|
||||
)?;
|
||||
validate_non_empty(&options.instruction, "epiphany job launch instruction")?;
|
||||
validate_non_empty(
|
||||
options.launch_document.thread_id(),
|
||||
"epiphany job launch document thread id",
|
||||
)?;
|
||||
validate_non_empty(
|
||||
&options.output_contract_id,
|
||||
"epiphany job launch output_contract_id",
|
||||
)?;
|
||||
if options.output_contract_id != options.launch_document.output_contract_id() {
|
||||
return Err(anyhow!(
|
||||
"epiphany job launch output_contract_id must match the typed launch document"
|
||||
));
|
||||
}
|
||||
validate_launch_organ_contract(
|
||||
&options.organ_launch_contract,
|
||||
&options.authority_scope,
|
||||
options.launch_document.document_kind(),
|
||||
&options.output_contract_id,
|
||||
)?;
|
||||
if let Some(max_runtime_seconds) = options.max_runtime_seconds
|
||||
&& max_runtime_seconds == 0
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"epiphany job launch max_runtime_seconds must be >= 1"
|
||||
));
|
||||
}
|
||||
let existing_binding = state
|
||||
.job_bindings
|
||||
.iter()
|
||||
.find(|binding| binding.id == options.binding_id);
|
||||
let latest_runtime_link = state.runtime_links.iter().find(|link| {
|
||||
link.binding_id == options.binding_id && !link.runtime_job_id.trim().is_empty()
|
||||
});
|
||||
if latest_runtime_link.is_some_and(|link| link.runtime_result_id.is_none())
|
||||
&& existing_binding.is_none_or(|binding| binding.blocking_reason.is_none())
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"epiphany job binding {:?} is already bound to an active heartbeat turn; interrupt it before launching a replacement",
|
||||
options.binding_id
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_status_for_verdict(verdict: &str) -> EpiphanyRuntimeJobStatus {
|
||||
if matches!(
|
||||
verdict,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::EpiphanyJobLaunchRequest;
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
|
@ -139,23 +138,6 @@ pub fn default_substrate_gate_cultnet_contracts() -> Vec<SubstrateGateCultNetCon
|
|||
]
|
||||
}
|
||||
|
||||
pub fn substrate_gate_repo_access_grant_for_launch(
|
||||
receipt_id: String,
|
||||
runtime_job_id: String,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
granted_at: String,
|
||||
) -> SubstrateGateRepoAccessGrantReceipt {
|
||||
substrate_gate_repo_access_grant_for_worker(
|
||||
receipt_id,
|
||||
runtime_job_id,
|
||||
request.binding_id.clone(),
|
||||
request.owner_role.clone(),
|
||||
request.authority_scope.clone(),
|
||||
request.binding_id == crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID,
|
||||
granted_at,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn substrate_gate_repo_access_grant_for_worker(
|
||||
receipt_id: String,
|
||||
runtime_job_id: String,
|
||||
|
|
@ -277,68 +259,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn substrate_gate_grant_for_launch_is_read_only() {
|
||||
let request = EpiphanyJobLaunchRequest {
|
||||
expected_revision: Some(7),
|
||||
binding_id: "research-source-gather-worker".to_string(),
|
||||
kind: epiphany_state_model::EpiphanyJobKind::Specialist,
|
||||
scope: "research".to_string(),
|
||||
owner_role: "epiphany-eyes".to_string(),
|
||||
authority_scope: "epiphany.role.research".to_string(),
|
||||
linked_subgoal_ids: Vec::new(),
|
||||
linked_graph_node_ids: Vec::new(),
|
||||
instruction: "Gather source evidence.".to_string(),
|
||||
launch_document: crate::EpiphanyWorkerLaunchDocument::Role(
|
||||
crate::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".to_string(),
|
||||
role_id: "research".to_string(),
|
||||
state_revision: 7,
|
||||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
proposal_modeling_context: None,
|
||||
frontier_verdict_modeling_context: None,
|
||||
frontier_planning_context: None,
|
||||
frontier_research_context: None,
|
||||
frontier_verification_context: None,
|
||||
frontier_plan_mind_context: None,
|
||||
imagination_consideration_context: None,
|
||||
admitted_model_direction_consideration_context: None,
|
||||
active_subgoal_id: None,
|
||||
active_subgoals: Vec::new(),
|
||||
active_graph_node_ids: Vec::new(),
|
||||
investigation_checkpoint: None,
|
||||
scratch: None,
|
||||
invariants: Vec::new(),
|
||||
graphs: None,
|
||||
recent_evidence: Vec::new(),
|
||||
recent_observations: Vec::new(),
|
||||
graph_frontier: None,
|
||||
graph_checkpoint: None,
|
||||
planning: None,
|
||||
churn: None,
|
||||
},
|
||||
),
|
||||
output_contract_id: crate::ROLE_WORKER_OUTPUT_CONTRACT_ID.to_string(),
|
||||
organ_launch_contract: crate::default_launch_organ_contract(
|
||||
"epiphany.role.research",
|
||||
"role",
|
||||
crate::ROLE_WORKER_OUTPUT_CONTRACT_ID,
|
||||
),
|
||||
max_runtime_seconds: None,
|
||||
proposal_modeling_request_id: None,
|
||||
frontier_planning_request_id: None,
|
||||
frontier_plan_mind_request_id: None,
|
||||
imagination_consideration_request_id: None,
|
||||
admitted_model_direction_consideration_request_id: None,
|
||||
repo_frontier_modeling_request_id: None,
|
||||
repo_frontier_research_request_id: None,
|
||||
repo_frontier_verification_request_id: None,
|
||||
};
|
||||
let grant = substrate_gate_repo_access_grant_for_launch(
|
||||
fn substrate_gate_grant_for_worker_is_read_only() {
|
||||
let grant = substrate_gate_repo_access_grant_for_worker(
|
||||
"grant-1".to_string(),
|
||||
"job-1".to_string(),
|
||||
&request,
|
||||
"research-source-gather-worker".to_string(),
|
||||
"epiphany-eyes".to_string(),
|
||||
"epiphany.role.research".to_string(),
|
||||
true,
|
||||
"2026-05-30T00:00:00Z".to_string(),
|
||||
);
|
||||
assert!(grant.granted_operations.contains(&"read".to_string()));
|
||||
|
|
|
|||
|
|
@ -1,638 +0,0 @@
|
|||
use epiphany_state_model::EpiphanyRetrievalState;
|
||||
use epiphany_state_model::EpiphanyRetrievalStatus;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyRetrievalFreshnessStatus {
|
||||
Missing,
|
||||
Ready,
|
||||
Stale,
|
||||
Indexing,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EpiphanyRetrievalFreshness {
|
||||
pub status: EpiphanyRetrievalFreshnessStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub semantic_available: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_indexed_at_unix_seconds: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub indexed_file_count: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub indexed_chunk_count: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub dirty_paths: Vec<PathBuf>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyGraphFreshnessStatus {
|
||||
Missing,
|
||||
Stale,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EpiphanyGraphFreshness {
|
||||
pub status: EpiphanyGraphFreshnessStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub checkpoint_id: Option<String>,
|
||||
pub dirty_path_count: u32,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub dirty_paths: Vec<PathBuf>,
|
||||
pub open_question_count: u32,
|
||||
pub open_gap_count: u32,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyInvalidationStatus {
|
||||
Unavailable,
|
||||
Clean,
|
||||
Changed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EpiphanyInvalidationInput {
|
||||
pub status: EpiphanyInvalidationStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub watched_root: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub observed_at_unix_seconds: Option<i64>,
|
||||
pub changed_path_count: u32,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub changed_paths: Vec<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub graph_node_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub active_frontier_node_ids: Vec<String>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EpiphanyFreshnessWatcherInput<'a> {
|
||||
pub available: bool,
|
||||
pub workspace_root: Option<&'a Path>,
|
||||
pub observed_at_unix_seconds: Option<i64>,
|
||||
pub changed_paths: &'a [PathBuf],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EpiphanyFreshnessInput<'a> {
|
||||
pub state: Option<&'a EpiphanyThreadState>,
|
||||
pub retrieval_override: Option<&'a EpiphanyRetrievalState>,
|
||||
pub watcher: Option<EpiphanyFreshnessWatcherInput<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EpiphanyFreshnessView {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub state_revision: Option<u64>,
|
||||
pub retrieval: EpiphanyRetrievalFreshness,
|
||||
pub graph: EpiphanyGraphFreshness,
|
||||
pub watcher: EpiphanyInvalidationInput,
|
||||
}
|
||||
|
||||
pub fn derive_freshness(input: EpiphanyFreshnessInput<'_>) -> EpiphanyFreshnessView {
|
||||
let retrieval = retrieval_state_for_reflection(input.state, input.retrieval_override);
|
||||
EpiphanyFreshnessView {
|
||||
state_revision: input.state.map(|state| state.revision),
|
||||
retrieval: retrieval_freshness(retrieval),
|
||||
graph: graph_freshness(input.state),
|
||||
watcher: invalidation_input(input.state, input.watcher),
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieval_state_for_reflection<'a>(
|
||||
state: Option<&'a EpiphanyThreadState>,
|
||||
retrieval_override: Option<&'a EpiphanyRetrievalState>,
|
||||
) -> Option<&'a EpiphanyRetrievalState> {
|
||||
retrieval_override.or_else(|| state.and_then(|state| state.retrieval.as_ref()))
|
||||
}
|
||||
|
||||
fn retrieval_freshness(retrieval: Option<&EpiphanyRetrievalState>) -> EpiphanyRetrievalFreshness {
|
||||
let Some(retrieval) = retrieval else {
|
||||
return EpiphanyRetrievalFreshness {
|
||||
status: EpiphanyRetrievalFreshnessStatus::Missing,
|
||||
semantic_available: None,
|
||||
last_indexed_at_unix_seconds: None,
|
||||
indexed_file_count: None,
|
||||
indexed_chunk_count: None,
|
||||
dirty_paths: Vec::new(),
|
||||
note: "No retrieval freshness is available for this thread view.".to_string(),
|
||||
};
|
||||
};
|
||||
|
||||
let dirty_path_count = retrieval.dirty_paths.len();
|
||||
let note = match retrieval.status {
|
||||
EpiphanyRetrievalStatus::Ready if dirty_path_count == 0 => {
|
||||
"Legacy retrieval reports Ready, but has no Body-bound coverage receipt.".to_string()
|
||||
}
|
||||
EpiphanyRetrievalStatus::Ready => {
|
||||
format!("Retrieval catalog is stale because {dirty_path_count} dirty path(s) remain.")
|
||||
}
|
||||
EpiphanyRetrievalStatus::Stale => {
|
||||
format!("Retrieval catalog is stale; {dirty_path_count} dirty path(s) need refresh.")
|
||||
}
|
||||
EpiphanyRetrievalStatus::Indexing => "Retrieval catalog is indexing.".to_string(),
|
||||
EpiphanyRetrievalStatus::Unavailable => "Retrieval catalog is unavailable.".to_string(),
|
||||
};
|
||||
|
||||
EpiphanyRetrievalFreshness {
|
||||
status: match retrieval.status {
|
||||
EpiphanyRetrievalStatus::Ready if dirty_path_count == 0 => {
|
||||
EpiphanyRetrievalFreshnessStatus::Missing
|
||||
}
|
||||
EpiphanyRetrievalStatus::Ready => EpiphanyRetrievalFreshnessStatus::Stale,
|
||||
EpiphanyRetrievalStatus::Stale => EpiphanyRetrievalFreshnessStatus::Stale,
|
||||
EpiphanyRetrievalStatus::Indexing => EpiphanyRetrievalFreshnessStatus::Indexing,
|
||||
EpiphanyRetrievalStatus::Unavailable => EpiphanyRetrievalFreshnessStatus::Unavailable,
|
||||
},
|
||||
semantic_available: Some(retrieval.semantic_available),
|
||||
last_indexed_at_unix_seconds: retrieval.last_indexed_at_unix_seconds,
|
||||
indexed_file_count: retrieval.indexed_file_count,
|
||||
indexed_chunk_count: retrieval.indexed_chunk_count,
|
||||
dirty_paths: retrieval.dirty_paths.clone(),
|
||||
note,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn graph_freshness(state: Option<&EpiphanyThreadState>) -> EpiphanyGraphFreshness {
|
||||
let Some(state) = state else {
|
||||
return EpiphanyGraphFreshness {
|
||||
status: EpiphanyGraphFreshnessStatus::Missing,
|
||||
checkpoint_id: None,
|
||||
dirty_path_count: 0,
|
||||
dirty_paths: Vec::new(),
|
||||
open_question_count: 0,
|
||||
open_gap_count: 0,
|
||||
note: "Epiphany state is missing, so graph freshness cannot be assessed.".to_string(),
|
||||
};
|
||||
};
|
||||
|
||||
let frontier = state.graph_frontier.as_ref();
|
||||
let dirty_paths = frontier
|
||||
.map(|frontier| frontier.dirty_paths.clone())
|
||||
.unwrap_or_default();
|
||||
let dirty_path_count = dirty_paths.len() as u32;
|
||||
let open_question_count = frontier
|
||||
.map(|frontier| frontier.open_question_ids.len() as u32)
|
||||
.unwrap_or_default();
|
||||
let open_gap_count = frontier
|
||||
.map(|frontier| frontier.open_gap_ids.len() as u32)
|
||||
.unwrap_or_default();
|
||||
let has_pressure = dirty_path_count > 0 || open_question_count > 0 || open_gap_count > 0;
|
||||
let checkpoint_id = state
|
||||
.graph_checkpoint
|
||||
.as_ref()
|
||||
.map(|checkpoint| checkpoint.checkpoint_id.clone());
|
||||
let status = if has_pressure {
|
||||
EpiphanyGraphFreshnessStatus::Stale
|
||||
} else {
|
||||
EpiphanyGraphFreshnessStatus::Missing
|
||||
};
|
||||
let note = if status == EpiphanyGraphFreshnessStatus::Stale {
|
||||
format!(
|
||||
"Graph freshness is stale; frontier has {dirty_path_count} dirty path(s), {open_question_count} open question id(s), and {open_gap_count} open gap id(s)."
|
||||
)
|
||||
} else {
|
||||
"Canonical RepoModel admission freshness is not available to this legacy thread projection; legacy checkpoint and churn hints cannot prove readiness."
|
||||
.to_string()
|
||||
};
|
||||
|
||||
EpiphanyGraphFreshness {
|
||||
status,
|
||||
checkpoint_id,
|
||||
dirty_path_count,
|
||||
dirty_paths,
|
||||
open_question_count,
|
||||
open_gap_count,
|
||||
note,
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidation_input(
|
||||
state: Option<&EpiphanyThreadState>,
|
||||
watcher: Option<EpiphanyFreshnessWatcherInput<'_>>,
|
||||
) -> EpiphanyInvalidationInput {
|
||||
let Some(watcher) = watcher else {
|
||||
return EpiphanyInvalidationInput {
|
||||
status: EpiphanyInvalidationStatus::Unavailable,
|
||||
watched_root: None,
|
||||
observed_at_unix_seconds: None,
|
||||
changed_path_count: 0,
|
||||
changed_paths: Vec::new(),
|
||||
graph_node_ids: Vec::new(),
|
||||
active_frontier_node_ids: Vec::new(),
|
||||
note: "Watcher-backed invalidation inputs are only available for loaded threads."
|
||||
.to_string(),
|
||||
};
|
||||
};
|
||||
|
||||
if !watcher.available {
|
||||
return EpiphanyInvalidationInput {
|
||||
status: EpiphanyInvalidationStatus::Unavailable,
|
||||
watched_root: None,
|
||||
observed_at_unix_seconds: None,
|
||||
changed_path_count: 0,
|
||||
changed_paths: Vec::new(),
|
||||
graph_node_ids: Vec::new(),
|
||||
active_frontier_node_ids: Vec::new(),
|
||||
note: "The workspace watcher is unavailable for this app-server process.".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let changed_paths = watcher.changed_paths.to_vec();
|
||||
let changed_path_count = changed_paths.len() as u32;
|
||||
let watched_root = watcher.workspace_root.map(Path::to_path_buf);
|
||||
|
||||
if changed_paths.is_empty() {
|
||||
return EpiphanyInvalidationInput {
|
||||
status: EpiphanyInvalidationStatus::Unavailable,
|
||||
watched_root,
|
||||
observed_at_unix_seconds: watcher.observed_at_unix_seconds,
|
||||
changed_path_count,
|
||||
changed_paths,
|
||||
graph_node_ids: Vec::new(),
|
||||
active_frontier_node_ids: Vec::new(),
|
||||
note: "Watcher supplied no observation; absence of changed paths is not proof of a clean workspace."
|
||||
.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let graph_node_ids = state
|
||||
.map(|state| {
|
||||
graph_node_ids_for_changed_paths(state, &changed_paths, watcher.workspace_root)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let active_frontier_node_ids = state
|
||||
.map(|state| {
|
||||
active_frontier_node_ids_for_changed_paths(
|
||||
state,
|
||||
&changed_paths,
|
||||
watcher.workspace_root,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let note = if graph_node_ids.is_empty() {
|
||||
format!(
|
||||
"Watcher observed {changed_path_count} recent changed path(s), but no mapped graph node code refs matched yet."
|
||||
)
|
||||
} else if active_frontier_node_ids.is_empty() {
|
||||
format!(
|
||||
"Watcher observed {changed_path_count} recent changed path(s) touching {} mapped graph node(s).",
|
||||
graph_node_ids.len()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Watcher observed {changed_path_count} recent changed path(s) touching {} mapped graph node(s), including {} active frontier node(s).",
|
||||
graph_node_ids.len(),
|
||||
active_frontier_node_ids.len()
|
||||
)
|
||||
};
|
||||
|
||||
EpiphanyInvalidationInput {
|
||||
status: EpiphanyInvalidationStatus::Changed,
|
||||
watched_root,
|
||||
observed_at_unix_seconds: watcher.observed_at_unix_seconds,
|
||||
changed_path_count,
|
||||
changed_paths,
|
||||
graph_node_ids,
|
||||
active_frontier_node_ids,
|
||||
note,
|
||||
}
|
||||
}
|
||||
|
||||
fn graph_node_ids_for_changed_paths(
|
||||
state: &EpiphanyThreadState,
|
||||
changed_paths: &[PathBuf],
|
||||
workspace_root: Option<&Path>,
|
||||
) -> Vec<String> {
|
||||
let changed_path_keys: HashSet<String> = changed_paths
|
||||
.iter()
|
||||
.map(|path| epiphany_path_key(path.as_path()))
|
||||
.collect();
|
||||
let mut node_ids = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for node in state
|
||||
.graphs
|
||||
.architecture
|
||||
.nodes
|
||||
.iter()
|
||||
.chain(state.graphs.dataflow.nodes.iter())
|
||||
{
|
||||
let matches_changed_path = node.code_refs.iter().any(|code_ref| {
|
||||
changed_path_keys.contains(&code_ref_path_key(
|
||||
Path::new(&code_ref.path),
|
||||
workspace_root,
|
||||
))
|
||||
});
|
||||
if matches_changed_path && seen.insert(node.id.as_str()) {
|
||||
node_ids.push(node.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
node_ids
|
||||
}
|
||||
|
||||
fn active_frontier_node_ids_for_changed_paths(
|
||||
state: &EpiphanyThreadState,
|
||||
changed_paths: &[PathBuf],
|
||||
workspace_root: Option<&Path>,
|
||||
) -> Vec<String> {
|
||||
let graph_node_ids = graph_node_ids_for_changed_paths(state, changed_paths, workspace_root);
|
||||
let frontier_node_ids = state
|
||||
.graph_frontier
|
||||
.as_ref()
|
||||
.map(|frontier| frontier.active_node_ids.as_slice())
|
||||
.unwrap_or_default();
|
||||
let graph_node_ids: HashSet<&str> = graph_node_ids.iter().map(String::as_str).collect();
|
||||
|
||||
frontier_node_ids
|
||||
.iter()
|
||||
.filter(|node_id| graph_node_ids.contains(node_id.as_str()))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn code_ref_path_key(path: &Path, workspace_root: Option<&Path>) -> String {
|
||||
if let Some(workspace_root) = workspace_root
|
||||
&& let Ok(relative_path) = path.strip_prefix(workspace_root)
|
||||
{
|
||||
return epiphany_path_key(relative_path);
|
||||
}
|
||||
epiphany_path_key(path)
|
||||
}
|
||||
|
||||
fn epiphany_path_key(path: &Path) -> String {
|
||||
path.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.trim_start_matches("./")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_state_model::EpiphanyCodeRef;
|
||||
use epiphany_state_model::EpiphanyGraphCheckpoint;
|
||||
use epiphany_state_model::EpiphanyGraphFrontier;
|
||||
use epiphany_state_model::EpiphanyGraphNode;
|
||||
use epiphany_state_model::EpiphanyGraphs;
|
||||
|
||||
#[test]
|
||||
fn missing_state_reports_missing_freshness() {
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: None,
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(view.state_revision, None);
|
||||
assert_eq!(
|
||||
view.retrieval.status,
|
||||
EpiphanyRetrievalFreshnessStatus::Missing
|
||||
);
|
||||
assert_eq!(view.graph.status, EpiphanyGraphFreshnessStatus::Missing);
|
||||
assert_eq!(view.watcher.status, EpiphanyInvalidationStatus::Unavailable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_state_is_not_graph_ready() {
|
||||
let state = EpiphanyThreadState::default();
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(view.graph.status, EpiphanyGraphFreshnessStatus::Missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_checkpoint_cannot_authorize_graph_ready() {
|
||||
let state = EpiphanyThreadState {
|
||||
graph_checkpoint: Some(EpiphanyGraphCheckpoint {
|
||||
checkpoint_id: "graph-1".to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(view.graph.status, EpiphanyGraphFreshnessStatus::Missing);
|
||||
assert!(view.graph.note.contains("Canonical RepoModel admission"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_checkpoint_alone_is_missing() {
|
||||
let checkpoint_only = EpiphanyThreadState {
|
||||
graph_checkpoint: Some(EpiphanyGraphCheckpoint {
|
||||
checkpoint_id: "graph-1".to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&checkpoint_only),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(view.graph.status, EpiphanyGraphFreshnessStatus::Missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_pressure_makes_legacy_checkpoint_stale() {
|
||||
let state = EpiphanyThreadState {
|
||||
graph_checkpoint: Some(EpiphanyGraphCheckpoint {
|
||||
checkpoint_id: "graph-1".to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
graph_frontier: Some(EpiphanyGraphFrontier {
|
||||
open_question_ids: vec!["q-1".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(view.graph.status, EpiphanyGraphFreshnessStatus::Stale);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_watcher_without_observation_is_not_clean() {
|
||||
let changed_paths = Vec::new();
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: None,
|
||||
retrieval_override: None,
|
||||
watcher: Some(EpiphanyFreshnessWatcherInput {
|
||||
available: true,
|
||||
workspace_root: None,
|
||||
observed_at_unix_seconds: None,
|
||||
changed_paths: &changed_paths,
|
||||
}),
|
||||
});
|
||||
|
||||
assert_eq!(view.watcher.status, EpiphanyInvalidationStatus::Unavailable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_retrieval_and_frontier_make_graph_stale() {
|
||||
let state = EpiphanyThreadState {
|
||||
revision: 7,
|
||||
retrieval: Some(EpiphanyRetrievalState {
|
||||
workspace_root: PathBuf::from("E:/repo"),
|
||||
status: EpiphanyRetrievalStatus::Stale,
|
||||
semantic_available: true,
|
||||
indexed_file_count: Some(12),
|
||||
dirty_paths: vec![PathBuf::from("src/lib.rs")],
|
||||
..Default::default()
|
||||
}),
|
||||
graph_frontier: Some(EpiphanyGraphFrontier {
|
||||
dirty_paths: vec![PathBuf::from("src/lib.rs")],
|
||||
open_gap_ids: vec!["gap-1".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(view.state_revision, Some(7));
|
||||
assert_eq!(
|
||||
view.retrieval.status,
|
||||
EpiphanyRetrievalFreshnessStatus::Stale
|
||||
);
|
||||
assert_eq!(view.retrieval.indexed_file_count, Some(12));
|
||||
assert_eq!(view.graph.status, EpiphanyGraphFreshnessStatus::Stale);
|
||||
assert_eq!(view.graph.open_gap_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_retrieval_with_dirty_paths_is_stale() {
|
||||
let state = EpiphanyThreadState {
|
||||
retrieval: Some(EpiphanyRetrievalState {
|
||||
workspace_root: PathBuf::from("E:/repo"),
|
||||
status: EpiphanyRetrievalStatus::Ready,
|
||||
dirty_paths: vec![PathBuf::from("src/dirty.rs")],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
view.retrieval.status,
|
||||
EpiphanyRetrievalFreshnessStatus::Stale
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ready_retrieval_cannot_mint_body_coverage() {
|
||||
let state = EpiphanyThreadState {
|
||||
retrieval: Some(EpiphanyRetrievalState {
|
||||
workspace_root: PathBuf::from("E:/repo"),
|
||||
status: EpiphanyRetrievalStatus::Ready,
|
||||
semantic_available: true,
|
||||
dirty_paths: Vec::new(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
view.retrieval.status,
|
||||
EpiphanyRetrievalFreshnessStatus::Missing
|
||||
);
|
||||
assert!(view.retrieval.note.contains("Body-bound coverage receipt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watcher_maps_changed_paths_to_graph_and_frontier() {
|
||||
let state = EpiphanyThreadState {
|
||||
graph_frontier: Some(EpiphanyGraphFrontier {
|
||||
active_node_ids: vec!["node-1".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
graphs: EpiphanyGraphs {
|
||||
architecture: epiphany_state_model::EpiphanyGraph {
|
||||
nodes: vec![EpiphanyGraphNode {
|
||||
id: "node-1".to_string(),
|
||||
title: "node".to_string(),
|
||||
purpose: "module".to_string(),
|
||||
mechanism: None,
|
||||
metaphor: None,
|
||||
status: None,
|
||||
code_refs: vec![EpiphanyCodeRef {
|
||||
path: PathBuf::from("src/lib.rs"),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
symbol: None,
|
||||
note: None,
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let changed_paths = vec![PathBuf::from("./src/lib.rs")];
|
||||
|
||||
let view = derive_freshness(EpiphanyFreshnessInput {
|
||||
state: Some(&state),
|
||||
retrieval_override: None,
|
||||
watcher: Some(EpiphanyFreshnessWatcherInput {
|
||||
available: true,
|
||||
workspace_root: None,
|
||||
observed_at_unix_seconds: Some(42),
|
||||
changed_paths: &changed_paths,
|
||||
}),
|
||||
});
|
||||
|
||||
assert_eq!(view.watcher.status, EpiphanyInvalidationStatus::Changed);
|
||||
assert_eq!(view.watcher.graph_node_ids, vec!["node-1".to_string()]);
|
||||
assert_eq!(
|
||||
view.watcher.active_frontier_node_ids,
|
||||
vec!["node-1".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,760 +0,0 @@
|
|||
use epiphany_state_model::EpiphanyCodeRef;
|
||||
use epiphany_state_model::EpiphanyEvidenceRecord;
|
||||
use epiphany_state_model::EpiphanyGraphCheckpoint;
|
||||
use epiphany_state_model::EpiphanyGraphEdge;
|
||||
use epiphany_state_model::EpiphanyGraphFrontier;
|
||||
use epiphany_state_model::EpiphanyGraphLink;
|
||||
use epiphany_state_model::EpiphanyGraphNode;
|
||||
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
|
||||
use epiphany_state_model::EpiphanyObservation;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EpiphanyContextStateStatus {
|
||||
Missing,
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyGraphContext {
|
||||
pub architecture_nodes: Vec<EpiphanyGraphNode>,
|
||||
pub architecture_edges: Vec<EpiphanyGraphEdge>,
|
||||
pub dataflow_nodes: Vec<EpiphanyGraphNode>,
|
||||
pub dataflow_edges: Vec<EpiphanyGraphEdge>,
|
||||
pub links: Vec<EpiphanyGraphLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyContext {
|
||||
pub graph: EpiphanyGraphContext,
|
||||
pub frontier: Option<EpiphanyGraphFrontier>,
|
||||
pub checkpoint: Option<EpiphanyGraphCheckpoint>,
|
||||
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
|
||||
pub observations: Vec<EpiphanyObservation>,
|
||||
pub evidence: Vec<EpiphanyEvidenceRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyContextMissing {
|
||||
pub graph_node_ids: Vec<String>,
|
||||
pub graph_edge_ids: Vec<String>,
|
||||
pub observation_ids: Vec<String>,
|
||||
pub evidence_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyContextParams {
|
||||
pub graph_node_ids: Vec<String>,
|
||||
pub graph_edge_ids: Vec<String>,
|
||||
pub observation_ids: Vec<String>,
|
||||
pub evidence_ids: Vec<String>,
|
||||
pub include_active_frontier: Option<bool>,
|
||||
pub include_linked_evidence: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EpiphanyContextView {
|
||||
pub state_status: EpiphanyContextStateStatus,
|
||||
pub state_revision: Option<u64>,
|
||||
pub context: EpiphanyContext,
|
||||
pub missing: EpiphanyContextMissing,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EpiphanyGraphQueryKind {
|
||||
Node,
|
||||
Path,
|
||||
FrontierNeighborhood,
|
||||
Neighbors,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EpiphanyGraphQueryDirection {
|
||||
Incoming,
|
||||
Outgoing,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EpiphanyGraphQuery {
|
||||
pub kind: EpiphanyGraphQueryKind,
|
||||
pub node_ids: Vec<String>,
|
||||
pub edge_ids: Vec<String>,
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub symbols: Vec<String>,
|
||||
pub edge_kinds: Vec<String>,
|
||||
pub direction: Option<EpiphanyGraphQueryDirection>,
|
||||
pub depth: Option<u32>,
|
||||
pub include_links: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyGraphQueryMatched {
|
||||
pub node_ids: Vec<String>,
|
||||
pub edge_ids: Vec<String>,
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub symbols: Vec<String>,
|
||||
pub edge_kinds: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyGraphQueryMissing {
|
||||
pub node_ids: Vec<String>,
|
||||
pub edge_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EpiphanyGraphQueryView {
|
||||
pub state_status: EpiphanyContextStateStatus,
|
||||
pub state_revision: Option<u64>,
|
||||
pub graph: EpiphanyGraphContext,
|
||||
pub frontier: Option<EpiphanyGraphFrontier>,
|
||||
pub checkpoint: Option<EpiphanyGraphCheckpoint>,
|
||||
pub matched: EpiphanyGraphQueryMatched,
|
||||
pub missing: EpiphanyGraphQueryMissing,
|
||||
}
|
||||
|
||||
pub fn derive_context(
|
||||
state: Option<&EpiphanyThreadState>,
|
||||
params: &EpiphanyContextParams,
|
||||
) -> EpiphanyContextView {
|
||||
let include_active_frontier = params.include_active_frontier.unwrap_or(true);
|
||||
let include_linked_evidence = params.include_linked_evidence.unwrap_or(true);
|
||||
let Some(state) = state else {
|
||||
return EpiphanyContextView {
|
||||
state_status: EpiphanyContextStateStatus::Missing,
|
||||
state_revision: None,
|
||||
context: EpiphanyContext::default(),
|
||||
missing: EpiphanyContextMissing {
|
||||
graph_node_ids: unique_strings(params.graph_node_ids.iter().cloned()),
|
||||
graph_edge_ids: unique_strings(params.graph_edge_ids.iter().cloned()),
|
||||
observation_ids: unique_strings(params.observation_ids.iter().cloned()),
|
||||
evidence_ids: unique_strings(params.evidence_ids.iter().cloned()),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let mut graph_node_ids = unique_strings(params.graph_node_ids.iter().cloned());
|
||||
let mut graph_edge_ids = unique_strings(params.graph_edge_ids.iter().cloned());
|
||||
if include_active_frontier && let Some(frontier) = state.graph_frontier.as_ref() {
|
||||
extend_unique_strings(
|
||||
&mut graph_node_ids,
|
||||
frontier.active_node_ids.iter().cloned(),
|
||||
);
|
||||
extend_unique_strings(
|
||||
&mut graph_edge_ids,
|
||||
frontier.active_edge_ids.iter().cloned(),
|
||||
);
|
||||
}
|
||||
|
||||
let graph_node_id_set: HashSet<&str> = graph_node_ids.iter().map(String::as_str).collect();
|
||||
let graph_edge_id_set: HashSet<&str> = graph_edge_ids.iter().map(String::as_str).collect();
|
||||
|
||||
let architecture_nodes = state
|
||||
.graphs
|
||||
.architecture
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| graph_node_id_set.contains(node.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let dataflow_nodes = state
|
||||
.graphs
|
||||
.dataflow
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| graph_node_id_set.contains(node.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let found_node_ids: HashSet<String> = architecture_nodes
|
||||
.iter()
|
||||
.chain(dataflow_nodes.iter())
|
||||
.map(|node| node.id.clone())
|
||||
.collect();
|
||||
|
||||
let architecture_edges = select_graph_edges(
|
||||
&state.graphs.architecture.edges,
|
||||
&graph_node_id_set,
|
||||
&graph_edge_id_set,
|
||||
);
|
||||
let dataflow_edges = select_graph_edges(
|
||||
&state.graphs.dataflow.edges,
|
||||
&graph_node_id_set,
|
||||
&graph_edge_id_set,
|
||||
);
|
||||
|
||||
let found_edge_ids: HashSet<String> = architecture_edges
|
||||
.iter()
|
||||
.chain(dataflow_edges.iter())
|
||||
.filter_map(|edge| edge.id.clone())
|
||||
.filter(|id| graph_edge_id_set.contains(id.as_str()))
|
||||
.collect();
|
||||
|
||||
let links = state
|
||||
.graphs
|
||||
.links
|
||||
.iter()
|
||||
.filter(|link| {
|
||||
graph_node_id_set.contains(link.architecture_node_id.as_str())
|
||||
|| graph_node_id_set.contains(link.dataflow_node_id.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let observation_ids = unique_strings(params.observation_ids.iter().cloned());
|
||||
let observation_id_set: HashSet<&str> = observation_ids.iter().map(String::as_str).collect();
|
||||
let observations = state
|
||||
.observations
|
||||
.iter()
|
||||
.filter(|observation| observation_id_set.contains(observation.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let found_observation_ids: HashSet<String> = observations
|
||||
.iter()
|
||||
.map(|observation| observation.id.clone())
|
||||
.collect();
|
||||
|
||||
let mut evidence_ids = unique_strings(params.evidence_ids.iter().cloned());
|
||||
if include_linked_evidence {
|
||||
for observation in &observations {
|
||||
extend_unique_strings(&mut evidence_ids, observation.evidence_ids.iter().cloned());
|
||||
}
|
||||
}
|
||||
let evidence_id_set: HashSet<&str> = evidence_ids.iter().map(String::as_str).collect();
|
||||
let evidence = state
|
||||
.recent_evidence
|
||||
.iter()
|
||||
.filter(|evidence| evidence_id_set.contains(evidence.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let found_evidence_ids: HashSet<String> = evidence
|
||||
.iter()
|
||||
.map(|evidence| evidence.id.clone())
|
||||
.collect();
|
||||
|
||||
EpiphanyContextView {
|
||||
state_status: EpiphanyContextStateStatus::Ready,
|
||||
state_revision: Some(state.revision),
|
||||
context: EpiphanyContext {
|
||||
graph: EpiphanyGraphContext {
|
||||
architecture_nodes,
|
||||
architecture_edges,
|
||||
dataflow_nodes,
|
||||
dataflow_edges,
|
||||
links,
|
||||
},
|
||||
frontier: include_active_frontier
|
||||
.then(|| state.graph_frontier.clone())
|
||||
.flatten(),
|
||||
checkpoint: state.graph_checkpoint.clone(),
|
||||
investigation_checkpoint: state.investigation_checkpoint.clone(),
|
||||
observations,
|
||||
evidence,
|
||||
},
|
||||
missing: EpiphanyContextMissing {
|
||||
graph_node_ids: graph_node_ids
|
||||
.iter()
|
||||
.filter(|id| !found_node_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
graph_edge_ids: graph_edge_ids
|
||||
.iter()
|
||||
.filter(|id| !found_edge_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
observation_ids: observation_ids
|
||||
.iter()
|
||||
.filter(|id| !found_observation_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
evidence_ids: evidence_ids
|
||||
.iter()
|
||||
.filter(|id| !found_evidence_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_graph_query(
|
||||
state: Option<&EpiphanyThreadState>,
|
||||
query: &EpiphanyGraphQuery,
|
||||
) -> EpiphanyGraphQueryView {
|
||||
let Some(state) = state else {
|
||||
return EpiphanyGraphQueryView {
|
||||
state_status: EpiphanyContextStateStatus::Missing,
|
||||
state_revision: None,
|
||||
graph: EpiphanyGraphContext::default(),
|
||||
frontier: None,
|
||||
checkpoint: None,
|
||||
matched: EpiphanyGraphQueryMatched::default(),
|
||||
missing: EpiphanyGraphQueryMissing {
|
||||
node_ids: unique_strings(query.node_ids.iter().cloned()),
|
||||
edge_ids: unique_strings(query.edge_ids.iter().cloned()),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let mut node_ids = unique_strings(query.node_ids.iter().cloned());
|
||||
let mut edge_ids = unique_strings(query.edge_ids.iter().cloned());
|
||||
let mut matched_paths = Vec::new();
|
||||
let mut matched_symbols = Vec::new();
|
||||
let mut matched_edge_kinds = Vec::new();
|
||||
|
||||
match query.kind {
|
||||
EpiphanyGraphQueryKind::Node => {}
|
||||
EpiphanyGraphQueryKind::Path => {
|
||||
collect_graph_query_path_matches(
|
||||
state,
|
||||
query,
|
||||
&mut node_ids,
|
||||
&mut edge_ids,
|
||||
&mut matched_paths,
|
||||
&mut matched_symbols,
|
||||
);
|
||||
}
|
||||
EpiphanyGraphQueryKind::FrontierNeighborhood => {
|
||||
if let Some(frontier) = state.graph_frontier.as_ref() {
|
||||
extend_unique_strings(&mut node_ids, frontier.active_node_ids.iter().cloned());
|
||||
extend_unique_strings(&mut edge_ids, frontier.active_edge_ids.iter().cloned());
|
||||
}
|
||||
expand_graph_query_neighbors(state, query, &mut node_ids, &mut edge_ids);
|
||||
}
|
||||
EpiphanyGraphQueryKind::Neighbors => {
|
||||
expand_graph_query_neighbors(state, query, &mut node_ids, &mut edge_ids);
|
||||
}
|
||||
}
|
||||
|
||||
collect_graph_query_edge_kind_matches(
|
||||
state,
|
||||
query,
|
||||
&mut node_ids,
|
||||
&mut edge_ids,
|
||||
&mut matched_edge_kinds,
|
||||
);
|
||||
|
||||
if query.include_links.unwrap_or(true) {
|
||||
expand_graph_query_links(state, &mut node_ids);
|
||||
}
|
||||
|
||||
let node_id_set: HashSet<&str> = node_ids.iter().map(String::as_str).collect();
|
||||
let edge_id_set: HashSet<&str> = edge_ids.iter().map(String::as_str).collect();
|
||||
|
||||
let architecture_nodes = state
|
||||
.graphs
|
||||
.architecture
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| node_id_set.contains(node.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let dataflow_nodes = state
|
||||
.graphs
|
||||
.dataflow
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| node_id_set.contains(node.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let found_node_ids: HashSet<String> = architecture_nodes
|
||||
.iter()
|
||||
.chain(dataflow_nodes.iter())
|
||||
.map(|node| node.id.clone())
|
||||
.collect();
|
||||
let matched_node_ids = architecture_nodes
|
||||
.iter()
|
||||
.chain(dataflow_nodes.iter())
|
||||
.map(|node| node.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let architecture_edges =
|
||||
select_graph_edges(&state.graphs.architecture.edges, &node_id_set, &edge_id_set);
|
||||
let dataflow_edges =
|
||||
select_graph_edges(&state.graphs.dataflow.edges, &node_id_set, &edge_id_set);
|
||||
let found_edge_ids: HashSet<String> = architecture_edges
|
||||
.iter()
|
||||
.chain(dataflow_edges.iter())
|
||||
.filter_map(|edge| edge.id.clone())
|
||||
.collect();
|
||||
let matched_edge_ids = architecture_edges
|
||||
.iter()
|
||||
.chain(dataflow_edges.iter())
|
||||
.filter_map(|edge| edge.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let links = state
|
||||
.graphs
|
||||
.links
|
||||
.iter()
|
||||
.filter(|link| {
|
||||
node_id_set.contains(link.architecture_node_id.as_str())
|
||||
|| node_id_set.contains(link.dataflow_node_id.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
EpiphanyGraphQueryView {
|
||||
state_status: EpiphanyContextStateStatus::Ready,
|
||||
state_revision: Some(state.revision),
|
||||
graph: EpiphanyGraphContext {
|
||||
architecture_nodes,
|
||||
architecture_edges,
|
||||
dataflow_nodes,
|
||||
dataflow_edges,
|
||||
links,
|
||||
},
|
||||
frontier: state.graph_frontier.clone(),
|
||||
checkpoint: state.graph_checkpoint.clone(),
|
||||
matched: EpiphanyGraphQueryMatched {
|
||||
node_ids: matched_node_ids,
|
||||
edge_ids: matched_edge_ids,
|
||||
paths: matched_paths,
|
||||
symbols: matched_symbols,
|
||||
edge_kinds: matched_edge_kinds,
|
||||
},
|
||||
missing: EpiphanyGraphQueryMissing {
|
||||
node_ids: node_ids
|
||||
.iter()
|
||||
.filter(|id| !found_node_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
edge_ids: edge_ids
|
||||
.iter()
|
||||
.filter(|id| !found_edge_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_graph_query_path_matches(
|
||||
state: &EpiphanyThreadState,
|
||||
query: &EpiphanyGraphQuery,
|
||||
node_ids: &mut Vec<String>,
|
||||
edge_ids: &mut Vec<String>,
|
||||
matched_paths: &mut Vec<PathBuf>,
|
||||
matched_symbols: &mut Vec<String>,
|
||||
) {
|
||||
if query.paths.is_empty() && query.symbols.is_empty() {
|
||||
return;
|
||||
}
|
||||
for node in state
|
||||
.graphs
|
||||
.architecture
|
||||
.nodes
|
||||
.iter()
|
||||
.chain(state.graphs.dataflow.nodes.iter())
|
||||
{
|
||||
if code_refs_match_query(&node.code_refs, query, matched_paths, matched_symbols) {
|
||||
extend_unique_strings(node_ids, [node.id.clone()]);
|
||||
}
|
||||
}
|
||||
for edge in state
|
||||
.graphs
|
||||
.architecture
|
||||
.edges
|
||||
.iter()
|
||||
.chain(state.graphs.dataflow.edges.iter())
|
||||
{
|
||||
if code_refs_match_query(&edge.code_refs, query, matched_paths, matched_symbols) {
|
||||
if let Some(id) = edge.id.clone() {
|
||||
extend_unique_strings(edge_ids, [id]);
|
||||
}
|
||||
extend_unique_strings(node_ids, [edge.source_id.clone(), edge.target_id.clone()]);
|
||||
}
|
||||
}
|
||||
for link in &state.graphs.links {
|
||||
if code_refs_match_query(&link.code_refs, query, matched_paths, matched_symbols) {
|
||||
extend_unique_strings(
|
||||
node_ids,
|
||||
[
|
||||
link.dataflow_node_id.clone(),
|
||||
link.architecture_node_id.clone(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_graph_query_edge_kind_matches(
|
||||
state: &EpiphanyThreadState,
|
||||
query: &EpiphanyGraphQuery,
|
||||
node_ids: &mut Vec<String>,
|
||||
edge_ids: &mut Vec<String>,
|
||||
matched_edge_kinds: &mut Vec<String>,
|
||||
) {
|
||||
if query.edge_kinds.is_empty() {
|
||||
return;
|
||||
}
|
||||
let wanted: HashSet<&str> = query.edge_kinds.iter().map(String::as_str).collect();
|
||||
for edge in state
|
||||
.graphs
|
||||
.architecture
|
||||
.edges
|
||||
.iter()
|
||||
.chain(state.graphs.dataflow.edges.iter())
|
||||
{
|
||||
if wanted.contains(edge.kind.as_str()) {
|
||||
extend_unique_strings(matched_edge_kinds, [edge.kind.clone()]);
|
||||
if let Some(id) = edge.id.clone() {
|
||||
extend_unique_strings(edge_ids, [id]);
|
||||
}
|
||||
extend_unique_strings(node_ids, [edge.source_id.clone(), edge.target_id.clone()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_graph_query_neighbors(
|
||||
state: &EpiphanyThreadState,
|
||||
query: &EpiphanyGraphQuery,
|
||||
node_ids: &mut Vec<String>,
|
||||
edge_ids: &mut Vec<String>,
|
||||
) {
|
||||
let direction = query.direction.unwrap_or(EpiphanyGraphQueryDirection::Both);
|
||||
let depth = query.depth.unwrap_or(1).clamp(1, 3);
|
||||
for _ in 0..depth {
|
||||
let current: HashSet<String> = node_ids.iter().cloned().collect();
|
||||
let before = node_ids.len();
|
||||
for edge in state
|
||||
.graphs
|
||||
.architecture
|
||||
.edges
|
||||
.iter()
|
||||
.chain(state.graphs.dataflow.edges.iter())
|
||||
{
|
||||
let source_selected = current.contains(&edge.source_id);
|
||||
let target_selected = current.contains(&edge.target_id);
|
||||
let include_outgoing = matches!(
|
||||
direction,
|
||||
EpiphanyGraphQueryDirection::Outgoing | EpiphanyGraphQueryDirection::Both
|
||||
) && source_selected;
|
||||
let include_incoming = matches!(
|
||||
direction,
|
||||
EpiphanyGraphQueryDirection::Incoming | EpiphanyGraphQueryDirection::Both
|
||||
) && target_selected;
|
||||
if include_outgoing || include_incoming {
|
||||
if let Some(id) = edge.id.clone() {
|
||||
extend_unique_strings(edge_ids, [id]);
|
||||
}
|
||||
}
|
||||
if include_outgoing {
|
||||
extend_unique_strings(node_ids, [edge.target_id.clone()]);
|
||||
}
|
||||
if include_incoming {
|
||||
extend_unique_strings(node_ids, [edge.source_id.clone()]);
|
||||
}
|
||||
}
|
||||
if node_ids.len() == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_graph_query_links(state: &EpiphanyThreadState, node_ids: &mut Vec<String>) {
|
||||
let current: HashSet<&str> = node_ids.iter().map(String::as_str).collect();
|
||||
let mut linked = Vec::new();
|
||||
for link in &state.graphs.links {
|
||||
if current.contains(link.dataflow_node_id.as_str()) {
|
||||
linked.push(link.architecture_node_id.clone());
|
||||
}
|
||||
if current.contains(link.architecture_node_id.as_str()) {
|
||||
linked.push(link.dataflow_node_id.clone());
|
||||
}
|
||||
}
|
||||
extend_unique_strings(node_ids, linked);
|
||||
}
|
||||
|
||||
fn code_refs_match_query(
|
||||
code_refs: &[EpiphanyCodeRef],
|
||||
query: &EpiphanyGraphQuery,
|
||||
matched_paths: &mut Vec<PathBuf>,
|
||||
matched_symbols: &mut Vec<String>,
|
||||
) -> bool {
|
||||
code_refs.iter().any(|code_ref| {
|
||||
let path_matches = query.paths.is_empty()
|
||||
|| query.paths.iter().any(|path| {
|
||||
code_ref.path == *path
|
||||
|| code_ref.path.ends_with(path)
|
||||
|| path.ends_with(code_ref.path.as_path())
|
||||
});
|
||||
let symbol_matches = query.symbols.is_empty()
|
||||
|| code_ref
|
||||
.symbol
|
||||
.as_deref()
|
||||
.is_some_and(|symbol| query.symbols.iter().any(|wanted| wanted == symbol));
|
||||
if path_matches && symbol_matches {
|
||||
if !query.paths.is_empty() {
|
||||
extend_unique_paths(matched_paths, [code_ref.path.clone()]);
|
||||
}
|
||||
if let Some(symbol) = code_ref.symbol.clone()
|
||||
&& !query.symbols.is_empty()
|
||||
{
|
||||
extend_unique_strings(matched_symbols, [symbol]);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extend_unique_paths(target: &mut Vec<PathBuf>, values: impl IntoIterator<Item = PathBuf>) {
|
||||
for value in values {
|
||||
if !target.iter().any(|existing| existing == &value) {
|
||||
target.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_graph_edges(
|
||||
edges: &[EpiphanyGraphEdge],
|
||||
graph_node_id_set: &HashSet<&str>,
|
||||
graph_edge_id_set: &HashSet<&str>,
|
||||
) -> Vec<EpiphanyGraphEdge> {
|
||||
edges
|
||||
.iter()
|
||||
.filter(|edge| {
|
||||
edge.id
|
||||
.as_deref()
|
||||
.is_some_and(|id| graph_edge_id_set.contains(id))
|
||||
|| graph_node_id_set.contains(edge.source_id.as_str())
|
||||
|| graph_node_id_set.contains(edge.target_id.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unique_strings(values: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||
let mut unique = Vec::new();
|
||||
extend_unique_strings(&mut unique, values);
|
||||
unique
|
||||
}
|
||||
|
||||
fn extend_unique_strings(target: &mut Vec<String>, values: impl IntoIterator<Item = String>) {
|
||||
for value in values {
|
||||
if !target.iter().any(|existing| existing == &value) {
|
||||
target.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn graph_state() -> EpiphanyThreadState {
|
||||
EpiphanyThreadState {
|
||||
revision: 9,
|
||||
graphs: epiphany_state_model::EpiphanyGraphs {
|
||||
architecture: epiphany_state_model::EpiphanyGraph {
|
||||
nodes: vec![
|
||||
EpiphanyGraphNode {
|
||||
id: "a".to_string(),
|
||||
title: "A".to_string(),
|
||||
purpose: "source".to_string(),
|
||||
mechanism: None,
|
||||
metaphor: None,
|
||||
status: None,
|
||||
code_refs: vec![EpiphanyCodeRef {
|
||||
path: PathBuf::from("src/a.rs"),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
symbol: Some("Alpha".to_string()),
|
||||
note: None,
|
||||
}],
|
||||
},
|
||||
EpiphanyGraphNode {
|
||||
id: "b".to_string(),
|
||||
title: "B".to_string(),
|
||||
purpose: "target".to_string(),
|
||||
mechanism: None,
|
||||
metaphor: None,
|
||||
status: None,
|
||||
code_refs: Vec::new(),
|
||||
},
|
||||
],
|
||||
edges: vec![EpiphanyGraphEdge {
|
||||
source_id: "a".to_string(),
|
||||
target_id: "b".to_string(),
|
||||
kind: "calls".to_string(),
|
||||
id: Some("edge-1".to_string()),
|
||||
label: None,
|
||||
mechanism: None,
|
||||
code_refs: Vec::new(),
|
||||
}],
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
graph_frontier: Some(EpiphanyGraphFrontier {
|
||||
active_node_ids: vec!["a".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_includes_active_frontier() {
|
||||
let state = graph_state();
|
||||
let view = derive_context(
|
||||
Some(&state),
|
||||
&EpiphanyContextParams {
|
||||
include_active_frontier: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(view.state_status, EpiphanyContextStateStatus::Ready);
|
||||
assert_eq!(view.state_revision, Some(9));
|
||||
assert_eq!(view.context.graph.architecture_nodes[0].id, "a");
|
||||
assert!(view.missing.graph_node_ids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_query_expands_neighbors() {
|
||||
let state = graph_state();
|
||||
let view = derive_graph_query(
|
||||
Some(&state),
|
||||
&EpiphanyGraphQuery {
|
||||
kind: EpiphanyGraphQueryKind::Neighbors,
|
||||
node_ids: vec!["a".to_string()],
|
||||
edge_ids: Vec::new(),
|
||||
paths: Vec::new(),
|
||||
symbols: Vec::new(),
|
||||
edge_kinds: Vec::new(),
|
||||
direction: Some(EpiphanyGraphQueryDirection::Outgoing),
|
||||
depth: Some(1),
|
||||
include_links: Some(false),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(view.state_status, EpiphanyContextStateStatus::Ready);
|
||||
assert_eq!(view.graph.architecture_nodes.len(), 2);
|
||||
assert_eq!(view.matched.edge_ids, vec!["edge-1".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_query_matches_paths_and_symbols() {
|
||||
let state = graph_state();
|
||||
let view = derive_graph_query(
|
||||
Some(&state),
|
||||
&EpiphanyGraphQuery {
|
||||
kind: EpiphanyGraphQueryKind::Path,
|
||||
node_ids: Vec::new(),
|
||||
edge_ids: Vec::new(),
|
||||
paths: vec![PathBuf::from("src/a.rs")],
|
||||
symbols: vec!["Alpha".to_string()],
|
||||
edge_kinds: Vec::new(),
|
||||
direction: None,
|
||||
depth: None,
|
||||
include_links: Some(false),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(view.matched.node_ids, vec!["a".to_string()]);
|
||||
assert_eq!(view.matched.symbols, vec!["Alpha".to_string()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
mod coordinator_contract;
|
||||
mod crrc;
|
||||
mod freshness;
|
||||
mod graph_context;
|
||||
mod jobs;
|
||||
mod planning;
|
||||
mod pressure;
|
||||
|
|
@ -22,30 +20,6 @@ pub use crrc::EpiphanyCrrcResultStatus;
|
|||
pub use crrc::EpiphanyCrrcSceneAction;
|
||||
pub use crrc::EpiphanyCrrcStateStatus;
|
||||
pub use crrc::recommend_crrc_action;
|
||||
pub use freshness::EpiphanyFreshnessInput;
|
||||
pub use freshness::EpiphanyFreshnessView;
|
||||
pub use freshness::EpiphanyFreshnessWatcherInput;
|
||||
pub use freshness::EpiphanyGraphFreshness;
|
||||
pub use freshness::EpiphanyGraphFreshnessStatus;
|
||||
pub use freshness::EpiphanyInvalidationInput;
|
||||
pub use freshness::EpiphanyInvalidationStatus;
|
||||
pub use freshness::EpiphanyRetrievalFreshness;
|
||||
pub use freshness::EpiphanyRetrievalFreshnessStatus;
|
||||
pub use freshness::derive_freshness;
|
||||
pub use graph_context::EpiphanyContext;
|
||||
pub use graph_context::EpiphanyContextMissing;
|
||||
pub use graph_context::EpiphanyContextParams;
|
||||
pub use graph_context::EpiphanyContextStateStatus;
|
||||
pub use graph_context::EpiphanyContextView;
|
||||
pub use graph_context::EpiphanyGraphContext;
|
||||
pub use graph_context::EpiphanyGraphQuery;
|
||||
pub use graph_context::EpiphanyGraphQueryDirection;
|
||||
pub use graph_context::EpiphanyGraphQueryKind;
|
||||
pub use graph_context::EpiphanyGraphQueryMatched;
|
||||
pub use graph_context::EpiphanyGraphQueryMissing;
|
||||
pub use graph_context::EpiphanyGraphQueryView;
|
||||
pub use graph_context::derive_context;
|
||||
pub use graph_context::derive_graph_query;
|
||||
pub use jobs::EpiphanyJobStatus;
|
||||
pub use jobs::EpiphanyJobView;
|
||||
pub use jobs::EpiphanyJobsInput;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ impl EpiphanyWorkerLaunchDocument {
|
|||
pub struct EpiphanyRoleWorkerLaunchDocument {
|
||||
pub thread_id: String,
|
||||
pub role_id: String,
|
||||
pub state_revision: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub objective: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -1538,39 +1538,13 @@ mod tests {
|
|||
store: &Path,
|
||||
launch: &epiphany_core::EpiphanyRuntimeWorkerLaunchRequest,
|
||||
) -> Result<()> {
|
||||
let request = epiphany_core::EpiphanyJobLaunchRequest {
|
||||
expected_revision: None,
|
||||
binding_id: launch.binding_id.clone(),
|
||||
kind: epiphany_state_model::EpiphanyJobKind::Specialist,
|
||||
scope: launch.authority_scope.clone(),
|
||||
owner_role: launch.role.clone(),
|
||||
authority_scope: launch.authority_scope.clone(),
|
||||
linked_subgoal_ids: Vec::new(),
|
||||
linked_graph_node_ids: Vec::new(),
|
||||
instruction: launch.instruction.clone(),
|
||||
launch_document: launch.launch_document()?,
|
||||
output_contract_id: launch.output_contract_id.clone(),
|
||||
organ_launch_contract: launch.organ_launch_contract.clone(),
|
||||
max_runtime_seconds: None,
|
||||
proposal_modeling_request_id: launch.proposal_modeling_request_id.clone(),
|
||||
frontier_planning_request_id: launch.frontier_planning_request_id.clone(),
|
||||
frontier_plan_mind_request_id: launch.frontier_plan_mind_request_id.clone(),
|
||||
imagination_consideration_request_id: launch
|
||||
.imagination_consideration_request_id
|
||||
.clone(),
|
||||
admitted_model_direction_consideration_request_id: launch
|
||||
.admitted_model_direction_consideration_request_id
|
||||
.clone(),
|
||||
repo_frontier_modeling_request_id: launch.repo_frontier_modeling_request_id.clone(),
|
||||
repo_frontier_research_request_id: launch.repo_frontier_research_request_id.clone(),
|
||||
repo_frontier_verification_request_id: launch
|
||||
.repo_frontier_verification_request_id
|
||||
.clone(),
|
||||
};
|
||||
let grant = epiphany_core::substrate_gate_repo_access_grant_for_launch(
|
||||
let grant = epiphany_core::substrate_gate_repo_access_grant_for_worker(
|
||||
format!("substrate-grant-{}", launch.job_id),
|
||||
launch.job_id.clone(),
|
||||
&request,
|
||||
launch.binding_id.clone(),
|
||||
launch.role.clone(),
|
||||
launch.authority_scope.clone(),
|
||||
launch.binding_id == epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID,
|
||||
now(),
|
||||
);
|
||||
epiphany_core::put_substrate_gate_repo_access_grant_receipt(store, &grant)?;
|
||||
|
|
@ -1598,7 +1572,6 @@ mod tests {
|
|||
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".to_string(),
|
||||
role_id: "verification".to_string(),
|
||||
state_revision: 1,
|
||||
objective: Some("Verify the worker loop.".to_string()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -1711,7 +1684,6 @@ mod tests {
|
|||
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".to_string(),
|
||||
role_id: "verification".to_string(),
|
||||
state_revision: 1,
|
||||
objective: Some("Verify runtime error sealing.".to_string()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -1848,7 +1820,6 @@ mod tests {
|
|||
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".to_string(),
|
||||
role_id: "verification".to_string(),
|
||||
state_revision: 1,
|
||||
objective: Some("Verify the worker loop ceiling.".to_string()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
|
|||
|
|
@ -3008,7 +3008,6 @@ mod tests {
|
|||
EpiphanyWorkerLaunchDocument::Role(epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".into(),
|
||||
role_id: "modeling".into(),
|
||||
state_revision: 1,
|
||||
objective: Some("obsolete aggregate objective must not cross inference".into()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -3418,7 +3417,6 @@ mod tests {
|
|||
EpiphanyWorkerLaunchDocument::Role(epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".into(),
|
||||
role_id: "imagination".into(),
|
||||
state_revision: 1,
|
||||
objective: Some("Plan one frontier.".into()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
|
|
@ -3913,7 +3911,6 @@ mod tests {
|
|||
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".to_string(),
|
||||
role_id: "modeling".to_string(),
|
||||
state_revision: 1,
|
||||
objective: Some("Map the machine.".to_string()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: Some(body_basis.clone()),
|
||||
|
|
@ -4203,7 +4200,6 @@ mod tests {
|
|||
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "thread-1".to_string(),
|
||||
role_id: "verification".to_string(),
|
||||
state_revision: 1,
|
||||
objective: Some("Verify Hands receipts.".to_string()),
|
||||
dynamic_prompt_context: Some(
|
||||
"<verification_work_loop_telemetry>hands receipts</verification_work_loop_telemetry>"
|
||||
|
|
|
|||
|
|
@ -5,79 +5,6 @@ use serde::Deserialize;
|
|||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
mod prompt;
|
||||
|
||||
pub use prompt::render_epiphany_state;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct EpiphanyStateItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub turn_id: Option<String>,
|
||||
pub state: EpiphanyThreadState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyThreadState {
|
||||
pub revision: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub objective: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub active_subgoal_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanySubgoal>")]
|
||||
pub subgoals: Vec<EpiphanySubgoal>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyInvariant>")]
|
||||
pub invariants: Vec<EpiphanyInvariant>,
|
||||
#[serde(default, skip_serializing_if = "EpiphanyGraphs::is_empty")]
|
||||
#[ts(type = "EpiphanyGraphs")]
|
||||
pub graphs: EpiphanyGraphs,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyGraphFrontier | null")]
|
||||
pub graph_frontier: Option<EpiphanyGraphFrontier>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyGraphCheckpoint | null")]
|
||||
pub graph_checkpoint: Option<EpiphanyGraphCheckpoint>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyRetrievalState | null")]
|
||||
pub retrieval: Option<EpiphanyRetrievalState>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyScratchPad | null")]
|
||||
pub scratch: Option<EpiphanyScratchPad>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyInvestigationCheckpoint | null")]
|
||||
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyJobBinding>")]
|
||||
pub job_bindings: Vec<EpiphanyJobBinding>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyAcceptanceReceipt>")]
|
||||
pub acceptance_receipts: Vec<EpiphanyAcceptanceReceipt>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyRuntimeLink>")]
|
||||
pub runtime_links: Vec<EpiphanyRuntimeLink>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyObservation>")]
|
||||
pub observations: Vec<EpiphanyObservation>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyEvidenceRecord>")]
|
||||
pub recent_evidence: Vec<EpiphanyEvidenceRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyChurnState | null")]
|
||||
pub churn: Option<EpiphanyChurnState>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyModeState | null")]
|
||||
pub mode: Option<EpiphanyModeState>,
|
||||
#[serde(default, skip_serializing_if = "EpiphanyPlanningState::is_empty")]
|
||||
#[ts(type = "EpiphanyPlanningState")]
|
||||
pub planning: EpiphanyPlanningState,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub last_updated_turn_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyAcceptanceReceipt {
|
||||
pub id: String,
|
||||
|
|
|
|||
|
|
@ -1,938 +0,0 @@
|
|||
use crate::EpiphanyChurnState;
|
||||
use crate::EpiphanyCodeRef;
|
||||
use crate::EpiphanyEvidenceRecord;
|
||||
use crate::EpiphanyGraph;
|
||||
use crate::EpiphanyGraphCheckpoint;
|
||||
use crate::EpiphanyGraphEdge;
|
||||
use crate::EpiphanyGraphFrontier;
|
||||
use crate::EpiphanyGraphLink;
|
||||
use crate::EpiphanyGraphNode;
|
||||
use crate::EpiphanyInvestigationCheckpoint;
|
||||
use crate::EpiphanyInvestigationDisposition;
|
||||
use crate::EpiphanyModeState;
|
||||
use crate::EpiphanyObservation;
|
||||
use crate::EpiphanyPlanningState;
|
||||
use crate::EpiphanyScratchPad;
|
||||
use crate::EpiphanyThreadState;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
const SUBGOAL_LIMIT: usize = 4;
|
||||
const INVARIANT_LIMIT: usize = 4;
|
||||
const NODE_LIMIT: usize = 3;
|
||||
const EDGE_LIMIT: usize = 2;
|
||||
const LINK_LIMIT: usize = 3;
|
||||
const OBSERVATION_LIMIT: usize = 3;
|
||||
const EVIDENCE_LIMIT: usize = 3;
|
||||
const PLANNING_LIMIT: usize = 3;
|
||||
const CODE_REF_LIMIT: usize = 2;
|
||||
const DIRTY_PATH_LIMIT: usize = 4;
|
||||
const EPIPHANY_STATE_INTRO: &str = include_str!("prompts/epiphany_state_intro.md");
|
||||
const EPIPHANY_DOCTRINE_SECTION: &str = include_str!("prompts/epiphany_doctrine.md");
|
||||
|
||||
pub fn render_epiphany_state(state: &EpiphanyThreadState) -> String {
|
||||
let mut sections = vec![EPIPHANY_STATE_INTRO.trim_end().to_string()];
|
||||
|
||||
sections.push(render_overview(state));
|
||||
sections.push(EPIPHANY_DOCTRINE_SECTION.trim_end().to_string());
|
||||
|
||||
if let Some(planning) = render_planning(&state.planning) {
|
||||
sections.push(planning);
|
||||
}
|
||||
if let Some(subgoals) = render_subgoals(state) {
|
||||
sections.push(subgoals);
|
||||
}
|
||||
if let Some(invariants) = render_invariants(state) {
|
||||
sections.push(invariants);
|
||||
}
|
||||
if let Some(graphs) = render_graphs(state) {
|
||||
sections.push(graphs);
|
||||
}
|
||||
if let Some(checkpoint) =
|
||||
render_investigation_checkpoint(state.investigation_checkpoint.as_ref())
|
||||
{
|
||||
sections.push(checkpoint);
|
||||
}
|
||||
if let Some(scratch) = render_scratch(state.scratch.as_ref()) {
|
||||
sections.push(scratch);
|
||||
}
|
||||
if let Some(observations) = render_observations(&state.observations) {
|
||||
sections.push(observations);
|
||||
}
|
||||
if let Some(evidence) = render_evidence(&state.recent_evidence) {
|
||||
sections.push(evidence);
|
||||
}
|
||||
if let Some(churn) = render_churn(state.churn.as_ref()) {
|
||||
sections.push(churn);
|
||||
}
|
||||
if let Some(mode) = render_mode(state.mode.as_ref()) {
|
||||
sections.push(mode);
|
||||
}
|
||||
|
||||
format!("{}\n", sections.join("\n\n"))
|
||||
}
|
||||
|
||||
fn render_overview(state: &EpiphanyThreadState) -> String {
|
||||
let mut lines = vec![format!("- Revision: {}", state.revision)];
|
||||
if let Some(objective) = state.objective.as_deref() {
|
||||
lines.push(format!("- Objective: {}", compact_text(objective)));
|
||||
}
|
||||
if let Some(active_subgoal_id) = state.active_subgoal_id.as_deref() {
|
||||
lines.push(format!("- Active subgoal: `{active_subgoal_id}`"));
|
||||
}
|
||||
if let Some(last_updated_turn_id) = state.last_updated_turn_id.as_deref() {
|
||||
lines.push(format!("- Last updated turn: `{last_updated_turn_id}`"));
|
||||
}
|
||||
if let Some(frontier) = state.graph_frontier.as_ref() {
|
||||
let focus_ids = collect_focus_node_ids(Some(frontier), state.graph_checkpoint.as_ref());
|
||||
if !focus_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Frontier focus: {}",
|
||||
render_inline_ids(&focus_ids)
|
||||
));
|
||||
}
|
||||
}
|
||||
format!("## Overview\n{}", lines.join("\n"))
|
||||
}
|
||||
|
||||
fn render_subgoals(state: &EpiphanyThreadState) -> Option<String> {
|
||||
if state.subgoals.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for subgoal in state.subgoals.iter().take(SUBGOAL_LIMIT) {
|
||||
let active_suffix = state
|
||||
.active_subgoal_id
|
||||
.as_deref()
|
||||
.filter(|active_id| *active_id == subgoal.id)
|
||||
.map(|_| "; active")
|
||||
.unwrap_or("");
|
||||
lines.push(format!(
|
||||
"- `{}` [{}{}]: {}",
|
||||
subgoal.id,
|
||||
subgoal.status,
|
||||
active_suffix,
|
||||
compact_text(&subgoal.title)
|
||||
));
|
||||
if let Some(summary) = subgoal.summary.as_deref() {
|
||||
lines.push(format!(" summary: {}", compact_text(summary)));
|
||||
}
|
||||
}
|
||||
push_omitted_count(&mut lines, state.subgoals.len(), SUBGOAL_LIMIT, "subgoals");
|
||||
|
||||
Some(format!("## Subgoals\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_planning(planning: &EpiphanyPlanningState) -> Option<String> {
|
||||
if planning.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = vec![format!(
|
||||
"- Counts: {} captures, {} backlog items, {} roadmap streams, {} objective drafts",
|
||||
planning.captures.len(),
|
||||
planning.backlog_items.len(),
|
||||
planning.roadmap_streams.len(),
|
||||
planning.objective_drafts.len()
|
||||
)];
|
||||
lines.push(
|
||||
"- Boundary: planning records are not the active objective until explicit human adoption."
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
for draft in planning.objective_drafts.iter().take(PLANNING_LIMIT) {
|
||||
lines.push(format!(
|
||||
"- Objective draft `{}` [{}]: {}",
|
||||
draft.id,
|
||||
draft.status,
|
||||
compact_text(&draft.title)
|
||||
));
|
||||
if !draft.acceptance_criteria.is_empty() {
|
||||
lines.push(format!(
|
||||
" acceptance: {}",
|
||||
draft
|
||||
.acceptance_criteria
|
||||
.iter()
|
||||
.take(2)
|
||||
.map(|criterion| compact_text(criterion))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
));
|
||||
}
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
planning.objective_drafts.len(),
|
||||
PLANNING_LIMIT,
|
||||
"objective drafts",
|
||||
);
|
||||
|
||||
for item in planning.backlog_items.iter().take(PLANNING_LIMIT) {
|
||||
lines.push(format!(
|
||||
"- Backlog `{}` [{} / {} / {}]: {}",
|
||||
item.id,
|
||||
item.status,
|
||||
item.priority.value,
|
||||
item.horizon,
|
||||
compact_text(&item.title)
|
||||
));
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
planning.backlog_items.len(),
|
||||
PLANNING_LIMIT,
|
||||
"backlog items",
|
||||
);
|
||||
|
||||
for capture in planning.captures.iter().take(PLANNING_LIMIT) {
|
||||
lines.push(format!(
|
||||
"- Capture `{}` [{} via {}]: {}",
|
||||
capture.id,
|
||||
capture.status,
|
||||
capture.source.kind,
|
||||
compact_text(&capture.title)
|
||||
));
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
planning.captures.len(),
|
||||
PLANNING_LIMIT,
|
||||
"captures",
|
||||
);
|
||||
|
||||
Some(format!("## Planning\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_invariants(state: &EpiphanyThreadState) -> Option<String> {
|
||||
if state.invariants.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for invariant in state.invariants.iter().take(INVARIANT_LIMIT) {
|
||||
lines.push(format!(
|
||||
"- `{}` [{}]: {}",
|
||||
invariant.id,
|
||||
invariant.status,
|
||||
compact_text(&invariant.description)
|
||||
));
|
||||
if let Some(rationale) = invariant.rationale.as_deref() {
|
||||
lines.push(format!(" rationale: {}", compact_text(rationale)));
|
||||
}
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
state.invariants.len(),
|
||||
INVARIANT_LIMIT,
|
||||
"invariants",
|
||||
);
|
||||
|
||||
Some(format!("## Invariants\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_graphs(state: &EpiphanyThreadState) -> Option<String> {
|
||||
if state.graphs.is_empty() && state.graph_frontier.is_none() && state.graph_checkpoint.is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let focus_node_ids = collect_focus_node_ids(
|
||||
state.graph_frontier.as_ref(),
|
||||
state.graph_checkpoint.as_ref(),
|
||||
);
|
||||
|
||||
if let Some(frontier) = state.graph_frontier.as_ref() {
|
||||
append_frontier_lines(&mut lines, frontier);
|
||||
}
|
||||
if let Some(checkpoint) = state.graph_checkpoint.as_ref() {
|
||||
append_checkpoint_lines(&mut lines, checkpoint);
|
||||
}
|
||||
|
||||
append_graph_lines(
|
||||
&mut lines,
|
||||
"Architecture graph",
|
||||
&state.graphs.architecture,
|
||||
&focus_node_ids,
|
||||
state.graph_frontier.as_ref(),
|
||||
);
|
||||
append_graph_lines(
|
||||
&mut lines,
|
||||
"Dataflow graph",
|
||||
&state.graphs.dataflow,
|
||||
&focus_node_ids,
|
||||
state.graph_frontier.as_ref(),
|
||||
);
|
||||
|
||||
if !state.graphs.links.is_empty() {
|
||||
for link in select_links(&state.graphs.links, &focus_node_ids)
|
||||
.into_iter()
|
||||
.take(LINK_LIMIT)
|
||||
{
|
||||
let relationship = link
|
||||
.relationship
|
||||
.as_deref()
|
||||
.map(|value| format!(" ({})", compact_text(value)))
|
||||
.unwrap_or_default();
|
||||
let code_refs = render_code_refs(&link.code_refs);
|
||||
lines.push(format!(
|
||||
"- Link: `{}` -> `{}`{}{}",
|
||||
link.dataflow_node_id,
|
||||
link.architecture_node_id,
|
||||
relationship,
|
||||
render_ref_suffix(&code_refs)
|
||||
));
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
state.graphs.links.len(),
|
||||
LINK_LIMIT,
|
||||
"graph links",
|
||||
);
|
||||
}
|
||||
|
||||
Some(format!("## Graphs\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn append_frontier_lines(lines: &mut Vec<String>, frontier: &EpiphanyGraphFrontier) {
|
||||
if !frontier.active_node_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Frontier active nodes: {}",
|
||||
render_inline_ids(&frontier.active_node_ids)
|
||||
));
|
||||
}
|
||||
if !frontier.active_edge_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Frontier active edges: {}",
|
||||
render_inline_ids(&frontier.active_edge_ids)
|
||||
));
|
||||
}
|
||||
if !frontier.open_question_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Open questions: {}",
|
||||
render_inline_ids(&frontier.open_question_ids)
|
||||
));
|
||||
}
|
||||
if !frontier.open_gap_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Open gaps: {}",
|
||||
render_inline_ids(&frontier.open_gap_ids)
|
||||
));
|
||||
}
|
||||
if !frontier.dirty_paths.is_empty() {
|
||||
let dirty_paths = frontier
|
||||
.dirty_paths
|
||||
.iter()
|
||||
.take(DIRTY_PATH_LIMIT)
|
||||
.map(|path| format!("`{}`", normalize_path(path)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let omitted_suffix =
|
||||
omitted_suffix(frontier.dirty_paths.len(), DIRTY_PATH_LIMIT, "dirty paths");
|
||||
lines.push(format!("- Dirty paths: {dirty_paths}{omitted_suffix}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn append_checkpoint_lines(lines: &mut Vec<String>, checkpoint: &EpiphanyGraphCheckpoint) {
|
||||
lines.push(format!("- Checkpoint: `{}`", checkpoint.checkpoint_id));
|
||||
if let Some(summary) = checkpoint.summary.as_deref() {
|
||||
lines.push(format!("- Checkpoint summary: {}", compact_text(summary)));
|
||||
}
|
||||
if !checkpoint.frontier_node_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Checkpoint frontier nodes: {}",
|
||||
render_inline_ids(&checkpoint.frontier_node_ids)
|
||||
));
|
||||
}
|
||||
if !checkpoint.open_question_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Checkpoint open questions: {}",
|
||||
render_inline_ids(&checkpoint.open_question_ids)
|
||||
));
|
||||
}
|
||||
if !checkpoint.open_gap_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Checkpoint open gaps: {}",
|
||||
render_inline_ids(&checkpoint.open_gap_ids)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn append_graph_lines(
|
||||
lines: &mut Vec<String>,
|
||||
label: &str,
|
||||
graph: &EpiphanyGraph,
|
||||
focus_node_ids: &[String],
|
||||
frontier: Option<&EpiphanyGraphFrontier>,
|
||||
) {
|
||||
if graph.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_nodes = select_nodes(graph, focus_node_ids);
|
||||
let selected_node_ids = selected_nodes
|
||||
.iter()
|
||||
.map(|node| node.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let active_edge_ids = frontier
|
||||
.map(|graph_frontier| {
|
||||
graph_frontier
|
||||
.active_edge_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
lines.push(format!(
|
||||
"- {label}: {} nodes, {} edges",
|
||||
graph.nodes.len(),
|
||||
graph.edges.len()
|
||||
));
|
||||
|
||||
for node in &selected_nodes {
|
||||
let status = node
|
||||
.status
|
||||
.as_deref()
|
||||
.map(|value| format!(" [{}]", compact_text(value)))
|
||||
.unwrap_or_default();
|
||||
lines.push(format!(
|
||||
" - Focus node `{}`{}: {}",
|
||||
node.id,
|
||||
status,
|
||||
compact_text(&node.title)
|
||||
));
|
||||
lines.push(format!(" purpose: {}", compact_text(&node.purpose)));
|
||||
if let Some(mechanism) = node.mechanism.as_deref() {
|
||||
lines.push(format!(" mechanism: {}", compact_text(mechanism)));
|
||||
}
|
||||
if let Some(metaphor) = node.metaphor.as_deref() {
|
||||
lines.push(format!(" metaphor: {}", compact_text(metaphor)));
|
||||
}
|
||||
let code_refs = render_code_refs(&node.code_refs);
|
||||
if !code_refs.is_empty() {
|
||||
lines.push(format!(" code refs: {}", code_refs.join(", ")));
|
||||
}
|
||||
}
|
||||
push_omitted_count(lines, graph.nodes.len(), NODE_LIMIT, "graph nodes");
|
||||
|
||||
let selected_edges = select_edges(graph, &selected_node_ids, &active_edge_ids);
|
||||
for edge in selected_edges.iter().take(EDGE_LIMIT) {
|
||||
let edge_id = edge
|
||||
.id
|
||||
.as_deref()
|
||||
.map(|value| format!(" `{value}`"))
|
||||
.unwrap_or_default();
|
||||
let label_suffix = edge
|
||||
.label
|
||||
.as_deref()
|
||||
.map(|value| format!(" [{}]", compact_text(value)))
|
||||
.unwrap_or_default();
|
||||
let mechanism_suffix = edge
|
||||
.mechanism
|
||||
.as_deref()
|
||||
.map(|value| format!("; {}", compact_text(value)))
|
||||
.unwrap_or_default();
|
||||
let code_refs = render_code_refs(&edge.code_refs);
|
||||
lines.push(format!(
|
||||
" - Edge{edge_id}{label_suffix}: `{}` -> `{}` ({}){}{}",
|
||||
edge.source_id,
|
||||
edge.target_id,
|
||||
edge.kind,
|
||||
mechanism_suffix,
|
||||
render_ref_suffix(&code_refs)
|
||||
));
|
||||
}
|
||||
push_omitted_count(
|
||||
lines,
|
||||
selected_edges.len(),
|
||||
EDGE_LIMIT,
|
||||
"focused graph edges",
|
||||
);
|
||||
}
|
||||
|
||||
fn render_scratch(scratch: Option<&EpiphanyScratchPad>) -> Option<String> {
|
||||
let scratch = scratch?;
|
||||
let mut lines = Vec::new();
|
||||
if let Some(summary) = scratch.summary.as_deref() {
|
||||
lines.push(format!("- Summary: {}", compact_text(summary)));
|
||||
}
|
||||
if let Some(hypothesis) = scratch.hypothesis.as_deref() {
|
||||
lines.push(format!("- Hypothesis: {}", compact_text(hypothesis)));
|
||||
}
|
||||
if let Some(next_probe) = scratch.next_probe.as_deref() {
|
||||
lines.push(format!("- Next probe: {}", compact_text(next_probe)));
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("## Scratch\n{}", lines.join("\n")))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_investigation_checkpoint(
|
||||
checkpoint: Option<&EpiphanyInvestigationCheckpoint>,
|
||||
) -> Option<String> {
|
||||
let checkpoint = checkpoint?;
|
||||
let mut lines = vec![
|
||||
format!("- Id: `{}`", checkpoint.checkpoint_id),
|
||||
format!("- Kind: {}", compact_text(&checkpoint.kind)),
|
||||
format!(
|
||||
"- Disposition: {}",
|
||||
match checkpoint.disposition {
|
||||
EpiphanyInvestigationDisposition::ResumeReady => "resume_ready",
|
||||
EpiphanyInvestigationDisposition::RegatherRequired => "regather_required",
|
||||
}
|
||||
),
|
||||
format!("- Focus: {}", compact_text(&checkpoint.focus)),
|
||||
];
|
||||
if let Some(summary) = checkpoint.summary.as_deref() {
|
||||
lines.push(format!("- Summary: {}", compact_text(summary)));
|
||||
}
|
||||
if let Some(next_action) = checkpoint.next_action.as_deref() {
|
||||
lines.push(format!("- Next action: {}", compact_text(next_action)));
|
||||
}
|
||||
if let Some(turn_id) = checkpoint.captured_at_turn_id.as_deref() {
|
||||
lines.push(format!("- Captured at turn: `{turn_id}`"));
|
||||
}
|
||||
if !checkpoint.open_questions.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Open questions: {}",
|
||||
checkpoint
|
||||
.open_questions
|
||||
.iter()
|
||||
.map(|question| compact_text(question))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
));
|
||||
}
|
||||
if !checkpoint.evidence_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Evidence ids: {}",
|
||||
render_inline_ids(&checkpoint.evidence_ids)
|
||||
));
|
||||
}
|
||||
let code_refs = render_code_refs(&checkpoint.code_refs);
|
||||
if !code_refs.is_empty() {
|
||||
lines.push(format!("- Code refs: {}", code_refs.join(", ")));
|
||||
}
|
||||
|
||||
Some(format!("## Investigation Checkpoint\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_observations(observations: &[EpiphanyObservation]) -> Option<String> {
|
||||
if observations.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for observation in observations.iter().take(OBSERVATION_LIMIT) {
|
||||
lines.push(format!(
|
||||
"- `{}` [{} via {}]: {}",
|
||||
observation.id,
|
||||
observation.status,
|
||||
observation.source_kind,
|
||||
compact_text(&observation.summary)
|
||||
));
|
||||
if !observation.evidence_ids.is_empty() {
|
||||
lines.push(format!(
|
||||
" evidence: {}",
|
||||
render_inline_ids(&observation.evidence_ids)
|
||||
));
|
||||
}
|
||||
let code_refs = render_code_refs(&observation.code_refs);
|
||||
if !code_refs.is_empty() {
|
||||
lines.push(format!(" code refs: {}", code_refs.join(", ")));
|
||||
}
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
observations.len(),
|
||||
OBSERVATION_LIMIT,
|
||||
"observations",
|
||||
);
|
||||
|
||||
Some(format!("## Recent Observations\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_evidence(evidence: &[EpiphanyEvidenceRecord]) -> Option<String> {
|
||||
if evidence.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for record in evidence.iter().take(EVIDENCE_LIMIT) {
|
||||
let code_refs = render_code_refs(&record.code_refs);
|
||||
lines.push(format!(
|
||||
"- `{}` [{} / {}]: {}{}",
|
||||
record.id,
|
||||
record.kind,
|
||||
record.status,
|
||||
compact_text(&record.summary),
|
||||
render_ref_suffix(&code_refs)
|
||||
));
|
||||
}
|
||||
push_omitted_count(
|
||||
&mut lines,
|
||||
evidence.len(),
|
||||
EVIDENCE_LIMIT,
|
||||
"evidence records",
|
||||
);
|
||||
|
||||
Some(format!("## Recent Evidence\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_churn(churn: Option<&EpiphanyChurnState>) -> Option<String> {
|
||||
let churn = churn?;
|
||||
let mut lines = vec![
|
||||
format!(
|
||||
"- Understanding status: {}",
|
||||
compact_text(&churn.understanding_status)
|
||||
),
|
||||
format!("- Diff pressure: {}", compact_text(&churn.diff_pressure)),
|
||||
];
|
||||
if let Some(warning) = churn.warning.as_deref() {
|
||||
lines.push(format!("- Warning: {}", compact_text(warning)));
|
||||
}
|
||||
if let Some(unexplained_writes) = churn.unexplained_writes {
|
||||
lines.push(format!("- Unexplained writes: {unexplained_writes}"));
|
||||
}
|
||||
|
||||
Some(format!("## Churn\n{}", lines.join("\n")))
|
||||
}
|
||||
|
||||
fn render_mode(mode: Option<&EpiphanyModeState>) -> Option<String> {
|
||||
let mode = mode?;
|
||||
let mut line = format!("- Name: {}", compact_text(&mode.name));
|
||||
if let Some(kind) = mode.kind {
|
||||
line.push_str(&format!(" ({kind:?})"));
|
||||
}
|
||||
Some(format!("## Mode\n{line}"))
|
||||
}
|
||||
|
||||
fn collect_focus_node_ids(
|
||||
frontier: Option<&EpiphanyGraphFrontier>,
|
||||
checkpoint: Option<&EpiphanyGraphCheckpoint>,
|
||||
) -> Vec<String> {
|
||||
let mut focus_node_ids = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for node_id in frontier
|
||||
.into_iter()
|
||||
.flat_map(|value| value.active_node_ids.iter())
|
||||
.chain(
|
||||
checkpoint
|
||||
.into_iter()
|
||||
.flat_map(|value| value.frontier_node_ids.iter()),
|
||||
)
|
||||
{
|
||||
if seen.insert(node_id.as_str()) {
|
||||
focus_node_ids.push(node_id.clone());
|
||||
}
|
||||
}
|
||||
focus_node_ids
|
||||
}
|
||||
|
||||
fn select_nodes<'a>(
|
||||
graph: &'a EpiphanyGraph,
|
||||
focus_node_ids: &[String],
|
||||
) -> Vec<&'a EpiphanyGraphNode> {
|
||||
let mut selected = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
let focus_set = focus_node_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for node in &graph.nodes {
|
||||
if focus_set.contains(node.id.as_str()) && seen.insert(node.id.as_str()) {
|
||||
selected.push(node);
|
||||
if selected.len() == NODE_LIMIT {
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for node in &graph.nodes {
|
||||
if seen.insert(node.id.as_str()) {
|
||||
selected.push(node);
|
||||
if selected.len() == NODE_LIMIT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected
|
||||
}
|
||||
|
||||
fn select_edges<'a>(
|
||||
graph: &'a EpiphanyGraph,
|
||||
selected_node_ids: &HashSet<&'a str>,
|
||||
active_edge_ids: &HashSet<&str>,
|
||||
) -> Vec<&'a EpiphanyGraphEdge> {
|
||||
graph
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|edge| {
|
||||
edge.id
|
||||
.as_deref()
|
||||
.is_some_and(|edge_id| active_edge_ids.contains(edge_id))
|
||||
|| selected_node_ids.contains(edge.source_id.as_str())
|
||||
|| selected_node_ids.contains(edge.target_id.as_str())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn select_links<'a>(
|
||||
links: &'a [EpiphanyGraphLink],
|
||||
focus_node_ids: &[String],
|
||||
) -> Vec<&'a EpiphanyGraphLink> {
|
||||
let focus_set = focus_node_ids
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<_>>();
|
||||
let mut focused = Vec::new();
|
||||
let mut fallback = Vec::new();
|
||||
for link in links {
|
||||
if focus_set.contains(link.dataflow_node_id.as_str())
|
||||
|| focus_set.contains(link.architecture_node_id.as_str())
|
||||
{
|
||||
focused.push(link);
|
||||
} else {
|
||||
fallback.push(link);
|
||||
}
|
||||
}
|
||||
focused.extend(fallback);
|
||||
focused
|
||||
}
|
||||
|
||||
fn render_code_refs(code_refs: &[EpiphanyCodeRef]) -> Vec<String> {
|
||||
let mut rendered = code_refs
|
||||
.iter()
|
||||
.take(CODE_REF_LIMIT)
|
||||
.map(render_code_ref)
|
||||
.collect::<Vec<_>>();
|
||||
if code_refs.len() > CODE_REF_LIMIT {
|
||||
rendered.push(format!(
|
||||
"... {} more refs",
|
||||
code_refs.len() - CODE_REF_LIMIT
|
||||
));
|
||||
}
|
||||
rendered
|
||||
}
|
||||
|
||||
fn render_code_ref(code_ref: &EpiphanyCodeRef) -> String {
|
||||
let mut location = normalize_path(&code_ref.path);
|
||||
match (code_ref.start_line, code_ref.end_line) {
|
||||
(Some(start_line), Some(end_line)) if end_line != start_line => {
|
||||
location.push_str(&format!(":{start_line}-{end_line}"));
|
||||
}
|
||||
(Some(start_line), _) => {
|
||||
location.push_str(&format!(":{start_line}"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut metadata = Vec::new();
|
||||
if let Some(symbol) = code_ref.symbol.as_deref() {
|
||||
metadata.push(format!("symbol: {}", compact_text(symbol)));
|
||||
}
|
||||
if let Some(note) = code_ref.note.as_deref() {
|
||||
metadata.push(compact_text(note));
|
||||
}
|
||||
|
||||
if metadata.is_empty() {
|
||||
format!("`{location}`")
|
||||
} else {
|
||||
format!("`{location}` ({})", metadata.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_ref_suffix(code_refs: &[String]) -> String {
|
||||
if code_refs.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("; refs: {}", code_refs.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_inline_ids(ids: &[String]) -> String {
|
||||
ids.iter()
|
||||
.map(|id| format!("`{id}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn normalize_path(path: &Path) -> String {
|
||||
path.to_string_lossy().replace('\\', "/")
|
||||
}
|
||||
|
||||
fn compact_text(value: &str) -> String {
|
||||
value.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn omitted_suffix(total: usize, shown: usize, label: &str) -> String {
|
||||
if total > shown {
|
||||
format!("; ... {} more {label} not shown", total - shown)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn push_omitted_count(lines: &mut Vec<String>, total: usize, shown: usize, label: &str) {
|
||||
if total > shown {
|
||||
lines.push(format!("- ... {} more {label} not shown", total - shown));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::render_epiphany_state;
|
||||
use crate::EpiphanyChurnState;
|
||||
use crate::EpiphanyCodeRef;
|
||||
use crate::EpiphanyGraph;
|
||||
use crate::EpiphanyGraphCheckpoint;
|
||||
use crate::EpiphanyGraphEdge;
|
||||
use crate::EpiphanyGraphFrontier;
|
||||
use crate::EpiphanyGraphNode;
|
||||
use crate::EpiphanyGraphs;
|
||||
use crate::EpiphanyInvestigationCheckpoint;
|
||||
use crate::EpiphanyInvestigationDisposition;
|
||||
use crate::EpiphanyModeKind;
|
||||
use crate::EpiphanyModeState;
|
||||
use crate::EpiphanyThreadState;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn render_prefers_frontier_focus_and_omits_empty_sections() {
|
||||
let state = EpiphanyThreadState {
|
||||
revision: 7,
|
||||
objective: Some("Map the actual turn loop.".to_string()),
|
||||
graphs: EpiphanyGraphs {
|
||||
architecture: EpiphanyGraph {
|
||||
nodes: vec![
|
||||
EpiphanyGraphNode {
|
||||
id: "arch-overview".to_string(),
|
||||
title: "Overview".to_string(),
|
||||
purpose: "Loose context nobody is touching.".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
EpiphanyGraphNode {
|
||||
id: "arch-session".to_string(),
|
||||
title: "Session".to_string(),
|
||||
purpose: "Owns prompt assembly.".to_string(),
|
||||
mechanism: Some("Builds the initial context bundle.".to_string()),
|
||||
metaphor: Some("The foreman keeping the queue honest.".to_string()),
|
||||
code_refs: vec![EpiphanyCodeRef {
|
||||
path: PathBuf::from("core/src/session/mod.rs"),
|
||||
start_line: Some(2433),
|
||||
end_line: Some(2617),
|
||||
symbol: Some("build_initial_context".to_string()),
|
||||
note: None,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
EpiphanyGraphNode {
|
||||
id: "arch-rollout".to_string(),
|
||||
title: "Rollout".to_string(),
|
||||
purpose: "Persists thread state.".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
EpiphanyGraphNode {
|
||||
id: "arch-extra".to_string(),
|
||||
title: "Extra".to_string(),
|
||||
purpose: "Should be trimmed.".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
edges: vec![EpiphanyGraphEdge {
|
||||
source_id: "arch-session".to_string(),
|
||||
target_id: "arch-rollout".to_string(),
|
||||
kind: "writes".to_string(),
|
||||
id: Some("edge-session-rollout".to_string()),
|
||||
label: Some("persists".to_string()),
|
||||
mechanism: Some("Writes baseline state after a turn.".to_string()),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
dataflow: EpiphanyGraph {
|
||||
nodes: vec![EpiphanyGraphNode {
|
||||
id: "flow-build-context".to_string(),
|
||||
title: "Build context".to_string(),
|
||||
purpose: "Turns state into prompt context.".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
graph_frontier: Some(EpiphanyGraphFrontier {
|
||||
active_node_ids: vec!["arch-session".to_string(), "flow-build-context".to_string()],
|
||||
active_edge_ids: vec!["edge-session-rollout".to_string()],
|
||||
dirty_paths: vec![PathBuf::from("core/src/session/mod.rs")],
|
||||
..Default::default()
|
||||
}),
|
||||
graph_checkpoint: Some(EpiphanyGraphCheckpoint {
|
||||
checkpoint_id: "cp-7".to_string(),
|
||||
frontier_node_ids: vec!["arch-session".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
|
||||
checkpoint_id: "ix-1".to_string(),
|
||||
kind: "slice_planning".to_string(),
|
||||
disposition: EpiphanyInvestigationDisposition::ResumeReady,
|
||||
focus: "Map the state update seam before broad edits.".to_string(),
|
||||
next_action: Some(
|
||||
"Re-read the write path, then patch one bounded field.".to_string(),
|
||||
),
|
||||
open_questions: vec![
|
||||
"Do scene and context both need the resume packet?".to_string(),
|
||||
],
|
||||
code_refs: vec![EpiphanyCodeRef {
|
||||
path: PathBuf::from("core/src/codex_thread.rs"),
|
||||
start_line: Some(376),
|
||||
end_line: Some(424),
|
||||
symbol: Some("epiphany_update_state".to_string()),
|
||||
note: Some("Checkpoint writes must use the same red pen.".to_string()),
|
||||
}],
|
||||
evidence_ids: vec!["ev-1".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
churn: Some(EpiphanyChurnState {
|
||||
understanding_status: "stable".to_string(),
|
||||
diff_pressure: "low".to_string(),
|
||||
warning: None,
|
||||
unexplained_writes: Some(0),
|
||||
}),
|
||||
mode: Some(EpiphanyModeState {
|
||||
name: "epiphany".to_string(),
|
||||
kind: Some(EpiphanyModeKind::Default),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let rendered = render_epiphany_state(&state);
|
||||
|
||||
assert!(rendered.contains("`arch-session`"));
|
||||
assert!(rendered.contains("`flow-build-context`"));
|
||||
assert!(rendered.contains("## Epiphany Doctrine"));
|
||||
assert!(rendered.contains("become the Perfect Machine"));
|
||||
assert!(rendered.contains("Persistent state is the mind"));
|
||||
assert!(rendered.contains("Imagination makes futures more selectable"));
|
||||
assert!(rendered.contains("evidence is a distilled durable ledger"));
|
||||
assert!(rendered.contains("modeling is Modeling"));
|
||||
assert!(rendered.contains("the Body is the substrate being sensed"));
|
||||
assert!(rendered.contains("implementation is the Hands"));
|
||||
assert!(rendered.contains("research is the Eyes"));
|
||||
assert!(rendered.contains("Implement user-specified algorithms first"));
|
||||
assert!(rendered.contains("bank scratch/map/evidence before the dark"));
|
||||
assert!(rendered.contains("## Investigation Checkpoint"));
|
||||
assert!(rendered.contains("resume_ready"));
|
||||
assert!(rendered.contains("Map the state update seam before broad edits."));
|
||||
assert!(!rendered.contains("## Scratch"));
|
||||
assert!(!rendered.contains("## Invariants"));
|
||||
assert!(rendered.contains("`core/src/session/mod.rs:2433-2617`"));
|
||||
assert!(rendered.contains("... 1 more graph nodes not shown"));
|
||||
}
|
||||
}
|
||||
Reference in a new issue