---
title: "Data Model & Prisma Schema"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-08
related: [auth-and-sessions.md](./auth-and-sessions.md)
---

## Datamodel

> Bron van waarheid is [`prisma/schema.prisma`](../../prisma/schema.prisma); dit document samenvat de tabellen en sleutelinvarianten. Bij twijfel wint het schema.

### `users`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| username | String | unique, not null, min 3 | Inlognaam |
| email | String? | unique | Optioneel; gebruikt voor wachtwoord-reset-flows |
| password_hash | String | not null | bcrypt hash (cost factor 12) |
| is_demo | Boolean | default false | Demo-gebruiker heeft read-only rechten |
| bio | String? | max 160 | Korte profielomschrijving |
| bio_detail | String? | max 2000 | Uitgebreide profielbeschrijving |
| must_reset_password | Boolean | default false | Forceert wachtwoord-reset bij volgende login |
| avatar_data | Bytes? | | Profielfoto als WebP bytea (max 700×700) |
| active_product_id | String? | FK → products (SetNull) | Persistente actieve PB-keuze (M9) |
| idea_code_counter | Int | default 0 | Sequentiële teller voor user-scoped idea-codes |
| min_quota_pct | Int | default 20 | Worker stand-by-drempel (M13 quota-check) |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | Cache-buster voor avatar-URL |

**Indexes:** `username` (unique), `email` (unique), `active_product_id`

---

### `user_roles`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| role | Enum | `PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER \| ADMIN` | |

**Constraint:** unique `(user_id, role)`

---

### `api_tokens`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| token_hash | String | unique, not null | SHA-256 hash van het token |
| label | String? | | Bijv. "Claude Code — laptop" |
| created_at | DateTime | default now() | |
| revoked_at | DateTime? | | Null = actief |

**Indexes:** `token_hash` (unique). Eén token kan max. één `claude_workers`-record hebben.

---

### `products`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | Eigenaar |
| name | String | not null | Uniek per gebruiker |
| code | String? | max 30 | Optionele afkorting; uniek per gebruiker als gezet |
| description | String? | | |
| repo_url | String? | | Gevalideerde URL |
| definition_of_done | String | not null | Vaste instelling per product |
| auto_pr | Boolean | default false | Automatische PR creëren na sprint-completion |
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH`, default `SPRINT` | Granulariteit voor auto-PR |
| archived | Boolean | default false | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |

**Indexes:** `(user_id, archived)` — standaard query filtert op actieve producten
**Constraints:** unique `(user_id, name)`, unique `(user_id, code)`

---

### `pbis`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (Cascade) | |
| code | String | max 30, not null | Verplicht; auto-gegenereerd of handmatig |
| title | String | not null | |
| description | String? | | |
| priority | Int | 1–4 | 1 = Kritiek, 4 = Laag |
| sort_order | Float | not null | Float voor volgorde tussen items zonder renummering |
| status | Enum | `READY \| BLOCKED \| FAILED \| DONE`, default `READY` | Auto-promotie naar DONE bij sprint-close |
| pr_url | String? | | URL van de PR die deze PBI dekt (PBI-strategie) |
| pr_merged_at | DateTime? | | Gezet wanneer de PR daadwerkelijk gemerged is |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |

**Indexes:** `(product_id, priority, sort_order)`, `(product_id, status)`
**Constraint:** unique `(product_id, code)`

**Cascade-regel (sprint-close):** wanneer een Sprint wordt afgerond via `completeSprintAction` en alle stories van een PBI eindigen op DONE, zet diezelfde transactie de PBI-status op DONE. Promotie alléén — een DONE-PBI wordt nooit automatisch teruggezet. Stories die niet in deze Sprint zaten worden meegerekend op hun huidige DB-status. Een PBI zonder stories blijft READY.

---

### `stories`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| pbi_id | String | FK → pbis (Cascade) | |
| product_id | String | FK → products | Denormalisatie voor snellere queries |
| sprint_id | String? | FK → sprints | Null = in Product Backlog |
| assignee_id | String? | FK → users (SetNull) | Story-claim op het Solo-bord |
| code | String | max 30, not null | Auto-gegenereerd of handmatig |
| title | String | not null | |
| description | String? | | |
| acceptance_criteria | String? | | |
| priority | Int | 1–4 | |
| sort_order | Float | not null | |
| status | Enum | `OPEN \| IN_SPRINT \| DONE \| FAILED`, default `OPEN` | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |

**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`, `(sprint_id, assignee_id)`
**Constraint:** unique `(product_id, code)`

