const REVIEW_GATE: &str = "review";
#[derive(Debug, PartialEq, Eq)]
pub struct Act {
pub name: &'static str,
pub task_kind: &'static str,
pub evidence_kind: &'static str,
pub gates: &'static [&'static str],
pub requires_tag: Option<&'static str>,
}
pub const ACTS: [Act; 7] = [
Act {
name: "spec",
task_kind: "act:spec",
evidence_kind: "spec",
gates: &[],
requires_tag: None,
},
Act {
name: "plan",
task_kind: "act:plan",
evidence_kind: "plan",
gates: &["plan_approval"],
requires_tag: None,
},
Act {
name: "execute",
task_kind: "act:execute",
evidence_kind: "diff",
gates: &[],
requires_tag: None,
},
Act {
name: "verify",
task_kind: "act:verify",
evidence_kind: "verify",
gates: &["verify"],
requires_tag: None,
},
Act {
name: "sweep",
task_kind: "act:sweep",
evidence_kind: "sweep",
gates: &[],
requires_tag: None,
},
Act {
name: "eval",
task_kind: "act:eval",
evidence_kind: "eval",
gates: &["eval"],
requires_tag: Some("ai"),
},
Act {
name: "ship",
task_kind: "act:ship",
evidence_kind: "review",
gates: &[REVIEW_GATE],
requires_tag: None,
},
];
pub const SHIP_AUDITS: [(&str, &str); 9] = [
("security", "audit:security"),
("auth", "audit:security"),
("secrets", "audit:security"),
("external-system", "audit:security"),
("billing", "audit:security"),
("ui", "audit:ui"),
("frontend", "audit:ui"),
("infra", "audit:infra"),
("data-migration", "audit:migration"),
];
pub const TAGS: [&str; 11] = [
"ai",
"auth",
"billing",
"data-migration",
"external-system",
"frontend",
"infra",
"observability",
"secrets",
"security",
"ui",
];
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[error("no tag {0:?}; one of: {known}", known = TAGS.join(", "))]
pub struct UnknownTag(pub String);
#[derive(Debug, PartialEq, Eq)]
pub struct PhaseAct {
pub act: &'static Act,
pub gates: Vec<&'static str>,
}
pub fn acts_for(tags: &[&str]) -> Result<Vec<PhaseAct>, UnknownTag> {
if let Some(unknown) = tags.iter().find(|tag| !TAGS.contains(tag)) {
return Err(UnknownTag((*unknown).to_owned()));
}
Ok(ACTS
.iter()
.filter(|act| act.requires_tag.is_none_or(|tag| tags.contains(&tag)))
.map(|act| {
let mut gates: Vec<&'static str> = act.gates.to_vec();
if act.gates.contains(&REVIEW_GATE) {
for (tag, audit) in SHIP_AUDITS {
if tags.contains(&tag) && !gates.contains(&audit) {
gates.push(audit);
}
}
}
PhaseAct { act, gates }
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn names(acts: &[PhaseAct]) -> Vec<&'static str> {
acts.iter().map(|resolved| resolved.act.name).collect()
}
#[test]
fn the_seven_acts_hold_lifecycle_order() {
let names: Vec<&str> = ACTS.iter().map(|act| act.name).collect();
assert_eq!(
names,
["spec", "plan", "execute", "verify", "sweep", "eval", "ship"]
);
for act in &ACTS {
assert_eq!(act.task_kind, format!("act:{}", act.name));
}
}
#[test]
fn every_kind_the_template_names_is_a_modest_open_string() {
let mut kinds: Vec<&str> = Vec::new();
for act in &ACTS {
kinds.extend([act.name, act.task_kind, act.evidence_kind]);
kinds.extend(act.gates);
}
kinds.extend(SHIP_AUDITS.iter().map(|(_, audit)| *audit));
kinds.extend(TAGS);
for kind in kinds {
assert!(!kind.is_empty() && kind.len() <= 32, "{kind:?}");
assert!(
kind.chars().all(|c| c.is_ascii_graphic()),
"{kind:?} would not survive a rendered row"
);
}
for (tag, _) in SHIP_AUDITS {
assert!(
TAGS.contains(&tag),
"{tag:?} fires an audit nobody can declare"
);
}
for tag in TAGS {
let fires = SHIP_AUDITS.iter().any(|(audit_tag, _)| *audit_tag == tag);
let admits = ACTS.iter().any(|act| act.requires_tag == Some(tag));
assert!(
fires || admits || tag == "observability",
"{tag:?} is declarable and does nothing"
);
}
}
#[test]
fn eval_rides_only_the_ai_tag() {
let untagged = acts_for(&[]).expect("no tags is a legal declaration");
assert_eq!(
names(&untagged),
["spec", "plan", "execute", "verify", "sweep", "ship"]
);
let tagged = acts_for(&["ai"]).expect("known tag");
assert_eq!(
names(&tagged),
["spec", "plan", "execute", "verify", "sweep", "eval", "ship"]
);
assert_eq!(tagged[5].gates, ["eval"]);
}
#[test]
fn ship_fires_the_audits_the_tags_declare_in_table_order() {
let acts = acts_for(&["ui", "infra", "security"]).expect("known tags");
let ship = acts.last().expect("ship is always last");
assert_eq!(
ship.gates,
["review", "audit:security", "audit:ui", "audit:infra"]
);
let untagged = acts_for(&[]).expect("no tags");
assert_eq!(untagged.last().expect("ship").gates, ["review"]);
}
#[test]
fn two_tags_naming_one_audit_fire_it_once() {
let acts = acts_for(&["secrets", "auth"]).expect("known tags");
assert_eq!(
acts.last().expect("ship").gates,
["review", "audit:security"]
);
let classified = acts_for(&["observability"]).expect("known tag");
assert_eq!(classified.last().expect("ship").gates, ["review"]);
}
#[test]
fn an_unknown_tag_is_refused_rather_than_silently_unaudited() {
let refusal = acts_for(&["ui", "securty"]).expect_err("misspelled");
assert_eq!(refusal, UnknownTag("securty".to_owned()));
let message = refusal.to_string();
assert!(message.contains("securty"), "{message}");
assert!(message.contains("security"), "{message}");
}
}