2.2.2 Default-workflow seed wired into `createProject`
Estimate: 14m · Depends on: 2.2.1
Add lib/workflows/defaultWorkflow.ts — a typed constant defining the v1 default per-project workflow: six statuses covering the full lifecycle including a non-terminal blocked state and a terminal cancelled state (the two most common admin-added statuses in real Jira/Linear installs, baked into the default so every project validates the multi-terminal-status + non-linear-graph paths from day one).
The six statuses (key, label, category, isInitial):
todo· "To Do" ·todo· initialblocked· "Blocked" ·todo· — (non-terminal "can't proceed" state; complements 1.4.3'swork_item_link.is_blocked_byrelation — the link expresses a specific blocking issue, the status expresses "blocked, full stop" including external blockers)in_progress· "In Progress" ·in_progress· —in_review· "In Review" ·in_progress· —done· "Done" ·done· —cancelled· "Cancelled" ·done· — (terminal — "won't do / duplicate / out-of-scope"; covered by finding #21's readiness predicate viacategory = 'done')
The transition graph (12 transitions, restricted-mode default):
- Forward main path:
todo→in_progress,in_progress→in_review,in_review→done - Block / unblock:
todo→blocked,in_progress→blocked,blocked→todo,blocked→in_progress(block from either active state; unblock returns to either) - Backward / rework:
in_review→in_progress,in_progress→todo - Reopen:
done→in_progress,cancelled→todo(cancellation is reversible; the most common ask after "won't do" turns out to be a real fix) - Cancellation:
todo→cancelled,in_progress→cancelled,in_review→cancelled,blocked→cancelled(any non-terminal state can cancel) — that's 4 more, bringing the total to 15 transitions
Then extend projectsService.createProject to call workflowsService.seedDefaultWorkflow(projectId, tx) inside the same transaction as the project insert, so a project either has its workflow or doesn't exist.
Why a typed constant + service-layer seed, not a SQL migration INSERT: the migration can't set app.workspace_id for an arbitrary project; the seed must run under the request context where the GUC is already bound. The service-layer seed also keeps the default editable post-creation (users can rename "In Review", reorder, delete the back-transition, etc.) without forcing a migration. Same pattern as Story 1.2's owner-membership row written by insertWorkspaceWithOwner — application code writes the relational shape; the migration only creates the empty tables.
Workflow policy: default seed sets project.workflow_policy_mode = 'restricted' and writes the six transition rows above. A project starts guarded; an admin can flip to 'open' via 2.2.5's settings UI without deleting the transition rows (they're kept so a flip back to 'restricted' restores the curated graph).
Acceptance criteria
lib/workflows/defaultWorkflow.tsexports the typed default (6 statuses, 15 transitions, the initial-status flag ontodo, position values via the fractional-indexing helpers from Story 1.4).workflowsService.seedDefaultWorkflow(projectId, tx)inserts the sixworkflow_statusrows + fifteenworkflow_transitionrows inside the supplied transaction client; never opens its own transaction.projectsService.createProjectcallsseedDefaultWorkflowafter the project insert, in the same$transaction; rolling back the project insert also rolls back the workflow seed.- Vitest (real Postgres) proving: a fresh
createProjectends with 6 statuses + 15 transitions; thetodorow hasisInitial = true; any other call to set a second initial-status fails with the partial-unique violation from 2.2.1. - An end-to-end "create project → list workflow → see 6 statuses in display order" service test, verified through the workspace context.
getTerminalStatusKeyson a default-seeded project returnsnew Set(['done', 'cancelled'])— the two terminal statuses out of the box (proves the multi-terminal path is exercised on day one, not only after admin customization).- No schema change in this Subtask (2.2.1 owns the schema). Existing project rows from older tests/migrations get a separate one-off
backfillDefaultWorkflow(projectId)service method (called only by an admin/CLI path in this Story; production has no projects pre-existing this Story yet).
Context refs
lib/services/projectsService.ts+lib/repositories/projectsRepository.ts— Story 1.3's createProjectlib/workspaces/insertWorkspaceWithOwner.ts— the existing service-seed pattern this mirrors- Story 1.4's fractional-indexing helpers (
lib/workItems/positioning.tsor equivalent) — forworkflow_status.position motir-core/CLAUDE.md— transaction-client passing rule (write repos requiretx)- Finding #21 — terminal status generalizes from
'done'tocategory = 'done'