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
  • #10

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

[Tracker] QA Brain Integration — Collaborative QA Institutional Memory (MVP Phase 1 & 2)

สรุปภาพรวม (Overview)

แผนงานบูรณาการ QA Brain (คลังบทเรียนและ Gotchas ของ QA) เข้ากับ Happy QA Platform & MCP เพื่อให้คนในทีมและ AI Agent ทุกตัวสามารถค้นหาและร่วมกันบันทึกบทเรียนการทดสอบได้อย่างปลอดภัย

เอกสารอ้างอิง: Canonical RFC v4.1 (Implementation-Ready Core Spec) แนบอยู่ส่วนท้ายของ Issue นี้อย่างสมบูรณ์


เป้าหมายของ MVP (Phase 1 & 2)

  1. กำจัดคอขวด Single-Writer PGLite ท้องถิ่น: ย้ายคลังความรู้เข้าสู่ PostgreSQL กลางของ Happy QA
  2. เปิดรับทั้งคนและ Agent: ให้ Agent (Claude Code, ChatGPT, Cursor, AIEM) และคนสามารถบันทึก Gotchas ได้ โดยเริ่มต้นสถานะเป็น DRAFT เสมอ
  3. ระบบสิทธิ์และ Audit รัดกุม (Omnipresent Scope Authorization & Immutable Audit Trail): กรองสิทธิ์ก่อนค้นหาและก่อนส่งข้อมูลกลับในทุกมิติ พร้อมลงบันทึกใน qa_audit_event ด้วย Hash Chain
  4. นำเข้าข้อมูลเดิม 70+ รายการ: ผ่านกระบวนการ Dry-Run และ Seeding อย่างปลอดภัยโดยไม่เขียนทับข้อมูลที่มีคนแก้ไข

แผนผังลำดับการพัฒนา (Dependency Graph & Execution Order)

