Extract Epiphany graph context into core
This commit is contained in:
parent
8ad676017a
commit
954103ec4d
9 changed files with 959 additions and 520 deletions
|
|
@ -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;
|
||||
|
|
|
|||
760
epiphany-core/src/surfaces/graph_context.rs
Normal file
760
epiphany-core/src/surfaces/graph_context.rs
Normal 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()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
Reference in a new issue