Extract Epiphany graph context into core

This commit is contained in:
Meta Vens 2026-05-12 21:58:52 +01:00
commit 954103ec4d
9 changed files with 959 additions and 520 deletions

View file

@ -132,6 +132,11 @@ pub use state_ledger::close_state_branch;
pub use state_ledger::load_state_ledger;
pub use state_ledger::state_ledger_status;
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;
@ -153,8 +158,15 @@ 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;
@ -203,14 +215,13 @@ pub use surfaces::EpiphanySceneSource;
pub use surfaces::EpiphanySceneStateStatus;
pub use surfaces::EpiphanySceneStatusCount;
pub use surfaces::EpiphanySceneSubgoal;
pub use surfaces::EpiphanyViewInput;
pub use surfaces::EpiphanyViewLens;
pub use surfaces::EpiphanyViewOutput;
pub use surfaces::build_reorient_acceptance_bundle;
pub use surfaces::build_role_acceptance_bundle;
pub use surfaces::coordinator_automation_action;
pub use surfaces::crrc_scene_action_to_coordinator_scene_action;
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;

View file

@ -0,0 +1,760 @@
use codex_protocol::protocol::EpiphanyCodeRef;
use codex_protocol::protocol::EpiphanyEvidenceRecord;
use codex_protocol::protocol::EpiphanyGraphCheckpoint;
use codex_protocol::protocol::EpiphanyGraphEdge;
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphLink;
use codex_protocol::protocol::EpiphanyGraphNode;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyObservation;
use codex_protocol::protocol::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: codex_protocol::protocol::EpiphanyGraphs {
architecture: codex_protocol::protocol::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()]);
}
}

View file

@ -1,6 +1,7 @@
mod coordinator;
mod crrc;
mod freshness;
mod graph_context;
mod jobs;
mod planning;
mod pressure;
@ -42,6 +43,20 @@ 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;
@ -103,18 +118,3 @@ pub use scene::EpiphanySceneStateStatus;
pub use scene::EpiphanySceneStatusCount;
pub use scene::EpiphanySceneSubgoal;
pub use scene::derive_scene;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpiphanyViewInput {
pub pressure: Option<EpiphanyPressure>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EpiphanyViewLens {
Pressure,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EpiphanyViewOutput {
pub pressure: Option<EpiphanyPressure>,
}

View file

@ -1177,6 +1177,7 @@ Code refs:
- [app-server-protocol/common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350)
- [app-server-protocol/v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4182)
- [epiphany-core/src/surfaces/graph_context.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/graph_context.rs:104)
- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4186)
- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10791)
@ -1187,10 +1188,12 @@ The app-server handler:
1. parses the thread id.
2. checks whether the thread is loaded so the response can report live/stored source.
3. calls `read_thread_view(..., include_turns: false)` to reuse the live/stored Epiphany projection path.
4. passes the optional `thread.epiphany_state` and selectors into `map_epiphany_context`.
5. returns the derived context shard and missing-id lists.
4. maps protocol selectors into `epiphany-core::EpiphanyContextParams`.
5. calls `epiphany-core::derive_context`.
6. maps the core context view back into the protocol response.
7. returns the derived context shard and missing-id lists.
`map_epiphany_context`:
`epiphany-core::derive_context`:
1. returns missing state with an empty context and echoes requested ids when no Epiphany state exists.
2. defaults `includeActiveFrontier` to true, so a client can ask for the current working slice with only a thread id.
@ -1261,10 +1264,12 @@ The app-server handler:
1. parses the thread id.
2. requires a loaded thread.
3. calls `read_thread_view(..., include_turns: false)` to reuse the live Epiphany projection path.
4. passes the optional `thread.epiphany_state` and query into `map_epiphany_graph_query`.
5. returns the selected graph records, current frontier/checkpoint, matched selectors, and missing explicit ids.
4. maps the protocol query into `epiphany-core::EpiphanyGraphQuery`.
5. calls `epiphany-core::derive_graph_query`.
6. maps the core graph query view back into the protocol response.
7. returns the selected graph records, current frontier/checkpoint, matched selectors, and missing explicit ids.
`map_epiphany_graph_query`:
`epiphany-core::derive_graph_query`:
1. returns missing state with an empty graph and echoes requested node/edge ids when no Epiphany state exists.
2. starts from explicit node/edge ids.

