Operant Studio
OPER-68

Add hasMigration to tasks_ready_view + drift-detection check

Suggested
Justin Cooke

Body

feature_id: FEAT-studio-dispatcher

## Context

Surfaced during the OPER-60 rebase against `main` ([REDACTED-DOB]): a `pnpm install` + `prisma generate` run showed that the `tasks_ready_view` database view (the dispatcher's canonical "what is ready to promote" query) does NOT include the `Task.hasMigration` column that OPER-60's admission gate and OPER-64's size gate both read.

Concretely:
- `apps/web/src/lib/plan-validation-runner.ts` calls `tx.task.findFirst({ where: { id: taskId } })` and reads `task.hasMigration` inside the admission block.
- The dispatcher upstream of the runner queries `tasks_ready_view` (not `Task` directly) to enumerate candidates for planning.
- `tasks_ready_view` is defined in the initial migration and never updated when `hasMigration` was added to `Task`. Anything reading from the view for hasMigration gets `undefined`, and any join that expects the column ends up with a silent NULL.

Impact today: LOW. The admission gate reads from the raw `Task` row, not the view, so admission itself sees the correct value. But:
- The dispatcher's candidate query cannot filter or order by `hasMigration` (e.g. "promote non-migration tickets first"), which is a stated OPER-64 follow-up.
- Any dashboard or report joining `tasks_ready_view` with other tables silently loses the hasMigration signal.
- Future admission rules that want to be evaluated at candidate-enumeration time (i.e. before the ticket is picked up by a planner) cannot rely on the view.

Impact tomorrow: MEDIUM. The next admission rule that runs at enumeration time — for example "do not enumerate tickets whose declared `hasMigration=true` outside a migration-window" — will require adding the column at that point, under time pressure. Fixing it now is cheap.

## Scope

1. Add a Prisma migration `packages/database/prisma/schema/migrations/YYYYMMDDHHMMSS_oper_68_tasks_ready_view_hasmigration/migration.sql` that:
   - Drops the existing view.
   - Recreates it with `t."hasMigration"` selected from the underlying `Task` join.
   - Preserves every column, filter, and order currently in the view (grep for `tasks_ready_view` first and inventory usage before dropping).
2. Update any Prisma raw-query in `apps/web/src/lib/**` and `packages/dispatcher/src/**` that reads from `tasks_ready_view` to type-include the new column (`{ hasMigration: boolean }` in the row shape).
3. Add a one-shot check to CI (or a `pnpm` script — pick whichever is faster to wire) that fails if a column exists on `Task` but not on `tasks_ready_view`. Cheap Postgres query, prevents this drift in the future.

## Acceptance Criteria

- `SELECT column_name FROM information_schema.columns WHERE table_name = 'tasks_ready_view' AND column_name = 'hasMigration';` returns one row against a fresh `prisma migrate deploy`.
- No consumer of the view is broken by the recreation (verified by `grep -r tasks_ready_view apps/ packages/` and hand-reading each caller).
- The drift-detection check runs green on this PR and fires red on a synthetic follow-up PR that adds a `Task` column without also patching the view.
- Migration is idempotent: running it twice is a no-op after the first success.

## Non-goals

- Rewriting the dispatcher candidate query to use `hasMigration`. That is the next ticket after this one lands.
- Materializing the view. It stays a plain SQL view; materialized view planning is a separate discussion.
- Adding hasMigration-aware admission rules. Same reason.

## Verification

`pnpm --filter web prisma migrate deploy` followed by the SQL check above.

Attachments

Loading attachments…

Comments

Loading comments…