De submodule stond 27 commits achter (3c77342, v1.0.0-147), waardoor sync-schema.sh prisma/schema.prisma terugzette naar een versie zonder IDEA_REVIEW_PLAN. Bumpt naar huidige app-main + re-synct het schema; enige inhoudelijke wijziging is het nieuwe User.settings-veld. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
655 lines
20 KiB
Text
655 lines
20 KiB
Text
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
PRODUCT_OWNER
|
|
SCRUM_MASTER
|
|
DEVELOPER
|
|
ADMIN
|
|
}
|
|
|
|
enum StoryStatus {
|
|
OPEN
|
|
IN_SPRINT
|
|
DONE
|
|
FAILED
|
|
}
|
|
|
|
enum PbiStatus {
|
|
READY
|
|
BLOCKED
|
|
FAILED
|
|
DONE
|
|
}
|
|
|
|
enum ClaudeJobStatus {
|
|
QUEUED
|
|
CLAIMED
|
|
RUNNING
|
|
DONE
|
|
FAILED
|
|
CANCELLED
|
|
SKIPPED
|
|
}
|
|
|
|
enum VerifyResult {
|
|
ALIGNED
|
|
PARTIAL
|
|
EMPTY
|
|
DIVERGENT
|
|
}
|
|
|
|
enum VerifyRequired {
|
|
ALIGNED
|
|
ALIGNED_OR_PARTIAL
|
|
ANY
|
|
}
|
|
|
|
enum TaskStatus {
|
|
TO_DO
|
|
IN_PROGRESS
|
|
REVIEW
|
|
DONE
|
|
FAILED
|
|
EXCLUDED
|
|
}
|
|
|
|
enum LogType {
|
|
IMPLEMENTATION_PLAN
|
|
TEST_RESULT
|
|
COMMIT
|
|
}
|
|
|
|
enum TestStatus {
|
|
PASSED
|
|
FAILED
|
|
}
|
|
|
|
enum SprintStatus {
|
|
OPEN
|
|
CLOSED
|
|
ARCHIVED
|
|
FAILED
|
|
}
|
|
|
|
enum SprintRunStatus {
|
|
QUEUED
|
|
RUNNING
|
|
PAUSED
|
|
DONE
|
|
FAILED
|
|
CANCELLED
|
|
}
|
|
|
|
enum PrStrategy {
|
|
SPRINT
|
|
STORY
|
|
SPRINT_BATCH
|
|
}
|
|
|
|
enum IdeaStatus {
|
|
DRAFT
|
|
GRILLING
|
|
GRILL_FAILED
|
|
GRILLED
|
|
PLANNING
|
|
PLAN_FAILED
|
|
PLAN_READY
|
|
REVIEWING_PLAN
|
|
PLAN_REVIEW_FAILED
|
|
PLAN_REVIEWED
|
|
PLANNED
|
|
}
|
|
|
|
enum ClaudeJobKind {
|
|
TASK_IMPLEMENTATION
|
|
IDEA_GRILL
|
|
IDEA_MAKE_PLAN
|
|
IDEA_REVIEW_PLAN
|
|
PLAN_CHAT
|
|
SPRINT_IMPLEMENTATION
|
|
}
|
|
|
|
enum SprintTaskExecutionStatus {
|
|
PENDING
|
|
RUNNING
|
|
DONE
|
|
FAILED
|
|
SKIPPED
|
|
}
|
|
|
|
enum IdeaLogType {
|
|
DECISION
|
|
NOTE
|
|
GRILL_RESULT
|
|
PLAN_RESULT
|
|
PLAN_REVIEW_RESULT
|
|
STATUS_CHANGE
|
|
JOB_EVENT
|
|
}
|
|
|
|
enum UserQuestionStatus {
|
|
pending
|
|
answered
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
username String @unique
|
|
email String? @unique
|
|
password_hash String
|
|
is_demo Boolean @default(false)
|
|
bio String? @db.VarChar(160)
|
|
bio_detail String? @db.VarChar(2000)
|
|
must_reset_password Boolean @default(false)
|
|
avatar_data Bytes?
|
|
active_product_id String?
|
|
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
|
idea_code_counter Int @default(0)
|
|
min_quota_pct Int @default(20)
|
|
settings Json @default("{}")
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
roles UserRole[]
|
|
api_tokens ApiToken[]
|
|
products Product[]
|
|
ideas Idea[]
|
|
product_members ProductMember[]
|
|
assigned_stories Story[] @relation("StoryAssignee")
|
|
login_pairings LoginPairing[]
|
|
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
|
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
|
claude_jobs ClaudeJob[]
|
|
claude_workers ClaudeWorker[]
|
|
started_sprint_runs SprintRun[] @relation("SprintRunStartedBy")
|
|
push_subscriptions PushSubscription[]
|
|
|
|
@@index([active_product_id])
|
|
@@map("users")
|
|
}
|
|
|
|
model UserRole {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
role Role
|
|
|
|
@@unique([user_id, role])
|
|
@@map("user_roles")
|
|
}
|
|
|
|
model ApiToken {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
token_hash String @unique
|
|
label String?
|
|
created_at DateTime @default(now())
|
|
revoked_at DateTime?
|
|
claimed_jobs ClaudeJob[]
|
|
claude_worker ClaudeWorker?
|
|
|
|
@@index([token_hash])
|
|
@@map("api_tokens")
|
|
}
|
|
|
|
model Product {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
name String
|
|
code String? @db.VarChar(30)
|
|
description String?
|
|
repo_url String?
|
|
definition_of_done String
|
|
auto_pr Boolean @default(false)
|
|
pr_strategy PrStrategy @default(SPRINT)
|
|
preferred_model String?
|
|
thinking_budget_default Int?
|
|
preferred_permission_mode String?
|
|
archived Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
pbis Pbi[]
|
|
sprints Sprint[]
|
|
stories Story[]
|
|
tasks Task[]
|
|
members ProductMember[]
|
|
active_for_users User[] @relation("UserActiveProduct")
|
|
claude_questions ClaudeQuestion[]
|
|
claude_jobs ClaudeJob[]
|
|
ideas Idea[]
|
|
idea_products IdeaProduct[]
|
|
|
|
@@unique([user_id, name])
|
|
@@unique([user_id, code])
|
|
@@index([user_id, archived])
|
|
@@map("products")
|
|
}
|
|
|
|
model Pbi {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
code String @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
priority Int
|
|
sort_order Float
|
|
status PbiStatus @default(READY)
|
|
pr_url String?
|
|
pr_merged_at DateTime?
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
stories Story[]
|
|
idea Idea?
|
|
|
|
@@unique([product_id, code])
|
|
@@index([product_id, priority, sort_order])
|
|
@@index([product_id, status])
|
|
@@map("pbis")
|
|
}
|
|
|
|
model Story {
|
|
id String @id @default(cuid())
|
|
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
|
|
pbi_id String
|
|
product Product @relation(fields: [product_id], references: [id])
|
|
product_id String
|
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
|
sprint_id String?
|
|
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
|
|
assignee_id String?
|
|
code String @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
acceptance_criteria String?
|
|
priority Int
|
|
sort_order Float
|
|
status StoryStatus @default(OPEN)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
logs StoryLog[]
|
|
tasks Task[]
|
|
claude_questions ClaudeQuestion[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([pbi_id, priority, sort_order])
|
|
@@index([sprint_id, sort_order])
|
|
@@index([product_id, status])
|
|
@@index([sprint_id, assignee_id])
|
|
@@map("stories")
|
|
}
|
|
|
|
model StoryLog {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
type LogType
|
|
content String
|
|
status TestStatus?
|
|
commit_hash String?
|
|
commit_message String?
|
|
metadata Json?
|
|
created_at DateTime @default(now())
|
|
|
|
@@index([story_id, created_at])
|
|
@@map("story_logs")
|
|
}
|
|
|
|
model Sprint {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
code String @db.VarChar(30)
|
|
sprint_goal String
|
|
status SprintStatus @default(OPEN)
|
|
start_date DateTime? @db.Date
|
|
end_date DateTime? @db.Date
|
|
created_at DateTime @default(now())
|
|
completed_at DateTime?
|
|
stories Story[]
|
|
tasks Task[]
|
|
sprint_runs SprintRun[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([product_id, status])
|
|
@@map("sprints")
|
|
}
|
|
|
|
model SprintRun {
|
|
id String @id @default(cuid())
|
|
sprint Sprint @relation(fields: [sprint_id], references: [id], onDelete: Cascade)
|
|
sprint_id String
|
|
started_by User @relation("SprintRunStartedBy", fields: [started_by_id], references: [id])
|
|
started_by_id String
|
|
status SprintRunStatus @default(QUEUED)
|
|
pr_strategy PrStrategy
|
|
branch String?
|
|
pr_url String?
|
|
started_at DateTime?
|
|
finished_at DateTime?
|
|
failure_reason String?
|
|
failed_task Task? @relation("SprintRunFailedTask", fields: [failed_task_id], references: [id], onDelete: SetNull)
|
|
failed_task_id String?
|
|
pause_context Json?
|
|
previous_run_id String? @unique
|
|
previous_run SprintRun? @relation("SprintRunChain", fields: [previous_run_id], references: [id], onDelete: SetNull)
|
|
next_run SprintRun? @relation("SprintRunChain")
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
jobs ClaudeJob[]
|
|
|
|
@@index([sprint_id, status])
|
|
@@index([started_by_id, status])
|
|
@@map("sprint_runs")
|
|
}
|
|
|
|
model Task {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
|
sprint_id String?
|
|
code String @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
implementation_plan String?
|
|
priority Int
|
|
sort_order Float
|
|
status TaskStatus @default(TO_DO)
|
|
verify_only Boolean @default(false)
|
|
verify_required VerifyRequired @default(ALIGNED_OR_PARTIAL)
|
|
requires_opus Boolean @default(false)
|
|
// Override product.repo_url for branch/worktree/push purposes. Set when
|
|
// a task targets a different repo than its parent product (e.g. an
|
|
// MCP-server task tracked under the main product's PBI). Falls back to
|
|
// product.repo_url when null.
|
|
repo_url String?
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
claude_questions ClaudeQuestion[]
|
|
claude_jobs ClaudeJob[]
|
|
sprint_run_failures SprintRun[] @relation("SprintRunFailedTask")
|
|
sprint_task_executions SprintTaskExecution[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([story_id, priority, sort_order])
|
|
@@index([sprint_id, status])
|
|
@@index([product_id])
|
|
@@map("tasks")
|
|
}
|
|
|
|
model ClaudeJob {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
task Task? @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
|
task_id String?
|
|
idea Idea? @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
|
idea_id String?
|
|
sprint_run SprintRun? @relation(fields: [sprint_run_id], references: [id], onDelete: SetNull)
|
|
sprint_run_id String?
|
|
kind ClaudeJobKind @default(TASK_IMPLEMENTATION)
|
|
status ClaudeJobStatus @default(QUEUED)
|
|
claimed_by_token ApiToken? @relation(fields: [claimed_by_token_id], references: [id], onDelete: SetNull)
|
|
claimed_by_token_id String?
|
|
claimed_at DateTime?
|
|
started_at DateTime?
|
|
finished_at DateTime?
|
|
pushed_at DateTime?
|
|
verify_result VerifyResult?
|
|
model_id String?
|
|
input_tokens Int?
|
|
output_tokens Int?
|
|
cache_read_tokens Int?
|
|
cache_write_tokens Int?
|
|
requested_model String?
|
|
requested_thinking_budget Int?
|
|
requested_permission_mode String?
|
|
actual_thinking_tokens Int?
|
|
plan_snapshot String?
|
|
base_sha String?
|
|
head_sha String?
|
|
branch String?
|
|
pr_url String?
|
|
summary String?
|
|
error String?
|
|
retry_count Int @default(0)
|
|
lease_until DateTime?
|
|
task_executions SprintTaskExecution[] @relation("SprintJobExecutions")
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
@@index([user_id, status])
|
|
@@index([task_id, status])
|
|
@@index([idea_id, status])
|
|
@@index([sprint_run_id, status])
|
|
@@index([status, claimed_at])
|
|
@@index([status, finished_at])
|
|
@@index([status, lease_until])
|
|
@@map("claude_jobs")
|
|
}
|
|
|
|
// PBI-50: frozen scope-snapshot per SPRINT_IMPLEMENTATION-claim. 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.
|
|
model SprintTaskExecution {
|
|
id String @id @default(cuid())
|
|
sprint_job ClaudeJob @relation("SprintJobExecutions", fields: [sprint_job_id], references: [id], onDelete: Cascade)
|
|
sprint_job_id String
|
|
task Task @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
|
task_id String
|
|
order Int
|
|
plan_snapshot String @db.Text
|
|
verify_required_snapshot VerifyRequired
|
|
verify_only_snapshot Boolean @default(false)
|
|
base_sha String?
|
|
head_sha String?
|
|
status SprintTaskExecutionStatus @default(PENDING)
|
|
verify_result VerifyResult?
|
|
verify_summary String? @db.Text
|
|
skip_reason String? @db.Text
|
|
started_at DateTime?
|
|
finished_at DateTime?
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
@@unique([sprint_job_id, task_id])
|
|
@@index([sprint_job_id, order])
|
|
@@map("sprint_task_executions")
|
|
}
|
|
|
|
model ModelPrice {
|
|
id String @id @default(cuid())
|
|
model_id String @unique
|
|
input_price_per_1m Decimal @db.Decimal(12, 6)
|
|
output_price_per_1m Decimal @db.Decimal(12, 6)
|
|
cache_read_price_per_1m Decimal @db.Decimal(12, 6)
|
|
cache_write_price_per_1m Decimal @db.Decimal(12, 6)
|
|
currency String @default("USD")
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
@@map("model_prices")
|
|
}
|
|
|
|
model ClaudeWorker {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
token ApiToken @relation(fields: [token_id], references: [id], onDelete: Cascade)
|
|
token_id String
|
|
product_id String?
|
|
started_at DateTime @default(now())
|
|
last_seen_at DateTime @default(now())
|
|
last_quota_pct Int?
|
|
last_quota_check_at DateTime?
|
|
|
|
@@unique([token_id])
|
|
@@index([user_id, last_seen_at])
|
|
@@map("claude_workers")
|
|
}
|
|
|
|
model ProductMember {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
created_at DateTime @default(now())
|
|
|
|
@@unique([product_id, user_id])
|
|
@@index([user_id])
|
|
@@map("product_members")
|
|
}
|
|
|
|
model Idea {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
|
|
product_id String?
|
|
code String @db.VarChar(30)
|
|
title String
|
|
description String? @db.VarChar(4000)
|
|
grill_md String? @db.Text
|
|
plan_md String? @db.Text
|
|
plan_review_log Json? // ReviewLog from orchestrator (all rounds, convergence metrics, approval status)
|
|
reviewed_at DateTime? // When last reviewed
|
|
pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull)
|
|
pbi_id String? @unique
|
|
status IdeaStatus @default(DRAFT)
|
|
archived Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
questions ClaudeQuestion[]
|
|
jobs ClaudeJob[]
|
|
logs IdeaLog[]
|
|
user_questions UserQuestion[]
|
|
secondary_products IdeaProduct[]
|
|
|
|
@@unique([user_id, code])
|
|
@@index([user_id, archived, status])
|
|
@@index([user_id, product_id])
|
|
@@map("ideas")
|
|
}
|
|
|
|
model IdeaProduct {
|
|
id String @id @default(cuid())
|
|
idea_id String
|
|
product_id String
|
|
created_at DateTime @default(now())
|
|
|
|
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([idea_id, product_id])
|
|
@@index([product_id])
|
|
@@map("idea_products")
|
|
}
|
|
|
|
model IdeaLog {
|
|
id String @id @default(cuid())
|
|
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
|
idea_id String
|
|
type IdeaLogType
|
|
content String @db.Text
|
|
metadata Json?
|
|
created_at DateTime @default(now())
|
|
|
|
@@index([idea_id, created_at])
|
|
@@map("idea_logs")
|
|
}
|
|
|
|
model UserQuestion {
|
|
id String @id @default(cuid())
|
|
idea_id String
|
|
user_id String
|
|
question String @db.Text
|
|
answer String? @db.Text
|
|
status UserQuestionStatus @default(pending)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([idea_id, status])
|
|
@@index([user_id])
|
|
@@map("user_questions")
|
|
}
|
|
|
|
model LoginPairing {
|
|
id String @id @default(cuid())
|
|
secret_hash String
|
|
desktop_token_hash String
|
|
status String
|
|
user_id String?
|
|
user User? @relation(fields: [user_id], references: [id], onDelete: SetNull)
|
|
desktop_ua String? @db.VarChar(255)
|
|
desktop_ip String? @db.VarChar(45)
|
|
created_at DateTime @default(now())
|
|
expires_at DateTime
|
|
approved_at DateTime?
|
|
consumed_at DateTime?
|
|
|
|
@@index([expires_at])
|
|
@@index([status, expires_at])
|
|
@@map("login_pairings")
|
|
}
|
|
|
|
model ClaudeQuestion {
|
|
id String @id @default(cuid())
|
|
story Story? @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String?
|
|
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
|
|
task_id String?
|
|
idea Idea? @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
|
idea_id String?
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String // gedenormaliseerd uit story.product_id voor SSE-filter
|
|
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
|
|
asked_by String // user_id van token-houder (= Claude-token)
|
|
question String @db.Text
|
|
options Json? // string[] voor multi-choice; null voor free-text
|
|
status String // 'open' | 'answered' | 'cancelled' | 'expired'
|
|
answer String? @db.Text
|
|
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
|
|
answered_by String?
|
|
answered_at DateTime?
|
|
created_at DateTime @default(now())
|
|
expires_at DateTime // ingesteld door MCP-tool, default now() + 24h
|
|
|
|
@@index([story_id, status])
|
|
@@index([idea_id, status])
|
|
@@index([product_id, status])
|
|
@@index([status, expires_at])
|
|
@@map("claude_questions")
|
|
}
|
|
|
|
model PushSubscription {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
endpoint String @unique
|
|
p256dh String
|
|
auth String
|
|
user_agent String?
|
|
created_at DateTime @default(now())
|
|
last_used_at DateTime @default(now())
|
|
|
|
@@index([user_id])
|
|
@@map("push_subscriptions")
|
|
}
|