wip(929): stage 1 — migrate chat/commands/* off yaml_legacy
Each chat command that previously read parse_front_matter for story metadata (name, agent, depends_on, blocked, retry_count, merge_failure, qa_mode) now reads from the typed CRDT API: - WorkItem (via crdt_state::read_item) for pipeline-item registers. - MergeJobView (via crdt_state::read_merge_job) for the merge failure detail text, which has its own LWW-map CRDT entry. Files migrated: depends.rs, freeze.rs, move_story.rs, overview.rs, status/render.rs, triage.rs, unblock.rs, unreleased.rs. unblock.rs: also removes the legacy front-matter cleanup branch that fired when the typed Blocked→Coding transition failed. Post-929 there is no YAML on disk to clean; the fallback now just resets retry_count in the CRDT. triage.rs: drops the YAML-only `review_hold` and `coverage_baseline` fields from the dump. These have no CRDT register and were never load-bearing on the triage output; if needed later, add a CRDT register and surface it back. Tests: - The three status/render merge-failure rendering tests now seed a MergeJob CRDT entry via write_merge_job instead of writing YAML. - The unblock test that asserted YAML cleanup on disk is now an assertion on the CRDT registers (blocked=false, retry_count=0). Also re-seeded in `2_blocked` stage so the typed Blocked → Coding transition actually fires (not the fallback path). All 2855 tests pass; fmt clean; clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,6 @@
|
|||||||
//! Passing no dependency numbers clears the field entirely.
|
//! Passing no dependency numbers clears the field entirely.
|
||||||
|
|
||||||
use super::CommandContext;
|
use super::CommandContext;
|
||||||
use crate::db::yaml_legacy::parse_front_matter;
|
|
||||||
|
|
||||||
/// Handle the `depends` command.
|
/// Handle the `depends` command.
|
||||||
///
|
///
|
||||||
@@ -51,7 +50,7 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
||||||
let (story_id, _stage_dir, _path, content) =
|
let (story_id, _stage_dir, _path, _content) =
|
||||||
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
||||||
Some(found) => found,
|
Some(found) => found,
|
||||||
None => {
|
None => {
|
||||||
@@ -61,10 +60,10 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let story_name = content
|
// Story name comes from the CRDT register, not the on-disk YAML
|
||||||
.as_deref()
|
// (story 929 — CRDT is the sole source of story metadata).
|
||||||
.and_then(|c| parse_front_matter(c).ok())
|
let story_name = crate::crdt_state::read_item(&story_id)
|
||||||
.and_then(|m| m.name)
|
.and_then(|w| w.name().map(str::to_string))
|
||||||
.unwrap_or_else(|| story_id.clone());
|
.unwrap_or_else(|| story_id.clone());
|
||||||
|
|
||||||
// Write depends_on to the typed CRDT register — single source of truth.
|
// Write depends_on to the typed CRDT register — single source of truth.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
//! advancement and auto-assign until `unfreeze <number>` restores the prior stage.
|
//! advancement and auto-assign until `unfreeze <number>` restores the prior stage.
|
||||||
|
|
||||||
use super::CommandContext;
|
use super::CommandContext;
|
||||||
use crate::db::yaml_legacy::parse_front_matter;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Handle the `freeze` command.
|
/// Handle the `freeze` command.
|
||||||
@@ -93,15 +92,13 @@ fn unfreeze_by_story_id(story_id: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up the display name for a story by reading its content store entry.
|
/// Look up the display name for a story from the CRDT name register
|
||||||
|
/// (story 929 — CRDT is the sole source of story metadata).
|
||||||
///
|
///
|
||||||
/// Falls back to `story_id` if the content is missing or the front matter
|
/// Falls back to `story_id` if no CRDT entry exists.
|
||||||
/// cannot be parsed.
|
|
||||||
fn resolve_story_name(story_id: &str) -> String {
|
fn resolve_story_name(story_id: &str) -> String {
|
||||||
crate::db::read_content(story_id)
|
crate::crdt_state::read_item(story_id)
|
||||||
.as_deref()
|
.and_then(|w| w.name().map(str::to_string))
|
||||||
.and_then(|c| parse_front_matter(c).ok())
|
|
||||||
.and_then(|m| m.name)
|
|
||||||
.unwrap_or_else(|| story_id.to_string())
|
.unwrap_or_else(|| story_id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
||||||
let (story_id, _stage_dir, _path, content) =
|
let (story_id, _stage_dir, _path, _content) =
|
||||||
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
||||||
Some(found) => found,
|
Some(found) => found,
|
||||||
None => {
|
None => {
|
||||||
@@ -56,9 +56,9 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let found_name = content
|
// Display name comes from the CRDT name register (story 929).
|
||||||
.and_then(|c| crate::db::yaml_legacy::parse_front_matter(&c).ok())
|
let found_name =
|
||||||
.and_then(|m| m.name);
|
crate::crdt_state::read_item(&story_id).and_then(|w| w.name().map(str::to_string));
|
||||||
|
|
||||||
let display_name = found_name.as_deref().unwrap_or(&story_id);
|
let display_name = found_name.as_deref().unwrap_or(&story_id);
|
||||||
|
|
||||||
|
|||||||
@@ -105,13 +105,11 @@ fn find_story_merge_commit(root: &std::path::Path, num_str: &str) -> Option<Stri
|
|||||||
if hash.is_empty() { None } else { Some(hash) }
|
if hash.is_empty() { None } else { Some(hash) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the human-readable name of a story by searching CRDT then content store.
|
/// Find the human-readable name of a story from the CRDT name register
|
||||||
|
/// (story 929 — CRDT is the sole source of story metadata).
|
||||||
fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
|
fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
|
||||||
let (_, _, _, content) = crate::chat::lookup::find_story_by_number(root, num_str)?;
|
let (story_id, _, _, _) = crate::chat::lookup::find_story_by_number(root, num_str)?;
|
||||||
let content = content?;
|
crate::crdt_state::read_item(&story_id).and_then(|w| w.name().map(str::to_string))
|
||||||
crate::db::yaml_legacy::parse_front_matter(&content)
|
|
||||||
.ok()
|
|
||||||
.and_then(|m| m.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the `git show --stat` output for a commit.
|
/// Return the `git show --stat` output for a commit.
|
||||||
|
|||||||
@@ -81,14 +81,15 @@ pub(crate) fn build_status_from_items(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
// Merge-failure detail now lives on the typed MergeJob CRDT entry
|
||||||
|
// (story 929 — CRDT is the sole source of metadata).
|
||||||
let merge_failures: HashMap<String, String> = items
|
let merge_failures: HashMap<String, String> = items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| matches!(i.stage, Stage::Merge { .. }))
|
.filter(|i| matches!(i.stage, Stage::Merge { .. }))
|
||||||
.filter_map(|i| {
|
.filter_map(|i| {
|
||||||
let content = crate::db::read_content(&i.story_id.0)?;
|
let job = crate::crdt_state::read_merge_job(&i.story_id.0)?;
|
||||||
let meta = crate::db::yaml_legacy::parse_front_matter(&content).ok()?;
|
let err = job.error?;
|
||||||
let mf = meta.merge_failure?;
|
Some((i.story_id.0.clone(), err))
|
||||||
Some((i.story_id.0.clone(), mf))
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -522,14 +522,15 @@ fn merge_item_with_active_agent_shows_robot() {
|
|||||||
fn merge_item_with_failure_shows_stop_sign_and_snippet() {
|
fn merge_item_with_failure_shows_stop_sign_and_snippet() {
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
crate::db::ensure_content_store();
|
crate::db::ensure_content_store();
|
||||||
crate::db::write_item_with_content(
|
// Post-929: merge_failure detail lives on the MergeJob CRDT entry, not in YAML.
|
||||||
|
crate::crdt_state::write_merge_job(
|
||||||
"903_story_merge_fail",
|
"903_story_merge_fail",
|
||||||
"4_merge",
|
"failed",
|
||||||
"---\nname: Failed Story\nmerge_failure: \"conflicts in src/lib.rs\"\n---\n",
|
0.0,
|
||||||
crate::db::ItemMeta::from_yaml(
|
None,
|
||||||
"---\nname: Failed Story\nmerge_failure: \"conflicts in src/lib.rs\"\n---\n",
|
Some("conflicts in src/lib.rs"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
let items = vec![make_item(
|
let items = vec![make_item(
|
||||||
"903_story_merge_fail",
|
"903_story_merge_fail",
|
||||||
@@ -552,14 +553,16 @@ fn merge_item_with_failure_shows_stop_sign_and_snippet() {
|
|||||||
fn merge_item_failure_snippet_truncated_at_120_chars() {
|
fn merge_item_failure_snippet_truncated_at_120_chars() {
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
crate::db::ensure_content_store();
|
crate::db::ensure_content_store();
|
||||||
let long_reason = "x".repeat(200);
|
let long_reason = "x".repeat(200);
|
||||||
let content = format!("---\nname: Long Fail\nmerge_failure: \"{long_reason}\"\n---\n");
|
// Post-929: merge_failure detail lives on the MergeJob CRDT entry, not in YAML.
|
||||||
crate::db::write_item_with_content(
|
crate::crdt_state::write_merge_job(
|
||||||
"904_story_long_fail",
|
"904_story_long_fail",
|
||||||
"4_merge",
|
"failed",
|
||||||
&content,
|
0.0,
|
||||||
crate::db::ItemMeta::from_yaml(&content),
|
None,
|
||||||
|
Some(&long_reason),
|
||||||
);
|
);
|
||||||
let items = vec![make_item("904_story_long_fail", "Long Fail", merge_stage())];
|
let items = vec![make_item("904_story_long_fail", "Long Fail", merge_stage())];
|
||||||
let agents = AgentPool::new_test(3000);
|
let agents = AgentPool::new_test(3000);
|
||||||
@@ -585,27 +588,16 @@ fn merge_item_failure_snippet_truncated_at_120_chars() {
|
|||||||
fn merge_item_failure_snippet_is_first_non_empty_line() {
|
fn merge_item_failure_snippet_is_first_non_empty_line() {
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
crate::db::ensure_content_store();
|
crate::db::ensure_content_store();
|
||||||
// Multi-line failure reason — first non-empty line should be used.
|
// Multi-line failure reason — first non-empty line should be used.
|
||||||
let reason = "\nfirst line of error\nsecond line";
|
// Post-929: merge_failure detail lives on the MergeJob CRDT entry.
|
||||||
let content = format!(
|
crate::crdt_state::write_merge_job(
|
||||||
"---\nname: Multi Line\nmerge_failure: \"{}\" \n---\n",
|
|
||||||
reason.replace('\n', "\\n")
|
|
||||||
);
|
|
||||||
crate::db::write_item_with_content(
|
|
||||||
"905_story_multiline",
|
"905_story_multiline",
|
||||||
"4_merge",
|
"failed",
|
||||||
&content,
|
0.0,
|
||||||
crate::db::ItemMeta::from_yaml(&content),
|
None,
|
||||||
);
|
Some("\nfirst line of error\nsecond line"),
|
||||||
// Write with literal \n as the content (simulating stored text with newlines).
|
|
||||||
let content2 =
|
|
||||||
"---\nname: Multi Line\nmerge_failure: |\n \n first line of error\n second line\n---\n";
|
|
||||||
crate::db::write_item_with_content(
|
|
||||||
"905_story_multiline",
|
|
||||||
"4_merge",
|
|
||||||
content2,
|
|
||||||
crate::db::ItemMeta::from_yaml(content2),
|
|
||||||
);
|
);
|
||||||
let items = vec![make_item(
|
let items = vec![make_item(
|
||||||
"905_story_multiline",
|
"905_story_multiline",
|
||||||
|
|||||||
@@ -69,10 +69,12 @@ fn build_triage_dump(
|
|||||||
None => return format!("Story {num_str}: content not found in content store."),
|
None => return format!("Story {num_str}: content not found in content store."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let meta = crate::db::yaml_legacy::parse_front_matter(&contents).ok();
|
// Story metadata now comes from the CRDT registers and adjacent CRDT entries
|
||||||
let name = meta
|
// (MergeJob.error), not from YAML front matter (story 929).
|
||||||
|
let crdt_item = crate::crdt_state::read_item(story_id);
|
||||||
|
let name = crdt_item
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|m| m.name.as_deref())
|
.and_then(|w| w.name())
|
||||||
.unwrap_or("(unnamed)");
|
.unwrap_or("(unnamed)");
|
||||||
|
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
@@ -83,40 +85,36 @@ fn build_triage_dump(
|
|||||||
let dir_name = crate::pipeline_state::stage_dir_name(&item.stage);
|
let dir_name = crate::pipeline_state::stage_dir_name(&item.stage);
|
||||||
out.push_str(&format!("**Stage:** {stage_name} (`{dir_name}`)\n\n"));
|
out.push_str(&format!("**Stage:** {stage_name} (`{dir_name}`)\n\n"));
|
||||||
|
|
||||||
// ---- Front matter fields ----
|
// ---- CRDT metadata ----
|
||||||
if let Some(ref m) = meta {
|
if let Some(ref w) = crdt_item {
|
||||||
let mut fields: Vec<String> = Vec::new();
|
let mut fields: Vec<String> = Vec::new();
|
||||||
if let Some(true) = m.blocked {
|
if w.blocked() {
|
||||||
fields.push("**blocked:** true".to_string());
|
fields.push("**blocked:** true".to_string());
|
||||||
}
|
}
|
||||||
if let Some(ref agent) = m.agent {
|
if let Some(agent) = w.agent() {
|
||||||
fields.push(format!("**agent:** {agent}"));
|
fields.push(format!("**agent:** {agent}"));
|
||||||
}
|
}
|
||||||
if let Some(ref qa) = m.qa {
|
if let Some(qa) = w.qa_mode() {
|
||||||
fields.push(format!("**qa:** {qa}"));
|
fields.push(format!("**qa:** {qa}"));
|
||||||
}
|
}
|
||||||
if let Some(true) = m.review_hold {
|
let rc = w.retry_count();
|
||||||
fields.push("**review_hold:** true".to_string());
|
if rc > 0 {
|
||||||
}
|
|
||||||
if let Some(rc) = m.retry_count
|
|
||||||
&& rc > 0
|
|
||||||
{
|
|
||||||
fields.push(format!("**retry_count:** {rc}"));
|
fields.push(format!("**retry_count:** {rc}"));
|
||||||
}
|
}
|
||||||
if let Some(ref cb) = m.coverage_baseline {
|
// merge_failure detail lives on the MergeJob CRDT entry, not on the
|
||||||
fields.push(format!("**coverage_baseline:** {cb}"));
|
// pipeline item itself.
|
||||||
}
|
if let Some(job) = crate::crdt_state::read_merge_job(story_id)
|
||||||
if let Some(ref mf) = m.merge_failure {
|
&& let Some(err) = job.error
|
||||||
fields.push(format!("**merge_failure:** {mf}"));
|
|
||||||
}
|
|
||||||
if let Some(ref deps) = m.depends_on
|
|
||||||
&& !deps.is_empty()
|
|
||||||
{
|
{
|
||||||
|
fields.push(format!("**merge_failure:** {err}"));
|
||||||
|
}
|
||||||
|
let deps = w.depends_on();
|
||||||
|
if !deps.is_empty() {
|
||||||
let nums: Vec<String> = deps.iter().map(|n| format!("#{n}")).collect();
|
let nums: Vec<String> = deps.iter().map(|n| format!("#{n}")).collect();
|
||||||
fields.push(format!("**depends_on:** {}", nums.join(", ")));
|
fields.push(format!("**depends_on:** {}", nums.join(", ")));
|
||||||
}
|
}
|
||||||
if !fields.is_empty() {
|
if !fields.is_empty() {
|
||||||
out.push_str("**Front matter:**\n");
|
out.push_str("**Metadata:**\n");
|
||||||
for f in &fields {
|
for f in &fields {
|
||||||
out.push_str(&format!(" • {f}\n"));
|
out.push_str(&format!(" • {f}\n"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
//! and returns a confirmation.
|
//! and returns a confirmation.
|
||||||
|
|
||||||
use super::CommandContext;
|
use super::CommandContext;
|
||||||
use crate::db::yaml_legacy::clear_front_matter_field_in_content;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Handle the `unblock` command.
|
/// Handle the `unblock` command.
|
||||||
@@ -80,37 +79,14 @@ fn unblock_by_story_id(story_id: &str) -> String {
|
|||||||
// the CRDT.
|
// the CRDT.
|
||||||
if let Err(e) = crate::agents::lifecycle::transition_to_unblocked(story_id) {
|
if let Err(e) = crate::agents::lifecycle::transition_to_unblocked(story_id) {
|
||||||
// If the typed transition fails (e.g. a legacy Archived item with no
|
// If the typed transition fails (e.g. a legacy Archived item with no
|
||||||
// valid `Unblock` transition out of its current stage), fall back to
|
// valid `Unblock` transition out of its current stage), at least
|
||||||
// a direct CRDT/content cleanup. The legacy front-matter cleanup is
|
// reset retry_count directly in the CRDT so the agent doesn't stay
|
||||||
// gated on content actually still containing YAML, so post-865
|
// tagged with a stale fail counter. Post-929 there's no FS shadow
|
||||||
// CRDT-only stories don't hit a parse error.
|
// to clean up alongside.
|
||||||
crate::slog_warn!(
|
crate::slog_warn!(
|
||||||
"[unblock] State-machine transition failed for '{story_id}': {e}. \
|
"[unblock] State-machine transition failed for '{story_id}': {e}. \
|
||||||
Falling back to direct CRDT cleanup."
|
Falling back to direct CRDT retry_count reset."
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(content) = crate::db::read_content(story_id) {
|
|
||||||
// Only run legacy front-matter cleanup if the stored content still
|
|
||||||
// begins with a `---` YAML block. Post-865 content has been
|
|
||||||
// stripped and would no-op here anyway.
|
|
||||||
if content.trim_start().starts_with("---") {
|
|
||||||
let mut updated = content;
|
|
||||||
updated = clear_front_matter_field_in_content(&updated, "blocked");
|
|
||||||
updated = clear_front_matter_field_in_content(&updated, "merge_failure");
|
|
||||||
updated = clear_front_matter_field_in_content(&updated, "retry_count");
|
|
||||||
crate::db::write_content(story_id, &updated);
|
|
||||||
let stage = typed_item
|
|
||||||
.as_ref()
|
|
||||||
.map(|i| i.stage.dir_name().to_string())
|
|
||||||
.unwrap_or_else(|| "2_current".to_string());
|
|
||||||
crate::db::write_item_with_content(
|
|
||||||
story_id,
|
|
||||||
&stage,
|
|
||||||
&updated,
|
|
||||||
crate::db::ItemMeta::from_yaml(&updated),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::crdt_state::set_retry_count(story_id, 0);
|
crate::crdt_state::set_retry_count(story_id, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,14 +197,15 @@ mod tests {
|
|||||||
// global content store.
|
// global content store.
|
||||||
write_story_file(
|
write_story_file(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"2_current",
|
"2_blocked",
|
||||||
"9903_story_stuck.md",
|
"9903_story_stuck.md",
|
||||||
"---\nname: Stuck Story\nblocked: true\nretry_count: 5\n---\n# Story\n",
|
"---\nname: Stuck Story\nblocked: true\nretry_count: 5\n---\n# Story\n",
|
||||||
);
|
);
|
||||||
// Seed the story in the CRDT with retry_count=5 so set_retry_count can reset it.
|
// Seed the story in the CRDT in 2_blocked stage so the typed
|
||||||
|
// Blocked → Coding transition fires and clears `blocked` properly.
|
||||||
crate::crdt_state::write_item(
|
crate::crdt_state::write_item(
|
||||||
"9903_story_stuck",
|
"9903_story_stuck",
|
||||||
"2_current",
|
"2_blocked",
|
||||||
Some("Stuck Story"),
|
Some("Stuck Story"),
|
||||||
None,
|
None,
|
||||||
Some(5),
|
Some(5),
|
||||||
@@ -249,25 +226,9 @@ mod tests {
|
|||||||
"should include story_id in response: {output}"
|
"should include story_id in response: {output}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// The unblock command writes back via the content store; blocked field should be gone.
|
// Post-929: the CRDT is the sole source of truth; we no longer clean
|
||||||
let contents = crate::db::read_content("9903_story_stuck")
|
// YAML front matter from on-disk content. Verify the CRDT registers
|
||||||
.or_else(|| {
|
// were updated correctly.
|
||||||
std::fs::read_to_string(
|
|
||||||
tmp.path()
|
|
||||||
.join(".huskies/work/2_current/9903_story_stuck.md"),
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.expect("story content should be readable after unblock");
|
|
||||||
assert!(
|
|
||||||
!contents.contains("blocked:"),
|
|
||||||
"blocked field should be removed: {contents}"
|
|
||||||
);
|
|
||||||
// retry_count is now in the CRDT, not in front-matter.
|
|
||||||
assert!(
|
|
||||||
!contents.contains("retry_count:"),
|
|
||||||
"retry_count should be cleared from front-matter after unblock: {contents}"
|
|
||||||
);
|
|
||||||
let item = crate::crdt_state::read_item("9903_story_stuck")
|
let item = crate::crdt_state::read_item("9903_story_stuck")
|
||||||
.expect("story should be in CRDT after unblock");
|
.expect("story should be in CRDT after unblock");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -275,6 +236,10 @@ mod tests {
|
|||||||
0,
|
0,
|
||||||
"retry_count should be reset to 0 in CRDT after unblock"
|
"retry_count should be reset to 0 in CRDT after unblock"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
!item.blocked(),
|
||||||
|
"blocked flag should be cleared in CRDT after unblock"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -140,55 +140,13 @@ fn slug_to_name(slug: &str) -> String {
|
|||||||
words.join(" ")
|
words.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the human-readable name of a story by searching content store then filesystem.
|
/// Find the human-readable name of a story from the CRDT (story 929 —
|
||||||
fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
|
/// CRDT is the sole source of story metadata).
|
||||||
// Try content store first.
|
fn find_story_name(_root: &std::path::Path, num_str: &str) -> Option<String> {
|
||||||
for id in crate::db::all_content_ids() {
|
let items = crate::crdt_state::read_all_items()?;
|
||||||
let file_num = id.split('_').next().unwrap_or("");
|
for item in items {
|
||||||
if file_num == num_str
|
if item.story_id().split('_').next().unwrap_or("") == num_str {
|
||||||
&& let Some(c) = crate::db::read_content(&id)
|
return item.name().map(str::to_string);
|
||||||
{
|
|
||||||
return crate::db::yaml_legacy::parse_front_matter(&c)
|
|
||||||
.ok()
|
|
||||||
.and_then(|m| m.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: filesystem scan.
|
|
||||||
const STAGES: &[&str] = &[
|
|
||||||
"1_backlog",
|
|
||||||
"2_current",
|
|
||||||
"3_qa",
|
|
||||||
"4_merge",
|
|
||||||
"5_done",
|
|
||||||
"6_archived",
|
|
||||||
];
|
|
||||||
for stage in STAGES {
|
|
||||||
let dir = root.join(".huskies").join("work").join(stage);
|
|
||||||
if !dir.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Ok(entries) = std::fs::read_dir(&dir) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
|
||||||
let file_num = stem
|
|
||||||
.split('_')
|
|
||||||
.next()
|
|
||||||
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
|
|
||||||
.unwrap_or("");
|
|
||||||
if file_num == num_str {
|
|
||||||
return std::fs::read_to_string(&path).ok().and_then(|c| {
|
|
||||||
crate::db::yaml_legacy::parse_front_matter(&c)
|
|
||||||
.ok()
|
|
||||||
.and_then(|m| m.name)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
Reference in New Issue
Block a user