Skip to content

GitLab

  • Menu
Projects Groups Snippets
    • Loading...
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in
  • H happy-qa
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 11
    • Issues 11
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 0
    • Merge requests 0
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Monitor
    • Monitor
    • Metrics
    • Incidents
  • Packages & Registries
    • Packages & Registries
    • Package Registry
    • Infrastructure Registry
  • Analytics
    • Analytics
    • CI/CD
    • Repository
    • Value stream
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • Pornpavit Tantilavasute
  • happy-qa
  • Issues
  • #1

Closed
Open
Created Sep 23, 2026 by Pornpavit Tantilavasute@joe_qaMaintainer0 of 5 tasks completed0/5 tasks

[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:

  1. chk_brain_principal_channel: ตรวจสอบความสอดคล้องของ Channel และ Principal
    ALTER 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)
    );
  2. 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')
    );
  3. 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)
    );
  4. Audit Workspace-Level Check Extension: ขยาย Constraint ให้รองรับตระกูล 'brain.' เมื่อ project_id IS NULL
    ALTER 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 ด้วย operation brain.record_gotcha ผ่าน Check Constraint สำเร็จ
Edited Sep 24, 2026 by Pornpavit Tantilavasute
Assignee
Assign to
Time tracking