feat(server): workflow state transition API endpoint (COD-376) - #3
Conversation
Add PATCH /api/v1/content/:id/state endpoint that validates transitions through the domain WorkflowTransition::apply() state machine. - storage.rs: apply_workflow_transition() for atomic read-validate-write under a single mutex lock (prevents concurrent transition races). Also get_workflow_state() and update_workflow_state() (returns rows-affected bool). Fixed save_content() to write explicit WorkflowState::Draft instead of deriving state from the variant list (was correct-by-accident). - api.rs: transition_workflow_state handler with 422 for invalid transitions, 404 for not found. Response serialized via serde (removed manual match helper). - cli.rs: new State subcommand with --id and --transition flags - main.rs: handler calling PATCH endpoint - Tests: storage round-trip + atomic transition test (valid + invalid + ghost) Co-authored-by: Archon <archon@purelymail.com>
🤖 Automated Review Panel
GPT-5.5 (openai/gpt-5.5)Verdict: ✅ APPROVE — meets all acceptance criteria All four CI gates pass (fmt, test = 10 green, clippy, build). Goal alignment — all criteria met:
Correctness findings:
Nothing breaks existing functionality. The new route is additive. Gemini 3 Flash (google/gemini-3-flash-preview)Verdict: Achieves the stated goal and passes all CI gates, but flagged two correctness issues — both fixed. 🔴 Bug 1 (BLOCKING) — 🔴 Bug 2 — Read-then-write was not atomic 🟡 Bug 3 — Post-review fixes appliedAll four issues were addressed before the PR was opened:
Two new tests added: |
🤖 Auto-Merge GateConfidence: 0.95 — MERGE Rationale
Hard filters verified
Merged by CodeFold Auto-Merge Gate (cron ceb0befd1f30). |
Summary
Implements COD-376: Workflow state transition API endpoint.
Adds
PATCH /api/v1/content/:id/statethat validates transitions through the domainWorkflowTransition::apply()state machine and persists the new state.Changes
Storage layer (
storage.rs)apply_workflow_transition()— new atomic method: reads current state, validates via domain state machine, and persists — all under a single mutex lock. Prevents concurrent transition races (flagged by review panel).get_workflow_state()— reads the workflow state for a content item.update_workflow_state()— persists a new state, returnsbool(checks rows-affected to detect non-existent IDs).save_content()was derivingworkflow_statefrom the variant list (content.variants.first().map(|_| WorkflowState::Draft).unwrap_or_default()) — correct only by coincidence since both branches yieldedDraft. Now writes explicitWorkflowState::Draft.API layer (
api.rs)PATCH /api/v1/content/:id/statewith body{"transition": "submit_for_review" | "approve" | "schedule" | "publish" | "archive" | "request_changes"}#[serde(rename_all = "snake_case")](removed redundant manual match helper)CLI (
cli.rs+main.rs)postghost state --id <uuid> --transition <transition>commandTests
test_workflow_state_read_and_update— round-trip + ghost IDtest_apply_workflow_transition_atomic— valid transition, invalid transition (verifies state unchanged after rejection), ghost IDVerification
cargo fmt --all -- --check✅cargo clippy --all-targets -- -D warnings✅cargo test— 11/11 passed ✅Acceptance Criteria
PATCH /api/v1/content/:id/statevalidates transitions via the domain state machinepostghost stateCLI command worksCloses COD-376