Scrum4Me/prisma/schema.prisma
Madhura68 3eaaacaeb8 ST-1243: F1 schema + propagateStatusUpwards-helper voor sprint-flow
Schema-uitbreidingen voor de sprint-niveau jobflow (PBI-46):
- TaskStatus, StoryStatus, PbiStatus, SprintStatus krijgen FAILED
- Nieuwe enums: SprintRunStatus, PrStrategy
- Nieuw SprintRun-model dat per-task ClaudeJobs groepeert
- ClaudeJob.sprint_run_id koppeling + index
- Product.pr_strategy (default SPRINT)
- Bijhorende Prisma-migratie

propagateStatusUpwards vervangt updateTaskStatusWithStoryPromotion en
herevalueert de keten Task → Story → PBI → Sprint → SprintRun bij elke
task-statuswijziging. Bij FAILED cancelt het sibling-jobs in dezelfde
SprintRun. PBI-status BLOCKED blijft handmatig en wordt niet overschreven.

Status-mappers + theme krijgen failed-token + label-uitbreidingen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:41:38 +02:00

573 lines
17 KiB
Text

generator client {
provider = "prisma-client-js"
}
generator erd {
provider = "prisma-erd-generator"
output = "../docs/erd.svg"
}
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
}
enum LogType {
IMPLEMENTATION_PLAN
TEST_RESULT
COMMIT
}
enum TestStatus {
PASSED
FAILED
}
enum SprintStatus {
ACTIVE
COMPLETED
FAILED
}
enum SprintRunStatus {
QUEUED
RUNNING
PAUSED
DONE
FAILED
CANCELLED
}
enum PrStrategy {
SPRINT
STORY
}
enum IdeaStatus {
DRAFT
GRILLING
GRILL_FAILED
GRILLED
PLANNING
PLAN_FAILED
PLAN_READY
PLANNED
}
enum ClaudeJobKind {
TASK_IMPLEMENTATION
IDEA_GRILL
IDEA_MAKE_PLAN
PLAN_CHAT
}
enum IdeaLogType {
DECISION
NOTE
GRILL_RESULT
PLAN_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)
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")
@@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)
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
sprint_goal String
status SprintStatus @default(ACTIVE)
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[]
@@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?
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)
// 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")
@@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?
plan_snapshot String?
branch String?
pr_url String?
summary String?
error String?
retry_count Int @default(0)
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])
@@map("claude_jobs")
}
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
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")
}