huskies: merge 984
This commit is contained in:
@@ -69,6 +69,20 @@ pub fn apply_transition(
|
||||
// Write the new stage to the CRDT (with optional content transform).
|
||||
crate::db::move_item_stage(story_id, new_dir, content_transform);
|
||||
|
||||
// Write stage-specific metadata into the shared `resume_to` register.
|
||||
// Story 984: Superseded and Rejected stages reuse `resume_to` to carry
|
||||
// their metadata (superseded_by ID and rejection reason respectively),
|
||||
// since these stages never have a resume target.
|
||||
match &after {
|
||||
super::Stage::Superseded { superseded_by, .. } => {
|
||||
crate::crdt_state::set_resume_to_raw(story_id, &superseded_by.0);
|
||||
}
|
||||
super::Stage::Rejected { reason, .. } => {
|
||||
crate::crdt_state::set_resume_to_raw(story_id, reason);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let fired = TransitionFired {
|
||||
story_id: StoryId(story_id.to_string()),
|
||||
before,
|
||||
|
||||
@@ -71,7 +71,14 @@ impl TransitionSubscriber for AutoAssignSubscriber {
|
||||
"auto-assign"
|
||||
}
|
||||
fn on_transition(&self, f: &TransitionFired) {
|
||||
if matches!(f.after, Stage::Done { .. } | Stage::Archived { .. }) {
|
||||
if matches!(
|
||||
f.after,
|
||||
Stage::Done { .. }
|
||||
| Stage::Archived { .. }
|
||||
| Stage::Abandoned { .. }
|
||||
| Stage::Superseded { .. }
|
||||
| Stage::Rejected { .. }
|
||||
) {
|
||||
crate::slog!(
|
||||
"[pipeline/auto-assign] story {} reached {}; checking for promotable backlog items",
|
||||
f.story_id,
|
||||
|
||||
@@ -259,13 +259,7 @@ fn abandon_from_any_active_or_done() {
|
||||
},
|
||||
] {
|
||||
let result = transition(s, PipelineEvent::Abandon);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Ok(Stage::Archived {
|
||||
reason: ArchiveReason::Abandoned,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(result, Ok(Stage::Abandoned { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,13 +280,7 @@ fn supersede_from_any_active_or_done() {
|
||||
by: sid("999_story_new"),
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Ok(Stage::Archived {
|
||||
reason: ArchiveReason::Superseded { .. },
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(result, Ok(Stage::Superseded { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,13 +452,7 @@ fn cannot_triage_from_backlog() {
|
||||
#[test]
|
||||
fn abandon_from_upcoming() {
|
||||
let result = transition(Stage::Upcoming, PipelineEvent::Abandon).unwrap();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Stage::Archived {
|
||||
reason: ArchiveReason::Abandoned,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(result, Stage::Abandoned { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -482,13 +464,7 @@ fn supersede_from_upcoming() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Stage::Archived {
|
||||
reason: ArchiveReason::Superseded { .. },
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(result, Stage::Superseded { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -511,13 +487,7 @@ fn reject_from_active_stages() {
|
||||
reason: "not needed".into(),
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Ok(Stage::Archived {
|
||||
reason: ArchiveReason::Rejected { .. },
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(result, Ok(Stage::Rejected { .. })));
|
||||
}
|
||||
|
||||
let m = Stage::Merge {
|
||||
@@ -530,13 +500,7 @@ fn reject_from_active_stages() {
|
||||
reason: "not needed".into(),
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Ok(Stage::Archived {
|
||||
reason: ArchiveReason::Rejected { .. },
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(result, Ok(Stage::Rejected { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -249,29 +249,23 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
| (Coding, Abandon)
|
||||
| (Qa, Abandon)
|
||||
| (Merge { .. }, Abandon)
|
||||
| (Done { .. }, Abandon) => Ok(Archived {
|
||||
archived_at: now,
|
||||
reason: ArchiveReason::Abandoned,
|
||||
}),
|
||||
| (Done { .. }, Abandon) => Ok(Abandoned { ts: now }),
|
||||
|
||||
(Upcoming, Supersede { by })
|
||||
| (Backlog, Supersede { by })
|
||||
| (Coding, Supersede { by })
|
||||
| (Qa, Supersede { by })
|
||||
| (Merge { .. }, Supersede { by })
|
||||
| (Done { .. }, Supersede { by }) => Ok(Archived {
|
||||
archived_at: now,
|
||||
reason: ArchiveReason::Superseded { by },
|
||||
| (Done { .. }, Supersede { by }) => Ok(Superseded {
|
||||
ts: now,
|
||||
superseded_by: by,
|
||||
}),
|
||||
|
||||
// ── Reject from any active stage or QA ──────────────────────────
|
||||
(Backlog, Reject { reason })
|
||||
| (Coding, Reject { reason })
|
||||
| (Qa, Reject { reason })
|
||||
| (Merge { .. }, Reject { reason }) => Ok(Archived {
|
||||
archived_at: now,
|
||||
reason: ArchiveReason::Rejected { reason },
|
||||
}),
|
||||
| (Merge { .. }, Reject { reason }) => Ok(Rejected { ts: now, reason }),
|
||||
|
||||
// ── Demote: send an active item back to backlog ────────────────
|
||||
// `Blocked + Demote → Backlog` lets operators park a stuck story in
|
||||
|
||||
@@ -119,24 +119,24 @@ impl MergeFailureKind {
|
||||
/// - `retry_count` — also local
|
||||
/// - `blocked` — now a first-class `Blocked { reason }` stage
|
||||
///
|
||||
/// ## Canonical state machine (story 857)
|
||||
/// ## Canonical state machine (story 857 / 984)
|
||||
///
|
||||
/// The following named lifecycle states map to `Stage` variants:
|
||||
///
|
||||
/// | Lifecycle state | Stage variant |
|
||||
/// |-----------------|-----------------------------------|
|
||||
/// | upcoming | `Upcoming` |
|
||||
/// | backlog | `Backlog` |
|
||||
/// | current | `Coding` |
|
||||
/// | qa_pending | `Qa` |
|
||||
/// | merge_pending | `Merge { .. }` |
|
||||
/// | merge_failure | `MergeFailure { .. }` |
|
||||
/// | done | `Done { .. }` |
|
||||
/// | blocked | `Blocked { .. }` |
|
||||
/// | archived | `Archived { Completed }` |
|
||||
/// | superseded | `Archived { Superseded { .. } }` |
|
||||
/// | rejected | `Archived { Rejected { .. } }` |
|
||||
/// | abandoned | `Archived { Abandoned }` |
|
||||
/// | Lifecycle state | Stage variant |
|
||||
/// |-----------------|------------------------|
|
||||
/// | upcoming | `Upcoming` |
|
||||
/// | backlog | `Backlog` |
|
||||
/// | current | `Coding` |
|
||||
/// | qa_pending | `Qa` |
|
||||
/// | merge_pending | `Merge { .. }` |
|
||||
/// | merge_failure | `MergeFailure { .. }` |
|
||||
/// | done | `Done { .. }` |
|
||||
/// | blocked | `Blocked { .. }` |
|
||||
/// | archived | `Archived { .. }` |
|
||||
/// | superseded | `Superseded { .. }` |
|
||||
/// | rejected | `Rejected { .. }` |
|
||||
/// | abandoned | `Abandoned { .. }` |
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Stage {
|
||||
/// Story has been created but not yet triaged into the backlog.
|
||||
@@ -215,26 +215,43 @@ pub enum Stage {
|
||||
resume_to: Box<Stage>,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
/// Story was abandoned by the user — no further work planned.
|
||||
/// Carries the timestamp of the abandonment. Replaces the legacy
|
||||
/// `Archived { reason: ArchiveReason::Abandoned }` (story 984).
|
||||
Abandoned { ts: DateTime<Utc> },
|
||||
|
||||
/// Story was superseded by another work item.
|
||||
/// Carries the timestamp and the ID of the replacing story. Replaces
|
||||
/// the legacy `Archived { reason: ArchiveReason::Superseded { .. } }` (story 984).
|
||||
Superseded {
|
||||
ts: DateTime<Utc>,
|
||||
superseded_by: StoryId,
|
||||
},
|
||||
|
||||
/// Story was permanently rejected (e.g. by QA or a reviewer).
|
||||
/// Carries the timestamp and the rejection reason. Replaces the legacy
|
||||
/// `Archived { reason: ArchiveReason::Rejected { .. } }` (story 984).
|
||||
Rejected { ts: DateTime<Utc>, reason: String },
|
||||
}
|
||||
|
||||
/// Why a story was archived. Subsumes the old `blocked`, `merge_failure`,
|
||||
/// and `review_hold` front-matter fields (story 436).
|
||||
/// Why a story was archived.
|
||||
///
|
||||
/// Story 984: `Abandoned`, `Superseded`, and `Rejected` are now first-class
|
||||
/// `Stage` variants and are no longer stored here. The remaining variants
|
||||
/// cover completion paths that stay under `Stage::Archived`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ArchiveReason {
|
||||
/// Normal happy-path completion.
|
||||
Completed,
|
||||
/// User explicitly abandoned the story.
|
||||
Abandoned,
|
||||
/// Replaced by another story.
|
||||
Superseded { by: StoryId },
|
||||
/// Manually blocked, awaiting human resolution.
|
||||
/// Manually blocked, awaiting human resolution (legacy — kept for CRDT
|
||||
/// backward compatibility; new blocked stories use `Stage::Blocked`).
|
||||
Blocked { reason: String },
|
||||
/// Mergemaster failed beyond the retry budget.
|
||||
MergeFailed { reason: String },
|
||||
/// Held in review at human request.
|
||||
/// Held in review at human request (legacy — kept for CRDT backward
|
||||
/// compatibility; new review-held stories use `Stage::ReviewHold`).
|
||||
ReviewHeld { reason: String },
|
||||
/// Story rejected by QA or reviewer with an explanation.
|
||||
Rejected { reason: String },
|
||||
}
|
||||
|
||||
// ── Stage convenience methods ──────────────────────────────────────────────
|
||||
@@ -326,6 +343,17 @@ impl Stage {
|
||||
archived_at: DateTime::<Utc>::UNIX_EPOCH,
|
||||
reason: ArchiveReason::Completed,
|
||||
}),
|
||||
"abandoned" => Some(Stage::Abandoned {
|
||||
ts: DateTime::<Utc>::UNIX_EPOCH,
|
||||
}),
|
||||
"superseded" => Some(Stage::Superseded {
|
||||
ts: DateTime::<Utc>::UNIX_EPOCH,
|
||||
superseded_by: StoryId(String::new()),
|
||||
}),
|
||||
"rejected" => Some(Stage::Rejected {
|
||||
ts: DateTime::<Utc>::UNIX_EPOCH,
|
||||
reason: String::new(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -418,6 +446,9 @@ pub fn stage_label(s: &Stage) -> &'static str {
|
||||
Stage::Frozen { .. } => "Frozen",
|
||||
Stage::ReviewHold { .. } => "ReviewHold",
|
||||
Stage::Archived { .. } => "Archived",
|
||||
Stage::Abandoned { .. } => "Abandoned",
|
||||
Stage::Superseded { .. } => "Superseded",
|
||||
Stage::Rejected { .. } => "Rejected",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,5 +471,8 @@ pub fn stage_dir_name(s: &Stage) -> &'static str {
|
||||
Stage::ReviewHold { .. } => "review_hold",
|
||||
Stage::Done { .. } => "done",
|
||||
Stage::Archived { .. } => "archived",
|
||||
Stage::Abandoned { .. } => "abandoned",
|
||||
Stage::Superseded { .. } => "superseded",
|
||||
Stage::Rejected { .. } => "rejected",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user