use scema_memory::{MemoryBody, MemoryKind, MemoryStore, Recall};
use scema_world::{
Action, Domain, Goal, Hypothesis, HypothesisOrigin, Polarity, Reversibility, RiskClass,
Signal, WorldState,
};
pub trait Hypothesizer {
fn name(&self) -> &str;
fn propose(&self, world: &WorldState, goal: &Goal) -> Vec<Hypothesis>;
}
fn edit_reversibility(domain: Domain) -> Reversibility {
match domain {
Domain::Software => Reversibility::Recoverable,
_ => Reversibility::Unknown,
}
}
#[derive(Clone, Debug, Default)]
pub struct SignalHypothesizer;
impl SignalHypothesizer {
fn action_for(&self, world: &WorldState, s: &Signal, idx: usize) -> Action {
let target = s
.targets
.first()
.cloned()
.unwrap_or_else(|| world.entity.locator.clone());
Action::new(
format!("a{idx}"),
RiskClass::Write,
target,
format!("address `{}`", s.label),
edit_reversibility(world.domain),
)
}
}
impl Hypothesizer for SignalHypothesizer {
fn name(&self) -> &str {
"signal"
}
fn propose(&self, world: &WorldState, _goal: &Goal) -> Vec<Hypothesis> {
world
.signals
.iter()
.enumerate()
.filter(|(_, s)| s.measured)
.map(|(i, s)| {
let verb = match s.polarity {
Polarity::Risk => "mitigate",
Polarity::Opportunity => "take",
};
Hypothesis::new(
format!("h-{}", s.id.replace([':', '/', ' '], "-")),
format!("{verb}: {}", s.label),
HypothesisOrigin::Heuristic { rule: format!("one branch per counted signal ({})", s.id) },
)
.because(format!("{} — {}", s.detail, s.evidence.join("; ")))
.grounded(s.id.clone())
.doing(self.action_for(world, s, i))
})
.collect()
}
}
#[derive(Clone, Debug, Default)]
pub struct GoalHypothesizer;
impl Hypothesizer for GoalHypothesizer {
fn name(&self) -> &str {
"goal"
}
fn propose(&self, world: &WorldState, goal: &Goal) -> Vec<Hypothesis> {
if goal.statement.trim().is_empty() {
return vec![];
}
let mut h = Hypothesis::new("h-goal", goal.statement.clone(), HypothesisOrigin::Human)
.because(if goal.grounded_in.is_empty() {
"the operator asked for this and cited no counted signal; an instruction is not evidence"
.to_string()
} else {
format!(
"the operator asserts this addresses: {}",
goal.grounded_in.join(", ")
)
})
.doing(Action::new(
"a0",
RiskClass::Write,
world.entity.locator.clone(),
goal.statement.clone(),
edit_reversibility(world.domain),
));
for g in &goal.grounded_in {
h = h.grounded(g.clone());
}
vec![h]
}
}
pub struct MemoryHypothesizer<'a> {
store: &'a MemoryStore,
}
impl<'a> MemoryHypothesizer<'a> {
pub fn new(store: &'a MemoryStore) -> Self {
MemoryHypothesizer { store }
}
}
impl Hypothesizer for MemoryHypothesizer<'_> {
fn name(&self) -> &str {
"memory"
}
fn propose(&self, world: &WorldState, _goal: &Goal) -> Vec<Hypothesis> {
let query = Recall::about(world.entity.locator.clone()).limit(5);
let Ok(hits) = self.store.recall(MemoryKind::Procedural, &query) else {
return vec![];
};
hits.iter()
.filter_map(|r| match &r.body {
MemoryBody::Procedure { name, steps, successes, failures } => {
if failures > successes {
return None;
}
let mut h = Hypothesis::new(
format!("h-mem-{}", r.id),
format!("apply the `{name}` procedure"),
HypothesisOrigin::Memory { record: r.id.clone() },
)
.because(format!(
"{successes} success(es), {failures} failure(s) recorded against this procedure"
));
for (i, step) in steps.iter().enumerate() {
h = h.doing(Action::new(
format!("a{i}"),
RiskClass::Write,
world.entity.locator.clone(),
step.clone(),
edit_reversibility(world.domain),
));
}
Some(h)
}
_ => None,
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use scema_world::{Entity, EntityKind, Extent, Object, Provenance};
fn signal(id: &str, label: &str, measured: bool) -> Signal {
Signal {
id: id.into(),
polarity: Polarity::Risk,
label: label.into(),
detail: "d".into(),
magnitude: 0.5,
measured,
targets: vec!["unit:crates/x".into()],
evidence: vec!["counted".into()],
}
}
fn world(signals: Vec<Signal>, domain: Domain) -> WorldState {
WorldState {
observer: "t".into(),
entity: Entity {
kind: EntityKind::Repository,
locator: "/repo".into(),
label: "repo".into(),
},
domain,
observed_at: 0,
objects: vec![Object::new("o", "file", "o", Provenance::Live { age_secs: 0 })],
facts: vec![],
signals,
extent: Extent::complete(1, "t"),
blind_spots: vec![],
}
}
#[test]
fn only_counted_signals_become_branches() {
let w = world(
vec![signal("s1", "counted thing", true), signal("s2", "guessed thing", false)],
Domain::Software,
);
let hs = SignalHypothesizer.propose(&w, &Goal::new("g", "x"));
assert_eq!(hs.len(), 1);
assert_eq!(hs[0].grounded_in, vec!["s1".to_string()]);
}
#[test]
fn every_signal_branch_is_grounded_by_construction() {
let w = world(vec![signal("s1", "a", true), signal("s2", "b", true)], Domain::Software);
let hs = SignalHypothesizer.propose(&w, &Goal::new("g", "x"));
assert!(hs.iter().all(|h| !h.grounded_in.is_empty()));
}
#[test]
fn a_goal_never_grounds_itself_from_its_own_wording() {
let w = world(vec![signal("untested:x", "`x` has no tests", true)], Domain::Software);
let hs = GoalHypothesizer.propose(&w, &Goal::new("g", "add tests to the x crate"));
assert_eq!(hs.len(), 1);
assert!(
hs[0].grounded_in.is_empty(),
"an instruction is not evidence; this branch must not borrow grounding"
);
}
#[test]
fn an_operator_can_ground_a_goal_deliberately() {
let w = world(vec![signal("untested:x", "`x` has no tests", true)], Domain::Software);
let g = Goal::new("g", "add tests to the x crate").grounded("untested:x");
let hs = GoalHypothesizer.propose(&w, &g);
assert_eq!(hs[0].grounded_in, vec!["untested:x".to_string()]);
assert!(hs[0].rationale.contains("operator asserts"));
}
#[test]
fn an_unknown_domain_yields_unclassified_reversibility() {
let w = world(vec![signal("s1", "a", true)], Domain::Unknown);
let hs = SignalHypothesizer.propose(&w, &Goal::new("g", "x"));
assert_eq!(hs[0].worst_reversibility(), Some(Reversibility::Unknown));
}
#[test]
fn an_empty_goal_proposes_nothing() {
let w = world(vec![], Domain::Software);
assert!(GoalHypothesizer.propose(&w, &Goal::new("g", " ")).is_empty());
}
}