From 954103ec4db040d9cd954dac5dcdac421baaf51a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 12 May 2026 21:58:52 +0100 Subject: [PATCH] Extract Epiphany graph context into core --- epiphany-core/src/lib.rs | 17 +- epiphany-core/src/surfaces/graph_context.rs | 760 ++++++++++++++++++ epiphany-core/src/surfaces/mod.rs | 30 +- notes/epiphany-current-algorithmic-map.md | 17 +- ...piphany-ideal-architecture-rebuild-plan.md | 15 +- notes/fresh-workspace-handoff.md | 11 +- state/ledgers.msgpack | Bin 66292 -> 66742 bytes state/map.yaml | 8 +- .../app-server/src/codex_message_processor.rs | 637 ++++----------- 9 files changed, 967 insertions(+), 528 deletions(-) create mode 100644 epiphany-core/src/surfaces/graph_context.rs diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index dcc0ec81..ad0c0188 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -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; diff --git a/epiphany-core/src/surfaces/graph_context.rs b/epiphany-core/src/surfaces/graph_context.rs new file mode 100644 index 00000000..13235a5e --- /dev/null +++ b/epiphany-core/src/surfaces/graph_context.rs @@ -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, + pub architecture_edges: Vec, + pub dataflow_nodes: Vec, + pub dataflow_edges: Vec, + pub links: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct EpiphanyContext { + pub graph: EpiphanyGraphContext, + pub frontier: Option, + pub checkpoint: Option, + pub investigation_checkpoint: Option, + pub observations: Vec, + pub evidence: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct EpiphanyContextMissing { + pub graph_node_ids: Vec, + pub graph_edge_ids: Vec, + pub observation_ids: Vec, + pub evidence_ids: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct EpiphanyContextParams { + pub graph_node_ids: Vec, + pub graph_edge_ids: Vec, + pub observation_ids: Vec, + pub evidence_ids: Vec, + pub include_active_frontier: Option, + pub include_linked_evidence: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EpiphanyContextView { + pub state_status: EpiphanyContextStateStatus, + pub state_revision: Option, + 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, + pub edge_ids: Vec, + pub paths: Vec, + pub symbols: Vec, + pub edge_kinds: Vec, + pub direction: Option, + pub depth: Option, + pub include_links: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct EpiphanyGraphQueryMatched { + pub node_ids: Vec, + pub edge_ids: Vec, + pub paths: Vec, + pub symbols: Vec, + pub edge_kinds: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct EpiphanyGraphQueryMissing { + pub node_ids: Vec, + pub edge_ids: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EpiphanyGraphQueryView { + pub state_status: EpiphanyContextStateStatus, + pub state_revision: Option, + pub graph: EpiphanyGraphContext, + pub frontier: Option, + pub checkpoint: Option, + 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::>(); + let dataflow_nodes = state + .graphs + .dataflow + .nodes + .iter() + .filter(|node| graph_node_id_set.contains(node.id.as_str())) + .cloned() + .collect::>(); + + let found_node_ids: HashSet = 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 = 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::>(); + let found_observation_ids: HashSet = 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::>(); + let found_evidence_ids: HashSet = 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::>(); + let dataflow_nodes = state + .graphs + .dataflow + .nodes + .iter() + .filter(|node| node_id_set.contains(node.id.as_str())) + .cloned() + .collect::>(); + let found_node_ids: HashSet = 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::>(); + + 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 = 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::>(); + + 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::>(); + + 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, + edge_ids: &mut Vec, + matched_paths: &mut Vec, + matched_symbols: &mut Vec, +) { + 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, + edge_ids: &mut Vec, + matched_edge_kinds: &mut Vec, +) { + 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, + edge_ids: &mut Vec, +) { + 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 = 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) { + 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, + matched_symbols: &mut Vec, +) -> 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, values: impl IntoIterator) { + 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 { + 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) -> Vec { + let mut unique = Vec::new(); + extend_unique_strings(&mut unique, values); + unique +} + +fn extend_unique_strings(target: &mut Vec, values: impl IntoIterator) { + 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()]); + } +} diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index a2cc4016..bbfc32f7 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -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, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EpiphanyViewLens { - Pressure, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct EpiphanyViewOutput { - pub pressure: Option, -} diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index a01aa05f..93e00f06 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -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. diff --git a/notes/epiphany-ideal-architecture-rebuild-plan.md b/notes/epiphany-ideal-architecture-rebuild-plan.md index 5e335065..ed89e5f2 100644 --- a/notes/epiphany-ideal-architecture-rebuild-plan.md +++ b/notes/epiphany-ideal-architecture-rebuild-plan.md @@ -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. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 700b6b77..dada2c88 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 4e736b406037db347f7e94a2b0eaf2b7043b9308..c899a59b896ab2d42d89e37b69ea06a4a6868881 100644 GIT binary patch delta 330 zcmY+AKTE_w5XISWS_oEFHs{=ll{tgODcIQxVy9puo9tXNT(TQyH{lLM3$d^hg5GDa zw9)21gdfJ%KQU3za)vi=-tWELbe?ZJck5o~;%VbSh{x->c$o{)7x#f!Ss3o_4R@;j zlg<9+p(#;8WVFMH1F03nEb{~$%M#hjwCa5Z+nLNQgr?A{7B_+bOH!yK1e-`!e=g4p zuKy7-kdD%#-KC_*wfV2D54m(nV5eDV(g0PA zgE7_VGU=iOtq&S0PQw4b-|;iDmUKKI1&q1rv8$&oI7X%sHK>d&!dXjQ9lftD{}d23 OY_nHSbQhbFPQL)18h3gC delta 49 zcmdni$?~O@Wr8vj)2@xG(-;|7H1A>DzK4-fiBYU!i;;nmnXZAUuAxzgk%5(ov6Z1& F6ab-04yXVC diff --git a/state/map.yaml b/state/map.yaml index 6783d9dc..2d761394 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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. diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index 581afd41..b5d2300f 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -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::>(); - let dataflow_nodes = state - .graphs - .dataflow - .nodes - .iter() - .filter(|node| graph_node_id_set.contains(node.id.as_str())) - .cloned() - .collect::>(); - - let found_node_ids: HashSet = 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 = 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::>(); - let found_observation_ids: HashSet = 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::>(); - let found_evidence_ids: HashSet = 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, + 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 = Vec::new(); - let mut matched_symbols: Vec = Vec::new(); - let mut matched_edge_kinds: Vec = 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::>(); - let dataflow_nodes = state - .graphs - .dataflow - .nodes - .iter() - .filter(|node| node_id_set.contains(node.id.as_str())) - .cloned() - .collect::>(); - let found_node_ids: HashSet = 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::>(); - - 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 = 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::>(); - - 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::>(); +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, + ThreadEpiphanyGraphContext, + Option, + Option, + 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, - edge_ids: &mut Vec, - matched_paths: &mut Vec, - matched_symbols: &mut Vec, -) { - 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, - edge_ids: &mut Vec, - matched_edge_kinds: &mut Vec, -) { - 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, - edge_ids: &mut Vec, -) { - 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 = 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) { - 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, - matched_symbols: &mut Vec, -) -> 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, values: impl IntoIterator) { - 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 { - 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) -> Vec {