MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-76

2.2.2 Default-workflow seed wired into `createProject`

Done
Description

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 · initial
  • blocked · "Blocked" · todo · — (non-terminal "can't proceed" state; complements 1.4.3's work_item_link.is_blocked_by relation — 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 via category = '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.ts exports the typed default (6 statuses, 15 transitions, the initial-status flag on todo, position values via the fractional-indexing helpers from Story 1.4).
  • workflowsService.seedDefaultWorkflow(projectId, tx) inserts the six workflow_status rows + fifteen workflow_transition rows inside the supplied transaction client; never opens its own transaction.
  • projectsService.createProject calls seedDefaultWorkflow after the project insert, in the same $transaction; rolling back the project insert also rolls back the workflow seed.
  • Vitest (real Postgres) proving: a fresh createProject ends with 6 statuses + 15 transitions; the todo row has isInitial = 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.
  • getTerminalStatusKeys on a default-seeded project returns new 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 createProject
  • lib/workspaces/insertWorkspaceWithOwner.ts — the existing service-seed pattern this mirrors
  • Story 1.4's fractional-indexing helpers (lib/workItems/positioning.ts or equivalent) — for workflow_status.position
  • motir-core/CLAUDE.md — transaction-client passing rule (write repos require tx)
  • Finding #21 — terminal status generalizes from 'done' to category = 'done'