2026-05-08 14:24:20 +00:00
|
|
|
//! Pure-content helpers and CRDT-backed metadata lookups.
|
|
|
|
|
//!
|
|
|
|
|
//! Story 865 stripped YAML front matter from stored content and the codebase
|
|
|
|
|
//! at large; the only remaining functions here read the CRDT or operate on
|
|
|
|
|
//! the markdown body directly.
|
|
|
|
|
use super::types::QaMode;
|
2026-04-28 16:25:47 +00:00
|
|
|
|
|
|
|
|
/// Parse unchecked todo items (`- [ ] ...`) from a markdown string.
|
|
|
|
|
pub fn parse_unchecked_todos(contents: &str) -> Vec<String> {
|
|
|
|
|
contents
|
|
|
|
|
.lines()
|
|
|
|
|
.filter_map(|line| {
|
|
|
|
|
let trimmed = line.trim();
|
|
|
|
|
trimmed.strip_prefix("- [ ] ").map(|text| text.to_string())
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 14:24:20 +00:00
|
|
|
/// Resolve the effective QA mode for a story by ID via the CRDT.
|
2026-04-28 16:25:47 +00:00
|
|
|
///
|
2026-05-08 14:24:20 +00:00
|
|
|
/// Returns `default` when the story has no entry or its `qa_mode` register is
|
|
|
|
|
/// unset. Spikes are **not** handled here — callers override to `Human` for
|
|
|
|
|
/// spikes themselves.
|
|
|
|
|
pub fn resolve_qa_mode(story_id: &str, default: QaMode) -> QaMode {
|
|
|
|
|
crate::crdt_state::read_item(story_id)
|
|
|
|
|
.and_then(|view| view.qa_mode)
|
|
|
|
|
.as_deref()
|
|
|
|
|
.and_then(QaMode::from_str)
|
|
|
|
|
.unwrap_or(default)
|
2026-04-28 16:25:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 14:24:20 +00:00
|
|
|
/// 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)
|
2026-04-28 16:25:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 22:12:23 +00:00
|
|
|
/// Return `true` if the story is in the `Frozen` pipeline stage.
|
2026-04-28 16:25:47 +00:00
|
|
|
///
|
2026-05-08 14:24:20 +00:00
|
|
|
/// Checks the typed CRDT stage via `read_typed`. Used by the pipeline advance
|
2026-04-29 22:12:23 +00:00
|
|
|
/// code to suppress stage transitions for frozen stories.
|
2026-04-28 16:25:47 +00:00
|
|
|
pub fn is_story_frozen_in_store(story_id: &str) -> bool {
|
2026-04-29 22:12:23 +00:00
|
|
|
crate::pipeline_state::read_typed(story_id)
|
2026-04-28 16:25:47 +00:00
|
|
|
.ok()
|
2026-04-29 22:12:23 +00:00
|
|
|
.flatten()
|
|
|
|
|
.map(|item| item.stage.is_frozen())
|
2026-04-28 16:25:47 +00:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_unchecked_todos_mixed() {
|
|
|
|
|
let input = "## AC\n- [ ] First thing\n- [x] Done thing\n- [ ] Second thing\n";
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_unchecked_todos(input),
|
|
|
|
|
vec!["First thing", "Second thing"]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_unchecked_todos_all_checked() {
|
|
|
|
|
let input = "- [x] Done\n- [x] Also done\n";
|
|
|
|
|
assert!(parse_unchecked_todos(input).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_unchecked_todos_no_checkboxes() {
|
|
|
|
|
let input = "# Story\nSome text\n- A bullet\n";
|
|
|
|
|
assert!(parse_unchecked_todos(input).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_unchecked_todos_leading_whitespace() {
|
|
|
|
|
let input = " - [ ] Indented item\n";
|
|
|
|
|
assert_eq!(parse_unchecked_todos(input), vec!["Indented item"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-08 14:24:20 +00:00
|
|
|
fn resolve_qa_mode_falls_back_to_default_when_crdt_empty() {
|
|
|
|
|
crate::crdt_state::init_for_test();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolve_qa_mode("9999_no_such_story", QaMode::Server),
|
|
|
|
|
QaMode::Server
|
|
|
|
|
);
|
2026-04-28 16:25:47 +00:00
|
|
|
}
|
|
|
|
|
}
|