feat(933): add item_type + epic CRDT registers + migrate epic mechanism
Replaces the YAML-only `type: epic` / `epic: <id>` front-matter fields with typed CRDT registers on PipelineItemCrdt. The epic-mechanism MCP tools (`tool_list_epics`, `tool_show_epic`), the epic-context injection in agent spawn, and the type-classifier helpers (`item_type_from_id`, `is_bug_item`, `is_refactor_item`) now all read from the CRDT. Schema: - PipelineItemCrdt: `item_type: LwwRegisterCrdt<String>` and `epic: LwwRegisterCrdt<String>` registers. - WorkItem: typed `item_type()` and `epic()` accessors returning `Option<&str>`. - crdt_state::set_item_type(story_id, Option<&str>) and crdt_state::set_epic(story_id, Option<&str>) typed setters. Write paths populate the new registers: - create_story_file / create_bug_file / create_spike_file / create_refactor_file / create_epic_file — each calls set_item_type after write_story_content. - tool_update_story intercepts `epic` and `type` fields and routes them to the typed setters (same pattern as qa / depends_on). Read paths migrated off yaml_legacy: - http/mcp/story_tools/epic.rs: tool_list_epics + tool_show_epic. - agents/lifecycle.rs::item_type_from_id (numeric-only IDs). - agents/pool/start/spawn.rs epic-context injection. - http/workflow/bug_ops/bug.rs::is_bug_item, refactor.rs::is_refactor_item. - http/workflow/pipeline.rs::load_pipeline_state — review_hold/qa/epic_id all come from the CRDT now; only merge_failure is still YAML (sweep in 929 stage 10). All `yaml_residue(...)` wraps for item_type / epic are removed; the remaining residue marker doc no longer references 933. cargo fmt --check, clippy --all-targets -- -D warnings, and the 2857-test suite all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,23 +66,24 @@ pub fn create_bug_file(
|
||||
// Sync depends_on to the typed CRDT register.
|
||||
crate::crdt_state::set_depends_on(&bug_id, depends_on.unwrap_or(&[]));
|
||||
|
||||
// Story 933: typed CRDT register for item_type.
|
||||
crate::crdt_state::set_item_type(&bug_id, Some("bug"));
|
||||
|
||||
Ok(bug_id)
|
||||
}
|
||||
|
||||
/// Returns true if the item stem is a bug item.
|
||||
///
|
||||
/// Checks the slug-based ID format first (e.g. `"4_bug_login_crash"`), then
|
||||
/// falls back to reading `type: bug` from the content store for numeric-only IDs.
|
||||
/// consults the typed CRDT `item_type` register for numeric-only IDs (story 933).
|
||||
pub(super) fn is_bug_item(stem: &str) -> bool {
|
||||
let after_num = stem.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
if after_num.starts_with("_bug_") {
|
||||
return true;
|
||||
}
|
||||
// Numeric-only ID: check content store front matter.
|
||||
if after_num.is_empty() {
|
||||
return crate::db::read_content(stem)
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.and_then(|m| m.item_type)
|
||||
return crate::crdt_state::read_item(stem)
|
||||
.and_then(|v| v.item_type().map(str::to_string))
|
||||
.map(|t| t == "bug")
|
||||
.unwrap_or(false);
|
||||
}
|
||||
|
||||
@@ -72,5 +72,8 @@ pub fn create_epic_file(
|
||||
// Epics are stored in backlog (no pipeline advancement).
|
||||
write_story_content(root, &epic_id, "1_backlog", &content);
|
||||
|
||||
// Story 933: typed CRDT register for item_type.
|
||||
crate::crdt_state::set_item_type(&epic_id, Some("epic"));
|
||||
|
||||
Ok(epic_id)
|
||||
}
|
||||
|
||||
@@ -62,23 +62,24 @@ pub fn create_refactor_file(
|
||||
// Sync depends_on to the typed CRDT register.
|
||||
crate::crdt_state::set_depends_on(&refactor_id, depends_on.unwrap_or(&[]));
|
||||
|
||||
// Story 933: typed CRDT register for item_type.
|
||||
crate::crdt_state::set_item_type(&refactor_id, Some("refactor"));
|
||||
|
||||
Ok(refactor_id)
|
||||
}
|
||||
|
||||
/// Returns true if the item stem is a refactor item.
|
||||
///
|
||||
/// Checks the slug-based ID format first (e.g. `"5_refactor_split_agents_rs"`), then
|
||||
/// falls back to reading `type: refactor` from the content store for numeric-only IDs.
|
||||
/// Checks the slug-based ID format first (e.g. `"5_refactor_split_agents_rs"`),
|
||||
/// then consults the typed CRDT `item_type` register for numeric-only IDs (933).
|
||||
pub(super) fn is_refactor_item(stem: &str) -> bool {
|
||||
let after_num = stem.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
if after_num.starts_with("_refactor_") {
|
||||
return true;
|
||||
}
|
||||
// Numeric-only ID: check content store front matter.
|
||||
if after_num.is_empty() {
|
||||
return crate::db::read_content(stem)
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.and_then(|m| m.item_type)
|
||||
return crate::crdt_state::read_item(stem)
|
||||
.and_then(|v| v.item_type().map(str::to_string))
|
||||
.map(|t| t == "refactor")
|
||||
.unwrap_or(false);
|
||||
}
|
||||
|
||||
@@ -66,5 +66,8 @@ pub fn create_spike_file(
|
||||
// Sync depends_on to the typed CRDT register.
|
||||
crate::crdt_state::set_depends_on(&spike_id, depends_on.unwrap_or(&[]));
|
||||
|
||||
// Story 933: typed CRDT register for item_type.
|
||||
crate::crdt_state::set_item_type(&spike_id, Some("spike"));
|
||||
|
||||
Ok(spike_id)
|
||||
}
|
||||
|
||||
@@ -95,18 +95,16 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
let sid = &item.story_id.0;
|
||||
let agent = agent_map.get(sid).cloned();
|
||||
|
||||
// Enrich with content-derived metadata (merge_failure, review_hold, qa, epic_id).
|
||||
let (merge_failure, review_hold, qa, epic_id) = crate::db::read_content(sid)
|
||||
// Stories 929/932/933: review_hold, qa_mode, epic_id come from typed
|
||||
// CRDT registers. merge_failure remains in YAML for now (tracked by
|
||||
// 929 stage 10's sweep).
|
||||
let view = crate::crdt_state::read_item(sid);
|
||||
let review_hold = view.as_ref().map(|v| v.review_hold()).filter(|b| *b);
|
||||
let qa = view.as_ref().and_then(|v| v.qa_mode().map(str::to_string));
|
||||
let epic_id = view.as_ref().and_then(|v| v.epic().map(str::to_string));
|
||||
let merge_failure = crate::db::read_content(sid)
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.map(|meta| {
|
||||
(
|
||||
meta.merge_failure,
|
||||
meta.review_hold,
|
||||
meta.qa.map(|m| m.as_str().to_string()),
|
||||
meta.epic,
|
||||
)
|
||||
})
|
||||
.unwrap_or((None, None, None, None));
|
||||
.and_then(|meta| meta.merge_failure);
|
||||
|
||||
let story = UpcomingStory {
|
||||
story_id: sid.clone(),
|
||||
@@ -211,9 +209,8 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
|
||||
.filter(|item| matches!(item.stage, Stage::Backlog))
|
||||
.map(|item| {
|
||||
let sid = &item.story_id.0;
|
||||
let epic_id = crate::db::read_content(sid)
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.and_then(|meta| meta.epic);
|
||||
let epic_id =
|
||||
crate::crdt_state::read_item(sid).and_then(|v| v.epic().map(str::to_string));
|
||||
UpcomingStory {
|
||||
story_id: item.story_id.0.clone(),
|
||||
name: if item.name.is_empty() {
|
||||
|
||||
@@ -71,6 +71,9 @@ pub fn create_story_file(
|
||||
// Sync depends_on to the typed CRDT register.
|
||||
crate::crdt_state::set_depends_on(&story_id, depends_on.unwrap_or(&[]));
|
||||
|
||||
// Story 933: typed CRDT register for item_type.
|
||||
crate::crdt_state::set_item_type(&story_id, Some("story"));
|
||||
|
||||
Ok(story_id)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user