[QA Brain MVP] T1: Data Model, Composite Relations, and SQL Check Constraints
สรุปงาน (Summary)
สร้าง Prisma Schema และ Migration สำหรับระบบ QA Brain (Institutional Memory) เพื่อรองรับการจัดเก็บ Gotchas, ประวัติการแก้ไข (Revision History), ตารางจัดการ Idempotency, มิติ Authorship และการเชื่อมต่อกับระบบ Immutable Audit Trail (qa_audit_event) ในฐานข้อมูล PostgreSQL ของ Happy QA โดยบังคับใช้กฎ Composite Uniqueness, Composite Foreign Keys, และ SQL Check Constraints อย่างเข้มงวดตาม Canonical RFC v4.1
อ้างอิงข้อกำหนดสถาปัตยกรรม: Canonical RFC v4.1 (Section 4)
ลำดับการทำงาน (Dependency): Foundation Ticket (ไม่ต้องรองานอื่น)
ขอบเขตและงานที่ต้องทำ (Scope of Work)
1. เพิ่ม Prisma Enums
ใน apps/web/prisma/schema.prisma:
-
QaBrainGotchaStatus:DRAFT,VERIFIED,DEPRECATED -
QaBrainEvidenceType:RUN_RESULT,GIT_COMMIT,ISSUE,LOG_EXCERPT -
QaBrainChannel:WEB_UI,MCP_DELEGATED,MCP_MACHINE,IMPORT_SEED -
QaBrainPrincipalType:USER,MACHINE -
QaBrainAuthorType:HUMAN,AGENT,UNKNOWN
2. เพิ่ม Prisma Models (ครบทุกฟิลด์ตาม Canonical RFC v4.1)
QaBrainGotcha
model QaBrainGotcha {
id String @id @default(cuid())
workspaceId String @map("workspace_id")
projectId String? @map("project_id") // Null = แชร์เฉพาะใน Workspace เดียวกัน
title String
slug String
what String @db.Text
evidenceType QaBrainEvidenceType @map("evidence_type")
evidenceRef String @map("evidence_ref") @db.Text
evidenceDetail String? @map("evidence_detail") @db.Text
howToApply String @map("how_to_apply") @db.Text
tags String[]
contentHash String @map("content_hash") // SHA-256 ของเนื้อหาหลักที่คำนวณโดยเซิร์ฟเวอร์
// การแยก Accountability และ Channel ออกจาก Authorship
channel QaBrainChannel @map("channel")
authorType QaBrainAuthorType @default(HUMAN) @map("author_type")
agentName String? @map("agent_name")
agentVersion String? @map("agent_version")
actorUserId String? @map("actor_user_id") // มีค่าเมื่อเป็น WEB_UI หรือ MCP_DELEGATED
machineKeyId String? @map("machine_key_id") // มีค่าเมื่อเป็น MCP_MACHINE
lastMutationChannel QaBrainChannel @default(IMPORT_SEED) @map("last_mutation_channel")
manuallyModifiedAt DateTime? @map("manually_modified_at")
// วงจรการรับรอง (Verification Lifecycle)
status QaBrainGotchaStatus @default(DRAFT)
verifiedByUserId String? @map("verified_by_user_id")
verifiedAt DateTime? @map("verified_at")
verifiedRevision Int? @map("verified_revision")
// การยกระดับสู่ Workspace แบบ Promoted Copy
promotedFromGotchaId String? @map("promoted_from_gotcha_id")
sourceGotchaRevision Int? @map("source_gotcha_revision")
approvedRevision Int? @map("approved_revision") // เซ็ตเมื่อ Workspace Lead กดยืนยัน Verify
promotedAt DateTime? @map("promoted_at") // วันเวลาที่ได้รับอนุมัติเป็นความรู้ระดับ Workspace
// การจัดการข้อมูลตั้งต้น (Provenance & Stable Identity)
sourceRepo String? @map("source_repo")
sourcePath String? @map("source_path")
sourceFileChecksum String? @map("source_file_checksum")
// Metadata เผื่อ Vector Search ใน Phase 3
embeddingModel String? @map("embedding_model")
embeddingDim Int? @map("embedding_dim")
embeddingHash String? @map("embedding_hash")
version Int @default(1)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// ความสัมพันธ์ระดับตารางพร้อม Composite FK
workspace QaWorkspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
project QaProject? @relation(fields: [workspaceId, projectId], references: [workspaceId, id], onDelete: Cascade)
// Self-Relation ขอบเขต Workspace เดียวกัน (รองรับด้วย @@unique([workspaceId, id]))
promotedFrom QaBrainGotcha? @relation("GotchaPromotionLineage", fields: [workspaceId, promotedFromGotchaId], references: [workspaceId, id], onDelete: Restrict)
promotedCopies QaBrainGotcha[] @relation("GotchaPromotionLineage")
revisions QaBrainRevision[]
@@unique([workspaceId, id])
@@unique([workspaceId, slug])
@@unique([workspaceId, sourceRepo, sourcePath])
@@index([workspaceId, projectId, status])
@@map("qa_brain_gotcha")
}
QaBrainRevision
model QaBrainRevision {
id String @id @default(cuid())
workspaceId String @map("workspace_id")
gotchaId String @map("gotcha_id")
revisionNumber Int @map("revision_number")
title String
what String @db.Text
evidenceType QaBrainEvidenceType @map("evidence_type")
evidenceRef String @map("evidence_ref") @db.Text
evidenceDetail String? @map("evidence_detail") @db.Text
howToApply String @map("how_to_apply") @db.Text
tags String[]
status QaBrainGotchaStatus
authorType QaBrainAuthorType @default(HUMAN) @map("author_type")
agentName String? @map("agent_name")
agentVersion String? @map("agent_version")
changedByUserId String? @map("changed_by_user_id")
machineKeyId String? @map("machine_key_id")
changeReason String? @map("change_reason") @db.Text
createdAt DateTime @default(now()) @map("created_at")
gotcha QaBrainGotcha @relation(fields: [workspaceId, gotchaId], references: [workspaceId, id], onDelete: Restrict)
@@unique([gotchaId, revisionNumber])
@@index([workspaceId, gotchaId])
@@map("qa_brain_revision")
}
QaBrainIdempotencyKey
model QaBrainIdempotencyKey {
id String @id @default(cuid())
workspaceId String @map("workspace_id")
principalType QaBrainPrincipalType @map("principal_type")
principalId String @map("principal_id")
operation String
key String
sanitizerPolicyVer String @default("1.0.0") @map("sanitizer_policy_ver")
requestFingerprint String @map("request_fingerprint")
sanitizedPayloadHash String @map("sanitized_payload_hash")
responseSnapshot Json @map("response_snapshot")
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([workspaceId, principalType, principalId, operation, key])
@@index([expiresAt])
@@map("qa_brain_idempotency_key")
}
3. เพิ่ม SQL Check Constraints และการขยาย Workspace Audit Check ใน Migration
สร้าง Prisma migration (--create-only) แล้วเพิ่ม Raw SQL:
-
chk_brain_principal_channel: ตรวจสอบความสอดคล้องของ Channel และ PrincipalALTER TABLE qa_brain_gotcha ADD CONSTRAINT chk_brain_principal_channel CHECK ( (channel = 'MCP_MACHINE' AND machine_key_id IS NOT NULL AND actor_user_id IS NULL) OR (channel IN ('WEB_UI', 'MCP_DELEGATED') AND actor_user_id IS NOT NULL AND machine_key_id IS NULL) OR (channel = 'IMPORT_SEED' AND actor_user_id IS NULL AND machine_key_id IS NULL) ); -
chk_brain_verified_invariants: สถานะVERIFIEDต้องมีข้อมูลผู้ตรวจและ Revision ตรงกับ Version ปัจจุบันALTER TABLE qa_brain_gotcha ADD CONSTRAINT chk_brain_verified_invariants CHECK ( (status = 'VERIFIED' AND verified_by_user_id IS NOT NULL AND verified_at IS NOT NULL AND verified_revision IS NOT NULL AND verified_revision = version) OR (status != 'VERIFIED') ); -
chk_brain_source_provenance: Provenance ฟิลด์ต้องมีค่าครบชุดหรือว่างครบชุดALTER TABLE qa_brain_gotcha ADD CONSTRAINT chk_brain_source_provenance CHECK ( (source_repo IS NULL AND source_path IS NULL AND source_file_checksum IS NULL) OR (source_repo IS NOT NULL AND source_path IS NOT NULL AND source_file_checksum IS NOT NULL) ); -
Audit Workspace-Level Check Extension: ขยาย Constraint ให้รองรับตระกูล
'brain.'เมื่อproject_id IS NULLALTER TABLE "qa_audit_event" DROP CONSTRAINT "qa_audit_event_workspace_level_check"; ALTER TABLE "qa_audit_event" ADD CONSTRAINT "qa_audit_event_workspace_level_check" CHECK ("project_id" IS NOT NULL OR starts_with("operation", 'workspace_credential.') OR starts_with("operation", 'task.vocabulary.') OR starts_with("operation", 'tracker.') OR starts_with("operation", 'brain.')); ALTER TABLE "qa_activity_event" DROP CONSTRAINT "qa_activity_event_workspace_level_check"; ALTER TABLE "qa_activity_event" ADD CONSTRAINT "qa_activity_event_workspace_level_check" CHECK ("project_id" IS NOT NULL OR starts_with("kind", 'workspace_credential.') OR starts_with("kind", 'task.vocabulary.') OR starts_with("kind", 'tracker.') OR starts_with("kind", 'brain.'));
เกณฑ์การตรวจรับ (Acceptance Criteria)
-
คำสั่ง
npx prisma validateผ่าน 100% มีฟิลด์contentHash,lastMutationChannel,approvedRevision,promotedAtครบถ้วนตาม Canonical RFC v4.1 -
รัน
npx prisma migrate devผ่านบน PostgreSQL ทดสอบ และสร้างตารางครบทั้ง 3 ตาราง - การเชื่อมโยงข้าม Workspace ถูกปฏิเสธด้วย Foreign Key Constraint
-
Negative Test ใน DB ยืนยันว่าการบันทึกสถานะ
VERIFIEDโดยไม่มีverified_by_user_idถูกสกัดกั้นด้วย SQL Check Constraint ทันที -
ทดสอบบันทึกแถวระดับ Workspace (
projectId = null) ลงในqa_audit_eventด้วย operationbrain.record_gotchaผ่าน Check Constraint สำเร็จ