From cbbc1c0da58cfdfcd93afa2e5625a12196620290 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 21 Sep 2026 19:03:42 +0200 Subject: [PATCH] needs: `stages` - every stage the business named must be a state Found by simulating a generated site: the owner wrote "they come in, we go and look at the building, we send a quote", and the generated machine went open -> quoted. Nothing compared the machine to what the business said, so it passed. A need may now list its stages as the business describes them; each must be a state of the bucket and reachable through the handling group's desk. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH --- src/needs.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/needs.rs b/src/needs.rs index 27ce66d..949edb6 100644 --- a/src/needs.rs +++ b/src/needs.rs @@ -79,6 +79,13 @@ pub struct Need { /// What a simulation or a live bucket report counts toward. #[serde(default)] pub success: Vec, + /// Every stage the business said a case goes through, in its own + /// order, first to last. Written from how the business describes + /// its work, before any state machine exists, so that a machine + /// which quietly skips a stage ("we go and look, then we quote" + /// becoming open -> quoted) fails instead of passing. + #[serde(default)] + pub stages: Vec, } fn root() -> String { @@ -455,6 +462,25 @@ pub fn check( ); } } + for stage in &need.stages { + if !schema.has_state(stage) { + report( + Level::Fail, + format!( + "the business describes a stage {stage:?} that {:?} has no state for", + need.lands_in + ), + ); + } else if !reached.contains(stage) { + report( + Level::Fail, + format!( + "stage {stage:?} exists but {:?} is offered no moves that reach it", + need.handled_by + ), + ); + } + } for state in &need.success { if !need.done_when.contains(state) { report( @@ -841,6 +867,15 @@ mod tests { assert_eq!(model["buckets"]["projects"]["initial"], "open"); assert_eq!(model["personas"][0]["id"], "founder"); + // A stage the business named must exist as a state. + file.needs[0].stages = vec!["open".into(), "surveyed".into(), "quoted".into()]; + assert_eq!( + fails(&check(&file, &questions, &aggregates)), + vec!["the business describes a stage \"surveyed\" that \"projects\" has no state for"] + ); + file.needs[0].stages = vec!["open".into(), "quoted".into(), "won".into()]; + assert!(check(&file, &questions, &aggregates).is_empty()); + file.needs[0].success = vec!["quoted".to_string()]; assert_eq!( fails(&check(&file, &questions, &aggregates)),