Every SKU lives in exactly one lifecycle state. ALLOWED_TRANSITIONS is the map of which moves between states are legal. The agent can only propose a transition that exists in the map; anything else is surfaced for a human instead of forced as a write.
The states
- AUDIT — initial classification of an existing or new product.
- PUBLISHED — live, accumulating performance data.
- ACTIVE — live with enough data to be classified winner / mid / loser.
- OPTIMIZING — copy/image rewrite cycle in progress.
- ON_DISCOUNT — price reduced, last-chance test before drafting.
- DRAFTED — pulled from Shopify (status=draft), kept in the database.
- SEASONAL_VAULT — drafted but flagged for auto-revive at season.
- ARCHIVED — permanently dead, no further action (terminal).
The map
ALLOWED_TRANSITIONS = {
"AUDIT": {"ACTIVE", "PUBLISHED", "DRAFTED", "OPTIMIZING",
"ON_DISCOUNT", "SEASONAL_VAULT", "ARCHIVED"},
"PUBLISHED": {"ACTIVE", "DRAFTED"}, # got data, or zero traction
"ACTIVE": {"OPTIMIZING", "ON_DISCOUNT", "DRAFTED", "SEASONAL_VAULT"},
"OPTIMIZING": {"ACTIVE", "ON_DISCOUNT"}, # rewrite worked, or try discount
"ON_DISCOUNT": {"ACTIVE", "DRAFTED"}, # discount worked, or kill
"DRAFTED": {"ACTIVE", "SEASONAL_VAULT", "ARCHIVED"},
"SEASONAL_VAULT": {"PUBLISHED", "ARCHIVED"}, # auto-revive at season
"ARCHIVED": set(), # terminal
}Invalid moves become rows
If the agent wants ARCHIVED → ACTIVE (not in the map — ARCHIVED is terminal), it doesn't error and it doesn't mutate. It writes a review row describing the intent and the illegal transition, and asks an operator to open the transition or decline it. Those rows are where you learn your own rules are too strict.