**Auto-promotie/demotie via task-status:** zodra alle tasks van een story op `DONE` staan en de story-status nog niet `DONE` is, promoot dezelfde transactie de story naar `DONE`. Wordt een task van een `DONE`-story heropend, dan demoot de story terug naar `IN_SPRINT` — niet naar `OPEN` (dat zou "terug in productbacklog" betekenen, een sprint-management-actie). Logica zit in [lib/tasks-status-update.ts](../../lib/tasks-status-update.ts) en wordt aangeroepen door alle drie de task-status-write-paden (`updateTaskStatusAction`, `saveTask` edit-mode, REST `PATCH /api/tasks/[id]`).

---

### `story_logs`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| story_id | String | FK → stories (Cascade) | |
| type | Enum | `IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT` | |
| content | String | not null | |
| status | Enum | `PASSED \| FAILED`? | Alleen bij type `TEST_RESULT` |
| commit_hash | String? | | Alleen bij type `COMMIT` |
| commit_message | String? | | Alleen bij type `COMMIT` |
| metadata | Json? | | Vrije bag voor bron-info (bv. agent, model_id) |
| created_at | DateTime | default now() | |

**Indexes:** `(story_id, created_at)`

---

### `sprints`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (Cascade) | |
| code | String | max 30, not null | Auto-gegenereerd `SP-N` per product (PBI-59) |
| sprint_goal | String | not null | |
| status | Enum | `OPEN \| CLOSED \| ARCHIVED \| FAILED`, default `OPEN` | |
| start_date | Date? | | Optionele planningsmetadata |
| end_date | Date? | | Optionele planningsmetadata |
| created_at | DateTime | default now() | |
| completed_at | DateTime? | | Wordt gezet bij overgang naar CLOSED |

**Indexes:** `(product_id, status)`
**Constraint:** unique `(product_id, code)`

**Eén product, meerdere sprints (PBI-63):** een product kan tegelijk meer dan één sprint hebben. `OPEN` is geen exclusieve status; de sprint-switcher in de product-header laat de gebruiker tussen sprints kiezen. Stories in `IN_SPRINT` linken via `sprint_id` naar één specifieke sprint.

---

### `sprint_runs`

Eén `sprint_runs`-record per uitvoering van de SPRINT_IMPLEMENTATION-flow (PBI-46/47/50). Houdt status, branch en chained retries bij wanneer een run is gepauzeerd of mislukt.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| sprint_id | String | FK → sprints (Cascade) | |
| started_by_id | String | FK → users | Wie de run startte |
| status | Enum | `QUEUED \| RUNNING \| PAUSED \| DONE \| FAILED \| CANCELLED` | |
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH` | Snapshot van de strategie bij start |
| branch | String? | | Werkbranch voor de run |
| pr_url | String? | | |
| started_at / finished_at | DateTime? | | |
| failure_reason | String? | | Vrij tekstveld bij FAILED/CANCELLED |
| failed_task_id | String? | FK → tasks (SetNull) | Eerste task die de run brak (cascade-FAIL) |
| pause_context | Json? | | Gevalideerd door Zod (`lib/sprint-run/pause-context.ts`) |
| previous_run_id | String? | unique, FK → sprint_runs (SetNull) | Chain naar een eerdere run |
| created_at / updated_at | DateTime | | |

**Indexes:** `(sprint_id, status)`, `(started_by_id, status)`

---

### `tasks`

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| story_id | String | FK → stories (Cascade) | |
| product_id | String | FK → products (Cascade) | Denormalisatie voor product-scoped queries |
| sprint_id | String? | FK → sprints | Denormalisatie; geërfd van story bij sprint-toevoeging |
| code | String | max 30, not null | Auto-gegenereerd `T-N` per product |
| title | String | not null | |
| description | String? | | |
| implementation_plan | String? | | Opgeslagen via Server Action of `PATCH /api/tasks/:id` |
| priority | Int | 1–4 | |
| sort_order | Float | not null | |
| status | Enum | `TO_DO \| IN_PROGRESS \| REVIEW \| DONE \| FAILED \| EXCLUDED`, default `TO_DO` | `EXCLUDED` slaat verify-skip op tijdens een sprint-run |
| verify_only | Boolean | default false | Run-mode: alleen verifiëren, niet implementeren |
| verify_required | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY`, default `ALIGNED_OR_PARTIAL` | Drempel waarop een job's verify_result als acceptabel telt |
| repo_url | String? | | Optionele override van `product.repo_url` voor tasks die in een andere repo wonen |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |

