Files
huskies/server/src/http/workflow/bug_ops/refactor.rs
T
Timmy 69d91d7707 feat(929): delete db/yaml_legacy.rs entirely — CRDT is the sole source of truth
Final 929 sweep: every YAML-shaped helper is gone. No production code
parses or writes YAML front matter anywhere.

Surface removed:
- db/yaml_legacy.rs (FrontMatter/StoryMetadata structs, parse_front_matter,
  set_front_matter_field, yaml_residue marker) — file deleted.
- ItemMeta::from_yaml — deleted; callers pass typed ItemMeta::named(...) or
  ItemMeta::default() and use typed CRDT setters (set_depends_on,
  set_blocked, set_retry_count, set_agent, set_qa_mode, set_review_hold,
  set_item_type, set_epic, set_mergemaster_attempted) for the rest.
- write_coverage_baseline_to_story_file + read_coverage_percent_from_json —
  the coverage_baseline YAML field was write-only (nothing read it back);
  removed along with its caller in agent_tools/lifecycle.rs.
- update_story_in_file's generic `front_matter` HashMap parameter —
  tool_update_story now intercepts every known field name and routes it
  to a typed CRDT setter; unknown keys are rejected with an explicit error
  pointing at the typed setters. The function only takes user_story /
  description sections now.
- All 117 ItemMeta::from_yaml callsites migrated. Where tests previously
  passed a YAML-shaped content blob and relied on the helper to extract
  name/depends_on/blocked/agent/qa, they now pass:
    write_item_with_content(id, stage, content, ItemMeta::named("Foo"))
    crate::crdt_state::set_depends_on(id, &[...])    // when needed
    crate::crdt_state::set_blocked(id, true)         // when needed
    crate::crdt_state::set_agent(id, Some("..."))    // when needed
- write_story_content + write_story_file (test helper) now take an
  explicit `name: Option<&str>` instead of parsing it from content.
- db::ops::move_item_stage stopped re-parsing YAML on every stage
  transition; metadata is read straight from the CRDT view when mirroring
  the row into SQLite.

New CRDT setters added for symmetry:
- crdt_state::set_name (mirrors set_agent — explicit name updates).

cargo fmt --check, clippy --all-targets -- -D warnings, and the
2830-test suite all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:55:25 +01:00

112 lines
3.6 KiB
Rust

//! Refactor-item creation and listing operations.
use std::path::Path;
use super::super::{next_item_number, slugify_name, write_story_content};
/// Create a refactor work item and store it in the database.
///
/// Returns the refactor_id (e.g. `"5"`).
pub fn create_refactor_file(
root: &Path,
name: &str,
description: Option<&str>,
acceptance_criteria: Option<&[String]>,
depends_on: Option<&[u32]>,
) -> Result<String, String> {
let refactor_number = next_item_number(root)?;
let slug = slugify_name(name);
if slug.is_empty() {
return Err("Name must contain at least one alphanumeric character.".to_string());
}
let refactor_id = format!("{refactor_number}");
let mut content = String::new();
content.push_str("---\n");
content.push_str("type: refactor\n");
content.push_str(&format!("name: \"{}\"\n", name.replace('"', "\\\"")));
if let Some(deps) = depends_on.filter(|d| !d.is_empty()) {
let nums: Vec<String> = deps.iter().map(|n| n.to_string()).collect();
content.push_str(&format!("depends_on: [{}]\n", nums.join(", ")));
}
content.push_str("---\n\n");
content.push_str(&format!("# Refactor {refactor_number}: {name}\n\n"));
content.push_str("## Current State\n\n");
content.push_str("- TBD\n\n");
content.push_str("## Desired State\n\n");
if let Some(desc) = description {
content.push_str(desc);
content.push('\n');
} else {
content.push_str("- TBD\n");
}
content.push('\n');
content.push_str("## Acceptance Criteria\n\n");
if let Some(criteria) = acceptance_criteria {
for criterion in criteria {
content.push_str(&format!("- [ ] {criterion}\n"));
}
} else {
content.push_str("- [ ] Refactoring complete and all tests pass\n");
}
content.push('\n');
content.push_str("## Out of Scope\n\n");
content.push_str("- TBD\n");
// Write to database content store and CRDT.
write_story_content(root, &refactor_id, "1_backlog", &content, Some(name));
// Sync depends_on to the typed CRDT register.
crate::crdt_state::set_depends_on(&refactor_id, depends_on.unwrap_or(&[]));
// Story 933: typed CRDT register for item_type.
crate::crdt_state::set_item_type(&refactor_id, Some("refactor"));
Ok(refactor_id)
}
/// Returns true if the item stem is a refactor item.
///
/// Checks the slug-based ID format first (e.g. `"5_refactor_split_agents_rs"`),
/// then consults the typed CRDT `item_type` register for numeric-only IDs (933).
pub(super) fn is_refactor_item(stem: &str) -> bool {
let after_num = stem.trim_start_matches(|c: char| c.is_ascii_digit());
if after_num.starts_with("_refactor_") {
return true;
}
if after_num.is_empty() {
return crate::crdt_state::read_item(stem)
.and_then(|v| v.item_type().map(str::to_string))
.map(|t| t == "refactor")
.unwrap_or(false);
}
false
}
/// List all open refactors from CRDT + content store.
///
/// Returns a sorted list of `(refactor_id, name)` pairs.
pub fn list_refactor_files(_root: &Path) -> Result<Vec<(String, String)>, String> {
let mut refactors = Vec::new();
for item in crate::pipeline_state::read_all_typed() {
if !matches!(item.stage, crate::pipeline_state::Stage::Backlog)
|| !is_refactor_item(&item.story_id.0)
{
continue;
}
let sid = item.story_id.0;
let name = if item.name.is_empty() {
sid.clone()
} else {
item.name
};
refactors.push((sid, name));
}
refactors.sort_by(|a, b| a.0.cmp(&b.0));
Ok(refactors)
}