huskies: merge 1086 story Pipeline+Status split — Step C: migrate auto-assign, subscribers, and lifecycle transitions to read Pipeline + Status

This commit is contained in:
dave
2026-05-15 08:21:36 +00:00
parent 2d6105c778
commit e82602db77
11 changed files with 159 additions and 78 deletions
+20 -28
View File
@@ -598,56 +598,48 @@ fn project_stage_for_view(
}
}
/// Check whether a dependency (by numeric ID prefix) is in `5_done` or `6_archived`
/// according to CRDT state.
/// Check whether a dependency (by numeric ID prefix) is in `Pipeline::Done` or
/// `Pipeline::Archived` according to CRDT state.
///
/// Returns `true` if the dependency is satisfied (item found in a done stage).
/// Matches both legacy slug-form IDs (`"664_story_foo"`) and numeric-only IDs
/// (`"664"`) so the check remains correct after the slug→numeric migration.
/// See `dep_is_archived_crdt` to distinguish archive-satisfied from cleanly-done.
/// Returns `true` if the dependency is satisfied (item found in a Done or
/// Archived pipeline column). Matches both legacy slug-form IDs
/// (`"664_story_foo"`) and numeric-only IDs (`"664"`) so the check remains
/// correct after the slug→numeric migration. Story 1086 routes the check
/// through the `Pipeline` projection so that future Stage variants automatically
/// participate via [`crate::pipeline_state::Stage::pipeline`]. See
/// `dep_is_archived_crdt` to distinguish archive-satisfied from cleanly-done.
pub fn dep_is_done_crdt(dep_number: u32) -> bool {
use crate::pipeline_state::{Stage, read_all_typed};
use crate::pipeline_state::{Pipeline, read_all_typed};
let exact = dep_number.to_string();
let prefix = format!("{dep_number}_");
read_all_typed().into_iter().any(|item| {
(item.story_id.0 == exact || item.story_id.0.starts_with(&prefix))
&& matches!(
item.stage,
Stage::Done { .. }
| Stage::Archived { .. }
| Stage::Abandoned { .. }
| Stage::Superseded { .. }
| Stage::Rejected { .. }
)
&& matches!(item.stage.pipeline(), Pipeline::Done | Pipeline::Archived)
})
}
/// Check whether a dependency (by numeric ID prefix) is specifically in `6_archived`
/// according to CRDT state.
/// Check whether a dependency (by numeric ID prefix) is specifically in
/// `Pipeline::Archived` according to CRDT state.
///
/// Used to detect when a dependency is satisfied via archive rather than via a clean
/// completion through `5_done`. Returns `false` when the CRDT layer is not initialised.
/// Matches both legacy slug-form IDs (`"664_story_foo"`) and numeric-only IDs (`"664"`).
/// completion through `Pipeline::Done`. Returns `false` when the CRDT layer is not
/// initialised. Matches both legacy slug-form IDs (`"664_story_foo"`) and
/// numeric-only IDs (`"664"`).
pub fn dep_is_archived_crdt(dep_number: u32) -> bool {
use crate::pipeline_state::{Stage, read_all_typed};
use crate::pipeline_state::{Pipeline, read_all_typed};
let exact = dep_number.to_string();
let prefix = format!("{dep_number}_");
read_all_typed().into_iter().any(|item| {
(item.story_id.0 == exact || item.story_id.0.starts_with(&prefix))
&& matches!(
item.stage,
Stage::Archived { .. }
| Stage::Abandoned { .. }
| Stage::Superseded { .. }
| Stage::Rejected { .. }
)
&& item.stage.pipeline() == Pipeline::Archived
})
}
/// Check unmet dependencies for a story by reading its `depends_on` from the
/// CRDT document and checking each dependency against CRDT state.
///
/// Returns the list of dependency numbers that are NOT in `5_done` or `6_archived`.
/// Returns the list of dependency numbers whose stage is NOT in `Pipeline::Done`
/// or `Pipeline::Archived`.
pub fn check_unmet_deps_crdt(story_id: &str) -> Vec<u32> {
let item = match read_item(story_id) {
Some(i) => i,
+12
View File
@@ -105,6 +105,18 @@ pub struct PipelineItemCrdt {
/// means no merge task is in flight. Projected into `Stage::Merge {
/// server_start_time }` so callers never read this register directly.
pub merge_server_start: LwwRegisterCrdt<f64>,
/// Story 1086: kebab-case wire form of the [`crate::pipeline_state::Pipeline`]
/// projection of the current `stage`. Written by `write_item` alongside
/// `stage` so display/scan code on remote peers can route by pipeline column
/// without re-deriving from the stage string. Empty string means "use the
/// value derived from `stage`" (legacy items predating 1086).
pub pipeline: LwwRegisterCrdt<String>,
/// Story 1086: kebab-case wire form of the [`crate::pipeline_state::Status`]
/// projection of the current `stage`. Written alongside `stage` so badge
/// renderers can read the status directly without re-projecting from the
/// stage string. Empty string means "use the value derived from `stage`"
/// (legacy items predating 1086).
pub status: LwwRegisterCrdt<String>,
/// Story 1088: origin of the work item — who or what created it.
///
/// Stored as a compact JSON string, e.g.
+19
View File
@@ -281,6 +281,11 @@ pub fn write_item(
merged_at: Option<f64>,
) {
let stage_str = stage_dir_name(stage);
// Story 1086: persist the typed Pipeline + Status projections alongside
// the stage register so subscribers/display code on remote peers can route
// by them without re-deriving from the stage string.
let pipeline_str = stage.pipeline().as_str();
let status_str = stage.status().as_str();
let claim: Option<&AgentClaim> = match stage {
Stage::Coding { claim, .. } => claim.as_ref(),
Stage::Merge { claim, .. } => claim.as_ref(),
@@ -336,6 +341,14 @@ pub fn write_item(
apply_and_persist(&mut state, |s| {
s.crdt.doc.items[idx].stage.set(stage_str.to_string())
});
// Story 1086: keep `pipeline` and `status` registers in lock-step with
// the stage write so subscribers/display can read them directly.
apply_and_persist(&mut state, |s| {
s.crdt.doc.items[idx].pipeline.set(pipeline_str.to_string())
});
apply_and_persist(&mut state, |s| {
s.crdt.doc.items[idx].status.set(status_str.to_string())
});
if let Some(n) = name {
apply_and_persist(&mut state, |s| {
@@ -419,6 +432,9 @@ pub fn write_item(
"resume_to": "",
"plan_state": "",
"merge_server_start": merge_server_start_val,
// Story 1086: typed Pipeline + Status projections written at insert.
"pipeline": pipeline_str,
"status": status_str,
"origin": "",
})
.into();
@@ -450,6 +466,9 @@ pub fn write_item(
item.resume_to.advance_seq(floor);
item.plan_state.advance_seq(floor);
item.merge_server_start.advance_seq(floor);
// Story 1086.
item.pipeline.advance_seq(floor);
item.status.advance_seq(floor);
item.origin.advance_seq(floor);
}