**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`, `(product_id)`
**Constraint:** unique `(product_id, code)` — `code` blijft stabiel bij re-parenting (Jira-stijl)

---

### `claude_jobs`

Job-queue waarop `wait_for_job` (MCP) atomisch claimt via `FOR UPDATE SKIP LOCKED`. Eén rij per task-implementatie, idea-grill, idea-make-plan, plan-chat of sprint-run.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| product_id | String | FK → products (Cascade) | |
| task_id | String? | FK → tasks (Cascade) | Bij `TASK_IMPLEMENTATION` of `SPRINT_IMPLEMENTATION` |
| idea_id | String? | FK → ideas (Cascade) | Bij `IDEA_GRILL` / `IDEA_MAKE_PLAN` |
| sprint_run_id | String? | FK → sprint_runs (SetNull) | Koppel naar de bovenliggende run |
| kind | Enum | `TASK_IMPLEMENTATION \| IDEA_GRILL \| IDEA_MAKE_PLAN \| PLAN_CHAT \| SPRINT_IMPLEMENTATION` | |
| status | Enum | `QUEUED \| CLAIMED \| RUNNING \| DONE \| FAILED \| CANCELLED \| SKIPPED` | |
| claimed_by_token_id | String? | FK → api_tokens (SetNull) | Auth-koppel voor `update_job_status` |
| claimed_at / started_at / finished_at / pushed_at | DateTime? | | Lifecycle-stempels |
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | Alleen voor task-/sprint-jobs |
| model_id | String? | | Anthropic model dat de agent rapporteerde |
| input_tokens / output_tokens / cache_read_tokens / cache_write_tokens | Int? | | Token-usage voor billing-overzicht |
| plan_snapshot | String? | | Bevroren plan op claim-moment |
| base_sha / head_sha / branch / pr_url | String? | | Git-context |
| summary | String? | | Vrije agent-samenvatting bij DONE |
| error | String? | | Reden bij FAILED |
| retry_count | Int | default 0 | |
| lease_until | DateTime? | | Stale-CLAIMED → terug naar QUEUED na 30 min |
| created_at / updated_at | DateTime | | |

**Indexes:** `(user_id, status)`, `(task_id, status)`, `(idea_id, status)`, `(sprint_run_id, status)`, `(status, claimed_at)`, `(status, finished_at)`, `(status, lease_until)`

---

### `sprint_task_executions`

Bevroren scope-snapshot per `SPRINT_IMPLEMENTATION`-claim (PBI-50). Bij claim wordt voor elke `TO_DO`-task in scope één `PENDING`-record gemaakt met `implementation_plan` + `verify_required` gesnapshot. Worker en gate werken uitsluitend op deze rows; latere wijzigingen aan Task hebben geen invloed op de lopende batch.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| sprint_job_id | String | FK → claude_jobs (Cascade) | De parent SPRINT_IMPLEMENTATION-job |
| task_id | String | FK → tasks (Cascade) | |
| order | Int | not null | Volgorde binnen de batch |
| plan_snapshot | String (Text) | not null | Het bevroren implementation_plan |
| verify_required_snapshot | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY` | |
| verify_only_snapshot | Boolean | default false | |
| base_sha / head_sha | String? | | |
| status | Enum | `PENDING \| RUNNING \| DONE \| FAILED \| SKIPPED` | |
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | |
| verify_summary / skip_reason | String? (Text) | | |
| started_at / finished_at | DateTime? | | |
| created_at / updated_at | DateTime | | |