T1: Data Model & DB Constraints (#1)
  │
  ├──> T2: Pre-Ingress Sanitizer (#2)
  │      │
  │      ├──> T3: Atomic Idempotency (#3) ──┐
  │      │                                  │
  │      ├──> T4: Scoped Query Service (#4) ───> T6: MCP Tools Catalogue (#6)
  │      │                                  │
  │      └──> T5: CAS Web Domain (#5) ──────┼──> T7: Web UI & Verification (#7)
  │                                         │
  ├──> T8: Vault Seeding Tool (#8)          │
  │                                         │
  └──> T9: Security & E2E Test Suite (#9) <─┘
  │
  └──> T10: RFC v4.1 Coverage Closure & Hardening (#11) [Audit / Typed Evidence / Duplicate Advisory / Contract Alignment]

รายการ Implementation Tickets (T1 - T10)

  • #1 - T1: Data Model, Composite Relations, and SQL Check Constraints
  • #2 - T2: Pre-Ingress Sanitizer (v1.0.0) & Request Fingerprinting Pipeline
  • #3 - T3: Atomic Transactional Idempotency Engine for Brain Mutations
  • #4 - T4: Case-Insensitive Substring/Keyword Search Baseline & Query Service
  • #5 - T5: Web Domain Commands with CAS Concurrency & Promotion Copy Engine
  • #6 - T6: MCP Tool Catalogue Expansion & Ports Implementation
  • #7 - T7: Web UI Feed, Quick Entry Modal, and Lead Verification Dashboard
  • #8 - T8: Dry-Run Audit & Historical Vault Seeding Tool
  • #9 - T9: Security, Concurrency, and End-to-End Persona Verification Test Suite
  • #11 - T10: RFC v4.1 Coverage Closure & Hardening

เกณฑ์การตรวจรับรอง (Acceptance Gate)

  • ผ่านชุดการทดสอบความปลอดภัยและการทำงานร่วมกัน 15 ด่านใน #9 ครบ 100%
  • ผ่าน End-to-End Persona Acceptance Test (Agent A บันทึก $\rightarrow$ Lead กดรับรอง $\rightarrow$ Agent B ใน Session ใหม่ค้นเจอและหลบกับดักสำเร็จ)

Canonical RFC v4.1: Collaborative QA Institutional Memory Architecture

1. ที่มาและปัญหาเดิม (Background & Core Problems)

  • P-1: Agent ลืม ไม่ใช่คนลืม — เจอ Gotcha ในโปรเจกต์หนึ่ง พอเปิด Session ใหม่ Agent ไม่มีความรู้นั้นหลงเหลืออยู่
  • P-2: ไม่มีคลังกลาง — บทเรียนและ Gotcha กระจัดกระจายตามเครื่องของแต่ละคน
  • P-3: การจดเป็นภาระของมนุษย์ — หากไม่มีกลไกดักจับ ความรู้จะสูญหาย
  • เกณฑ์สำเร็จ: "เปลี่ยนตัว Agent แล้วยังทำงานต่อตามระบบได้โดยไม่ทำข้อผิดพลาดซ้ำ"

ข้อเท็จจริงและข้อจำกัดปัจจุบัน

  1. คลังเดิมผูกติดกับเครื่องส่วนบุคคลและติดคอขวด PGLite Single-Writer
  2. Gotcha เดิมเกิดจาก Stop Hook ท้องถิ่นบนเครื่อง Mac เครื่องเดียว
  3. Happy QA MCP ยึดหลัก Closed Data-Plane และ Governance เข้มงวด

2. หลักการและสัญญาระบบที่ล็อคใน RFC v4.1

2.1 ยุทธศาสตร์การค้นหาภาษาไทย (Thai Search Baseline)

  • MVP ใช้ Case-insensitive Substring / Keyword Search (ILIKE) บน title, what, evidenceRef, tags เป็น Baseline
  • รันชุดทดสอบวัดผล Benchmark ใน Phase 3 ก่อนพิจารณา Tokenizer หรือ Semantic Vector

2.2 วงจรการรับรองผูกกับ Revision และ CAS

  • ทุกคำสั่ง Edit, Verify, Deprecate, Promote ต้องส่ง expectedVersion
  • หากมี Version Conflict ตอบกลับ HTTP 409 VERSION_CONFLICT
  • การแก้ไขข้อมูลสำคัญปลดสถานะกลับเป็น DRAFT เสมอ และล้างข้อมูล verifiedByUserId
  • ประวัติการแก้ไขบันทึกลง qa_brain_revision และส่ง Event เข้า qa_audit_event

2.3 การยกระดับสู่ส่วนกลางแบบ Promoted Copy (Promotion Lifecycle)

  • ไม่ทำลายต้นฉบับเดิมในโปรเจกต์
  • คำสั่ง promote: สร้างแถวใหม่ projectId = null, promotedFromGotchaId = source.id, sourceGotchaRevision = source.version, status = DRAFT, approvedRevision = null, promotedAt = null
  • บังคับส่ง Neutralized Evidence ในคำขอ Promote ห้ามคัดลอกข้อมูลเฉพาะโปรเจกต์อัตโนมัติ
  • สิทธิ์การอนุมัติและ Publish: WORKSPACE_QA_LEAD หรือ WORKSPACE_ADMIN เท่านั้น
  • เมื่อ Lead กดยืนยัน Verify: ระบบจะรัน CAS อัปเดต status = VERIFIED, approvedRevision = version, และ promotedAt = now()

2.4 การตีกรอบ DRAFT อย่างเคร่งครัด

  • query: ส่งคืนเฉพาะ VERIFIED โดยตั้งต้น การอ่าน Draft ต้องส่ง includeDraft: true และมีสิทธิ์ QA_EDITOR+
  • get_entry: หากขอ DRAFT โดยไม่มีสิทธิ์ ตอบกลับ NOT_FOUND ทันที

2.5 นิยามความหมายของ projectId 3 รูปแบบ (3-Tier Project Semantics)

  1. ไม่ระบุ projectId (Omitted / Undefined): คืน Gotcha ส่วนกลางของ Workspace รวมกับทุกโปรเจกต์ที่ได้รับ Grant (workspace_id = :wsId AND (project_id IS NULL OR project_id IN (:authorizedProjectIds)))
  2. ระบุ projectId: null อย่างชัดแจ้ง: คืนเฉพาะ Gotcha ส่วนกลางระดับ Workspace เท่านั้น (workspace_id = :wsId AND project_id IS NULL)
  3. ระบุ ID โปรเจกต์ (projectId = "proj-123"): ตรวจสอบสิทธิ์ก่อน $\rightarrow$ คืน Gotcha ของโปรเจกต์นั้น รวมกับ Gotcha ส่วนกลาง (workspace_id = :wsId AND (project_id = 'proj-123' OR project_id IS NULL))
  • Machine Key อ่าน Gotcha ส่วนกลางได้ แต่อ่านข้ามโปรเจกต์ที่ไม่ได้รับ Grant ไม่ได้

2.6 Pre-Ingress Sanitizer (Policy v1.0.0) & Request Fingerprinting

  • ทุก Write Path (ทั้ง MCP record_gotcha, Web create, และ Web edit) ต้องผ่าน Sanitizer Policy v1.0.0
  • Mask Secrets ([REDACTED_SECRET]) และ PII ([REDACTED_PII])
  • ห้าม Outbound HTTP Requests (SSRF Immunity)
  • Fail-Closed หาก Sanitizer ขัดข้อง
  • Fingerprint คำนวณจาก Canonical Sanitized Payload

2.7 Concurrent Idempotency & Immutable Audit Trail

  • Atomic Idempotency Resolution: ใช้ Transaction-level Lock (SELECT pg_advisory_xact_lock(...) หรือ row-level lock) เพื่อให้คำขอขนานกันเข้าคิวเรียงหนึ่ง คำขอแรกทำหน้าที่สร้างข้อมูล (First Winner) คำขอที่เหลือรอและได้รับผลลัพธ์ Replay เดิม ป้องกัน Unique Constraint Violation Error 100%
  • Audit Trail Integration: เพิ่ม "brain" ใน QA_WORKSPACE_EVENT_FAMILIES ทุก Mutation บันทึกผ่าน appendAudit พร้อมต่อ Hash Chain ต่อเนื่อง

3. โครงสร้างฐานข้อมูล (Prisma Models)

enum QaBrainGotchaStatus {
  DRAFT
  VERIFIED
  DEPRECATED
}

enum QaBrainChannel {
  WEB_UI
  MCP_DELEGATED
  MCP_MACHINE
  IMPORT_SEED
}

enum QaBrainAuthorType {
  HUMAN
  AGENT
  UNKNOWN
}

enum QaBrainEvidenceType {
  RUN_RESULT
  GIT_COMMIT
  ISSUE
  LOG_EXCERPT
}

enum QaBrainPrincipalType {
  USER
  MACHINE
}

model QaBrainGotcha {
  id                   String              @id @default(cuid())
  workspaceId          String              @map("workspace_id")
  projectId            String?             @map("project_id")
  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")
  
  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")
  machineKeyId         String?             @map("machine_key_id")
  lastMutationChannel  QaBrainChannel      @default(IMPORT_SEED) @map("last_mutation_channel")
  manuallyModifiedAt   DateTime?           @map("manually_modified_at")
  
  status               QaBrainGotchaStatus @default(DRAFT)
  verifiedByUserId     String?             @map("verified_by_user_id")
  verifiedAt           DateTime?           @map("verified_at")
  verifiedRevision     Int?                @map("verified_revision")
  
  promotedFromGotchaId String?             @map("promoted_from_gotcha_id")
  sourceGotchaRevision Int?                @map("source_gotcha_revision")
  approvedRevision     Int?                @map("approved_revision")
  promotedAt           DateTime?           @map("promoted_at")
  
  sourceRepo           String?             @map("source_repo")
  sourcePath           String?             @map("source_path")
  sourceFileChecksum   String?             @map("source_file_checksum")
  
  version              Int                 @default(1)
  createdAt            DateTime            @default(now()) @map("created_at")
  updatedAt            DateTime            @updatedAt @map("updated_at")

  workspace            QaWorkspace         @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  project              QaProject?          @relation(fields: [workspaceId, projectId], references: [workspaceId, id], onDelete: Cascade)
  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")
}

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")
}

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")
}

4. SQL Check Constraints

-- 1. Principal สอดคล้องกับ Channel
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. สถานะ VERIFIED ต้องมีผู้รับรองและ Revision สอดคล้อง
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. 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. ขยาย Event Check รองรับ brain. ระดับเวิร์กสเปซ
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.'));
Edited Sep 24, 2026 by Pornpavit Tantilavasute
Assignee
Assign to
Time tracking