fix: read_all_items must use deduplicated index, not raw CRDT entries

read_all_items was iterating all CRDT entries including stale duplicates
from earlier stage writes. A story written multiple times (backlog →
current → done) would appear in the output multiple times with different
stages, causing ghost entries in the pipeline status and backlog views.

Now iterates only the index (story_id → visible_index map) which
represents the latest-wins deduplicated view of each story.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dave
2026-04-10 19:32:55 +00:00
parent 2e0ed98d42
commit ea36160667
9 changed files with 88 additions and 526 deletions
+10 -22
View File
@@ -158,13 +158,19 @@ pub(super) async fn tool_status(args: &Value, ctx: &AppContext) -> Result<String
let root = ctx.state.get_project_root()?;
// Read from CRDT/DB content store — works for stories in any pipeline stage.
let _typed_item = crate::pipeline_state::read_typed(story_id)
// Read from CRDT/DB content store — verify the item is in 2_current.
let typed_item = crate::pipeline_state::read_typed(story_id)
.map_err(|e| format!("Failed to read pipeline state: {e}"))?
.ok_or_else(|| format!(
"Story '{story_id}' not found in the pipeline."
"Story '{story_id}' not found in work/2_current/. Check the story_id and ensure it is in the current stage."
))?;
if typed_item.stage.dir_name() != "2_current" {
return Err(format!(
"Story '{story_id}' not found in work/2_current/. Check the story_id and ensure it is in the current stage."
));
}
let contents = crate::db::read_content(story_id).ok_or_else(|| {
format!("Story '{story_id}' has no content in the content store.")
})?;
@@ -328,7 +334,7 @@ mod tests {
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "999_story_nonexistent"}), &ctx).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found in the pipeline"));
assert!(result.unwrap_err().contains("not found in work/2_current/"));
}
#[tokio::test]
@@ -356,22 +362,4 @@ mod tests {
assert_eq!(ac[1]["text"], "Second criterion");
assert_eq!(ac[1]["checked"], true);
}
#[tokio::test]
async fn tool_status_works_for_story_in_backlog() {
let tmp = tempdir().unwrap();
crate::db::ensure_content_store();
let story_content = "---\nname: Backlog Story\n---\n\n## Acceptance Criteria\n\n- [ ] One thing\n";
crate::db::write_item_with_content("9887_story_backlog_test", "1_backlog", story_content);
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "9887_story_backlog_test"}), &ctx)
.await
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["story_id"], "9887_story_backlog_test");
assert_eq!(parsed["front_matter"]["name"], "Backlog Story");
}
}