**Constraint:** unique `(sprint_job_id, task_id)`
**Indexes:** `(sprint_job_id, order)`

---

### `model_prices`

Prijslookup voor Anthropic-modellen, gebruikt door de jobs-pagina om kosten te berekenen.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| model_id | String | unique | Anthropic model-id (bv. `claude-opus-4-7`) |
| input_price_per_1m | Decimal(12,6) | | USD per 1M tokens (default) |
| output_price_per_1m | Decimal(12,6) | | |
| cache_read_price_per_1m | Decimal(12,6) | | |
| cache_write_price_per_1m | Decimal(12,6) | | |
| currency | String | default `USD` | |
| created_at / updated_at | DateTime | | |

---

### `claude_workers`

Live-presence-record per actieve agent worker. Ingevoegd bij MCP-startup, geüpdatet via `worker_heartbeat` (5s), opgeruimd bij SIGTERM. NavBar telt actieve workers op `last_seen_at < now() - 15s`.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| token_id | String | unique, FK → api_tokens (Cascade) | Eén worker per token |
| product_id | String? | | Optioneel; gerapporteerd door de agent |
| started_at / last_seen_at | DateTime | default now() | |
| last_quota_pct | Int? | | M13 pre-flight quota-check |
| last_quota_check_at | DateTime? | | |

**Indexes:** `(user_id, last_seen_at)`

---

### `product_members`

Koppelt Developer-gebruikers aan een product backlog. De eigenaar (`products.user_id`) heeft altijd volledige toegang; via `product_members` kunnen aanvullende Developers leesrechten en schrijfrechten op stories, taken en sprints van dat product krijgen. Rollen worden niet hier opgeslagen — dat doet `user_roles`. Een gebruiker kan alleen worden toegevoegd als hij/zij de rol `DEVELOPER` heeft.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (Cascade) | |
| user_id | String | FK → users (Cascade) | |
| created_at | DateTime | default now() | |

**Constraint:** unique `(product_id, user_id)`
**Indexes:** `(user_id)`

---

### `ideas`

Idea-entity (M12) tussen losse notitie en PBI. Een idea wordt eerst _gegrilled_ (interactieve Q&A → `grill_md`), daarna gemateriaaliseerd tot een plan (`plan_md`) dat deterministisch geparseerd wordt naar PBI + stories + tasks. Vervangt de oude `todos`-tabel volledig (atomische migratie ST-1239 — todos zijn gedropt).

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| product_id | String? | FK → products (SetNull) | Primaire scope; null = unscoped capture |
| code | String | max 30, not null | Sequentieel per gebruiker via `idea_code_counter` |
| title | String | not null | |
| description | String? | max 4000 | Initiële tekst |
| grill_md | String? (Text) | | Output van IDEA_GRILL |
| plan_md | String? (Text) | | Output van IDEA_MAKE_PLAN |
| pbi_id | String? | unique, FK → pbis (SetNull) | Wordt gevuld na materialisatie |
| status | Enum | `DRAFT \| GRILLING \| GRILL_FAILED \| GRILLED \| PLANNING \| PLAN_FAILED \| PLAN_READY \| PLANNED`, default `DRAFT` | |
| archived | Boolean | default false | |
| created_at / updated_at | DateTime | | |

**Constraint:** unique `(user_id, code)`
**Indexes:** `(user_id, archived, status)`, `(user_id, product_id)`

---

### `idea_products`

