feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
Schema (prisma/schema.prisma):
- Nieuw model ClaudeQuestion: id (cuid), story_id (FK Cascade), task_id?
(FK SetNull), product_id (FK Cascade — gedenormaliseerd uit story.product_id
voor SSE-filter zonder join), asked_by (FK Restrict — Claude-token-houder),
question (Text), options (Json? — string[] voor multi-choice), status
('open'|'answered'|'cancelled'|'expired'), answer (Text?), answered_by
(FK SetNull), answered_at?, created_at, expires_at
- Indexes: (story_id, status), (product_id, status), (status, expires_at)
- Back-relations: User.asked_questions (ClaudeQuestionAsker),
User.answered_questions (ClaudeQuestionAnswerer), Story.claude_questions,
Task.claude_questions, Product.claude_questions
Migratie (20260427224849_add_claude_questions):
- Prisma-gegenereerde DDL voor claude_questions + indexes + 5 FK's
- Toegevoegde notify_question_change() functie + claude_questions_notify trigger
op AFTER INSERT/UPDATE
- Emit op BESTAANDE scrum4me_changes-channel met entity:'question' (i.t.t. M10
dat eigen scrum4me_pairing-channel kreeg) — solo-route in ST-1104 moet
entity='question' wegfilteren om regressie op solo-board te voorkomen
- Trigger leest story.assignee_id voor "wacht op jou"-emphase in payload
- DELETE niet ondersteund — questions gaan naar answered/cancelled/expired
Verification: Node pg-client roundtrip via DATABASE_URL toonde correcte payloads
bij INSERT (op=I, status=open) en UPDATE (op=U, status=answered) met alle FK-IDs
en assignee_id correct uit story-join.
Volgende stap M11: ST-1102 — vier MCP-tools in scrum4me-mcp-repo
(ask_user_question, get_question_answer, list_open_questions, cancel_question).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce674b7c21
commit
79367dda7b
2 changed files with 130 additions and 0 deletions
|
|
@ -0,0 +1,99 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "claude_questions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"story_id" TEXT NOT NULL,
|
||||
"task_id" TEXT,
|
||||
"product_id" TEXT NOT NULL,
|
||||
"asked_by" TEXT NOT NULL,
|
||||
"question" TEXT NOT NULL,
|
||||
"options" JSONB,
|
||||
"status" TEXT NOT NULL,
|
||||
"answer" TEXT,
|
||||
"answered_by" TEXT,
|
||||
"answered_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "claude_questions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "claude_questions_story_id_status_idx" ON "claude_questions"("story_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "claude_questions_product_id_status_idx" ON "claude_questions"("product_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "claude_questions_status_expires_at_idx" ON "claude_questions"("status", "expires_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "claude_questions" ADD CONSTRAINT "claude_questions_story_id_fkey" FOREIGN KEY ("story_id") REFERENCES "stories"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "claude_questions" ADD CONSTRAINT "claude_questions_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "claude_questions" ADD CONSTRAINT "claude_questions_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "claude_questions" ADD CONSTRAINT "claude_questions_asked_by_fkey" FOREIGN KEY ("asked_by") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "claude_questions" ADD CONSTRAINT "claude_questions_answered_by_fkey" FOREIGN KEY ("answered_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- ST-1101: Postgres LISTEN/NOTIFY voor het Claude vraag-antwoord-kanaal (M11).
|
||||
--
|
||||
-- AFTER INSERT/UPDATE-trigger op claude_questions emit een JSON-payload op het
|
||||
-- BESTAANDE `scrum4me_changes`-kanaal (zelfde als ST-801) met `entity: 'question'`.
|
||||
--
|
||||
-- De solo-realtime SSE-route `/api/realtime/solo` MOET in ST-1104 een
|
||||
-- `if (payload.entity === 'question') return false`-filter krijgen — anders
|
||||
-- ontvangen solo-clients ongewenst question-events.
|
||||
--
|
||||
-- De nieuwe user-scoped SSE-route `/api/realtime/notifications` (ST-1104)
|
||||
-- abonneert zich op hetzelfde kanaal en filtert op `entity === 'question'`
|
||||
-- + product-toegang van de gebruiker.
|
||||
--
|
||||
-- DELETE wordt niet ondersteund — questions gaan naar status='answered',
|
||||
-- 'cancelled' of 'expired', niet weg.
|
||||
--
|
||||
-- Payload shape:
|
||||
-- { op: 'I' | 'U',
|
||||
-- entity: 'question',
|
||||
-- id: text,
|
||||
-- product_id: text,
|
||||
-- story_id: text,
|
||||
-- task_id: text|null,
|
||||
-- assignee_id: text|null, // story.assignee_id, voor "wacht op jou"-emphase
|
||||
-- status: 'open'|'answered'|'cancelled'|'expired' }
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_question_change() RETURNS trigger AS $$
|
||||
DECLARE
|
||||
story_row record;
|
||||
payload jsonb;
|
||||
BEGIN
|
||||
SELECT assignee_id INTO story_row FROM stories WHERE id = NEW.story_id;
|
||||
|
||||
payload := jsonb_build_object(
|
||||
'op', CASE TG_OP
|
||||
WHEN 'INSERT' THEN 'I'
|
||||
WHEN 'UPDATE' THEN 'U'
|
||||
END,
|
||||
'entity', 'question',
|
||||
'id', NEW.id,
|
||||
'product_id', NEW.product_id,
|
||||
'story_id', NEW.story_id,
|
||||
'task_id', NEW.task_id,
|
||||
'assignee_id', story_row.assignee_id,
|
||||
'status', NEW.status
|
||||
);
|
||||
|
||||
PERFORM pg_notify('scrum4me_changes', payload::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS claude_questions_notify ON claude_questions;
|
||||
CREATE TRIGGER claude_questions_notify
|
||||
AFTER INSERT OR UPDATE ON claude_questions
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_question_change();
|
||||
|
|
@ -66,6 +66,8 @@ model User {
|
|||
product_members ProductMember[]
|
||||
assigned_stories Story[] @relation("StoryAssignee")
|
||||
login_pairings LoginPairing[]
|
||||
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
||||
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
||||
|
||||
@@index([active_product_id])
|
||||
@@map("users")
|
||||
|
|
@ -112,6 +114,7 @@ model Product {
|
|||
todos Todo[]
|
||||
members ProductMember[]
|
||||
active_for_users User[] @relation("UserActiveProduct")
|
||||
claude_questions ClaudeQuestion[]
|
||||
|
||||
@@unique([user_id, name])
|
||||
@@unique([user_id, code])
|
||||
|
|
@ -158,6 +161,7 @@ model Story {
|
|||
updated_at DateTime @updatedAt
|
||||
logs StoryLog[]
|
||||
tasks Task[]
|
||||
claude_questions ClaudeQuestion[]
|
||||
|
||||
@@unique([product_id, code])
|
||||
@@index([pbi_id, priority, sort_order])
|
||||
|
|
@ -212,6 +216,7 @@ model Task {
|
|||
status TaskStatus @default(TO_DO)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
claude_questions ClaudeQuestion[]
|
||||
|
||||
@@index([story_id, priority, sort_order])
|
||||
@@index([sprint_id, status])
|
||||
|
|
@ -267,3 +272,29 @@ model LoginPairing {
|
|||
@@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?
|
||||
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([product_id, status])
|
||||
@@index([status, expires_at])
|
||||
@@map("claude_questions")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue