wip(929): stage 5 — drop FS-based dep checks and qa-mode parser from io/story_metadata

Migrate the last three callers of the FS-scanning dependency helpers to the
CRDT-direct equivalents and delete the dead helpers:

- agents/pool/auto_assign/story_checks.rs: has_unmet_dependencies and
  check_archived_dependencies now wrap check_unmet_deps_crdt /
  check_archived_deps_crdt directly. Tests rewritten to seed the CRDT.
- http/mcp/story_tools/story/update.rs: bug-503 archived-dep warning now
  reads from CRDT instead of scanning 6_archived.
- agents/pool/pipeline/advance/helpers.rs: resolve_qa_mode_from_store is
  CRDT-only (the FS fallback for content-store-empty stories is gone).
- io/story_metadata/parser.rs: resolve_qa_mode_from_content removed.
- io/story_metadata/deps.rs: check_unmet_deps and dep_is_done deleted,
  along with the unused check_unmet_deps_from_list helper.
- io/story_metadata/mod.rs: re-exports trimmed accordingly.

check_archived_deps_from_list survives because story-creation still calls
it before the CRDT entry exists (used from story_tools/story/create.rs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-12 19:14:54 +01:00
parent f775f4cfb9
commit 6e704a33b7
10 changed files with 128 additions and 238 deletions
@@ -98,49 +98,24 @@ pub(super) fn has_mergemaster_attempted(
}
/// Return `true` if the story has any `depends_on` entries that are not yet in
/// `5_done` or `6_archived`.
///
/// Reads dependency state from the CRDT document first. Falls back to the
/// filesystem when the CRDT layer is not initialised.
pub(super) fn has_unmet_dependencies(project_root: &Path, stage_dir: &str, story_id: &str) -> bool {
// Prefer CRDT-based check.
let crdt_deps = crate::crdt_state::check_unmet_deps_crdt(story_id);
if !crdt_deps.is_empty() {
return true;
}
// If the CRDT had the item and returned empty deps, it means all are met.
if crate::pipeline_state::read_typed(story_id)
.ok()
.flatten()
.is_some()
{
return false;
}
// Fallback: filesystem check (CRDT not initialised or item not yet in CRDT).
!crate::io::story_metadata::check_unmet_deps(project_root, stage_dir, story_id).is_empty()
/// `5_done` or `6_archived`. Reads dependency state from the CRDT (story 929).
pub(super) fn has_unmet_dependencies(
_project_root: &Path,
_stage_dir: &str,
story_id: &str,
) -> bool {
!crate::crdt_state::check_unmet_deps_crdt(story_id).is_empty()
}
/// Return the list of dependency story numbers that are in `6_archived` (satisfied
/// via archive rather than via a clean `5_done` completion).
///
/// Used to emit a warning when backlog promotion fires because one or more deps were
/// archived. Returns an empty `Vec` when no deps are archived. Reads from CRDT
/// first; falls back to filesystem when CRDT is not initialised.
/// via archive rather than via a clean `5_done` completion). Reads from the CRDT
/// (story 929).
pub(super) fn check_archived_dependencies(
project_root: &Path,
stage_dir: &str,
_project_root: &Path,
_stage_dir: &str,
story_id: &str,
) -> Vec<u32> {
// Prefer CRDT-based check when the item is known to CRDT.
if crate::pipeline_state::read_typed(story_id)
.ok()
.flatten()
.is_some()
{
return crate::crdt_state::check_archived_deps_crdt(story_id);
}
// Fallback: filesystem.
crate::io::story_metadata::check_archived_deps(project_root, stage_dir, story_id)
crate::crdt_state::check_archived_deps_crdt(story_id)
}
/// Return `true` if the story is in the `Frozen` pipeline stage.
@@ -265,14 +240,20 @@ mod tests {
#[test]
fn has_unmet_dependencies_returns_true_when_dep_not_done() {
crate::crdt_state::init_for_test();
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
std::fs::create_dir_all(&current).unwrap();
std::fs::write(
current.join("10_story_blocked.md"),
"---\nname: Blocked\ndepends_on: [999]\n---\n",
)
.unwrap();
crate::crdt_state::write_item(
"10_story_blocked",
"2_current",
Some("Blocked"),
None,
None,
None,
Some("[999]"),
None,
None,
None,
);
assert!(has_unmet_dependencies(
tmp.path(),
"2_current",
@@ -282,17 +263,32 @@ mod tests {
#[test]
fn has_unmet_dependencies_returns_false_when_dep_done() {
crate::crdt_state::init_for_test();
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
let done = tmp.path().join(".huskies/work/5_done");
std::fs::create_dir_all(&current).unwrap();
std::fs::create_dir_all(&done).unwrap();
std::fs::write(done.join("999_story_dep.md"), "---\nname: Dep\n---\n").unwrap();
std::fs::write(
current.join("10_story_ok.md"),
"---\nname: Ok\ndepends_on: [999]\n---\n",
)
.unwrap();
crate::crdt_state::write_item(
"999_story_dep",
"5_done",
Some("Dep"),
None,
None,
None,
None,
None,
None,
None,
);
crate::crdt_state::write_item(
"10_story_ok",
"2_current",
Some("Ok"),
None,
None,
None,
Some("[999]"),
None,
None,
None,
);
assert!(!has_unmet_dependencies(
tmp.path(),
"2_current",
@@ -302,10 +298,20 @@ mod tests {
#[test]
fn has_unmet_dependencies_returns_false_when_no_deps() {
crate::crdt_state::init_for_test();
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
std::fs::create_dir_all(&current).unwrap();
std::fs::write(current.join("5_story_free.md"), "---\nname: Free\n---\n").unwrap();
crate::crdt_state::write_item(
"5_story_free",
"2_current",
Some("Free"),
None,
None,
None,
None,
None,
None,
None,
);
assert!(!has_unmet_dependencies(
tmp.path(),
"2_current",
@@ -318,21 +324,32 @@ mod tests {
/// check_archived_dependencies returns dep IDs that are in 6_archived.
#[test]
fn check_archived_dependencies_returns_archived_ids() {
crate::crdt_state::init_for_test();
let tmp = tempfile::tempdir().unwrap();
let backlog = tmp.path().join(".huskies/work/1_backlog");
let archived = tmp.path().join(".huskies/work/6_archived");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::create_dir_all(&archived).unwrap();
std::fs::write(
archived.join("500_spike_crdt.md"),
"---\nname: CRDT Spike\n---\n",
)
.unwrap();
std::fs::write(
backlog.join("503_story_dependent.md"),
"---\nname: Dependent\ndepends_on: [500]\n---\n",
)
.unwrap();
crate::crdt_state::write_item(
"500_spike_crdt",
"6_archived",
Some("CRDT Spike"),
None,
None,
None,
None,
None,
None,
None,
);
crate::crdt_state::write_item(
"503_story_dependent",
"1_backlog",
Some("Dependent"),
None,
None,
None,
Some("[500]"),
None,
None,
None,
);
let archived_deps =
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_dependent");
assert_eq!(archived_deps, vec![500]);
@@ -341,17 +358,32 @@ mod tests {
/// check_archived_dependencies returns empty when dep is in 5_done (not archived).
#[test]
fn check_archived_dependencies_empty_when_dep_in_done() {
crate::crdt_state::init_for_test();
let tmp = tempfile::tempdir().unwrap();
let backlog = tmp.path().join(".huskies/work/1_backlog");
let done = tmp.path().join(".huskies/work/5_done");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::create_dir_all(&done).unwrap();
std::fs::write(done.join("490_story_done.md"), "---\nname: Done\n---\n").unwrap();
std::fs::write(
backlog.join("503_story_waiting.md"),
"---\nname: Waiting\ndepends_on: [490]\n---\n",
)
.unwrap();
crate::crdt_state::write_item(
"490_story_done",
"5_done",
Some("Done"),
None,
None,
None,
None,
None,
None,
None,
);
crate::crdt_state::write_item(
"503_story_waiting",
"1_backlog",
Some("Waiting"),
None,
None,
None,
Some("[490]"),
None,
None,
None,
);
let archived_deps =
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_waiting");
assert!(archived_deps.is_empty());