wip(929): stage 10 sweep — production callsites move to CRDT, yaml_legacy shrinks
After 932 (review_hold register) and 933 (item_type + epic registers), the
remaining production yaml_legacy callers all had typed CRDT equivalents.
Migrated:
- agents/lifecycle.rs:
- transition_to_merge_failure writes to MergeJob.error CRDT entry instead
of YAML body. The legacy `merge_failure: "..."` front-matter write is gone.
- reject_story_from_qa inlines the QA-rejection notes append; no longer
needs yaml_legacy::write_rejection_notes_to_content.
- fields_to_clear_transform helper deleted along with all five callers —
blocked/retry_count/merge_failure are typed CRDT fields now, so clearing
the equivalent YAML keys is redundant.
- http/workflow/pipeline.rs:
- load_pipeline_state reads merge_failure from MergeJob.error (mirrors
status_tools.rs).
- validate_story_dirs checks the typed CRDT `name` register instead of
parsing YAML front matter.
- http/mcp/status_tools.rs: review_hold reads the typed CRDT register
(yaml_residue wrap was the last one in this file).
- http/mcp/story_tools/criteria.rs: story_name reads from CRDT.
- service/agents/mod.rs::get_work_item_content: name/agent come from CRDT.
- service/notifications/io/mod.rs::read_story_name: same.
- http/workflow/bug_ops/{bug,refactor}.rs: name-fallback paths drop YAML
parsing in favour of the CRDT-derived item.name.
Dead helpers removed from db/yaml_legacy.rs:
yaml_residue, write_merge_failure_in_content, write_rejection_notes_to_content,
clear_front_matter_field_in_content, write_review_hold_in_content,
clear_front_matter_field, write_review_hold (the last four shipped in 932).
Remaining surface: FrontMatter / StoryMetadata structs, parse_front_matter,
set_front_matter_field — kept for `coverage_baseline` writes via
test_results.rs and the generic update_story front_matter escape hatch.
Test fixtures rewritten to seed the CRDT register instead of relying on
YAML parsing during write_item_with_content:
- has_review_hold_returns_* tests
- item_type_from_id_uses_crdt_register_for_numeric_ids
- tool_list_epics_shows_member_rollup
- get_work_item_content (both copies — http/agents + service/agents)
- validate_story_dirs_missing_name_in_crdt
- server_side_merge_*_sets_merge_failure (assert MergeJob.error, not YAML)
cargo fmt --check, clippy --all-targets -- -D warnings, and the
2856-test suite all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -265,6 +265,7 @@ fn make_stage_dir(root: &path::Path, stage: &str) {
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_work_item_content_returns_content_from_backlog() {
|
||||
crate::crdt_state::init_for_test();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = tmp.path();
|
||||
make_stage_dir(root, "1_backlog");
|
||||
@@ -273,6 +274,19 @@ async fn get_work_item_content_returns_content_from_backlog() {
|
||||
"---\nname: \"Foo Story\"\n---\n\n# Story 42: Foo Story\n\nSome content.",
|
||||
)
|
||||
.unwrap();
|
||||
// Story 929: name lives in the typed CRDT register, not in YAML on disk.
|
||||
crate::crdt_state::write_item(
|
||||
"42_story_foo",
|
||||
"1_backlog",
|
||||
Some("Foo Story"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let ctx = AppContext::new_test(root.to_path_buf());
|
||||
let api = AgentsApi { ctx: Arc::new(ctx) };
|
||||
let result = api
|
||||
@@ -287,6 +301,7 @@ async fn get_work_item_content_returns_content_from_backlog() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_work_item_content_returns_content_from_current() {
|
||||
crate::crdt_state::init_for_test();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = tmp.path();
|
||||
make_stage_dir(root, "2_current");
|
||||
@@ -295,6 +310,18 @@ async fn get_work_item_content_returns_content_from_current() {
|
||||
"---\nname: \"Bar Story\"\n---\n\nBar content.",
|
||||
)
|
||||
.unwrap();
|
||||
crate::crdt_state::write_item(
|
||||
"43_story_bar",
|
||||
"2_current",
|
||||
Some("Bar Story"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let ctx = AppContext::new_test(root.to_path_buf());
|
||||
let api = AgentsApi { ctx: Arc::new(ctx) };
|
||||
let result = api
|
||||
|
||||
@@ -216,11 +216,10 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
|
||||
front_matter.insert("merge_failure".to_string(), json!(mf));
|
||||
}
|
||||
|
||||
// review_hold has no CRDT register yet — see story 932. Wrap the
|
||||
// yaml_legacy read in `yaml_residue(...)` so it's grep-findable.
|
||||
if let Ok(meta) =
|
||||
crate::db::yaml_legacy::yaml_residue(crate::db::yaml_legacy::parse_front_matter(&contents))
|
||||
&& let Some(true) = meta.review_hold
|
||||
// review_hold is a typed CRDT register (story 932).
|
||||
if crate::crdt_state::read_item(story_id)
|
||||
.map(|v| v.review_hold())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
front_matter.insert("review_hold".to_string(), json!(true));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use crate::agents::{
|
||||
close_bug_to_archive, feature_branch_has_unmerged_changes, move_story_to_done,
|
||||
};
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::workflow::{
|
||||
add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use crate::agents::{
|
||||
close_bug_to_archive, feature_branch_has_unmerged_changes, move_story_to_done,
|
||||
};
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::workflow::{
|
||||
add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file,
|
||||
@@ -38,7 +37,8 @@ pub(crate) fn tool_get_story_todos(args: &Value, ctx: &AppContext) -> Result<Str
|
||||
let contents = crate::http::workflow::read_story_content(&root, story_id)
|
||||
.map_err(|_| format!("Story file not found: {story_id}.md"))?;
|
||||
|
||||
let story_name = parse_front_matter(&contents).ok().and_then(|m| m.name);
|
||||
let story_name =
|
||||
crate::crdt_state::read_item(story_id).and_then(|v| v.name().map(str::to_string));
|
||||
let todos = parse_unchecked_todos(&contents);
|
||||
|
||||
serde_json::to_string_pretty(&json!({
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use crate::agents::{
|
||||
close_bug_to_archive, feature_branch_has_unmerged_changes, move_story_to_done,
|
||||
};
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::workflow::{
|
||||
add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use crate::agents::{
|
||||
close_bug_to_archive, feature_branch_has_unmerged_changes, move_story_to_done,
|
||||
};
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::workflow::{
|
||||
add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Bug-item creation and listing operations.
|
||||
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use std::path::Path;
|
||||
|
||||
use super::super::{next_item_number, slugify_name, write_story_content};
|
||||
@@ -90,16 +89,10 @@ pub(super) fn is_bug_item(stem: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Extract bug name from content (heading or front matter).
|
||||
/// Extract bug name from the `# Bug N: name` heading (last-resort fallback when
|
||||
/// the CRDT name register is empty).
|
||||
#[allow(clippy::string_slice)] // colon_pos from find(": "); +2 skips ASCII ": " → valid boundary
|
||||
pub(super) fn extract_bug_name_from_content(content: &str) -> Option<String> {
|
||||
// Try front matter first.
|
||||
if let Ok(meta) = parse_front_matter(content)
|
||||
&& let Some(name) = meta.name
|
||||
{
|
||||
return Some(name);
|
||||
}
|
||||
// Fallback: heading.
|
||||
for line in content.lines() {
|
||||
if let Some(rest) = line.strip_prefix("# Bug ")
|
||||
&& let Some(colon_pos) = rest.find(": ")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Refactor-item creation and listing operations.
|
||||
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use std::path::Path;
|
||||
|
||||
use super::super::{next_item_number, slugify_name, write_story_content};
|
||||
@@ -100,16 +99,10 @@ pub fn list_refactor_files(_root: &Path) -> Result<Vec<(String, String)>, String
|
||||
}
|
||||
let sid = item.story_id.0;
|
||||
let name = if item.name.is_empty() {
|
||||
None
|
||||
sid.clone()
|
||||
} else {
|
||||
Some(item.name)
|
||||
}
|
||||
.or_else(|| {
|
||||
crate::db::read_content(&sid)
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.and_then(|m| m.name)
|
||||
})
|
||||
.unwrap_or_else(|| sid.clone());
|
||||
item.name
|
||||
};
|
||||
refactors.push((sid, name));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Pipeline state — types and loading functions for the story pipeline.
|
||||
|
||||
use crate::agents::AgentStatus;
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use crate::http::context::AppContext;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -96,15 +95,13 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
let agent = agent_map.get(sid).cloned();
|
||||
|
||||
// Stories 929/932/933: review_hold, qa_mode, epic_id come from typed
|
||||
// CRDT registers. merge_failure remains in YAML for now (tracked by
|
||||
// 929 stage 10's sweep).
|
||||
// CRDT registers. merge_failure detail lives on the MergeJob CRDT
|
||||
// entry (same as status_tools.rs).
|
||||
let view = crate::crdt_state::read_item(sid);
|
||||
let review_hold = view.as_ref().map(|v| v.review_hold()).filter(|b| *b);
|
||||
let qa = view.as_ref().and_then(|v| v.qa_mode().map(str::to_string));
|
||||
let epic_id = view.as_ref().and_then(|v| v.epic().map(str::to_string));
|
||||
let merge_failure = crate::db::read_content(sid)
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.and_then(|meta| meta.merge_failure);
|
||||
let merge_failure = crate::crdt_state::read_merge_job(sid).and_then(|j| j.error);
|
||||
|
||||
let story = UpcomingStory {
|
||||
story_id: sid.clone(),
|
||||
@@ -252,7 +249,10 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
|
||||
Ok(stories)
|
||||
}
|
||||
|
||||
/// Validate story front matter for all backlog and current items.
|
||||
/// Validate stories for all backlog and current items.
|
||||
///
|
||||
/// Story 929: validation reads the typed CRDT `name` register; the legacy YAML
|
||||
/// front-matter parse is gone.
|
||||
pub fn validate_story_dirs(_root: &Path) -> Result<Vec<StoryValidationResult>, String> {
|
||||
use crate::pipeline_state::Stage;
|
||||
|
||||
@@ -260,44 +260,26 @@ pub fn validate_story_dirs(_root: &Path) -> Result<Vec<StoryValidationResult>, S
|
||||
|
||||
let typed_items = crate::pipeline_state::read_all_typed();
|
||||
for item in typed_items {
|
||||
// Only validate backlog and current items (matching the old behaviour).
|
||||
if !matches!(item.stage, Stage::Backlog | Stage::Coding) {
|
||||
continue;
|
||||
}
|
||||
let story_id = item.story_id.0.clone();
|
||||
let name = crate::crdt_state::read_item(&story_id)
|
||||
.and_then(|v| v.name().map(str::to_string))
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
match crate::db::read_content(&story_id) {
|
||||
Some(contents) => match parse_front_matter(&contents) {
|
||||
Ok(meta) => {
|
||||
let mut errors = Vec::new();
|
||||
if meta.name.is_none() {
|
||||
errors.push("Missing 'name' field".to_string());
|
||||
}
|
||||
if errors.is_empty() {
|
||||
results.push(StoryValidationResult {
|
||||
story_id,
|
||||
valid: true,
|
||||
error: None,
|
||||
});
|
||||
} else {
|
||||
results.push(StoryValidationResult {
|
||||
story_id,
|
||||
valid: false,
|
||||
error: Some(errors.join("; ")),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => results.push(StoryValidationResult {
|
||||
story_id,
|
||||
valid: false,
|
||||
error: Some(e.to_string()),
|
||||
}),
|
||||
},
|
||||
None => results.push(StoryValidationResult {
|
||||
if name.is_some() {
|
||||
results.push(StoryValidationResult {
|
||||
story_id,
|
||||
valid: true,
|
||||
error: None,
|
||||
});
|
||||
} else {
|
||||
results.push(StoryValidationResult {
|
||||
story_id,
|
||||
valid: false,
|
||||
error: Some("No content found in content store".to_string()),
|
||||
}),
|
||||
error: Some("Missing 'name' field".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,13 +563,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_story_dirs_missing_front_matter() {
|
||||
fn validate_story_dirs_missing_name_in_crdt() {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
// Item exists in CRDT but with no name register set.
|
||||
crate::db::write_item_with_content(
|
||||
"9875_story_no_fm",
|
||||
"2_current",
|
||||
"# No front matter\n",
|
||||
crate::db::ItemMeta::from_yaml("# No front matter\n"),
|
||||
crate::db::ItemMeta::default(),
|
||||
);
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -597,7 +581,7 @@ mod tests {
|
||||
.find(|r| r.story_id == "9875_story_no_fm")
|
||||
.unwrap();
|
||||
assert!(!r.valid);
|
||||
assert_eq!(r.error.as_deref(), Some("Missing front matter"));
|
||||
assert_eq!(r.error.as_deref(), Some("Missing 'name' field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user