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:
@@ -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
|
/// Return `true` if the story has any `depends_on` entries that are not yet in
|
||||||
/// `5_done` or `6_archived`.
|
/// `5_done` or `6_archived`. Reads dependency state from the CRDT (story 929).
|
||||||
///
|
pub(super) fn has_unmet_dependencies(
|
||||||
/// Reads dependency state from the CRDT document first. Falls back to the
|
_project_root: &Path,
|
||||||
/// filesystem when the CRDT layer is not initialised.
|
_stage_dir: &str,
|
||||||
pub(super) fn has_unmet_dependencies(project_root: &Path, stage_dir: &str, story_id: &str) -> bool {
|
story_id: &str,
|
||||||
// Prefer CRDT-based check.
|
) -> bool {
|
||||||
let crdt_deps = crate::crdt_state::check_unmet_deps_crdt(story_id);
|
!crate::crdt_state::check_unmet_deps_crdt(story_id).is_empty()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the list of dependency story numbers that are in `6_archived` (satisfied
|
/// Return the list of dependency story numbers that are in `6_archived` (satisfied
|
||||||
/// via archive rather than via a clean `5_done` completion).
|
/// via archive rather than via a clean `5_done` completion). Reads from the CRDT
|
||||||
///
|
/// (story 929).
|
||||||
/// 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.
|
|
||||||
pub(super) fn check_archived_dependencies(
|
pub(super) fn check_archived_dependencies(
|
||||||
project_root: &Path,
|
_project_root: &Path,
|
||||||
stage_dir: &str,
|
_stage_dir: &str,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
) -> Vec<u32> {
|
) -> Vec<u32> {
|
||||||
// Prefer CRDT-based check when the item is known to CRDT.
|
crate::crdt_state::check_archived_deps_crdt(story_id)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the story is in the `Frozen` pipeline stage.
|
/// Return `true` if the story is in the `Frozen` pipeline stage.
|
||||||
@@ -265,14 +240,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn has_unmet_dependencies_returns_true_when_dep_not_done() {
|
fn has_unmet_dependencies_returns_true_when_dep_not_done() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let current = tmp.path().join(".huskies/work/2_current");
|
crate::crdt_state::write_item(
|
||||||
std::fs::create_dir_all(¤t).unwrap();
|
"10_story_blocked",
|
||||||
std::fs::write(
|
"2_current",
|
||||||
current.join("10_story_blocked.md"),
|
Some("Blocked"),
|
||||||
"---\nname: Blocked\ndepends_on: [999]\n---\n",
|
None,
|
||||||
)
|
None,
|
||||||
.unwrap();
|
None,
|
||||||
|
Some("[999]"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(has_unmet_dependencies(
|
assert!(has_unmet_dependencies(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"2_current",
|
"2_current",
|
||||||
@@ -282,17 +263,32 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn has_unmet_dependencies_returns_false_when_dep_done() {
|
fn has_unmet_dependencies_returns_false_when_dep_done() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let current = tmp.path().join(".huskies/work/2_current");
|
crate::crdt_state::write_item(
|
||||||
let done = tmp.path().join(".huskies/work/5_done");
|
"999_story_dep",
|
||||||
std::fs::create_dir_all(¤t).unwrap();
|
"5_done",
|
||||||
std::fs::create_dir_all(&done).unwrap();
|
Some("Dep"),
|
||||||
std::fs::write(done.join("999_story_dep.md"), "---\nname: Dep\n---\n").unwrap();
|
None,
|
||||||
std::fs::write(
|
None,
|
||||||
current.join("10_story_ok.md"),
|
None,
|
||||||
"---\nname: Ok\ndepends_on: [999]\n---\n",
|
None,
|
||||||
)
|
None,
|
||||||
.unwrap();
|
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(
|
assert!(!has_unmet_dependencies(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"2_current",
|
"2_current",
|
||||||
@@ -302,10 +298,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn has_unmet_dependencies_returns_false_when_no_deps() {
|
fn has_unmet_dependencies_returns_false_when_no_deps() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let current = tmp.path().join(".huskies/work/2_current");
|
crate::crdt_state::write_item(
|
||||||
std::fs::create_dir_all(¤t).unwrap();
|
"5_story_free",
|
||||||
std::fs::write(current.join("5_story_free.md"), "---\nname: Free\n---\n").unwrap();
|
"2_current",
|
||||||
|
Some("Free"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(!has_unmet_dependencies(
|
assert!(!has_unmet_dependencies(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"2_current",
|
"2_current",
|
||||||
@@ -318,21 +324,32 @@ mod tests {
|
|||||||
/// check_archived_dependencies returns dep IDs that are in 6_archived.
|
/// check_archived_dependencies returns dep IDs that are in 6_archived.
|
||||||
#[test]
|
#[test]
|
||||||
fn check_archived_dependencies_returns_archived_ids() {
|
fn check_archived_dependencies_returns_archived_ids() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
crate::crdt_state::write_item(
|
||||||
let archived = tmp.path().join(".huskies/work/6_archived");
|
"500_spike_crdt",
|
||||||
std::fs::create_dir_all(&backlog).unwrap();
|
"6_archived",
|
||||||
std::fs::create_dir_all(&archived).unwrap();
|
Some("CRDT Spike"),
|
||||||
std::fs::write(
|
None,
|
||||||
archived.join("500_spike_crdt.md"),
|
None,
|
||||||
"---\nname: CRDT Spike\n---\n",
|
None,
|
||||||
)
|
None,
|
||||||
.unwrap();
|
None,
|
||||||
std::fs::write(
|
None,
|
||||||
backlog.join("503_story_dependent.md"),
|
None,
|
||||||
"---\nname: Dependent\ndepends_on: [500]\n---\n",
|
);
|
||||||
)
|
crate::crdt_state::write_item(
|
||||||
.unwrap();
|
"503_story_dependent",
|
||||||
|
"1_backlog",
|
||||||
|
Some("Dependent"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some("[500]"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
let archived_deps =
|
let archived_deps =
|
||||||
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_dependent");
|
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_dependent");
|
||||||
assert_eq!(archived_deps, vec![500]);
|
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).
|
/// check_archived_dependencies returns empty when dep is in 5_done (not archived).
|
||||||
#[test]
|
#[test]
|
||||||
fn check_archived_dependencies_empty_when_dep_in_done() {
|
fn check_archived_dependencies_empty_when_dep_in_done() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
crate::crdt_state::write_item(
|
||||||
let done = tmp.path().join(".huskies/work/5_done");
|
"490_story_done",
|
||||||
std::fs::create_dir_all(&backlog).unwrap();
|
"5_done",
|
||||||
std::fs::create_dir_all(&done).unwrap();
|
Some("Done"),
|
||||||
std::fs::write(done.join("490_story_done.md"), "---\nname: Done\n---\n").unwrap();
|
None,
|
||||||
std::fs::write(
|
None,
|
||||||
backlog.join("503_story_waiting.md"),
|
None,
|
||||||
"---\nname: Waiting\ndepends_on: [490]\n---\n",
|
None,
|
||||||
)
|
None,
|
||||||
.unwrap();
|
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 =
|
let archived_deps =
|
||||||
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_waiting");
|
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_waiting");
|
||||||
assert!(archived_deps.is_empty());
|
assert!(archived_deps.is_empty());
|
||||||
|
|||||||
@@ -52,29 +52,18 @@ pub(crate) fn spawn_pipeline_advance(
|
|||||||
|
|
||||||
/// Resolve QA mode for a story.
|
/// Resolve QA mode for a story.
|
||||||
///
|
///
|
||||||
/// Checks the typed `qa_mode` CRDT register first. If the register holds a
|
/// Story 929: reads the typed `qa_mode` register; the legacy YAML fallback
|
||||||
/// recognised value, returns it immediately without touching the content store.
|
/// is gone (the CRDT register is the only source).
|
||||||
/// Otherwise reads the story content and falls back to YAML front-matter
|
|
||||||
/// parsing via [`crate::io::story_metadata::resolve_qa_mode_from_content`].
|
|
||||||
pub(super) fn resolve_qa_mode_from_store(
|
pub(super) fn resolve_qa_mode_from_store(
|
||||||
_project_root: &Path,
|
_project_root: &Path,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
default: crate::io::story_metadata::QaMode,
|
default: crate::io::story_metadata::QaMode,
|
||||||
) -> crate::io::story_metadata::QaMode {
|
) -> crate::io::story_metadata::QaMode {
|
||||||
// CRDT register is the authoritative source; check it before the content store.
|
crate::crdt_state::read_item(story_id)
|
||||||
if let Some(view) = crate::crdt_state::read_item(story_id)
|
.and_then(|view| view.qa_mode().map(str::to_string))
|
||||||
&& let Some(s) = view.qa_mode()
|
.as_deref()
|
||||||
&& let Some(mode) = crate::io::story_metadata::QaMode::from_str(s)
|
.and_then(crate::io::story_metadata::QaMode::from_str)
|
||||||
{
|
.unwrap_or(default)
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
// Fall back to YAML front matter for backward compatibility.
|
|
||||||
if let Some(contents) = crate::db::read_content(story_id) {
|
|
||||||
return crate::io::story_metadata::resolve_qa_mode_from_content(
|
|
||||||
story_id, &contents, default,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write review_hold to the content store.
|
/// Write review_hold to the content store.
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ use crate::http::workflow::{
|
|||||||
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
||||||
update_story_in_file, validate_story_dirs,
|
update_story_in_file, validate_story_dirs,
|
||||||
};
|
};
|
||||||
use crate::io::story_metadata::{
|
use crate::io::story_metadata::{check_archived_deps_from_list, parse_unchecked_todos};
|
||||||
check_archived_deps, check_archived_deps_from_list, parse_unchecked_todos,
|
|
||||||
};
|
|
||||||
use crate::service::story::parse_test_cases;
|
use crate::service::story::parse_test_cases;
|
||||||
use crate::slog_warn;
|
use crate::slog_warn;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ use crate::http::workflow::{
|
|||||||
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
||||||
update_story_in_file, validate_story_dirs,
|
update_story_in_file, validate_story_dirs,
|
||||||
};
|
};
|
||||||
use crate::io::story_metadata::{
|
use crate::io::story_metadata::{check_archived_deps_from_list, parse_unchecked_todos};
|
||||||
check_archived_deps, check_archived_deps_from_list, parse_unchecked_todos,
|
|
||||||
};
|
|
||||||
use crate::service::story::parse_test_cases;
|
use crate::service::story::parse_test_cases;
|
||||||
use crate::slog_warn;
|
use crate::slog_warn;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ use crate::http::workflow::{
|
|||||||
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
||||||
update_story_in_file, validate_story_dirs,
|
update_story_in_file, validate_story_dirs,
|
||||||
};
|
};
|
||||||
use crate::io::story_metadata::{
|
use crate::io::story_metadata::{check_archived_deps_from_list, parse_unchecked_todos};
|
||||||
check_archived_deps, check_archived_deps_from_list, parse_unchecked_todos,
|
|
||||||
};
|
|
||||||
use crate::service::story::parse_test_cases;
|
use crate::service::story::parse_test_cases;
|
||||||
use crate::slog_warn;
|
use crate::slog_warn;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ use crate::http::workflow::{
|
|||||||
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
list_refactor_files, load_pipeline_state, load_upcoming_stories, remove_criterion_from_file,
|
||||||
update_story_in_file, validate_story_dirs,
|
update_story_in_file, validate_story_dirs,
|
||||||
};
|
};
|
||||||
use crate::io::story_metadata::{
|
use crate::io::story_metadata::{check_archived_deps_from_list, parse_unchecked_todos};
|
||||||
check_archived_deps, check_archived_deps_from_list, parse_unchecked_todos,
|
|
||||||
};
|
|
||||||
use crate::service::story::parse_test_cases;
|
use crate::service::story::parse_test_cases;
|
||||||
use crate::slog_warn;
|
use crate::slog_warn;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use crate::http::context::AppContext;
|
use crate::http::context::AppContext;
|
||||||
use crate::http::workflow::update_story_in_file;
|
use crate::http::workflow::update_story_in_file;
|
||||||
use crate::io::story_metadata::check_archived_deps;
|
|
||||||
use crate::slog_warn;
|
use crate::slog_warn;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -89,12 +88,8 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bug 503: warn if any depends_on in the (now updated) story points at an archived story.
|
// Bug 503: warn if any depends_on in the (now updated) story points at an archived story.
|
||||||
let stage = crate::pipeline_state::read_typed(story_id)
|
// Story 929: reads from the CRDT (was a FS-yaml scan).
|
||||||
.ok()
|
let archived_deps = crate::crdt_state::check_archived_deps_crdt(story_id);
|
||||||
.flatten()
|
|
||||||
.map(|i| i.stage.dir_name().to_string())
|
|
||||||
.unwrap_or_else(|| "1_backlog".to_string());
|
|
||||||
let archived_deps = check_archived_deps(&root, &stage, story_id);
|
|
||||||
if !archived_deps.is_empty() {
|
if !archived_deps.is_empty() {
|
||||||
slog_warn!(
|
slog_warn!(
|
||||||
"[update-story] Story '{story_id}' depends_on {archived_deps:?} which \
|
"[update-story] Story '{story_id}' depends_on {archived_deps:?} which \
|
||||||
|
|||||||
@@ -9,29 +9,6 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Return `true` if a story with the given numeric ID exists in `5_done` or `6_archived`.
|
|
||||||
fn dep_is_done(project_root: &Path, dep_number: u32) -> bool {
|
|
||||||
let prefix = format!("{dep_number}_");
|
|
||||||
let exact = dep_number.to_string();
|
|
||||||
for stage in &["5_done", "6_archived"] {
|
|
||||||
let dir = project_root.join(".huskies").join("work").join(stage);
|
|
||||||
if let Ok(entries) = 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())
|
|
||||||
&& (stem == exact || stem.starts_with(&prefix))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return `true` if a story with the given numeric ID exists specifically in `6_archived`.
|
/// Return `true` if a story with the given numeric ID exists specifically in `6_archived`.
|
||||||
fn dep_is_archived(project_root: &Path, dep_number: u32) -> bool {
|
fn dep_is_archived(project_root: &Path, dep_number: u32) -> bool {
|
||||||
let prefix = format!("{dep_number}_");
|
let prefix = format!("{dep_number}_");
|
||||||
@@ -56,19 +33,6 @@ fn dep_is_archived(project_root: &Path, dep_number: u32) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given an explicit list of dep numbers, return those that have NOT reached
|
|
||||||
/// `5_done` or `6_archived`.
|
|
||||||
///
|
|
||||||
/// Used by callers that have the dep list in memory (e.g. story update at
|
|
||||||
/// promotion time) and want a filesystem fact rather than an in-memory CRDT
|
|
||||||
/// state which may be stale during transitions.
|
|
||||||
pub fn check_unmet_deps_from_list(project_root: &Path, deps: &[u32]) -> Vec<u32> {
|
|
||||||
deps.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|&dep| !dep_is_done(project_root, dep))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given an explicit list of dep numbers, return those already in `6_archived`.
|
/// Given an explicit list of dep numbers, return those already in `6_archived`.
|
||||||
///
|
///
|
||||||
/// Used at story-creation time when the dep list is known in memory (before
|
/// Used at story-creation time when the dep list is known in memory (before
|
||||||
@@ -81,80 +45,10 @@ pub fn check_archived_deps_from_list(project_root: &Path, deps: &[u32]) -> Vec<u
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filesystem-backed unmet-dep check for a story file in `<stage_dir>/`.
|
|
||||||
///
|
|
||||||
/// Reads the story's `depends_on` list from its YAML front matter and returns
|
|
||||||
/// the numeric deps still pending (not yet in `5_done` or `6_archived`). This
|
|
||||||
/// is the legacy API used by the auto-assigner when the CRDT layer is not yet
|
|
||||||
/// initialised; CRDT-aware callers should prefer `check_unmet_deps_crdt`.
|
|
||||||
pub fn check_unmet_deps(project_root: &Path, stage_dir: &str, story_id: &str) -> Vec<u32> {
|
|
||||||
let path = project_root
|
|
||||||
.join(".huskies")
|
|
||||||
.join("work")
|
|
||||||
.join(stage_dir)
|
|
||||||
.join(format!("{story_id}.md"));
|
|
||||||
let contents = match fs::read_to_string(&path) {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(_) => return Vec::new(),
|
|
||||||
};
|
|
||||||
let deps = match crate::db::yaml_legacy::parse_front_matter(&contents)
|
|
||||||
.ok()
|
|
||||||
.and_then(|m| m.depends_on)
|
|
||||||
{
|
|
||||||
Some(d) => d,
|
|
||||||
None => return Vec::new(),
|
|
||||||
};
|
|
||||||
check_unmet_deps_from_list(project_root, &deps)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Filesystem-backed archived-dep check for a story file in `<stage_dir>/`.
|
|
||||||
///
|
|
||||||
/// Reads the story's `depends_on` list from its YAML front matter and returns
|
|
||||||
/// the numeric deps that satisfied via `6_archived` rather than `5_done`.
|
|
||||||
pub fn check_archived_deps(project_root: &Path, stage_dir: &str, story_id: &str) -> Vec<u32> {
|
|
||||||
let path = project_root
|
|
||||||
.join(".huskies")
|
|
||||||
.join("work")
|
|
||||||
.join(stage_dir)
|
|
||||||
.join(format!("{story_id}.md"));
|
|
||||||
let contents = match fs::read_to_string(&path) {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(_) => return Vec::new(),
|
|
||||||
};
|
|
||||||
let deps = match crate::db::yaml_legacy::parse_front_matter(&contents)
|
|
||||||
.ok()
|
|
||||||
.and_then(|m| m.depends_on)
|
|
||||||
{
|
|
||||||
Some(d) => d,
|
|
||||||
None => return Vec::new(),
|
|
||||||
};
|
|
||||||
check_archived_deps_from_list(project_root, &deps)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dep_is_done_finds_story_in_archived() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let archived = tmp.path().join(".huskies/work/6_archived");
|
|
||||||
std::fs::create_dir_all(&archived).unwrap();
|
|
||||||
std::fs::write(archived.join("100_story_old.md"), "# old\n").unwrap();
|
|
||||||
assert!(dep_is_done(tmp.path(), 100));
|
|
||||||
assert!(!dep_is_done(tmp.path(), 101));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn check_unmet_deps_from_list_returns_unmet_numbers() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let done = tmp.path().join(".huskies/work/5_done");
|
|
||||||
std::fs::create_dir_all(&done).unwrap();
|
|
||||||
std::fs::write(done.join("477_story_dep.md"), "# dep\n").unwrap();
|
|
||||||
let unmet = check_unmet_deps_from_list(tmp.path(), &[477, 478]);
|
|
||||||
assert_eq!(unmet, vec![478]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_archived_deps_from_list_returns_archived_ids() {
|
fn check_archived_deps_from_list_returns_archived_ids() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ mod deps;
|
|||||||
mod parser;
|
mod parser;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use deps::{check_archived_deps, check_archived_deps_from_list, check_unmet_deps};
|
pub use deps::check_archived_deps_from_list;
|
||||||
pub use parser::{
|
pub use parser::{is_story_frozen_in_store, parse_unchecked_todos, resolve_qa_mode};
|
||||||
is_story_frozen_in_store, parse_unchecked_todos, resolve_qa_mode, resolve_qa_mode_from_content,
|
|
||||||
};
|
|
||||||
pub use types::QaMode;
|
pub use types::QaMode;
|
||||||
|
|||||||
@@ -29,16 +29,6 @@ pub fn resolve_qa_mode(story_id: &str, default: QaMode) -> QaMode {
|
|||||||
.unwrap_or(default)
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the effective QA mode by parsing legacy YAML front matter from a
|
|
||||||
/// markdown body. Used during one-time fallbacks when the CRDT register isn't
|
|
||||||
/// set; new code should always read `qa_mode` from the CRDT.
|
|
||||||
pub fn resolve_qa_mode_from_content(_story_id: &str, content: &str, default: QaMode) -> QaMode {
|
|
||||||
crate::db::yaml_legacy::parse_front_matter(content)
|
|
||||||
.ok()
|
|
||||||
.and_then(|m| m.qa)
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return `true` if the story is in the `Frozen` pipeline stage.
|
/// Return `true` if the story is in the `Frozen` pipeline stage.
|
||||||
///
|
///
|
||||||
/// Checks the typed CRDT stage via `read_typed`. Used by the pipeline advance
|
/// Checks the typed CRDT stage via `read_typed`. Used by the pipeline advance
|
||||||
|
|||||||
Reference in New Issue
Block a user