View file

@ -337,15 +337,16 @@ Commit shape:
Purpose: stop fresh surface sprawl while the foundation is being rebuilt.
### 2. Introduce Core View Types
### 2. Introduce Core Surface Modules
Add `epiphany-core/src/surfaces/` with domain-level view input/output structs:
Add `epiphany-core/src/surfaces/` with domain-level surface contracts for the
live lenses and read models:
- `EpiphanyViewInput`
- `EpiphanyViewLens`
- `EpiphanyViewOutput`
- shared source snapshots for durable state, runtime state, retrieval summary,
watcher snapshot, and token pressure
- pressure, freshness, jobs, planning, scene, role board, reorient, CRRC, and
coordinator view derivation
- targeted graph/evidence context and bounded graph query derivation
- role and reorient result acceptance bundle policy
- typed inputs that carry only the source snapshots each surface actually needs
Do not move behavior yet.

View file

@ -32,7 +32,7 @@ remembers doctrine; the branch remembers the blade.
## Current Orientation
- Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`.
- Top priority is still foundation cleanup, not outward feature work. Read `notes/epiphany-architectural-teardown.md` before touching Epiphany control-plane code. The May 2026 suspicion pass found that the app-server Epiphany control plane had become architectural Jenga: too much policy, projection, lifecycle glue, prompt packet assembly, coordinator sequencing, and test authority lives in `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs`. The rebuild has begun: scene projection, freshness derivation, pressure policy, job/progress view derivation, planning view derivation, reorientation resume/regather verdict policy, pure CRRC recommendation policy, fixed-lane coordinator decision policy, role-board projection policy, role-result interpretation/self-persistence review policy, and role/reorient acceptance bundle policy moved into `epiphany-core`, typed acceptance receipts replaced summary-string identity for live accept paths, runtime links are dual-written on launch, and result read-back now prefers runtime links.
- Top priority is still foundation cleanup until the documented rebuild audit is closed. Read `notes/epiphany-architectural-teardown.md` before touching Epiphany control-plane code. The May 2026 suspicion pass found that the app-server Epiphany control plane had become architectural Jenga: too much policy, projection, lifecycle glue, prompt packet assembly, coordinator sequencing, and test authority lives in `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs`. The rebuild has moved scene projection, freshness derivation, pressure policy, targeted context shards, bounded graph traversal, job/progress view derivation, planning view derivation, reorientation resume/regather verdict policy, pure CRRC recommendation policy, fixed-lane coordinator decision policy, role-board projection policy, role-result interpretation/self-persistence review policy, and role/reorient acceptance bundle policy into `epiphany-core`; typed acceptance receipts replaced summary-string identity for live accept paths, runtime links are dual-written on launch, and result read-back now prefers runtime links.
- Read `notes/epiphany-ideal-architecture-rebuild-plan.md` immediately after the teardown. It defines the smallest coherent replacement architecture: durable `EpiphanyState`, runtime-spine-owned `RuntimeState`, separate role memory/heartbeat state, `epiphany-core` as policy owner, app-server as adapter, Aquarium as reflector, typed acceptance receipts, and derived view lenses instead of a protocol verb zoo.
- Do not continue Aquarium UI, bridge, Face, or dogfood expansion until the teardown has a source-grounded cleanup slice plan. Epiphany is the foundation; patches on patches are not a purification rite, they are how the altar becomes load-bearing garbage.
- Phase 1 through Phase 5 are complete enough.
@ -362,22 +362,23 @@ that change what the next agent should believe.
Do not continue implementation automatically from a rehydrate-only request.
The next real move is still foundation cleanup. Start from
The next real move is still the final foundation cleanup audit. Start from
`notes/epiphany-architectural-teardown.md` and
`notes/epiphany-ideal-architecture-rebuild-plan.md`, not from the older outward
Phase 6 bridge/UI trail. Landed rebuild cuts: `epiphany-core/src/surfaces/`
exists, pressure view derivation/tests, pure CRRC recommendation policy/tests,
fixed-lane coordinator decision policy/tests, role-board projection policy/tests,
role-result interpretation/self-persistence review policy/tests, and role/reorient
acceptance bundle policy/tests, scene projection/tests, freshness derivation/tests, planning view derivation/tests, reorientation resume/regather verdict policy/tests, and job/progress view derivation/tests live in core, live acceptance writes typed receipts keyed by runtime result id,
acceptance bundle policy/tests, scene projection/tests, freshness derivation/tests, targeted context and graph traversal derivation/tests, planning view derivation/tests, reorientation resume/regather verdict policy/tests, and job/progress view derivation/tests live in core, live acceptance writes typed receipts keyed by runtime result id,
summary-string acceptance identity is gone for live paths, launches dual-write
runtime links, result read-back prefers those runtime links, normal public
`raw_result` is sealed, MVP consumers use `thread/epiphany/view`, heartbeat
cognition is quarantined, and the old scene/jobs/roles/planning/pressure/reorient/
CRRC/coordinator read wrappers are deleted, and durable `jobBindings` no longer
carry launcher/backend/progress lifecycle fields. The next pass is to audit the
plan's remaining success criteria and cut whatever still makes app-server more
than routing/adaptation. No Agile confetti. No new balcony on the swaying tower.
plan's remaining success criteria and, if no remaining policy cluster is found,
mark the rebuild migration complete enough to defend. No Agile confetti. No new
balcony on the swaying tower.
The Phase 6 freshness slice is landed. It exposes read-only
`thread/epiphany/freshness` from live retrieval summaries plus graph

Binary file not shown.

View file

@ -113,8 +113,8 @@ accepted_design:
- The `jobs` view lens is a read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from authoritative state and retrieval summaries, then projects heartbeat-backed bindings as pending heartbeat turns. It is not the scheduler, notification stream, durable runtime job table, or SQLite reader.
- `thread/epiphany/jobsUpdated` is sealed legacy Codex evacuation telemetry. The handler no longer emits Epiphany updates from old runtime `agent_job_progress:{json}` background events; heartbeat/runtime-spine is the live activation and result path.
- `thread/epiphany/freshness` is the first read-only freshness surface: it derives retrieval and graph staleness from live retrieval summaries plus frontier/churn state, and for loaded threads reflects watcher-backed invalidation inputs with changed paths plus mapped graph/frontier hits, without mutating, scheduling, or performing automatic semantic invalidation.
- `thread/epiphany/context` is the first targeted read-only state-shard surface: it selects graph nodes/edges, active frontier, checkpoint, observations, and evidence from authoritative typed state without mutating or notifying.
- `thread/epiphany/graphQuery` is the first read-only graph traversal surface over authoritative typed state: it returns explicit nodes, path/symbol matches, bounded neighbors, and frontier neighborhoods with architecture/dataflow links without mutating, notifying, indexing, scheduling, proposing, or promoting.
- `thread/epiphany/context` is the targeted read-only state-shard surface: `epiphany-core` selects graph nodes/edges, active frontier, checkpoint, observations, and evidence from authoritative typed state while app-server only adapts protocol, without mutating or notifying.
- `thread/epiphany/graphQuery` is the read-only graph traversal surface over authoritative typed state: `epiphany-core` returns explicit nodes, path/symbol matches, bounded neighbors, and frontier neighborhoods with architecture/dataflow links while app-server only adapts protocol, without mutating, notifying, indexing, scheduling, proposing, or promoting.
- The `pressure` view lens derives context pressure from current context usage, not cumulative token spend, against auto-compact/context limits without compacting, scheduling, mutating, or notifying; compaction prep starts at 80% of the active limit.
- The `reorient` view lens derives a bounded read-only `resume` versus `regather` verdict from checkpoint, freshness, watcher, and pressure signals without mutating state, compacting, scheduling, or silently continuing work.
- The `crrc` view lens composes pressure, the reorientation verdict, fixed reorient-worker binding/result status, finding read-back, and available actions into one read-only recommendation without launching, accepting, compacting, scheduling, or mutating state.
@ -272,5 +272,5 @@ rejected_paths:
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
current_status:
summary: Phase 1 durable state through Phase 6 coordinator/specialist/CRRC/backend organs are landed, and the May 2026 rebuild has started cutting the app-server control-plane tumor. Scene projection, freshness derivation, pressure, job/progress view derivation, planning view derivation, reorientation resume/regather verdicts, CRRC, coordinator, role-board, role-result interpretation/self-persistence review, and role/reorient acceptance bundle policy now live in `epiphany-core`; live duplicate-acceptance checks use runtime result receipt identity instead of summary strings; launch writes durable `runtime_links`; role/reorient result read-back follows runtime links; and `jobBindings` no longer store launcher/backend/progress lifecycle fields.
next_action: Continue the teardown/rebuild migration from `notes/epiphany-ideal-architecture-rebuild-plan.md`. Landed cuts now shrink `jobBindings`, remove normal public `raw_result`, add the canonical `thread/epiphany/view` lens wrapper, migrate MVP consumers, quarantine heartbeat cognition blobs, delete the old scene/jobs/roles/planning/pressure/reorient/CRRC/coordinator read wrappers, and move scene projection, freshness derivation, pure CRRC, fixed-lane coordinator decision policy, job/progress view derivation, planning view derivation, reorientation resume/regather verdict policy, role-board projection policy, role-result interpretation/self-persistence review policy, and role/reorient acceptance bundle policy into `epiphany-core`. The next pass is to audit the plan's remaining success criteria and cut whatever still makes app-server more than routing/adaptation. Do not resume outward Aquarium/bridge/Face/dogfood expansion until this foundation cleanup is complete enough to defend.
summary: Phase 1 durable state through Phase 6 coordinator/specialist/CRRC/backend organs are landed, and the May 2026 rebuild has cut the main app-server control-plane tumor down to routing/adaptation for the documented MVP surfaces. Scene projection, freshness derivation, pressure, targeted context, bounded graph traversal, job/progress view derivation, planning view derivation, reorientation resume/regather verdicts, CRRC, coordinator, role-board, role-result interpretation/self-persistence review, and role/reorient acceptance bundle policy now live in `epiphany-core`; live duplicate-acceptance checks use runtime result receipt identity instead of summary strings; launch writes durable `runtime_links`; role/reorient result read-back follows runtime links; and `jobBindings` no longer store launcher/backend/progress lifecycle fields.
next_action: Audit the remaining success criteria in `notes/epiphany-ideal-architecture-rebuild-plan.md` after the context/graph extraction commit. If no app-server-owned Epiphany policy cluster remains in the documented migration scope, mark the rebuild migration complete enough to defend, commit the persistence update, and only then resume outward Aquarium/bridge/Face/dogfood expansion.

View file

@ -493,6 +493,11 @@ use codex_thread_store::ThreadStoreError;
use codex_thread_store::UpdateThreadMetadataParams as StoreUpdateThreadMetadataParams;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use epiphany_core::EpiphanyContext as CoreEpiphanyContext;
use epiphany_core::EpiphanyContextMissing as CoreEpiphanyContextMissing;
use epiphany_core::EpiphanyContextParams as CoreEpiphanyContextParams;
use epiphany_core::EpiphanyContextStateStatus as CoreEpiphanyContextStateStatus;
use epiphany_core::EpiphanyContextView;
use epiphany_core::EpiphanyCoordinatorAction as CoreEpiphanyCoordinatorAction;
use epiphany_core::EpiphanyCoordinatorAutomationAction as CoreEpiphanyCoordinatorAutomationAction;
use epiphany_core::EpiphanyCoordinatorCrrcRecommendation;
@ -514,8 +519,15 @@ use epiphany_core::EpiphanyCrrcStateStatus as CoreEpiphanyCrrcStateStatus;
use epiphany_core::EpiphanyFreshnessInput;
use epiphany_core::EpiphanyFreshnessView;
use epiphany_core::EpiphanyFreshnessWatcherInput;
use epiphany_core::EpiphanyGraphContext as CoreEpiphanyGraphContext;
use epiphany_core::EpiphanyGraphFreshness as CoreEpiphanyGraphFreshness;
use epiphany_core::EpiphanyGraphFreshnessStatus as CoreEpiphanyGraphFreshnessStatus;
use epiphany_core::EpiphanyGraphQuery as CoreEpiphanyGraphQuery;
use epiphany_core::EpiphanyGraphQueryDirection as CoreEpiphanyGraphQueryDirection;
use epiphany_core::EpiphanyGraphQueryKind as CoreEpiphanyGraphQueryKind;
use epiphany_core::EpiphanyGraphQueryMatched as CoreEpiphanyGraphQueryMatched;
use epiphany_core::EpiphanyGraphQueryMissing as CoreEpiphanyGraphQueryMissing;
use epiphany_core::EpiphanyGraphQueryView;
use epiphany_core::EpiphanyInvalidationInput as CoreEpiphanyInvalidationInput;
use epiphany_core::EpiphanyInvalidationStatus as CoreEpiphanyInvalidationStatus;
use epiphany_core::EpiphanyJobStatus as CoreEpiphanyJobStatus;
@ -568,7 +580,9 @@ use epiphany_core::EpiphanySceneSubgoal as CoreEpiphanySceneSubgoal;
use epiphany_core::build_reorient_acceptance_bundle;
use epiphany_core::build_role_acceptance_bundle;
use epiphany_core::coordinator_automation_action;
use epiphany_core::derive_context;
use epiphany_core::derive_freshness;
use epiphany_core::derive_graph_query;
use epiphany_core::derive_jobs;
use epiphany_core::derive_planning_view;
use epiphany_core::derive_pressure_view;
@ -15363,167 +15377,76 @@ fn map_epiphany_context(
ThreadEpiphanyContext,
ThreadEpiphanyContextMissing,
) {
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 (
ThreadEpiphanyContextStateStatus::Missing,
None,
ThreadEpiphanyContext::default(),
ThreadEpiphanyContextMissing {
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 {
if 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_epiphany_graph_edges(
&state.graphs.architecture.edges,
&graph_node_id_set,
&graph_edge_id_set,
);
let dataflow_edges = select_epiphany_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();
map_core_epiphany_context_view(derive_context(
state,
&CoreEpiphanyContextParams {
graph_node_ids: params.graph_node_ids.clone(),
graph_edge_ids: params.graph_edge_ids.clone(),
observation_ids: params.observation_ids.clone(),
evidence_ids: params.evidence_ids.clone(),
include_active_frontier: params.include_active_frontier,
include_linked_evidence: params.include_linked_evidence,
},
))
}
fn map_core_epiphany_context_view(
view: EpiphanyContextView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyContext,
ThreadEpiphanyContextMissing,
) {
(
ThreadEpiphanyContextStateStatus::Ready,
Some(state.revision),
ThreadEpiphanyContext {
graph: ThreadEpiphanyGraphContext {
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,
},
ThreadEpiphanyContextMissing {
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(),
},
map_core_epiphany_context_state_status(view.state_status),
view.state_revision,
map_core_epiphany_context(view.context),
map_core_epiphany_context_missing(view.missing),
)
}
fn map_core_epiphany_context_state_status(
status: CoreEpiphanyContextStateStatus,
) -> ThreadEpiphanyContextStateStatus {
match status {
CoreEpiphanyContextStateStatus::Missing => ThreadEpiphanyContextStateStatus::Missing,
CoreEpiphanyContextStateStatus::Ready => ThreadEpiphanyContextStateStatus::Ready,
}
}
fn map_core_epiphany_context(context: CoreEpiphanyContext) -> ThreadEpiphanyContext {
ThreadEpiphanyContext {
graph: map_core_epiphany_graph_context(context.graph),
frontier: context.frontier,
checkpoint: context.checkpoint,
investigation_checkpoint: context.investigation_checkpoint,
observations: context.observations,
evidence: context.evidence,
}
}
fn map_core_epiphany_graph_context(graph: CoreEpiphanyGraphContext) -> ThreadEpiphanyGraphContext {
ThreadEpiphanyGraphContext {
architecture_nodes: graph.architecture_nodes,
architecture_edges: graph.architecture_edges,
dataflow_nodes: graph.dataflow_nodes,
dataflow_edges: graph.dataflow_edges,
links: graph.links,
}
}
fn map_core_epiphany_context_missing(
missing: CoreEpiphanyContextMissing,
) -> ThreadEpiphanyContextMissing {
ThreadEpiphanyContextMissing {
graph_node_ids: missing.graph_node_ids,
graph_edge_ids: missing.graph_edge_ids,
observation_ids: missing.observation_ids,
evidence_ids: missing.evidence_ids,
}
}
fn map_epiphany_planning(
state: Option<&EpiphanyThreadState>,
) -> (
@ -15584,352 +15507,90 @@ fn map_epiphany_graph_query(
ThreadEpiphanyGraphQueryMatched,
ThreadEpiphanyGraphQueryMissing,
) {
let Some(state) = state else {
return (
ThreadEpiphanyContextStateStatus::Missing,
None,
ThreadEpiphanyGraphContext::default(),
None,
None,
ThreadEpiphanyGraphQueryMatched::default(),
ThreadEpiphanyGraphQueryMissing {
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<PathBuf> = Vec::new();
let mut matched_symbols: Vec<String> = Vec::new();
let mut matched_edge_kinds: Vec<String> = Vec::new();
match query.kind {
ThreadEpiphanyGraphQueryKind::Node => {}
ThreadEpiphanyGraphQueryKind::Path => {
collect_graph_query_path_matches(
state,
query,
&mut node_ids,
&mut edge_ids,
&mut matched_paths,
&mut matched_symbols,
);
}
ThreadEpiphanyGraphQueryKind::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);
}
ThreadEpiphanyGraphQueryKind::Neighbors => {
expand_graph_query_neighbors(state, query, &mut node_ids, &mut edge_ids);
}
}
collect_graph_query_edge_kind_matches(
map_core_epiphany_graph_query_view(derive_graph_query(
state,
query,
&mut node_ids,
&mut edge_ids,
&mut matched_edge_kinds,
);
&map_core_epiphany_graph_query(query),
))
}
if query.include_links.unwrap_or(true) {
expand_graph_query_links(state, &mut node_ids);
fn map_core_epiphany_graph_query(query: &ThreadEpiphanyGraphQuery) -> CoreEpiphanyGraphQuery {
CoreEpiphanyGraphQuery {
kind: map_core_epiphany_graph_query_kind(query.kind),
node_ids: query.node_ids.clone(),
edge_ids: query.edge_ids.clone(),
paths: query.paths.clone(),
symbols: query.symbols.clone(),
edge_kinds: query.edge_kinds.clone(),
direction: query.direction.map(map_core_epiphany_graph_query_direction),
depth: query.depth,
include_links: query.include_links,
}
}
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();
fn map_core_epiphany_graph_query_kind(
kind: ThreadEpiphanyGraphQueryKind,
) -> CoreEpiphanyGraphQueryKind {
match kind {
ThreadEpiphanyGraphQueryKind::Node => CoreEpiphanyGraphQueryKind::Node,
ThreadEpiphanyGraphQueryKind::Path => CoreEpiphanyGraphQueryKind::Path,
ThreadEpiphanyGraphQueryKind::FrontierNeighborhood => {
CoreEpiphanyGraphQueryKind::FrontierNeighborhood
}
ThreadEpiphanyGraphQueryKind::Neighbors => CoreEpiphanyGraphQueryKind::Neighbors,
}
}
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_epiphany_graph_edges(&state.graphs.architecture.edges, &node_id_set, &edge_id_set);
let dataflow_edges =
select_epiphany_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<_>>();
fn map_core_epiphany_graph_query_direction(
direction: ThreadEpiphanyGraphQueryDirection,
) -> CoreEpiphanyGraphQueryDirection {
match direction {
ThreadEpiphanyGraphQueryDirection::Incoming => CoreEpiphanyGraphQueryDirection::Incoming,
ThreadEpiphanyGraphQueryDirection::Outgoing => CoreEpiphanyGraphQueryDirection::Outgoing,
ThreadEpiphanyGraphQueryDirection::Both => CoreEpiphanyGraphQueryDirection::Both,
}
}
fn map_core_epiphany_graph_query_view(
view: EpiphanyGraphQueryView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyGraphContext,
Option<codex_protocol::protocol::EpiphanyGraphFrontier>,
Option<codex_protocol::protocol::EpiphanyGraphCheckpoint>,
ThreadEpiphanyGraphQueryMatched,
ThreadEpiphanyGraphQueryMissing,
) {
(
ThreadEpiphanyContextStateStatus::Ready,
Some(state.revision),
ThreadEpiphanyGraphContext {
architecture_nodes,
architecture_edges,
dataflow_nodes,
dataflow_edges,
links,
},
state.graph_frontier.clone(),
state.graph_checkpoint.clone(),
ThreadEpiphanyGraphQueryMatched {
node_ids: matched_node_ids,
edge_ids: matched_edge_ids,
paths: matched_paths,
symbols: matched_symbols,
edge_kinds: matched_edge_kinds,
},
ThreadEpiphanyGraphQueryMissing {
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(),
},
map_core_epiphany_context_state_status(view.state_status),
view.state_revision,
map_core_epiphany_graph_context(view.graph),
view.frontier,
view.checkpoint,
map_core_epiphany_graph_query_matched(view.matched),
map_core_epiphany_graph_query_missing(view.missing),
)
}
fn collect_graph_query_path_matches(
state: &EpiphanyThreadState,
query: &ThreadEpiphanyGraphQuery,
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 map_core_epiphany_graph_query_matched(
matched: CoreEpiphanyGraphQueryMatched,
) -> ThreadEpiphanyGraphQueryMatched {
ThreadEpiphanyGraphQueryMatched {
node_ids: matched.node_ids,
edge_ids: matched.edge_ids,
paths: matched.paths,
symbols: matched.symbols,
edge_kinds: matched.edge_kinds,
}
}
fn collect_graph_query_edge_kind_matches(
state: &EpiphanyThreadState,
query: &ThreadEpiphanyGraphQuery,
node_ids: &mut Vec<String>,
edge_ids: &mut Vec<String>,
matched_edge_kinds: &mut Vec<String>,
) {
if query.edge_kinds.is_empty() {
return;
fn map_core_epiphany_graph_query_missing(
missing: CoreEpiphanyGraphQueryMissing,
) -> ThreadEpiphanyGraphQueryMissing {
ThreadEpiphanyGraphQueryMissing {
node_ids: missing.node_ids,
edge_ids: missing.edge_ids,
}
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: &ThreadEpiphanyGraphQuery,
node_ids: &mut Vec<String>,
edge_ids: &mut Vec<String>,
) {
let direction = query
.direction
.unwrap_or(ThreadEpiphanyGraphQueryDirection::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,
ThreadEpiphanyGraphQueryDirection::Outgoing
| ThreadEpiphanyGraphQueryDirection::Both
) && source_selected;
let include_incoming = matches!(
direction,
ThreadEpiphanyGraphQueryDirection::Incoming
| ThreadEpiphanyGraphQueryDirection::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: &[codex_protocol::protocol::EpiphanyCodeRef],
query: &ThreadEpiphanyGraphQuery,
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_epiphany_graph_edges(
edges: &[codex_protocol::protocol::EpiphanyGraphEdge],
graph_node_id_set: &HashSet<&str>,
graph_edge_id_set: &HashSet<&str>,
) -> Vec<codex_protocol::protocol::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> {