This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
Epiphany/epiphany-core/src/proposal.rs
2026-07-15 20:21:38 +01:00

1727 lines
59 KiB
Rust

use anyhow::Result;
use anyhow::anyhow;
use epiphany_state_model::EpiphanyChurnState;
use epiphany_state_model::EpiphanyCodeRef;
use epiphany_state_model::EpiphanyEvidenceRecord;
use epiphany_state_model::EpiphanyGraphEdge;
use epiphany_state_model::EpiphanyGraphFrontier;
use epiphany_state_model::EpiphanyGraphNode;
use epiphany_state_model::EpiphanyGraphs;
use epiphany_state_model::EpiphanyObservation;
use epiphany_state_model::EpiphanyThreadState;
use sha1::Digest;
use sha1::Sha1;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
const SUMMARY_LIMIT: usize = 220;
const SEMANTIC_REUSE_MIN_SCORE: usize = 4;
const AUTO_OBSERVATION_SELECTION_LIMIT: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
struct SelectionQuality {
observation_count: usize,
evidence_count: usize,
source_kinds: Vec<String>,
priority_label: String,
primary_observation_id: String,
primary_observation_summary: String,
semantic_terms: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ArchitectureMatchKind {
ExactCodeRef,
SamePath,
DeterministicId,
Semantic,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ArchitectureMatch {
index: usize,
kind: ArchitectureMatchKind,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct MapDeltaJudgment {
exact_ref_reuses: usize,
same_path_reuses: usize,
deterministic_id_reuses: usize,
semantic_reuses: usize,
created_nodes: usize,
touched_paths: usize,
selected_observations: usize,
existing_unexplained_writes: bool,
}
impl MapDeltaJudgment {
fn reused_nodes(&self) -> usize {
self.exact_ref_reuses
+ self.same_path_reuses
+ self.deterministic_id_reuses
+ self.semantic_reuses
}
fn record_match(&mut self, kind: ArchitectureMatchKind) {
match kind {
ArchitectureMatchKind::ExactCodeRef => self.exact_ref_reuses += 1,
ArchitectureMatchKind::SamePath => self.same_path_reuses += 1,
ArchitectureMatchKind::DeterministicId => self.deterministic_id_reuses += 1,
ArchitectureMatchKind::Semantic => self.semantic_reuses += 1,
}
}
fn has_broadening_reuse(&self) -> bool {
self.same_path_reuses > 0 || self.semantic_reuses > 0
}
fn has_mixed_delta(&self) -> bool {
self.reused_nodes() > 0 && self.created_nodes > 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ObservationSelectionCandidate {
observation_id: String,
score: usize,
index: usize,
path_keys: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct ProposalFocus {
dirty_paths: HashSet<String>,
active_paths: HashSet<String>,
mapped_paths: HashSet<String>,
}
#[derive(Debug, Clone)]
pub struct EpiphanyMapProposalInput {
pub state: EpiphanyThreadState,
pub observation_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpiphanyMapProposal {
pub observation: EpiphanyObservation,
pub evidence: EpiphanyEvidenceRecord,
pub graphs: EpiphanyGraphs,
pub graph_frontier: EpiphanyGraphFrontier,
pub churn: EpiphanyChurnState,
}
pub fn propose_map_update(input: EpiphanyMapProposalInput) -> Result<EpiphanyMapProposal> {
let observation_ids = proposal_observation_ids(&input.state, input.observation_ids)?;
let observations = select_observations(&input.state, &observation_ids)?;
let selection_quality = evaluate_selection_quality(&input.state, &observations)?;
let code_refs = collect_code_refs(&observations)?;
let code_ref_paths = unique_code_ref_paths(&code_refs);
let fingerprint = fingerprint(&input.state.revision, &observation_ids, &code_refs);
let mut graphs = input.state.graphs.clone();
let mut active_node_ids = Vec::new();
let mut delta = MapDeltaJudgment {
touched_paths: code_ref_paths.len(),
selected_observations: selection_quality.observation_count,
existing_unexplained_writes: input
.state
.churn
.as_ref()
.and_then(|churn| churn.unexplained_writes)
.is_some_and(|unexplained_writes| unexplained_writes > 0),
..Default::default()
};
for path in &code_ref_paths {
let path_code_refs = code_refs_for_path(&code_refs, path);
let candidate_node_id = graph_node_id(path);
if let Some(node_match) = find_architecture_node_for_path(
&graphs,
&path_code_refs,
path,
&candidate_node_id,
&selection_quality.semantic_terms,
) {
let node = &mut graphs.architecture.nodes[node_match.index];
active_node_ids.push(node.id.clone());
merge_code_refs(&mut node.code_refs, path_code_refs);
delta.record_match(node_match.kind);
} else {
let node_id = candidate_node_id;
active_node_ids.push(node_id.clone());
graphs.architecture.nodes.push(EpiphanyGraphNode {
id: node_id,
title: title_from_path(path),
purpose: truncate_chars(
&format!(
"Candidate implementation surface from verified observation: {}",
selection_quality.primary_observation_summary
),
SUMMARY_LIMIT,
),
mechanism: Some(format!(
"Proposed from prioritized observation {:?} and selected observation ids: {}",
selection_quality.primary_observation_id,
observation_ids.join(", "),
)),
metaphor: None,
status: Some("candidate".to_string()),
code_refs: path_code_refs,
});
delta.created_nodes += 1;
}
}
let mut frontier = input.state.graph_frontier.clone().unwrap_or_default();
let frontier_node_ids = linked_frontier_node_ids(&graphs, unique_strings(active_node_ids));
let frontier_edge_ids = incident_edge_ids(&graphs, &frontier_node_ids);
merge_unique(&mut frontier.active_node_ids, frontier_node_ids);
merge_unique(&mut frontier.active_edge_ids, frontier_edge_ids);
merge_unique(&mut frontier.dirty_paths, code_ref_paths.clone());
let evidence_id = format!("ev-map-proposal-{fingerprint}");
let selection_summary = selection_quality_summary(&selection_quality, code_ref_paths.len());
let observation = EpiphanyObservation {
id: format!("obs-map-proposal-{fingerprint}"),
summary: truncate_chars(
&format!(
"Map/churn proposal from {}: {}",
selection_summary,
observation_ids.join(", "),
),
SUMMARY_LIMIT,
),
source_kind: "epiphany-map-proposal".to_string(),
status: "candidate".to_string(),
code_refs: code_refs.clone(),
evidence_ids: vec![evidence_id.clone()],
};
let evidence = EpiphanyEvidenceRecord {
id: evidence_id,
kind: "proposal".to_string(),
status: "candidate".to_string(),
summary: truncate_chars(
&format!(
"Proposed graph frontier and churn update from {}: {}; reused {reused_nodes} existing node(s), {semantic_reused_nodes} by semantic graph signal, created {created_nodes} new node(s)",
selection_summary,
observation_ids.join(", "),
reused_nodes = delta.reused_nodes(),
semantic_reused_nodes = delta.semantic_reuses,
created_nodes = delta.created_nodes,
),
SUMMARY_LIMIT,
),
code_refs,
};
let understanding_status = proposal_understanding_status(&delta);
let diff_pressure = proposal_diff_pressure(&delta, input.state.churn.as_ref());
let churn = EpiphanyChurnState {
understanding_status,
diff_pressure,
warning: Some(format!(
"Map/churn proposal derived from {selection_summary}; {}; promote only after verifier acceptance.",
proposal_delta_summary(&delta)
)),
unexplained_writes: input
.state
.churn
.as_ref()
.and_then(|churn| churn.unexplained_writes),
};
Ok(EpiphanyMapProposal {
observation,
evidence,
graphs,
graph_frontier: frontier,
churn,
})
}
fn proposal_observation_ids(
state: &EpiphanyThreadState,
observation_ids: Vec<String>,
) -> Result<Vec<String>> {
let normalized = normalize_observation_ids(observation_ids);
if normalized.is_empty() {
select_automatic_observation_ids(state)
} else {
Ok(normalized)
}
}
fn normalize_observation_ids(observation_ids: Vec<String>) -> Vec<String> {
let mut normalized = Vec::new();
let mut seen = HashSet::new();
for id in observation_ids {
let id = id.trim();
if id.is_empty() {
continue;
}
if seen.insert(id.to_string()) {
normalized.push(id.to_string());
}
}
normalized
}
fn select_automatic_observation_ids(state: &EpiphanyThreadState) -> Result<Vec<String>> {
let focus = proposal_focus(state);
let mut candidates = Vec::new();
for (index, observation) in state.observations.iter().enumerate() {
if !is_verified_status(&observation.status) || observation.code_refs.is_empty() {
continue;
}
let Some(evidence_score) = accepting_evidence_score(state, observation) else {
continue;
};
let path_keys = observation_path_keys(observation);
if path_keys.is_empty() {
continue;
}
let score = observation_priority_score(observation)
+ evidence_score
+ observation_focus_score(&path_keys, &focus);
candidates.push(ObservationSelectionCandidate {
observation_id: observation.id.clone(),
score,
index,
path_keys,
});
}
candidates.sort_by(|left, right| {
right
.score
.cmp(&left.score)
.then_with(|| left.index.cmp(&right.index))
.then_with(|| left.observation_id.cmp(&right.observation_id))
});
let Some(anchor) = candidates.first() else {
return Err(anyhow!(
"observationIds was omitted and no proposal-ready observations were found"
));
};
let anchor_path_keys = anchor.path_keys.iter().cloned().collect::<HashSet<_>>();
let mut selected = candidates
.iter()
.filter(|candidate| {
candidate
.path_keys
.iter()
.any(|path_key| anchor_path_keys.contains(path_key))
})
.take(AUTO_OBSERVATION_SELECTION_LIMIT)
.map(|candidate| (candidate.index, candidate.observation_id.clone()))
.collect::<Vec<_>>();
selected.sort_by_key(|(index, _)| *index);
Ok(selected
.into_iter()
.map(|(_, observation_id)| observation_id)
.collect())
}
fn accepting_evidence_score(
state: &EpiphanyThreadState,
observation: &EpiphanyObservation,
) -> Option<usize> {
if observation.evidence_ids.is_empty() {
return None;
}
let mut score = 0usize;
for evidence_id in &observation.evidence_ids {
let evidence = state
.recent_evidence
.iter()
.find(|evidence| evidence.id == *evidence_id)?;
if !is_verified_status(&evidence.status) {
return None;
}
score += evidence_priority_score(evidence);
}
Some(score)
}
fn proposal_focus(state: &EpiphanyThreadState) -> ProposalFocus {
let mut focus = ProposalFocus::default();
if let Some(frontier) = state.graph_frontier.as_ref() {
for dirty_path in &frontier.dirty_paths {
focus.dirty_paths.insert(path_key(dirty_path));
}
for active_node_id in &frontier.active_node_ids {
collect_active_node_paths(&mut focus.active_paths, &state.graphs, active_node_id);
}
}
collect_mapped_paths(&mut focus.mapped_paths, &state.graphs.architecture.nodes);
collect_mapped_paths(&mut focus.mapped_paths, &state.graphs.dataflow.nodes);
for link in &state.graphs.links {
collect_code_ref_paths(&mut focus.mapped_paths, &link.code_refs);
}
focus
}
fn collect_active_node_paths(
target: &mut HashSet<String>,
graphs: &EpiphanyGraphs,
active_node_id: &str,
) {
for node in graphs
.architecture
.nodes
.iter()
.chain(graphs.dataflow.nodes.iter())
{
if node.id == active_node_id {
collect_code_ref_paths(target, &node.code_refs);
}
}
for link in &graphs.links {
if link.architecture_node_id == active_node_id || link.dataflow_node_id == active_node_id {
collect_code_ref_paths(target, &link.code_refs);
}
}
}
fn collect_mapped_paths(target: &mut HashSet<String>, nodes: &[EpiphanyGraphNode]) {
for node in nodes {
collect_code_ref_paths(target, &node.code_refs);
}
}
fn collect_code_ref_paths(target: &mut HashSet<String>, code_refs: &[EpiphanyCodeRef]) {
for code_ref in code_refs {
target.insert(path_key(&code_ref.path));
}
}
fn observation_path_keys(observation: &EpiphanyObservation) -> Vec<String> {
let mut path_keys = Vec::new();
let mut seen = HashSet::new();
for code_ref in &observation.code_refs {
let key = path_key(&code_ref.path);
if seen.insert(key.clone()) {
path_keys.push(key);
}
}
path_keys
}
fn observation_focus_score(path_keys: &[String], focus: &ProposalFocus) -> usize {
let mut score = 0usize;
for path_key in path_keys {
if focus.dirty_paths.contains(path_key) {
score += 4;
}
if focus.active_paths.contains(path_key) {
score += 3;
}
if focus.mapped_paths.contains(path_key) {
score += 2;
}
}
score
}
fn select_observations<'a>(
state: &'a EpiphanyThreadState,
observation_ids: &[String],
) -> Result<Vec<&'a EpiphanyObservation>> {
let mut selected = Vec::new();
for observation_id in observation_ids {
let observation = state
.observations
.iter()
.find(|observation| observation.id == *observation_id)
.ok_or_else(|| anyhow!("observation id {observation_id:?} not found in state"))?;
if !is_verified_status(&observation.status) {
return Err(anyhow!(
"observation id {observation_id:?} must have verified/accepted status, got {:?}",
observation.status
));
}
selected.push(observation);
}
Ok(selected)
}
fn evaluate_selection_quality(
state: &EpiphanyThreadState,
observations: &[&EpiphanyObservation],
) -> Result<SelectionQuality> {
let mut evidence_ids = Vec::new();
let mut seen_evidence_ids = HashSet::new();
let mut source_kinds = Vec::new();
let mut seen_source_kinds = HashSet::new();
let mut semantic_terms = Vec::new();
let mut seen_semantic_terms = HashSet::new();
let mut primary_observation_id = String::new();
let mut primary_observation_summary = String::new();
let mut primary_score = 0usize;
let mut strong_signal_count = 0usize;
for observation in observations {
if observation.evidence_ids.is_empty() {
return Err(anyhow!(
"observation id {:?} must cite at least one evidence record",
observation.id
));
}
let mut has_accepting_evidence = false;
let mut observation_score = observation_priority_score(observation);
push_semantic_terms(
&mut semantic_terms,
&mut seen_semantic_terms,
&observation.summary,
);
push_semantic_terms(
&mut semantic_terms,
&mut seen_semantic_terms,
&observation.source_kind,
);
push_code_ref_semantic_terms(
&mut semantic_terms,
&mut seen_semantic_terms,
&observation.code_refs,
);
for evidence_id in &observation.evidence_ids {
let evidence = state
.recent_evidence
.iter()
.find(|evidence| evidence.id == *evidence_id)
.ok_or_else(|| {
anyhow!(
"observation id {:?} cites missing evidence id {:?}",
observation.id,
evidence_id
)
})?;
if !is_verified_status(&evidence.status) {
return Err(anyhow!(
"observation id {:?} cites non-accepting evidence id {:?} with status {:?}",
observation.id,
evidence_id,
evidence.status
));
}
has_accepting_evidence = true;
observation_score += evidence_priority_score(evidence);
if seen_evidence_ids.insert(evidence.id.clone()) {
evidence_ids.push(evidence.id.clone());
}
push_semantic_terms(
&mut semantic_terms,
&mut seen_semantic_terms,
&evidence.summary,
);
push_semantic_terms(
&mut semantic_terms,
&mut seen_semantic_terms,
&evidence.kind,
);
push_code_ref_semantic_terms(
&mut semantic_terms,
&mut seen_semantic_terms,
&evidence.code_refs,
);
}
if !has_accepting_evidence {
return Err(anyhow!(
"observation id {:?} must cite at least one accepting evidence record",
observation.id
));
}
let source_kind = observation.source_kind.trim();
if !source_kind.is_empty() && seen_source_kinds.insert(source_kind.to_string()) {
source_kinds.push(source_kind.to_string());
}
if observation_score >= 8 {
strong_signal_count += 1;
}
if primary_observation_id.is_empty() || observation_score > primary_score {
primary_observation_id = observation.id.clone();
primary_observation_summary = observation.summary.clone();
primary_score = observation_score;
}
}
Ok(SelectionQuality {
observation_count: observations.len(),
evidence_count: evidence_ids.len(),
source_kinds,
priority_label: selection_priority_label(strong_signal_count, observations.len()),
primary_observation_id,
primary_observation_summary,
semantic_terms,
})
}
fn observation_priority_score(observation: &EpiphanyObservation) -> usize {
signal_kind_weight(&observation.source_kind)
+ observation.code_refs.len().min(3)
+ usize::from(!observation.summary.trim().is_empty())
}
fn evidence_priority_score(evidence: &EpiphanyEvidenceRecord) -> usize {
signal_kind_weight(&evidence.kind)
+ evidence.code_refs.len().min(2)
+ usize::from(!evidence.summary.trim().is_empty())
}
fn signal_kind_weight(value: &str) -> usize {
let lower = value.trim().to_ascii_lowercase();
if lower.contains("verification") || lower.contains("verifier") {
4
} else if lower.contains("test") || lower.contains("smoke") {
3
} else if lower.contains("tool") || lower.contains("review") {
2
} else if lower.contains("observation") {
1
} else {
0
}
}
fn selection_priority_label(strong_signal_count: usize, observation_count: usize) -> String {
if strong_signal_count == observation_count {
"high-priority verified selection".to_string()
} else if strong_signal_count > 0 {
"mixed-priority verified selection".to_string()
} else {
"low-priority accepted selection".to_string()
}
}
fn collect_code_refs(observations: &[&EpiphanyObservation]) -> Result<Vec<EpiphanyCodeRef>> {
let mut code_refs = Vec::new();
let mut seen = HashSet::new();
for observation in observations {
for code_ref in &observation.code_refs {
let key = code_ref_key(code_ref);
if seen.insert(key) {
code_refs.push(code_ref.clone());
}
}
}
if code_refs.is_empty() {
Err(anyhow!(
"map proposals require at least one code ref on the selected observations"
))
} else {
Ok(code_refs)
}
}
fn unique_code_ref_paths(code_refs: &[EpiphanyCodeRef]) -> Vec<PathBuf> {
let mut paths = Vec::new();
let mut seen = HashSet::new();
for code_ref in code_refs {
let key = code_ref.path.to_string_lossy().to_string();
if seen.insert(key) {
paths.push(code_ref.path.clone());
}
}
paths
}
fn code_refs_for_path(code_refs: &[EpiphanyCodeRef], path: &Path) -> Vec<EpiphanyCodeRef> {
code_refs
.iter()
.filter(|code_ref| code_ref.path == path)
.cloned()
.collect()
}
fn find_architecture_node_for_path(
graphs: &EpiphanyGraphs,
path_code_refs: &[EpiphanyCodeRef],
path: &Path,
candidate_node_id: &str,
semantic_terms: &[String],
) -> Option<ArchitectureMatch> {
graphs
.architecture
.nodes
.iter()
.position(|node| has_exact_code_ref_overlap(&node.code_refs, path_code_refs))
.map(|index| ArchitectureMatch {
index,
kind: ArchitectureMatchKind::ExactCodeRef,
})
.or_else(|| {
graphs
.architecture
.nodes
.iter()
.position(|node| node.code_refs.iter().any(|code_ref| code_ref.path == path))
.map(|index| ArchitectureMatch {
index,
kind: ArchitectureMatchKind::SamePath,
})
})
.or_else(|| {
graphs
.architecture
.nodes
.iter()
.position(|node| node.id == candidate_node_id)
.map(|index| ArchitectureMatch {
index,
kind: ArchitectureMatchKind::DeterministicId,
})
})
.or_else(|| {
find_semantic_architecture_node(graphs, semantic_terms).map(|index| ArchitectureMatch {
index,
kind: ArchitectureMatchKind::Semantic,
})
})
}
fn has_exact_code_ref_overlap(
existing_code_refs: &[EpiphanyCodeRef],
proposed_code_refs: &[EpiphanyCodeRef],
) -> bool {
let existing_keys = existing_code_refs
.iter()
.map(code_ref_key)
.collect::<HashSet<_>>();
proposed_code_refs
.iter()
.map(code_ref_key)
.any(|key| existing_keys.contains(&key))
}
fn find_semantic_architecture_node(
graphs: &EpiphanyGraphs,
semantic_terms: &[String],
) -> Option<usize> {
if semantic_terms.is_empty() {
return None;
}
let mut best_index = None;
let mut best_score = 0usize;
let mut tied = false;
for (index, node) in graphs.architecture.nodes.iter().enumerate() {
let score = semantic_node_score(node, semantic_terms);
if !semantic_score_can_reuse(node, score) {
continue;
}
if score > best_score {
best_index = Some(index);
best_score = score;
tied = false;
} else if score == best_score {
tied = true;
}
}
if tied { None } else { best_index }
}
fn semantic_score_can_reuse(node: &EpiphanyGraphNode, score: usize) -> bool {
if score < SEMANTIC_REUSE_MIN_SCORE {
return false;
}
// Once a graph node is anchored to code, keep reuse on concrete ref/path/id checks.
// Semantic matching is only a rescue path for useful map prose that has no refs yet.
node.code_refs.is_empty()
}
fn semantic_node_score(node: &EpiphanyGraphNode, semantic_terms: &[String]) -> usize {
let mut node_terms = Vec::new();
let mut seen = HashSet::new();
push_semantic_terms(&mut node_terms, &mut seen, &node.id);
push_semantic_terms(&mut node_terms, &mut seen, &node.title);
push_semantic_terms(&mut node_terms, &mut seen, &node.purpose);
if let Some(mechanism) = node.mechanism.as_deref() {
push_semantic_terms(&mut node_terms, &mut seen, mechanism);
}
if let Some(metaphor) = node.metaphor.as_deref() {
push_semantic_terms(&mut node_terms, &mut seen, metaphor);
}
if let Some(status) = node.status.as_deref() {
push_semantic_terms(&mut node_terms, &mut seen, status);
}
push_code_ref_semantic_terms(&mut node_terms, &mut seen, &node.code_refs);
let node_terms = node_terms.into_iter().collect::<HashSet<_>>();
semantic_terms
.iter()
.filter(|term| node_terms.contains(term.as_str()))
.count()
}
fn merge_code_refs(target: &mut Vec<EpiphanyCodeRef>, additions: Vec<EpiphanyCodeRef>) {
let mut seen = target.iter().map(code_ref_key).collect::<HashSet<_>>();
for addition in additions {
if seen.insert(code_ref_key(&addition)) {
target.push(addition);
}
}
}
fn linked_frontier_node_ids(graphs: &EpiphanyGraphs, direct_node_ids: Vec<String>) -> Vec<String> {
let mut linked_node_ids = direct_node_ids;
let mut seen = linked_node_ids.iter().cloned().collect::<HashSet<_>>();
let mut changed = true;
while changed {
changed = false;
for link in &graphs.links {
if seen.contains(&link.architecture_node_id)
&& seen.insert(link.dataflow_node_id.clone())
{
linked_node_ids.push(link.dataflow_node_id.clone());
changed = true;
}
if seen.contains(&link.dataflow_node_id)
&& seen.insert(link.architecture_node_id.clone())
{
linked_node_ids.push(link.architecture_node_id.clone());
changed = true;
}
}
}
linked_node_ids
}
fn incident_edge_ids(graphs: &EpiphanyGraphs, node_ids: &[String]) -> Vec<String> {
let node_ids = node_ids.iter().map(String::as_str).collect::<HashSet<_>>();
let mut edge_ids = Vec::new();
let mut seen = HashSet::new();
for edge in graphs
.architecture
.edges
.iter()
.chain(graphs.dataflow.edges.iter())
{
if is_incident_to_any_node(edge, &node_ids)
&& let Some(edge_id) = edge.id.as_deref().map(str::trim)
&& !edge_id.is_empty()
&& seen.insert(edge_id.to_string())
{
edge_ids.push(edge_id.to_string());
}
}
edge_ids
}
fn is_incident_to_any_node(edge: &EpiphanyGraphEdge, node_ids: &HashSet<&str>) -> bool {
node_ids.contains(edge.source_id.as_str()) || node_ids.contains(edge.target_id.as_str())
}
fn merge_unique<T>(target: &mut Vec<T>, additions: Vec<T>)
where
T: Clone + Eq + std::hash::Hash,
{
let mut seen: HashSet<T> = target.iter().cloned().collect();
for addition in additions {
if seen.insert(addition.clone()) {
target.push(addition);
}
}
}
fn unique_strings(values: Vec<String>) -> Vec<String> {
let mut unique = Vec::new();
let mut seen = HashSet::new();
for value in values {
if seen.insert(value.clone()) {
unique.push(value);
}
}
unique
}
fn graph_node_id(path: &Path) -> String {
let mut hasher = Sha1::new();
hasher.update(path.to_string_lossy().as_bytes());
let digest = hasher.finalize();
let suffix: String = format!("{digest:x}").chars().take(12).collect();
format!("arch-path-{suffix}")
}
fn title_from_path(path: &Path) -> String {
if let Some(file_name) = path
.file_name()
.and_then(|value| value.to_str())
.filter(|value| !value.trim().is_empty())
{
file_name.to_string()
} else {
path.to_string_lossy().to_string()
}
}
fn fingerprint(
revision: &u64,
observation_ids: &[String],
code_refs: &[EpiphanyCodeRef],
) -> String {
let mut hasher = Sha1::new();
hasher.update(revision.to_string().as_bytes());
for observation_id in observation_ids {
hasher.update(b"\0");
hasher.update(observation_id.as_bytes());
}
for code_ref in code_refs {
hasher.update(b"\0");
hasher.update(code_ref_key(code_ref).as_bytes());
}
let digest = hasher.finalize();
format!("{digest:x}").chars().take(12).collect()
}
fn code_ref_key(code_ref: &EpiphanyCodeRef) -> String {
format!(
"{}:{}:{}:{}",
path_key(&code_ref.path),
code_ref.start_line.unwrap_or_default(),
code_ref.end_line.unwrap_or_default(),
code_ref.symbol.as_deref().unwrap_or_default()
)
}
fn path_key(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn push_code_ref_semantic_terms(
target: &mut Vec<String>,
seen: &mut HashSet<String>,
code_refs: &[EpiphanyCodeRef],
) {
for code_ref in code_refs {
push_path_semantic_terms(target, seen, &code_ref.path);
if let Some(symbol) = code_ref.symbol.as_deref() {
push_semantic_terms(target, seen, symbol);
}
if let Some(note) = code_ref.note.as_deref() {
push_semantic_terms(target, seen, note);
}
}
}
fn push_path_semantic_terms(target: &mut Vec<String>, seen: &mut HashSet<String>, path: &Path) {
if let Some(file_stem) = path.file_stem().and_then(|value| value.to_str()) {
push_semantic_terms(target, seen, file_stem);
}
if let Some(parent) = path.parent().and_then(|value| value.to_str()) {
push_semantic_terms(target, seen, parent);
}
}
fn push_semantic_terms(target: &mut Vec<String>, seen: &mut HashSet<String>, value: &str) {
for term in semantic_terms(value) {
if seen.insert(term.clone()) {
target.push(term);
}
}
}
fn semantic_terms(value: &str) -> Vec<String> {
value
.split(|character: char| !character.is_ascii_alphanumeric())
.map(str::trim)
.filter(|term| term.len() >= 3)
.map(str::to_ascii_lowercase)
.filter(|term| !is_semantic_stopword(term))
.collect()
}
fn is_semantic_stopword(value: &str) -> bool {
matches!(
value,
"and"
| "are"
| "but"
| "can"
| "for"
| "from"
| "has"
| "into"
| "not"
| "now"
| "one"
| "out"
| "the"
| "this"
| "that"
| "through"
| "with"
)
}
fn truncate_chars(value: &str, limit: usize) -> String {
if value.chars().count() <= limit {
return value.to_string();
}
let mut truncated = value
.chars()
.take(limit.saturating_sub(3))
.collect::<String>();
truncated.push_str("...");
truncated
}
fn proposal_understanding_status(delta: &MapDeltaJudgment) -> String {
match (delta.reused_nodes() > 0, delta.created_nodes > 0) {
(true, false) => "proposal_refines_map",
(false, true) => "proposal_expands_map",
(true, true) => "proposal_updates_map",
(false, false) => "proposal_ready",
}
.to_string()
}
fn proposal_diff_pressure(
delta: &MapDeltaJudgment,
existing_churn: Option<&EpiphanyChurnState>,
) -> String {
let proposal_pressure = if delta.created_nodes > 1
|| delta.has_mixed_delta()
|| delta.touched_paths > 2
|| delta.selected_observations > 3
|| delta.existing_unexplained_writes
{
"high"
} else if delta.created_nodes > 0
|| delta.has_broadening_reuse()
|| delta.touched_paths > 1
|| delta.selected_observations > 1
{
"medium"
} else {
"low"
};
let existing_pressure = existing_churn
.map(|churn| churn.diff_pressure.trim())
.filter(|value| !value.is_empty())
.unwrap_or("low");
max_pressure(existing_pressure, proposal_pressure).to_string()
}
fn proposal_delta_summary(delta: &MapDeltaJudgment) -> String {
let mut parts = Vec::new();
if delta.exact_ref_reuses > 0 {
parts.push(format!(
"{} exact-ref refinement(s)",
delta.exact_ref_reuses
));
}
if delta.same_path_reuses > 0 {
parts.push(format!(
"{} same-path broadening reuse(s)",
delta.same_path_reuses
));
}
if delta.deterministic_id_reuses > 0 {
parts.push(format!(
"{} deterministic-node reuse(s)",
delta.deterministic_id_reuses
));
}
if delta.semantic_reuses > 0 {
parts.push(format!(
"{} semantic unanchored-node reuse(s)",
delta.semantic_reuses
));
}
if delta.created_nodes > 0 {
parts.push(format!("{} new candidate node(s)", delta.created_nodes));
}
if parts.is_empty() {
parts.push("no graph node delta".to_string());
}
format!(
"delta judgment: {}; {} touched path(s), {} selected observation(s)",
parts.join(", "),
delta.touched_paths,
delta.selected_observations
)
}
fn max_pressure<'a>(left: &'a str, right: &'a str) -> &'a str {
if pressure_rank(left) >= pressure_rank(right) {
left
} else {
right
}
}
fn pressure_rank(value: &str) -> u8 {
match value.trim().to_ascii_lowercase().as_str() {
"critical" => 4,
"high" => 3,
"medium" => 2,
"low" => 1,
_ => 0,
}
}
fn selection_quality_summary(selection_quality: &SelectionQuality, path_count: usize) -> String {
let source_kinds = if selection_quality.source_kinds.is_empty() {
"unknown sources".to_string()
} else {
selection_quality.source_kinds.join("/")
};
format!(
"{} evidence-backed observation(s), {} accepting evidence record(s), {} code path(s), {}, primary observation: {}, source kind(s): {}",
selection_quality.observation_count,
selection_quality.evidence_count,
path_count,
selection_quality.priority_label,
selection_quality.primary_observation_id,
source_kinds
)
}
fn is_verified_status(status: &str) -> bool {
matches!(
status.trim().to_ascii_lowercase().as_str(),
"ok" | "accepted" | "verified" | "pass" | "passed"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::promotion::EpiphanyPromotionInput;
use crate::promotion::evaluate_promotion;
use epiphany_state_model::EpiphanyGraph;
use epiphany_state_model::EpiphanyGraphEdge;
use epiphany_state_model::EpiphanyGraphLink;
fn code_ref(path: &str) -> EpiphanyCodeRef {
EpiphanyCodeRef {
path: PathBuf::from(path),
start_line: Some(10),
end_line: Some(20),
symbol: Some("demo".to_string()),
note: None,
}
}
fn state_with_observation(status: &str) -> EpiphanyThreadState {
EpiphanyThreadState {
revision: 4,
observations: vec![EpiphanyObservation {
id: "obs-verified".to_string(),
summary: "The prompt renderer injects Epiphany state".to_string(),
source_kind: "verification".to_string(),
status: status.to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-verified".to_string()],
}],
recent_evidence: vec![EpiphanyEvidenceRecord {
id: "ev-verified".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verified prompt renderer behavior".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
}],
churn: Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
diff_pressure: "medium".to_string(),
..Default::default()
}),
..Default::default()
}
}
fn verifier() -> EpiphanyEvidenceRecord {
EpiphanyEvidenceRecord {
id: "ev-verifier".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verifier accepted proposal".to_string(),
code_refs: Vec::new(),
}
}
#[test]
fn propose_map_update_returns_candidate_patch_parts() {
let proposal = propose_map_update(EpiphanyMapProposalInput {
state: state_with_observation("verified"),
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert!(proposal.observation.id.starts_with("obs-map-proposal-"));
assert_eq!(proposal.evidence.status, "candidate");
assert_eq!(proposal.graphs.architecture.nodes.len(), 1);
assert_eq!(proposal.graphs.architecture.nodes[0].title, "prompt.rs");
assert_eq!(proposal.graph_frontier.active_node_ids.len(), 1);
assert_eq!(proposal.churn.understanding_status, "proposal_expands_map");
assert_eq!(proposal.churn.diff_pressure, "medium");
}
#[test]
fn propose_map_update_reuses_existing_architecture_node_for_matching_code_ref() {
let mut state = state_with_observation("verified");
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
status: Some("grounded".to_string()),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
state.observations[0].code_refs.push(EpiphanyCodeRef {
path: PathBuf::from("epiphany-state-model/src/prompt.rs"),
start_line: Some(30),
end_line: Some(40),
symbol: Some("render_epiphany_state".to_string()),
note: Some("newly verified line span".to_string()),
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.graphs.architecture.nodes.len(), 1);
let node = &proposal.graphs.architecture.nodes[0];
assert_eq!(node.id, "prompt-renderer");
assert_eq!(node.status.as_deref(), Some("grounded"));
assert_eq!(node.code_refs.len(), 2);
assert_eq!(
proposal.graph_frontier.active_node_ids,
vec!["prompt-renderer".to_string()]
);
assert_eq!(proposal.churn.understanding_status, "proposal_refines_map");
}
#[test]
fn propose_map_update_reuses_existing_deterministic_path_node_without_refs() {
let mut state = state_with_observation("verified");
let node_id = graph_node_id(Path::new("epiphany-state-model/src/prompt.rs"));
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: node_id.clone(),
title: "Prompt path".to_string(),
purpose: "Existing path-derived proposal node".to_string(),
..Default::default()
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.graphs.architecture.nodes.len(), 1);
assert_eq!(proposal.graphs.architecture.nodes[0].id, node_id);
assert_eq!(proposal.graphs.architecture.nodes[0].code_refs.len(), 1);
assert_eq!(proposal.churn.understanding_status, "proposal_refines_map");
}
#[test]
fn propose_map_update_reuses_strong_semantic_graph_match_without_refs() {
let mut state = state_with_observation("verified");
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Inject Epiphany state into developer context".to_string(),
mechanism: Some("Renders typed state as a bounded prompt fragment".to_string()),
..Default::default()
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.graphs.architecture.nodes.len(), 1);
assert_eq!(proposal.graphs.architecture.nodes[0].id, "prompt-renderer");
assert_eq!(proposal.graphs.architecture.nodes[0].code_refs.len(), 1);
assert_eq!(
proposal.graph_frontier.active_node_ids,
vec!["prompt-renderer".to_string()]
);
assert!(
proposal
.churn
.warning
.as_deref()
.unwrap_or_default()
.contains("1 semantic unanchored-node reuse")
);
}
#[test]
fn propose_map_update_judges_exact_ref_reuse_as_low_pressure_refinement() {
let mut state = state_with_observation("verified");
state.churn = Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
diff_pressure: "low".to_string(),
..Default::default()
});
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.churn.diff_pressure, "low");
assert!(
proposal
.churn
.warning
.as_deref()
.unwrap_or_default()
.contains("1 exact-ref refinement")
);
}
#[test]
fn propose_map_update_judges_same_path_reuse_as_broadening_pressure() {
let mut state = state_with_observation("verified");
state.churn = Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
diff_pressure: "low".to_string(),
..Default::default()
});
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
code_refs: vec![EpiphanyCodeRef {
path: PathBuf::from("epiphany-state-model/src/prompt.rs"),
start_line: Some(100),
end_line: Some(120),
symbol: Some("render_graphs".to_string()),
note: None,
}],
..Default::default()
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.churn.diff_pressure, "medium");
assert!(
proposal
.churn
.warning
.as_deref()
.unwrap_or_default()
.contains("1 same-path broadening reuse")
);
}
#[test]
fn propose_map_update_refuses_ambiguous_semantic_graph_match() {
let mut state = state_with_observation("verified");
for node_id in ["prompt-renderer-a", "prompt-renderer-b"] {
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: node_id.to_string(),
title: "Prompt renderer".to_string(),
purpose: "Inject Epiphany state into developer context".to_string(),
mechanism: Some("Renders typed state as a bounded prompt fragment".to_string()),
..Default::default()
});
}
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.graphs.architecture.nodes.len(), 3);
assert!(
proposal
.graph_frontier
.active_node_ids
.iter()
.any(|node_id| node_id.starts_with("arch-path-"))
);
assert_eq!(proposal.churn.understanding_status, "proposal_expands_map");
}
#[test]
fn propose_map_update_prioritizes_stronger_selected_observation() {
let mut state = state_with_observation("ok");
state.observations[0].id = "obs-weak".to_string();
state.observations[0].source_kind = "observation".to_string();
state.observations[0].summary = "Prompt renderer might be involved".to_string();
state.observations[0].evidence_ids = vec!["ev-weak".to_string()];
state.recent_evidence[0].id = "ev-weak".to_string();
state.recent_evidence[0].kind = "observation".to_string();
state.recent_evidence[0].summary = "Manual note mentioned prompt renderer".to_string();
state.observations.push(EpiphanyObservation {
id: "obs-strong".to_string(),
summary: "Live smoke verified prompt renderer Epiphany state injection".to_string(),
source_kind: "smoke".to_string(),
status: "ok".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-strong".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
id: "ev-strong".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verifier accepted prompt renderer state injection".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-weak".to_string(), "obs-strong".to_string()],
})
.expect("proposal");
assert!(
proposal.graphs.architecture.nodes[0]
.purpose
.contains("Live smoke verified")
);
assert!(
proposal
.observation
.summary
.contains("primary observation: obs-strong")
);
}
#[test]
fn propose_map_update_auto_selects_best_proposal_ready_observations_when_ids_are_omitted() {
let mut state = state_with_observation("ok");
state.observations[0].id = "obs-weak".to_string();
state.observations[0].source_kind = "observation".to_string();
state.observations[0].summary = "Prompt renderer might be involved".to_string();
state.observations[0].evidence_ids = vec!["ev-weak".to_string()];
state.recent_evidence[0].id = "ev-weak".to_string();
state.recent_evidence[0].kind = "observation".to_string();
state.recent_evidence[0].summary = "Manual note mentioned prompt renderer".to_string();
state.observations.push(EpiphanyObservation {
id: "obs-failed".to_string(),
summary: "Failed candidate should not be selected".to_string(),
source_kind: "verification".to_string(),
status: "failed".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-failed".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
id: "ev-failed".to_string(),
kind: "verification".to_string(),
status: "failed".to_string(),
summary: "Verifier rejected the failed candidate".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
});
state.observations.push(EpiphanyObservation {
id: "obs-strong".to_string(),
summary: "Live smoke verified prompt renderer Epiphany state injection".to_string(),
source_kind: "smoke".to_string(),
status: "ok".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-strong".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
id: "ev-strong".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verifier accepted prompt renderer state injection".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: Vec::new(),
})
.expect("auto-selected proposal");
assert!(
proposal
.observation
.summary
.contains("primary observation: obs-strong")
);
assert!(!proposal.observation.summary.contains("obs-failed"));
}
#[test]
fn propose_map_update_auto_selection_prefers_frontier_dirty_path_cluster() {
let mut state = state_with_observation("ok");
state.graph_frontier = Some(EpiphanyGraphFrontier {
dirty_paths: vec![PathBuf::from("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
state.observations.push(EpiphanyObservation {
id: "obs-other".to_string(),
summary: "Proposal engine update is verified but not current frontier focus"
.to_string(),
source_kind: "verification".to_string(),
status: "ok".to_string(),
code_refs: vec![code_ref("epiphany-core/src/proposal.rs")],
evidence_ids: vec!["ev-other".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
id: "ev-other".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verifier accepted proposal engine behavior".to_string(),
code_refs: vec![code_ref("epiphany-core/src/proposal.rs")],
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: Vec::new(),
})
.expect("auto-selected proposal");
assert!(
proposal.observation.summary.contains("obs-verified"),
"{}",
proposal.observation.summary
);
assert!(
proposal
.graph_frontier
.dirty_paths
.contains(&PathBuf::from("epiphany-state-model/src/prompt.rs"))
);
assert!(
!proposal
.graph_frontier
.dirty_paths
.contains(&PathBuf::from("epiphany-core/src/proposal.rs"))
);
}
#[test]
fn propose_map_update_auto_selection_rejects_when_no_candidate_is_ready() {
let mut state = state_with_observation("failed");
state.recent_evidence.clear();
let err = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: Vec::new(),
})
.expect_err("auto-selection needs proposal-ready observations");
assert!(
err.to_string()
.contains("no proposal-ready observations were found")
);
}
#[test]
fn propose_map_update_focuses_linked_nodes_and_incident_edges() {
let mut state = state_with_observation("verified");
state.graphs = EpiphanyGraphs {
architecture: EpiphanyGraph {
nodes: vec![
EpiphanyGraphNode {
id: "session".to_string(),
title: "Session".to_string(),
purpose: "Build turn context".to_string(),
..Default::default()
},
EpiphanyGraphNode {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
},
],
edges: vec![EpiphanyGraphEdge {
id: Some("edge-session-prompt".to_string()),
source_id: "session".to_string(),
target_id: "prompt-renderer".to_string(),
kind: "calls".to_string(),
..Default::default()
}],
},
dataflow: EpiphanyGraph {
nodes: vec![
EpiphanyGraphNode {
id: "developer-context".to_string(),
title: "Developer context".to_string(),
purpose: "Carry rendered instructions".to_string(),
..Default::default()
},
EpiphanyGraphNode {
id: "epiphany-state-fragment".to_string(),
title: "Epiphany state fragment".to_string(),
purpose: "Expose typed state to the model".to_string(),
..Default::default()
},
],
edges: vec![EpiphanyGraphEdge {
id: Some("edge-fragment-context".to_string()),
source_id: "epiphany-state-fragment".to_string(),
target_id: "developer-context".to_string(),
kind: "feeds".to_string(),
..Default::default()
}],
},
links: vec![EpiphanyGraphLink {
dataflow_node_id: "epiphany-state-fragment".to_string(),
architecture_node_id: "prompt-renderer".to_string(),
relationship: Some("rendered by".to_string()),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
}],
};
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(
proposal.graph_frontier.active_node_ids,
vec![
"prompt-renderer".to_string(),
"epiphany-state-fragment".to_string()
]
);
assert_eq!(
proposal.graph_frontier.active_edge_ids,
vec![
"edge-session-prompt".to_string(),
"edge-fragment-context".to_string()
]
);
}
#[test]
fn propose_map_update_returns_promotion_acceptable_patch_parts() {
let proposal = propose_map_update(EpiphanyMapProposalInput {
state: state_with_observation("verified"),
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
let decision = evaluate_promotion(EpiphanyPromotionInput {
has_state_replacements: true,
active_subgoal_id: None,
subgoals: None,
invariants: None,
graphs: Some(proposal.graphs),
graph_frontier: Some(proposal.graph_frontier),
graph_checkpoint: None,
investigation_checkpoint: None,
churn: Some(proposal.churn),
observations: vec![proposal.observation],
evidence: vec![proposal.evidence],
verifier_evidence: verifier(),
});
assert!(decision.accepted, "{:?}", decision.reasons);
}
#[test]
fn propose_map_update_rejects_unverified_observation() {
let err = propose_map_update(EpiphanyMapProposalInput {
state: state_with_observation("failed"),
observation_ids: vec!["obs-verified".to_string()],
})
.expect_err("unverified observation should fail");
assert!(
err.to_string()
.contains("must have verified/accepted status")
);
}
#[test]
fn propose_map_update_rejects_missing_code_refs() {
let mut state = state_with_observation("ok");
state.observations[0].code_refs.clear();
let err = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect_err("code refs are required");
assert!(err.to_string().contains("at least one code ref"));
}
#[test]
fn propose_map_update_rejects_observation_without_backing_evidence() {
let mut state = state_with_observation("ok");
state.recent_evidence.clear();
let err = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect_err("missing backing evidence should fail");
assert!(err.to_string().contains("cites missing evidence id"));
}
#[test]
fn propose_map_update_rejects_non_accepting_backing_evidence() {
let mut state = state_with_observation("ok");
state.recent_evidence[0].status = "failed".to_string();
let err = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect_err("non-accepting backing evidence should fail");
assert!(err.to_string().contains("non-accepting evidence"));
}
#[test]
fn propose_map_update_marks_mixed_multi_path_changes_as_high_pressure() {
let mut state = state_with_observation("ok");
state.churn = Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
diff_pressure: "low".to_string(),
..Default::default()
});
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
state.observations[0]
.code_refs
.push(code_ref("epiphany-core/src/proposal.rs"));
let proposal = propose_map_update(EpiphanyMapProposalInput {
state,
observation_ids: vec!["obs-verified".to_string()],
})
.expect("proposal");
assert_eq!(proposal.churn.understanding_status, "proposal_updates_map");
assert_eq!(proposal.churn.diff_pressure, "high");
}
}