use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlanStep {
pub id: String,
#[serde(default)]
pub preconditions: Vec<String>,
#[serde(default)]
pub add_effects: Vec<String>,
#[serde(default)]
pub del_effects: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlanCheckRequest {
#[serde(default)]
pub initial: Vec<String>,
#[serde(default)]
pub steps: Vec<PlanStep>,
#[serde(default)]
pub goal: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlanDefectKind {
UnmetPrecondition,
GoalNotAchieved,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlanDefect {
pub kind: PlanDefectKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step: Option<String>,
pub fact: String,
pub explanation: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlanCheckReport {
pub valid: bool,
pub defects: Vec<PlanDefect>,
pub final_state: Vec<String>,
}
pub fn check_plan(req: &PlanCheckRequest) -> PlanCheckReport {
let mut state: BTreeSet<String> = req.initial.iter().cloned().collect();
let mut defects = Vec::new();
for step in &req.steps {
for pre in &step.preconditions {
if !state.contains(pre) {
defects.push(PlanDefect {
kind: PlanDefectKind::UnmetPrecondition,
step: Some(step.id.clone()),
fact: pre.clone(),
explanation: format!(
"step '{}' requires '{}', which is not established by the preceding steps",
step.id, pre
),
});
}
}
for d in &step.del_effects {
state.remove(d);
}
for a in &step.add_effects {
state.insert(a.clone());
}
}
for g in &req.goal {
if !state.contains(g) {
defects.push(PlanDefect {
kind: PlanDefectKind::GoalNotAchieved,
step: None,
fact: g.clone(),
explanation: format!("goal fact '{g}' does not hold after the plan finishes"),
});
}
}
PlanCheckReport {
valid: defects.is_empty(),
defects,
final_state: state.into_iter().collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn step(id: &str, pre: &[&str], add: &[&str], del: &[&str]) -> PlanStep {
PlanStep {
id: id.into(),
preconditions: pre.iter().map(|s| s.to_string()).collect(),
add_effects: add.iter().map(|s| s.to_string()).collect(),
del_effects: del.iter().map(|s| s.to_string()).collect(),
}
}
fn req(initial: &[&str], steps: Vec<PlanStep>, goal: &[&str]) -> PlanCheckRequest {
PlanCheckRequest {
initial: initial.iter().map(|s| s.to_string()).collect(),
steps,
goal: goal.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn feasible_plan_is_valid() {
let r = check_plan(&req(
&["ingredients"],
vec![
step("cook", &["ingredients"], &["cooked"], &["ingredients"]),
step("serve", &["cooked"], &["served"], &[]),
],
&["served"],
));
assert!(r.valid, "{:?}", r.defects);
assert!(r.final_state.contains(&"served".to_string()));
}
#[test]
fn unmet_precondition_is_flagged_with_step_and_fact() {
let r = check_plan(&req(
&["ingredients"],
vec![step("serve", &["cooked"], &["served"], &[])],
&["served"],
));
assert!(!r.valid);
let d = &r.defects[0];
assert_eq!(d.kind, PlanDefectKind::UnmetPrecondition);
assert_eq!(d.step.as_deref(), Some("serve"));
assert_eq!(d.fact, "cooked");
}
#[test]
fn deleted_fact_breaks_a_later_step() {
let r = check_plan(&req(
&["key"],
vec![
step("open", &["key"], &["opened"], &["key"]),
step("relock", &["key"], &["locked"], &[]),
],
&[],
));
assert!(!r.valid);
assert!(r
.defects
.iter()
.any(|d| d.step.as_deref() == Some("relock") && d.fact == "key"));
}
#[test]
fn goal_not_achieved_is_flagged() {
let r = check_plan(&req(&["a"], vec![step("noop", &["a"], &[], &[])], &["b"]));
assert!(!r.valid);
assert_eq!(r.defects[0].kind, PlanDefectKind::GoalNotAchieved);
assert_eq!(r.defects[0].fact, "b");
}
#[test]
fn add_and_delete_same_fact_nets_to_true() {
let r = check_plan(&req(
&[],
vec![step("refresh", &[], &["x"], &["x"])],
&["x"],
));
assert!(r.valid, "{:?}", r.defects);
}
#[test]
fn all_defects_surfaced_in_one_pass() {
let r = check_plan(&req(
&[],
vec![step("s1", &["p1"], &[], &[]), step("s2", &["p2"], &[], &[])],
&["g"],
));
assert_eq!(r.defects.len(), 3);
}
#[test]
fn empty_plan_with_satisfied_goal_is_valid() {
let r = check_plan(&req(&["done"], vec![], &["done"]));
assert!(r.valid);
}
}