[QA Brain MVP] T4: Case-Insensitive Substring/Keyword Search Baseline & Query Service
สรุปงาน (Summary)
พัฒนา QA Brain Query Service สำหรับการค้นหาและเรียกดู Gotchas โดยวางโครงสร้าง Pre-Query Authorization Filtering ที่รัดกุม, การแยก Semantics ของพารามิเตอร์ projectId เป็น 3 รูปแบบอย่างชัดเจน, การปิดกั้นข้อมูล DRAFT เป็นค่าตั้งต้น (Strict DRAFT Boundary Isolation), การรองรับ includeDeprecated, และใช้ระบบค้นหา Substring/Keyword (ILIKE) ที่ครอบคลุมทั้ง title, what, tags และ evidenceRef เป็น MVP Baseline
อ้างอิงข้อกำหนดสถาปัตยกรรม: Canonical RFC v4.1 (Section 2.1, 2.4, 2.5, 5.2, 5.3)
ลำดับการทำงาน (Dependency): ขึ้นต่อกับ T1 (ต้องมี Data Model)
ขอบเขตและงานที่ต้องทำ (Scope of Work)
1. นิยามความหมายของ projectId 3 รูปแบบอย่างชัดเจน (3-Tier Project Semantics)
เพื่อป้องกันความคลุมเครือระหว่างการค้นหาทั่วทั้งเวิร์กสเปซกับการค้นหารายโปรเจกต์ ให้สร้าง Where Clause ตามพารามิเตอร์ที่ส่งเข้ามาดังนี้:
รูปแบบพารามิเตอร์ projectId
|
ความหมายและพฤติกรรมการค้นหา | เงื่อนไข SQL Where Clause |
|---|---|---|
| 1. ไม่ส่ง (Omitted / Undefined) | ค้นหา Gotcha ส่วนกลางของ Workspace รวมกับ ทุกโปรเจกต์ที่ Caller ถือ Grant อยู่ | workspace_id = :wsId AND (project_id IS NULL OR project_id IN (:authorizedProjectIds)) |
2. ส่งค่า null อย่างชัดแจ้ง (projectId: null) |
ค้นหา เฉพาะ Gotcha ส่วนกลางระดับ Workspace เท่านั้น (ไม่ดึงข้อมูลของโปรเจกต์ใดเลย แม้จะมีสิทธิ์) | workspace_id = :wsId AND project_id IS NULL |
3. ระบุ ID โปรเจกต์ (projectId: "proj-123") |
ตรวจสอบว่า Caller มี Grant ใน proj-123 หรือไม่ (หากไม่มี ให้ตอบกลับ HTTP 403 FORBIDDEN) $\rightarrow$ ค้นหา Gotcha ของโปรเจกต์นั้น รวมกับ Gotcha ส่วนกลางระดับ Workspace |
workspace_id = :wsId AND (project_id = 'proj-123' OR project_id IS NULL) |
- ข้อกำหนดสำคัญ: ห้าม ทำ Post-query in-memory filtering เพื่อป้องกันปัญหา Information Leakage และ Pagination Keyset เคลื่อน
2. DRAFT & DEPRECATED Boundary Isolation
- สำหรับคำสั่งค้นหา (
query):- ค่าตั้งต้น (
includeDraft = false, includeDeprecated = false): ต้องกรองstatus = 'VERIFIED'เท่านั้น - กรณีส่ง
includeDraft = true: ผู้เรียกต้องมีบทบาทระดับQA_EDITOR+ในโปรเจกต์นั้น (หรือWORKSPACE_EDITOR+สำหรับ Workspace Draft) หากไม่มีสิทธิ์ ให้ปฏิเสธหรือเพิกเฉย - กรณีส่ง
includeDeprecated = true: ยอมให้ผลลัพธ์รวมบทเรียนสถานะDEPRECATEDมาด้วย (สำหรับ Web UI แท็บ Deprecated) โดยยังคงซ่อนDRAFTไว้ตามเดิม
- ค่าตั้งต้น (
- สำหรับคำสั่งเรียกดูรายข้อ (
get_entry):- หาก Gotcha ที่ขอมีสถานะเป็น
DRAFTและผู้เรียกไม่ได้ส่งincludeDraft = trueหรือไม่มีสิทธิ์ Editor ให้ตอบกลับ HTTP 404NOT_FOUNDเพื่อป้องกัน Resource Enumeration - หาก Gotcha เป็น
DEPRECATEDต้องส่งincludeDeprecated = true(และมีสิทธิ์QA_EDITOR+) จึงจะอ่านได้
- หาก Gotcha ที่ขอมีสถานะเป็น
3. MVP Substring / Keyword Search Engine (รวม evidenceRef)
- ใช้ Case-Insensitive Substring Matching (
ILIKE) บน PostgreSQL:- ทำการตัดคำค้นหา (Query Tokens) ด้วยช่องว่าง
- จับคู่ค้นหาใน 4 ฟิลด์หลัก:
title,what,evidenceRef, และสมาชิกในtagsArray - รองรับการค้นหาคำสำคัญทั้งภาษาไทยและภาษาอังกฤษ
- มีคำเตือนมาตรฐาน (
disclaimer) แนบในผลลัพธ์:"Search uses case-insensitive keyword matching baseline. Complex semantic queries may require exact terms."
4. Cursor-Based Pagination
- รองรับพารามิเตอร์
limit(1 ถึง 20, ค่าตั้งต้น 5) - ใช้ Keyset / Cursor Pagination อิงตามคู่
(createdAt, id) - ส่งคืน
nextCursorที่ปลอดภัย (Base64 encoded) และtotalMatches
เกณฑ์การตรวจรับ (Acceptance Criteria)
-
Explicit Null Scope Test: เมื่อส่ง
projectId: nullระบบต้องส่งคืนเฉพาะ Gotcha ที่projectId IS NULLเท่านั้น ไม่ปน Gotcha ของโปรเจกต์ใดเลย -
Omitted Scope Test: เมื่อไม่ส่ง
projectIdระบบส่งคืน Gotcha ของทุกโปรเจกต์ที่ผู้ใช้มีสิทธิ์ รวมกับ Gotcha ส่วนกลาง -
Explicit ID Scope Test: ส่ง
projectId = "proj-A"ได้เฉพาะ Gotcha ของproj-A+ ส่วนกลาง; หากส่ง ID โปรเจกต์ที่ไม่มีสิทธิ์ ได้รับ HTTP 403FORBIDDEN - DRAFT Immunity Test: การเรียกค้นหาแบบปกติจะไม่เห็น Gotcha สถานะ DRAFT เด็ดขาด
-
EvidenceRef Search Test: ค้นหาด้วย Run ID หรือ Commit Hash ใน
evidenceRefแล้วได้ผลลัพธ์ถูกต้อง -
Deprecated Filter Test: เมื่อส่ง
includeDeprecated = trueได้รับ Gotcha สถานะ Deprecated ปรากฏในผลลัพธ์ -
Unverified Get Rejection: การยิง
get_entryขอ ID ของบทเรียน DRAFT โดยไม่ส่งสิทธิ์ Editor ต้องได้รับผลลัพธ์NOT_FOUND