feat(934): typed Stage enum replaces directory-string state model
The state machine's `Stage` enum becomes the source of truth for pipeline
state. Six stages of work land together:
1. Clean wire vocabulary (`coding`, `merge`, `merge_failure`, ...) replaces
legacy directory-style strings (`2_current`, `4_merge`, ...) on the wire.
`Stage::from_dir` accepted both during deployment; new writes always
emit the clean form via `stage_dir_name`. Lexicographic `dir >= "5_done"`
checks in lifecycle.rs become typed `matches!` checks since the new
vocabulary doesn't sort in pipeline order.
2. `crdt_state::write_item` takes typed `&Stage`, serialising via
`stage_dir_name` at the CRDT boundary. `#[cfg(test)] write_item_str`
parses legacy strings for test fixtures.
3. `WorkItem::stage()` returns typed `crdt_state::Stage`; `stage_str()`
is gone from the public API. Projection dispatches on the typed enum.
4. `frozen` becomes an orthogonal CRDT register. `Stage::Frozen` and
`PipelineEvent::Freeze`/`Unfreeze` are removed; `transition_to_frozen`/
`unfrozen` set the flag directly without touching the stage register.
5. Watcher sweep and `tool_update_story`'s `blocked` setter route through
`apply_transition` so the typed transition table validates every
stage change. `update_story` gains a `frozen` field for symmetry.
6. One-shot startup migration rewrites pre-934 directory-style stage
registers (and sets `frozen=true` on items previously at `7_frozen`).
`Stage::from_dir` drops legacy aliases. The db boundary keeps a small
normaliser so callers with legacy strings (MCP, tests) still work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,10 +28,8 @@ pub enum UnfreezeStatus {
|
||||
/// stage without making any CRDT writes. Returns `Err` if the state transition
|
||||
/// fails (e.g. the item is not found or is in a terminal stage).
|
||||
pub fn freeze(story_id: &str) -> Result<FreezeStatus, String> {
|
||||
let already_frozen = crate::pipeline_state::read_typed(story_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|i| i.stage.is_frozen())
|
||||
let already_frozen = crate::crdt_state::read_item(story_id)
|
||||
.map(|view| view.frozen())
|
||||
.unwrap_or(false);
|
||||
|
||||
if already_frozen {
|
||||
@@ -45,13 +43,11 @@ pub fn freeze(story_id: &str) -> Result<FreezeStatus, String> {
|
||||
|
||||
/// Unfreeze a work item, resuming normal pipeline behaviour.
|
||||
///
|
||||
/// Returns [`UnfreezeStatus::NotFrozen`] if the item is not currently in the
|
||||
/// frozen stage. Returns `Err` if the state transition fails.
|
||||
/// Returns [`UnfreezeStatus::NotFrozen`] if the item is not currently frozen.
|
||||
/// Returns `Err` if the state transition fails.
|
||||
pub fn unfreeze(story_id: &str) -> Result<UnfreezeStatus, String> {
|
||||
let is_frozen = crate::pipeline_state::read_typed(story_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|i| i.stage.is_frozen())
|
||||
let is_frozen = crate::crdt_state::read_item(story_id)
|
||||
.map(|view| view.frozen())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_frozen {
|
||||
@@ -92,7 +88,7 @@ mod tests {
|
||||
.expect("read_typed should succeed")
|
||||
.expect("item should be present");
|
||||
assert!(
|
||||
item.stage.is_frozen(),
|
||||
item.is_frozen(),
|
||||
"stage should be Frozen after freeze: {:?}",
|
||||
item.stage
|
||||
);
|
||||
@@ -141,7 +137,7 @@ mod tests {
|
||||
.expect("read_typed should succeed")
|
||||
.expect("item should be present");
|
||||
assert!(
|
||||
!item.stage.is_frozen(),
|
||||
!item.is_frozen(),
|
||||
"stage should not be Frozen after unfreeze: {:?}",
|
||||
item.stage
|
||||
);
|
||||
@@ -212,12 +208,12 @@ mod tests {
|
||||
.expect("MCP-path item should be in CRDT");
|
||||
|
||||
assert!(
|
||||
state_a.stage.is_frozen(),
|
||||
state_a.is_frozen(),
|
||||
"chat-path CRDT stage must be frozen: {:?}",
|
||||
state_a.stage
|
||||
);
|
||||
assert!(
|
||||
state_b.stage.is_frozen(),
|
||||
state_b.is_frozen(),
|
||||
"MCP-path CRDT stage must be frozen: {:?}",
|
||||
state_b.stage
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user