package domain import "fmt" // TaskStatus and its transition rules are the single source of truth for the // §4.4 status machine. Every handler and worker validates transitions here — // never inline. type TaskStatus string const ( StatusImported TaskStatus = "imported" StatusAtomizing TaskStatus = "atomizing" StatusAtomized TaskStatus = "atomized" StatusPublished TaskStatus = "published" StatusClaimRequested TaskStatus = "claim_requested" StatusAssigned TaskStatus = "assigned" StatusInProgress TaskStatus = "in_progress" StatusInReview TaskStatus = "in_review" StatusChangesRequested TaskStatus = "changes_requested" StatusApproved TaskStatus = "approved" StatusArchived TaskStatus = "archived" ) // AllStatuses lists every valid status (used by tests and validation). var AllStatuses = []TaskStatus{ StatusImported, StatusAtomizing, StatusAtomized, StatusPublished, StatusClaimRequested, StatusAssigned, StatusInProgress, StatusInReview, StatusChangesRequested, StatusApproved, StatusArchived, } func (s TaskStatus) Valid() bool { for _, v := range AllStatuses { if s == v { return true } } return false } // transitions encodes §4.4 exactly: // // imported ──subdivide──► atomizing ──service ok──► atomized (children created atomized) // atomizing ──service failed──► imported (recovery; archived also allowed from any) // atomized ──publish──► published // published ──dev requests──► claim_requested ──approve──► assigned // published ──assign AI──► assigned // assigned ──dev starts──► in_progress ──dev submits──► in_review // assigned ──AI callback succeeded──► in_review // in_review ──► approved | changes_requested ──► in_progress // claim_requested ──withdraw/decline──► published // assigned|in_progress ──unassign/abandon──► published // any ──► archived (terminal) var transitions = map[TaskStatus]map[TaskStatus]bool{ StatusImported: {StatusAtomizing: true}, StatusAtomizing: {StatusAtomized: true, StatusImported: true}, StatusAtomized: {StatusPublished: true, StatusAtomizing: true}, // re-run subdivision StatusPublished: {StatusClaimRequested: true, StatusAssigned: true}, StatusClaimRequested: { StatusAssigned: true, StatusPublished: true, }, StatusAssigned: { StatusInProgress: true, StatusInReview: true, // AI work-performer callback path StatusPublished: true, }, StatusInProgress: { StatusInReview: true, StatusPublished: true, }, StatusInReview: { StatusApproved: true, StatusChangesRequested: true, StatusAssigned: true, // AI job failed → back to assigned (§5.2) }, StatusChangesRequested: {StatusInProgress: true}, StatusApproved: {}, StatusArchived: {}, } // CanTransition reports whether from → to is a legal §4.4 move. Archiving is // allowed from any state except archived itself. func CanTransition(from, to TaskStatus) bool { if !from.Valid() || !to.Valid() { return false } if to == StatusArchived { return from != StatusArchived } return transitions[from][to] } // ErrBadTransition describes a rejected move for error envelopes. type ErrBadTransition struct { From, To TaskStatus } func (e *ErrBadTransition) Error() string { return fmt.Sprintf("illegal status transition %s → %s", e.From, e.To) } // Transition validates and returns the error form handlers translate to 409. func Transition(from, to TaskStatus) error { if !CanTransition(from, to) { return &ErrBadTransition{From: from, To: to} } return nil }