story-kit: merge 247_story_human_qa_gate_with_rejection_flow

This commit is contained in:
Dave
2026-03-18 15:45:45 +00:00
parent 1faacd7812
commit 9352443555
11 changed files with 557 additions and 26 deletions

View File

@@ -9,6 +9,7 @@ pub struct StoryMetadata {
pub merge_failure: Option<String>,
pub agent: Option<String>,
pub review_hold: Option<bool>,
pub manual_qa: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -33,6 +34,7 @@ struct FrontMatter {
merge_failure: Option<String>,
agent: Option<String>,
review_hold: Option<bool>,
manual_qa: Option<bool>,
}
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
@@ -67,6 +69,7 @@ fn build_metadata(front: FrontMatter) -> StoryMetadata {
merge_failure: front.merge_failure,
agent: front.agent,
review_hold: front.review_hold,
manual_qa: front.manual_qa,
}
}
@@ -193,6 +196,32 @@ pub fn set_front_matter_field(contents: &str, key: &str, value: &str) -> String
result
}
/// Append rejection notes to a story file body.
///
/// Adds a `## QA Rejection Notes` section at the end of the file so the coder
/// agent can see what needs fixing.
pub fn write_rejection_notes(path: &Path, notes: &str) -> Result<(), String> {
let contents =
fs::read_to_string(path).map_err(|e| format!("Failed to read story file: {e}"))?;
let section = format!("\n\n## QA Rejection Notes\n\n{notes}\n");
let updated = format!("{contents}{section}");
fs::write(path, &updated).map_err(|e| format!("Failed to write story file: {e}"))?;
Ok(())
}
/// Check whether a story requires manual QA (defaults to true).
pub fn requires_manual_qa(path: &Path) -> bool {
let contents = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return true,
};
match parse_front_matter(&contents) {
Ok(meta) => meta.manual_qa.unwrap_or(true),
Err(_) => true,
}
}
pub fn parse_unchecked_todos(contents: &str) -> Vec<String> {
contents
.lines()
@@ -367,4 +396,45 @@ workflow: tdd
assert!(contents.contains("review_hold: true"));
assert!(contents.contains("name: My Spike"));
}
#[test]
fn parses_manual_qa_from_front_matter() {
let input = "---\nname: Story\nmanual_qa: false\n---\n# Story\n";
let meta = parse_front_matter(input).expect("front matter");
assert_eq!(meta.manual_qa, Some(false));
}
#[test]
fn manual_qa_defaults_to_none() {
let input = "---\nname: Story\n---\n# Story\n";
let meta = parse_front_matter(input).expect("front matter");
assert_eq!(meta.manual_qa, None);
}
#[test]
fn requires_manual_qa_defaults_true() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("story.md");
std::fs::write(&path, "---\nname: Test\n---\n# Story\n").unwrap();
assert!(requires_manual_qa(&path));
}
#[test]
fn requires_manual_qa_false_when_set() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("story.md");
std::fs::write(&path, "---\nname: Test\nmanual_qa: false\n---\n# Story\n").unwrap();
assert!(!requires_manual_qa(&path));
}
#[test]
fn write_rejection_notes_appends_section() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("story.md");
std::fs::write(&path, "---\nname: Test\n---\n# Story\n").unwrap();
write_rejection_notes(&path, "Button color is wrong").unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("## QA Rejection Notes"));
assert!(contents.contains("Button color is wrong"));
}
}