--- 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).