use std::collections::BTreeSet;
use crate::error::Result;
use crate::residual::{
CorrectionDirection, IndependenceRoute, ResidualClass, ResidualEvent, ResidualSeverity,
SensorRef, SymbolRef,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GoalSpec {
pub node_id: String,
pub expected_symbols: Vec<String>,
}
impl GoalSpec {
pub fn new(node_id: impl Into<String>, expected_symbols: Vec<String>) -> Self {
Self {
node_id: node_id.into(),
expected_symbols,
}
}
pub fn is_empty(&self) -> bool {
self.expected_symbols.is_empty()
}
}
pub fn missing_symbols(spec: &GoalSpec, observed: &BTreeSet<String>) -> Vec<String> {
let mut seen = BTreeSet::new();
spec.expected_symbols
.iter()
.filter(|name| !observed.contains(*name))
.filter(|name| seen.insert((*name).clone()))
.cloned()
.collect()
}
pub fn goal_presence_sensor() -> SensorRef {
SensorRef::new("goal-presence", IndependenceRoute::DeterministicTool)
}
pub fn goal_presence_residual(
spec: &GoalSpec,
generation: u32,
observed: &BTreeSet<String>,
) -> Result<Option<ResidualEvent>> {
if spec.is_empty() {
return Ok(None);
}
let missing = missing_symbols(spec, observed);
if missing.is_empty() {
return Ok(None);
}
let summary = format!(
"goal not satisfied: {} expected symbol(s) absent from the workspace: {}",
missing.len(),
missing.join(", ")
);
let mut residual = ResidualEvent::new(
&spec.node_id,
generation,
ResidualClass::SymbolMismatch,
ResidualSeverity::Blocking,
missing.len() as f64,
goal_presence_sensor(),
)?;
residual.evidence.summary = summary;
residual.affected_symbols = missing
.iter()
.map(|name| SymbolRef {
name: name.clone(),
container: None,
})
.collect();
residual = residual.with_correction(
CorrectionDirection::new(
ResidualClass::SymbolMismatch,
format!(
"the requested work is missing: define {} so the goal is satisfied; \
do not stop until each named symbol exists",
missing.join(", ")
),
)
.with_rationale("an empty or placeholder file compiles but does not satisfy the goal"),
);
Ok(Some(residual))
}
#[cfg(test)]
mod tests {
use super::*;
fn observed(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn empty_spec_never_fires() {
let spec = GoalSpec::new("n1", vec![]);
assert!(goal_presence_residual(&spec, 0, &observed(&[]))
.unwrap()
.is_none());
}
#[test]
fn satisfied_goal_produces_no_residual() {
let spec = GoalSpec::new("n1", vec!["multiply".into()]);
let r = goal_presence_residual(&spec, 0, &observed(&["multiply", "helper"])).unwrap();
assert!(r.is_none());
}
#[test]
fn missing_symbol_is_blocking_residual() {
let spec = GoalSpec::new("n1", vec!["is_even".into()]);
let r = goal_presence_residual(&spec, 0, &observed(&["unrelated"]))
.unwrap()
.expect("missing symbol must produce a residual");
assert_eq!(r.class, ResidualClass::SymbolMismatch);
assert_eq!(r.severity, ResidualSeverity::Blocking);
assert_eq!(r.score, 1.0);
assert_eq!(r.affected_symbols.len(), 1);
assert_eq!(r.affected_symbols[0].name, "is_even");
assert_eq!(r.correction_directions.len(), 1);
}
#[test]
fn score_counts_all_missing_symbols() {
let spec = GoalSpec::new("n1", vec!["a".into(), "b".into(), "c".into()]);
let r = goal_presence_residual(&spec, 0, &observed(&["b"]))
.unwrap()
.unwrap();
assert_eq!(r.score, 2.0); }
#[test]
fn missing_symbols_dedup_and_order() {
let spec = GoalSpec::new("n1", vec!["a".into(), "a".into(), "b".into()]);
assert_eq!(missing_symbols(&spec, &observed(&[])), vec!["a", "b"]);
}
#[test]
fn symbol_mismatch_routes_to_structural_component() {
assert_eq!(
ResidualClass::SymbolMismatch.default_component(),
crate::residual::EnergyComponent::Str
);
}
}