Optionele secundaire producten waar een idea ook impact heeft.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| idea_id | String | FK → ideas (Cascade) | |
| product_id | String | FK → products (Cascade) | |
| created_at | DateTime | default now() | |

**Constraint:** unique `(idea_id, product_id)`
**Indexes:** `(product_id)`

---

### `idea_logs`

Activiteitenlog per idea — soortgelijke rol als `story_logs` voor stories.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| idea_id | String | FK → ideas (Cascade) | |
| type | Enum | `DECISION \| NOTE \| GRILL_RESULT \| PLAN_RESULT \| STATUS_CHANGE \| JOB_EVENT` | |
| content | String (Text) | not null | |
| metadata | Json? | | |
| created_at | DateTime | default now() | |

**Indexes:** `(idea_id, created_at)`

---

### `user_questions`

Vrije vragen die een gebruiker aan zichzelf stelt op een idea (M12). Wordt door de Idea-detail-UI ingelezen om te helpen bij het grillen.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| idea_id | String | FK → ideas (Cascade) | |
| user_id | String | not null | Eigenaar |
| question | String (Text) | not null | |
| answer | String? (Text) | | |
| status | Enum (lowercase) | `pending \| answered`, default `pending` | |
| created_at / updated_at | DateTime | | |

**Indexes:** `(idea_id, status)`, `(user_id)`

---

### `claude_questions`

Persistent vraag-antwoord-kanaal van een agent (via `mcp__scrum4me__ask_user_question`) richting de actieve gebruiker (M11). LISTEN/NOTIFY pusht het antwoord terug naar de wachtende agent. Zie [architecture/claude-question-channel.md](./claude-question-channel.md).

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| story_id | String? | FK → stories (Cascade) | Eén van story/task/idea is verplicht |
| task_id | String? | FK → tasks (SetNull) | |
| idea_id | String? | FK → ideas (Cascade) | |
| product_id | String | FK → products (Cascade) | Gedenormaliseerd voor het SSE-filter |
| asked_by | String | FK → users | Token-houder = Claude |
| question | String (Text) | not null | |
| options | Json? | | `string[]` voor multi-choice; null voor free-text |
| status | String | not null | `open \| answered \| cancelled \| expired` |
| answer | String? (Text) | | |
| answered_by | String? | FK → users | |
| answered_at | DateTime? | | |
| created_at | DateTime | default now() | |
| expires_at | DateTime | not null | Default `now() + 24h`, ingesteld door MCP-tool |

**Indexes:** `(story_id, status)`, `(idea_id, status)`, `(product_id, status)`, `(status, expires_at)`

---

### `login_pairings`

QR-pairing-flow (M10). Desktop start een pairing, telefoon scant en bevestigt; daarna kan de desktop ruilen voor een sessie.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| secret_hash | String | not null | Hash van het pairing-secret |
| desktop_token_hash | String | not null | Hash van het pre-auth desktop-token |
| status | String | not null | `pending \| approved \| consumed \| expired` |
| user_id | String? | FK → users (SetNull) | Gezet bij approval |
| desktop_ua | String? | max 255 | UA-string van de desktop-aanvraag |
| desktop_ip | String? | max 45 | IP voor audit |
| created_at | DateTime | default now() | |
| expires_at | DateTime | not null | TTL voor afhandeling |
| approved_at / consumed_at | DateTime? | | |

**Indexes:** `(expires_at)`, `(status, expires_at)`

---

### `push_subscriptions`

Web Push subscriptions per gebruiker, voor notificaties.

| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| endpoint | String | unique, not null | Push-service endpoint |
| p256dh / auth | String | not null | VAPID keys |
| user_agent | String? | | UA bij subscribe |
| created_at / last_used_at | DateTime | default now() | |

**Indexes:** `(user_id)`

---

## Toegangsmodel en schrijfbeveiliging

Producttoegang is centraal gedefinieerd als:

- eigenaar: `products.user_id === gebruiker.id`
- teamlid: `product_members` bevat `(product_id, user_id)`

