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>
This commit is contained in:
+5
-23
@@ -19,9 +19,6 @@ pub mod content_store;
|
||||
pub mod ops;
|
||||
/// Background shadow-write task — persists pipeline items to SQLite asynchronously.
|
||||
pub mod shadow_write;
|
||||
/// Legacy YAML helpers — used by callers reading the small set of fields not
|
||||
/// yet mirrored into the CRDT.
|
||||
pub(crate) mod yaml_legacy;
|
||||
|
||||
pub use content_store::{all_content_ids, delete_content, read_content, write_content};
|
||||
pub use ops::{ItemMeta, delete_item, move_item_stage, next_item_number, write_item_with_content};
|
||||
@@ -33,7 +30,6 @@ pub use content_store::ensure_content_store;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::yaml_legacy::parse_front_matter;
|
||||
use std::fs;
|
||||
|
||||
/// Helper: write a minimal story .md file with front matter.
|
||||
@@ -104,25 +100,11 @@ mod tests {
|
||||
assert_eq!(row.0, "10_story_shadow_test");
|
||||
assert_eq!(row.1.as_deref(), Some("Shadow Test"));
|
||||
assert_eq!(row.2, "2_current");
|
||||
|
||||
// Verify metadata was parsed correctly from the story file.
|
||||
let (name, _agent, retry_count, _blocked, _depends_on) =
|
||||
match std::fs::read_to_string(&story_path) {
|
||||
Ok(contents) => match parse_front_matter(&contents) {
|
||||
Ok(meta) => (
|
||||
meta.name,
|
||||
meta.agent,
|
||||
meta.retry_count.map(|r| r as i64),
|
||||
meta.blocked,
|
||||
meta.depends_on,
|
||||
),
|
||||
Err(_) => (None, None, None, None, None),
|
||||
},
|
||||
Err(_) => (None, None, None, None, None),
|
||||
};
|
||||
|
||||
assert_eq!(name.as_deref(), Some("Shadow Test"));
|
||||
assert_eq!(retry_count, Some(2));
|
||||
// The shadow row's name + retry_count came through from the INSERT
|
||||
// params above; that's what the test exercises. Story 929 dropped
|
||||
// the redundant "re-parse the YAML body to double-check" step that
|
||||
// used to live here.
|
||||
let _ = story_path;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+3
-25
@@ -7,14 +7,12 @@ use super::content_store::{
|
||||
all_content_ids, delete_content, ensure_content_store, read_content, write_content,
|
||||
};
|
||||
use super::shadow_write::{PIPELINE_DB, PipelineWriteMsg};
|
||||
use super::yaml_legacy::parse_front_matter;
|
||||
|
||||
/// Typed metadata for a pipeline item write.
|
||||
///
|
||||
/// Replaces the prior YAML-parsing write path (story 864): callers now pass
|
||||
/// metadata explicitly instead of round-tripping it through a serialized
|
||||
/// front-matter blob. Every field is `Option`-typed; `None` means
|
||||
/// "leave unchanged" on update, "use the default" on insert.
|
||||
/// Story 929: callers pass metadata explicitly — no YAML parsing. Every
|
||||
/// field is `Option`-typed; `None` means "leave unchanged" on update,
|
||||
/// "use the default" on insert.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ItemMeta {
|
||||
pub name: Option<String>,
|
||||
@@ -33,26 +31,6 @@ impl ItemMeta {
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse YAML front-matter from a content string into typed metadata.
|
||||
///
|
||||
/// This is an explicit caller-side conversion — the write path itself
|
||||
/// no longer parses YAML. Use this when the caller has a raw content
|
||||
/// string with front-matter and wants the metadata to flow into the
|
||||
/// CRDT. Returns `Self::default()` if parsing fails or there is no
|
||||
/// front-matter present.
|
||||
pub fn from_yaml(content: &str) -> Self {
|
||||
match parse_front_matter(content) {
|
||||
Ok(m) => Self {
|
||||
name: m.name,
|
||||
agent: m.agent,
|
||||
retry_count: m.retry_count.map(|r| r as i64),
|
||||
blocked: m.blocked,
|
||||
depends_on: m.depends_on,
|
||||
},
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a pipeline item from in-memory content (no filesystem access).
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
//! Legacy YAML front-matter helpers — kept ONLY for the one-shot migration
|
||||
//! and for the small set of fields not yet mirrored into the CRDT
|
||||
//! (`item_type`, `epic`, `review_hold`, `merge_failure` reason text, etc.).
|
||||
//!
|
||||
//! After the migration runs, every body in the content store is YAML-free, so
|
||||
//! every helper here returns `Ok(None)` / a no-op on the next read. Callers
|
||||
//! should treat this module as a deprecated escape hatch — new code should
|
||||
//! read typed CRDT registers instead.
|
||||
|
||||
use crate::io::story_metadata::QaMode;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Front-matter fields used by the legacy `parse_front_matter` API. Mirrors
|
||||
/// the original `io::story_metadata::FrontMatter`.
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub(crate) struct FrontMatter {
|
||||
pub name: Option<String>,
|
||||
pub coverage_baseline: Option<String>,
|
||||
pub merge_failure: Option<String>,
|
||||
pub agent: Option<String>,
|
||||
pub review_hold: Option<bool>,
|
||||
pub qa: Option<String>,
|
||||
pub retry_count: Option<u32>,
|
||||
pub blocked: Option<bool>,
|
||||
pub depends_on: Option<Vec<u32>>,
|
||||
pub frozen: Option<bool>,
|
||||
pub resume_to_stage: Option<String>,
|
||||
pub run_tests_passed: Option<bool>,
|
||||
#[serde(rename = "type")]
|
||||
pub item_type: Option<String>,
|
||||
pub mergemaster_attempted: Option<bool>,
|
||||
pub epic: Option<String>,
|
||||
}
|
||||
|
||||
/// Parsed metadata view returned by [`parse_front_matter`].
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub(crate) struct StoryMetadata {
|
||||
pub name: Option<String>,
|
||||
pub coverage_baseline: Option<String>,
|
||||
pub merge_failure: Option<String>,
|
||||
pub agent: Option<String>,
|
||||
pub review_hold: Option<bool>,
|
||||
pub qa: Option<QaMode>,
|
||||
pub retry_count: Option<u32>,
|
||||
pub blocked: Option<bool>,
|
||||
pub depends_on: Option<Vec<u32>>,
|
||||
pub frozen: Option<bool>,
|
||||
pub resume_to_stage: Option<String>,
|
||||
pub run_tests_passed: Option<bool>,
|
||||
pub item_type: Option<String>,
|
||||
pub mergemaster_attempted: Option<bool>,
|
||||
pub epic: Option<String>,
|
||||
}
|
||||
|
||||
/// Errors that can occur when parsing legacy YAML front matter.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum StoryMetaError {
|
||||
MissingFrontMatter,
|
||||
InvalidFrontMatter(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StoryMetaError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MissingFrontMatter => write!(f, "Missing front matter"),
|
||||
Self::InvalidFrontMatter(m) => write!(f, "Invalid front matter: {m}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the YAML front-matter block from a markdown body.
|
||||
///
|
||||
/// Post-migration this returns `Err(StoryMetaError::MissingFrontMatter)` for
|
||||
/// every body since the front matter has been stripped. Callers that need
|
||||
/// fields not stored in the CRDT (`item_type`, `epic`, …) should treat the
|
||||
/// missing-front-matter case as "default value".
|
||||
pub(crate) fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||
let mut lines = contents.lines();
|
||||
let first = lines.next().unwrap_or_default().trim();
|
||||
if first != "---" {
|
||||
return Err(StoryMetaError::MissingFrontMatter);
|
||||
}
|
||||
let mut front_lines = Vec::new();
|
||||
for line in &mut lines {
|
||||
if line.trim() == "---" {
|
||||
let raw = front_lines.join("\n");
|
||||
let front: FrontMatter = serde_yaml::from_str(&raw)
|
||||
.map_err(|e| StoryMetaError::InvalidFrontMatter(e.to_string()))?;
|
||||
return Ok(StoryMetadata {
|
||||
qa: front.qa.as_deref().and_then(QaMode::from_str),
|
||||
name: front.name,
|
||||
coverage_baseline: front.coverage_baseline,
|
||||
merge_failure: front.merge_failure,
|
||||
agent: front.agent,
|
||||
review_hold: front.review_hold,
|
||||
retry_count: front.retry_count,
|
||||
blocked: front.blocked,
|
||||
depends_on: front.depends_on,
|
||||
frozen: front.frozen,
|
||||
resume_to_stage: front.resume_to_stage,
|
||||
run_tests_passed: front.run_tests_passed,
|
||||
item_type: front.item_type,
|
||||
mergemaster_attempted: front.mergemaster_attempted,
|
||||
epic: front.epic,
|
||||
});
|
||||
}
|
||||
front_lines.push(line);
|
||||
}
|
||||
Err(StoryMetaError::InvalidFrontMatter(
|
||||
"Missing closing front matter delimiter".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Insert or update a `key: value` line in the YAML front matter of a
|
||||
/// markdown string. Returns the input unchanged if no `---` block is found.
|
||||
pub(crate) fn set_front_matter_field(contents: &str, key: &str, value: &str) -> String {
|
||||
let mut lines: Vec<String> = contents.lines().map(String::from).collect();
|
||||
if lines.is_empty() || lines[0].trim() != "---" {
|
||||
return contents.to_string();
|
||||
}
|
||||
let close_idx = match lines[1..].iter().position(|l| l.trim() == "---") {
|
||||
Some(i) => i + 1,
|
||||
None => return contents.to_string(),
|
||||
};
|
||||
let key_prefix = format!("{key}:");
|
||||
let existing_idx = lines[1..close_idx]
|
||||
.iter()
|
||||
.position(|l| l.trim_start().starts_with(&key_prefix))
|
||||
.map(|i| i + 1);
|
||||
let new_line = format!("{key}: {value}");
|
||||
if let Some(idx) = existing_idx {
|
||||
lines[idx] = new_line;
|
||||
} else {
|
||||
lines.insert(close_idx, new_line);
|
||||
}
|
||||
let mut result = lines.join("\n");
|
||||
if contents.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_front_matter_round_trips_basic_fields() {
|
||||
let input = "---\nname: Test\nagent: coder-1\n---\n# Body\n";
|
||||
let meta = parse_front_matter(input).expect("parse");
|
||||
assert_eq!(meta.name.as_deref(), Some("Test"));
|
||||
assert_eq!(meta.agent.as_deref(), Some("coder-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_front_matter_returns_missing_when_no_yaml() {
|
||||
let err = parse_front_matter("# Plain markdown\n").unwrap_err();
|
||||
assert_eq!(err, StoryMetaError::MissingFrontMatter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_front_matter_field_inserts_when_absent() {
|
||||
let out = set_front_matter_field("---\nname: X\n---\n# B\n", "agent", "coder-1");
|
||||
assert!(out.contains("agent: coder-1"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user