Code gebruikt hiervoor `productAccessFilter(userId)` uit `lib/product-access.ts`. Route Handlers en Server Actions mogen geen eigenaar-only filter (`user_id`) gebruiken voor product-scoped resources tenzij het expliciet om eigenaarsbeheer gaat (archiveren, teamleden beheren).

Schrijfoperaties volgen deze invarianten:

- Controleer eerst authenticatie en `session.isDemo`.
- Valideer input met Zod, maar behandel TypeScript types niet als runtime-beveiliging.
- Controleer de parent-resource met `productAccessFilter`.
- Vertrouw bulk-ID's nooit los: haal de records eerst op met `id in (...)` plus de parent-scope (`product_id`, `pbi_id`, `sprint_id` of `story_id`) en weiger de operatie als aantallen niet exact overeenkomen.
- Weiger dubbele IDs in reorder- en beslissingslijsten.
- Leid denormalized foreign keys af van de database-parent (`pbi.product_id`, `sprint.product_id`) en niet van form-data of JSON body.
- Delete of update alleen nadat de resource scoped is gevonden; gebruik scoped `deleteMany`/`updateMany` wanneer een unique `delete` anders onveilig zou zijn.

---

## Enums (overzicht)

| Enum | Waarden |
|---|---|
| `Role` | `PRODUCT_OWNER`, `SCRUM_MASTER`, `DEVELOPER`, `ADMIN` |
| `PbiStatus` | `READY`, `BLOCKED`, `FAILED`, `DONE` |
| `StoryStatus` | `OPEN`, `IN_SPRINT`, `DONE`, `FAILED` |
| `TaskStatus` | `TO_DO`, `IN_PROGRESS`, `REVIEW`, `DONE`, `FAILED`, `EXCLUDED` |
| `SprintStatus` | `OPEN`, `CLOSED`, `ARCHIVED`, `FAILED` |
| `SprintRunStatus` | `QUEUED`, `RUNNING`, `PAUSED`, `DONE`, `FAILED`, `CANCELLED` |
| `PrStrategy` | `SPRINT`, `STORY`, `SPRINT_BATCH` |
| `LogType` | `IMPLEMENTATION_PLAN`, `TEST_RESULT`, `COMMIT` |
| `TestStatus` | `PASSED`, `FAILED` |
| `ClaudeJobStatus` | `QUEUED`, `CLAIMED`, `RUNNING`, `DONE`, `FAILED`, `CANCELLED`, `SKIPPED` |
| `ClaudeJobKind` | `TASK_IMPLEMENTATION`, `IDEA_GRILL`, `IDEA_MAKE_PLAN`, `PLAN_CHAT`, `SPRINT_IMPLEMENTATION` |
| `VerifyResult` | `ALIGNED`, `PARTIAL`, `EMPTY`, `DIVERGENT` |
| `VerifyRequired` | `ALIGNED`, `ALIGNED_OR_PARTIAL`, `ANY` |
| `SprintTaskExecutionStatus` | `PENDING`, `RUNNING`, `DONE`, `FAILED`, `SKIPPED` |
| `IdeaStatus` | `DRAFT`, `GRILLING`, `GRILL_FAILED`, `GRILLED`, `PLANNING`, `PLAN_FAILED`, `PLAN_READY`, `PLANNED` |
| `IdeaLogType` | `DECISION`, `NOTE`, `GRILL_RESULT`, `PLAN_RESULT`, `STATUS_CHANGE`, `JOB_EVENT` |
| `UserQuestionStatus` | `pending`, `answered` (lowercase, niet UPPER_SNAKE) |

> API-grens: `TaskStatus` en `StoryStatus` worden tussen DB (UPPER_SNAKE) en API (lowercase) vertaald via `lib/task-status.ts` (zie [ADR-0004](../adr/0004-status-enum-mapping.md)).

---

## Prisma Schema

De volledige, levende definitie staat in [`prisma/schema.prisma`](../../prisma/schema.prisma).
