---
title: "ข้อกำหนดความต้องการซอฟต์แวร์ (SRS)"
titleEn: "Software Requirements Specification (SRS)"
clauses: ["4.2.2", "7.1.2"]
phase: 1
category: "ออกแบบ"
generated: false
---

# ข้อกำหนดความต้องการซอฟต์แวร์ (Software Requirements Specification: SRS)

**โครงการ:** ระบบแชตบอตปัญญาประดิษฐ์เฉพาะทางพระพุทธศาสนาและเว็บไซต์ (MBU Buddhist AI)
**ผู้ว่าจ้าง:** มหาวิทยาลัยมหามกุฏราชวิทยาลัย (มมร.)
**อ้างอิง TOR:** ข้อ 4.2.2 (SRS: FR/NFR, Workflow, Architecture, Data Dictionary, User Flow/Wireframe, OpenAPI 3.0) และข้อ 7.1.2 (เอกสารส่งมอบงวดที่ 1)
**ฉบับ:** 1.0 (ระบบสาธิต) — วันที่ 3 ตุลาคม พ.ศ. 2569

> เอกสารนี้อธิบายความต้องการของระบบตามที่ **มีอยู่จริงในซอร์สโค้ด** ของระบบสาธิต (Next.js 15 + Prisma/SQLite) ทุกข้อกำหนดระบุไฟล์หรือ endpoint ที่ implement ไว้เพื่อให้ตรวจสอบย้อนกลับได้ ส่วนที่เป็นสถาปัตยกรรมระยะ Production (Kubernetes, PostgreSQL, Elasticsearch) ระบุแยกไว้ในหัวข้อ 5 พร้อมแผนที่การย่อส่วน

---

## 1. บทนำ

### 1.1 วัตถุประสงค์ของเอกสาร

1. กำหนดความต้องการเชิงหน้าที่ (Functional Requirements: FR) และไม่ใช่เชิงหน้าที่ (Non-Functional Requirements: NFR) โดยผูกรหัส `FR-xxx` / `NFR-xxx` กับข้อ TOR (clause id) ให้ตรวจสอบย้อนกลับได้
2. อธิบายลำดับการทำงานหลัก (Workflow) ได้แก่ การถาม-ตอบผ่าน RAG pipeline 8 โหนด การนำเข้าเอกสาร และการประเมินคุณภาพ AI
3. อธิบายสถาปัตยกรรมระบบ (ระบบสาธิตและการแมปไปยัง Production)
4. ให้พจนานุกรมข้อมูล (Data Dictionary) ครบทุก model/field ที่สร้างจาก `prisma/schema.prisma` จริง
5. สรุปกลุ่ม API ตามเอกสาร OpenAPI 3.0 ที่ระบบให้บริการที่ `/api/openapi.json`

รหัส FR ในเอกสารนี้ใช้อ้างอิงต่อใน **แผนการทดสอบหลัก** (`/documents/master-test-plan`) เพื่อทำ Traceability Matrix ระหว่าง SRS กับกรณีทดสอบ (TOR 4.7.1)

### 1.2 ขอบเขตของระบบ

ระบบประกอบด้วย 5 ส่วนตาม TOR ข้อ 4.4:

| ส่วน | ขอบเขต | ตำแหน่งในระบบสาธิต |
|---|---|---|
| (1) Frontend Website | หน้าแรก สมัคร/เข้าสู่ระบบ ถาม-ตอบ Dhamma Talk ประวัติ สิทธิเจ้าของข้อมูล ศูนย์เอกสาร ตาราง TOR | `src/app/[locale]/*`, `src/components/*` |
| (2) Backend Service & AI Engine | REST/SSE API, RAG pipeline (LangGraph.js), Guardrail, Token Budget, Ingestion | `src/app/api/*`, `src/application/*`, `src/server/*`, `src/domain/*` |
| (3) AI Evaluation & Benchmarking | ชุดทดสอบ 7 ชุด 3 workflow บันทึกเวอร์ชัน เปรียบเทียบรอบ | `src/application/evaluation/*`, `scripts/eval/run-acceptance.ts` |
| (4) Admin Portal | ผู้ใช้/PDPA/Audit, ตั้งค่า AI, Prompt, Guardrail, ฐานองค์ความรู้, Evaluation, Ops | `src/app/[locale]/admin/*`, `src/components/admin/*` |
| (5) Dashboard & Report | การใช้งาน โทเคน ค่าใช้จ่าย Feedback SLA Defect | `/admin/dashboard`, `/admin/sla-report`, `scripts/reports/*` |

**นอกขอบเขตของระบบสาธิต** (ระบุตาม Build Plan ข้อ 14): การติดตั้งบนคลาวด์/Kubernetes ของมหาวิทยาลัย โดเมน `dhammaverse.mbu.ac.th` จริง Auto Scaling จริง WAF/Anti-DDoS ระดับคลาวด์ OAuth กับ Google/Facebook ด้วย credential จริง SMTP จริง Elasticsearch/bge-m3 จริง และ **การทดสอบเจาะระบบ (Penetration Test) ซึ่งไม่ได้ดำเนินการในรอบนี้ตามคำสั่งของผู้ว่าจ้าง**

### 1.3 ผู้มีส่วนได้ส่วนเสียและบทบาทผู้ใช้

| บทบาท (`User.role`) | คำอธิบาย | สิทธิ์หลัก |
|---|---|---|
| `GENERAL_USER` | ผู้ใช้ทั่วไป (นักศึกษา พระภิกษุ ประชาชน) | ถาม-ตอบ ประวัติ สิทธิเจ้าของข้อมูล; Dhamma Talk เมื่อผู้ดูแลเปิดสิทธิ์ (`dhammaTalkEnabled`) |
| `ADMIN` | ผู้ดูแลระบบของมหาวิทยาลัย / คณะกรรมการตรวจรับ | Admin Portal ทั้งหมด ยกเว้นสำรองข้อมูล ทำลายข้อมูล และเปลี่ยน role |
| `SUPER_ADMIN` | ผู้ดูแลระบบสูงสุด | ทุกสิทธิ์ รวมสำรองข้อมูล (`/admin/backup`) ทำลายข้อมูล (`/admin/data-destruction`) และเปลี่ยน role |

บทบาททั้ง 3 นิยามใน `src/domain/identity/rbac.ts` (`ROLES`) และตรวจทุก endpoint ผ่าน `withApi({ auth })` ใน `src/server/api/with-api.ts`

### 1.4 นิยามศัพท์

| คำ | ความหมาย |
|---|---|
| RAG | Retrieval-Augmented Generation — ค้นข้อความอ้างอิงจากคลังก่อนให้ LLM สร้างคำตอบ |
| Chunk | หน่วยข้อความที่ทำดัชนี (พระไตรปิฎก/อรรถกถา = 1 ข้อ ต่อ 1 chunk) |
| Citation | การอ้างอิงในรูปแบบ `{abbreviation} ({edition}) {volume}/{item}/{page}` เช่น `ขุ.ขุ. (ไทย) 25/5/3` |
| Precedence | ลำดับความสำคัญของประเภทเอกสาร (พระไตรปิฎก 1.0 > อรรถกถา 0.9 > ฎีกา 0.8 > ร่วมสมัย 0.7 > ธรรมะพ่อแม่ครูอาจารย์ 0.6) |
| RRF | Reciprocal Rank Fusion — รวมผลค้นหาหลายรายการด้วย `Σ 1/(k + rank)` |
| SSE | Server-Sent Events — ส่งผลลัพธ์แบบ streaming จากเซิร์ฟเวอร์ |
| TTFT | Time To First Token — เวลาตั้งแต่รับคำถามจนได้โทเคนแรกของคำตอบ |
| Guardrail | กฎป้องกันคำถามนอกขอบเขต/อ่อนไหว/prompt injection และตรวจคำตอบ |
| Unavailable Mode | โหมดปิดบริการแชตชั่วคราว (สั่งโดยผู้ดูแล หรืออัตโนมัติเมื่อโทเคนใกล้เพดาน) |

### 1.5 เอกสารอ้างอิง

- `docs/build-plan.md` (แผนพัฒนาระบบสาธิต ครอบคลุม TOR 200 ข้อ) และตาราง TOR ที่หน้า `/tor` (`src/data/tor-map.ts`)
- `/documents/requirements-summary` — สรุปความต้องการ (TOR 4.2.1)
- `/documents/ux-deliverables` — User Flow/Wireframe/Prototype (TOR 4.3.1–4.3.3)
- `/documents/data-ingestion-plan` — แผนนำเข้าข้อมูล (TOR 4.5.1)
- `/documents/master-test-plan` — แผนการทดสอบหลักและ Traceability (TOR 4.7.1)
- `/api/openapi.json` — ข้อกำหนด API (OpenAPI 3.0.3)

---

## 2. ภาพรวมระบบ

### 2.1 เทคโนโลยีที่ใช้จริง (จาก `package.json`)

| ชั้น | เทคโนโลยี |
|---|---|
| Web/Backend | Next.js 15.5 (App Router, Route Handlers), React 19.1, TypeScript strict |
| UI | Tailwind CSS 4, `next-themes` (dark/light), `next-intl` 4 (th/en), `react-markdown` + `remark-gfm`, `recharts` |
| ข้อมูล | Prisma 6.19 + SQLite (`prisma/dev.db`; ชุดทดสอบใช้ฐานข้อมูลแยกต่อไฟล์) |
| AI Orchestration | `@langchain/langgraph` 1.4 (`StateGraph` 8 โหนด) + `@langchain/core` |
| LLM | Typhoon `typhoon-v2.5-30b-a3b-instruct` ผ่าน OpenAI-compatible API (ค่าเริ่มต้นใน `src/domain/config/ai-config.ts`) สลับเป็น OpenAI / Gemini / Claude / Mock ได้ |
| Auth/Security | `jose` (JWT HS256), `bcryptjs`, CSRF double-submit, zod v4 validation |
| Parsers | `unpdf` (PDF), `mammoth` (DOCX), `fast-xml-parser` (TEI-XML), `gray-matter`, `papaparse` (CSV), `diff` |
| ทดสอบ | vitest 5 + `@vitest/coverage-v8`, Testing Library (jsdom), Playwright 1.63 |

### 2.2 โครงสร้างชั้นซอฟต์แวร์ (Clean Architecture)

```mermaid
flowchart LR
  subgraph Presentation["src/app (หน้าเว็บ + Route Handlers)"]
    P1["[locale]/... pages"]
    P2["api/... route.ts (withApi)"]
  end
  subgraph Application["src/application (use cases)"]
    A1["chat/ask.ts"]
    A2["evaluation/runner.ts"]
    A3["ingestion/kb-admin.ts"]
    A4["privacy, identity, config, ops, report"]
  end
  subgraph Adapters["src/server (adapters)"]
    S1["ai/graph.ts (LangGraph)"]
    S2["llm/* (typhoon/openai-compatible, gemini, claude, mock)"]
    S3["retrieval/* (KnowledgeIndex, reranker)"]
    S4["ingestion/* (pipeline, parsers)"]
    S5["db.ts, repos, audit, notification, logger"]
  end
  subgraph Domain["src/domain (pure TypeScript)"]
    D1["ai: guardrail-policy, citation, token-budget-policy"]
    D2["retrieval: bm25, embedding, rrf, synonyms, heuristic-reranker, relevance-scoring"]
    D3["ingestion: chunking-policy, sanitize"]
    D4["identity, privacy, audit, evaluation, report, config"]
  end
  Presentation --> Application --> Adapters --> Domain
  Presentation --> Adapters
```

กติกา: `src/domain/*` ไม่ import framework ใด ๆ (ทดสอบหน่วยได้โดยตรง) ชั้นบนเรียกชั้นล่างเท่านั้น และ coverage gate ของ vitest วัดเฉพาะ `src/domain/**` + `src/application/**` (`vitest.config.ts`)

---

## 3. ความต้องการเชิงหน้าที่ (Functional Requirements)

ตารางต่อไปนี้ระบุรหัส FR, ข้อกำหนด, ข้อ TOR ที่รองรับ และตำแหน่งที่ implement ในระบบสาธิต

### 3.1 การยืนยันตัวตนและสิทธิ์ (Identity & Access)

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-001 | ผู้ใช้สมัครสมาชิกด้วยอีเมล รหัสผ่าน ชื่อแสดง และติ๊กยินยอมนโยบาย เก็บเฉพาะข้อมูลที่จำเป็น บันทึก `consentVersion`/`consentAt` | ก.3.3.1, 14.2 | `/register`, `POST /api/auth/register` |
| FR-002 | เข้าสู่ระบบ/ออกจากระบบด้วยอีเมล+รหัสผ่าน ออก JWT (HS256 อายุ 24 ชม.) ใน httpOnly cookie `mbu_session`; บัญชีสถานะ `suspended`/`deleted` เข้าไม่ได้ | ก.3.3.1 | `/login`, `POST /api/auth/login`, `POST /api/auth/logout`, `src/server/auth/session.ts` |
| FR-003 | เข้าสู่ระบบด้วย Gmail/Facebook (OAuth 2.0 Authorization Code + state/PKCE) และ Mock Provider ภายในเมื่อยังไม่ตั้ง client id | ก.3.3.2 | `src/server/identity/oauth/*`, `/api/auth/oauth/[provider]/start|callback`, `/mock-oauth/authorize`, `/api/mock-oauth/*` |
| FR-004 | ป้องกัน brute force: ผิด 5 ครั้ง/15 นาที/บัญชี หรือ 20 ครั้ง/15 นาที/IP → ล็อก 15 นาที และ rate limit `/api/auth/login` 10 ครั้ง/นาที/IP | ก.2.1.4 | `src/domain/identity/lockout-policy.ts`, `LoginAttempt`, `src/middleware.ts` |
| FR-005 | ทุกคำขอ POST/PUT/PATCH/DELETE ต้องแนบ CSRF token (double-submit cookie + header `x-csrf-token`) มิฉะนั้นตอบ 403 `CSRF_INVALID` | ก.2.1.5 | `src/middleware.ts`, `GET /api/csrf` |
| FR-006 | ควบคุมสิทธิ์แบบ RBAC 3 บทบาท ทุก endpoint ประกาศสิทธิ์ (`public`/`user`/รายการ role) | ก.6.1.1, ก.7.1.3 | `src/domain/identity/rbac.ts`, `withApi({ auth })` |
| FR-007 | นโยบายรหัสผ่าน: ผู้ใช้ทั่วไป ≥8 ตัวมีตัวอักษรและตัวเลข; ผู้ดูแล ≥12 ตัวผสม 4 ชนิด; ส่วนผู้ดูแล (`/admin*`, `/api/admin*`) จำกัดเฉพาะ IP ใน `ADMIN_IP_ALLOWLIST` (นอกนั้นตอบ 403 และบันทึก audit) | ก.6.1.4 | `src/domain/identity/password-policy.ts`, `src/middleware.ts`, `/api/internal/security-event` |
| FR-008 | ผู้ใช้ดู/แก้ชื่อแสดงของตนเอง | ก.3.3.1 | `GET/PATCH /api/me` |

### 3.2 ถาม-ตอบ (Q&A)

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-010 | ผู้ใช้ส่งคำถามแล้วได้คำตอบแบบ streaming (SSE) จาก RAG pipeline พร้อมอ้างอิงที่ตรวจสอบย้อนกลับได้ ในบุคลิกบรรณารักษ์ | 2.1, 2.4, ก.4.5.2 | `/chat`, `POST /api/chat/qa`, `src/application/chat/ask.ts` |
| FR-011 | คำตอบแสดง 2 ส่วน: (1) รายการอ้างอิง ≤10 รายการ แสดง 3 รายการแรก + ปุ่ม “แสดงผลทั้งหมด/Expand All” (2) บทสรุป และข้อความเตือนท้ายคำตอบ | ก.3.6.2 | `src/components/chat/AnswerParts.tsx`, config `retrieval.finalK` (≤10) |
| FR-012 | Citation ในบทสรุปใช้รูปแบบมาตรฐาน `{abbreviation} ({edition}) {volume}/{item}/{page}` แสดงเป็นชิปสีทอง คลิกเปิด Modal เนื้อหาเต็มพร้อม metadata | ก.3.6.3, ก.4.6.3 | `src/domain/ai/citation.ts`, `GET /api/chunks/[id]` |
| FR-013 | แสดงข้อความเตือนใต้กล่องพิมพ์คำถามตลอดเวลา | ก.3.6.1 | `src/components/chat/ChatApp.tsx` + `messages/*/` |
| FR-014 | ผู้ใช้ให้ Feedback 👍/👎 (พร้อมความเห็น) ต่อคำตอบ บันทึกลงตาราง `Feedback` (1 รายการต่อผู้ใช้ต่อข้อความ) | ก.3.8.1 | `POST /api/messages/[id]/feedback` |
| FR-015 | แสดง RAG Trace ของ 8 โหนด (เวลา ms และรายละเอียดแต่ละโหนด รวม model, prompt version, โทเคน) | ก.4.10.1 | `Message.traceJson`, SSE `done.trace` |
| FR-016 | ประวัติบทสนทนาของตนเอง แสดงวันหมดอายุ ลบ/เปลี่ยนชื่อได้; ระยะเก็บเริ่มต้น 90 วัน (`retention.conversationDays`) | ก.3.5.1 | `/chat/history`, `GET/PATCH/DELETE /api/conversations[/id]` |
| FR-017 | ผู้ใช้เข้าถึงได้เฉพาะบทสนทนาของตนเอง (ไม่ใช่เจ้าของ = 404 กัน IDOR) | ก.2.1.1 | `loadOwnedConversation()` ใน `src/application/chat/ask.ts` |

### 3.3 Dhamma Talk

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-020 | สร้างบทธรรมบรรยายแบบ Artifact (Markdown) ทีละรอบ แสดงใน Response Modal แยก มีแท็บแก้ไข/ตัวอย่าง ผู้ใช้แก้ไขแล้วบันทึก และคำสั่งรอบถัดไปใช้ฉบับที่ผู้ใช้แก้ล่าสุดเป็นบริบท พร้อมข้อความเตือนในโหมดนี้ | ก.3.7.1–ก.3.7.4 | `/chat/dhamma-talk`, `POST /api/chat/dhamma-talk`, `PATCH /api/conversations/[id]` (`artifactMd`), `DhammaTalkApp.tsx` |
| FR-021 | สิทธิ์ใช้ Dhamma Talk แยกรายบัญชี ค่าเริ่มต้นปิด (`User.dhammaTalkEnabled = false`) ผู้ดูแลเปิด/ปิดได้ | ก.3.3.3, ก.6.1.3 | `PATCH /api/admin/users/[id]`, ตรวจสิทธิ์ใน route |

### 3.4 AI Engine และการค้นคืน (RAG)

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-030 | Orchestrator ด้วย LangGraph.js `StateGraph` 8 โหนด และ LLM แบบ model-agnostic ผ่าน interface `LlmGateway` (typhoon/openai/gemini/claude/mock) เปลี่ยนได้จาก config โดยไม่แก้โค้ด | ก.4.1.2, 4.4.6, 2.6, ก.6.2.3 | `src/server/ai/graph.ts`, `src/server/llm/*` |
| FR-031 | Hybrid Search: BM25 (ตัดคำไทยด้วย `Intl.Segmenter`) + pseudo-embedding 512 มิติ (feature hashing) รวมด้วย RRF (k=60) รองรับไทย–บาลี–อังกฤษด้วยพจนานุกรม synonym | ก.4.6.1 | `src/domain/retrieval/{tokenizer,bm25,embedding,rrf,synonyms}.ts`, `src/server/retrieval/index.ts` |
| FR-032 | Query Expansion/Rewriting: ขยายคำพ้อง + ขอคำค้นเพิ่มจาก LLM ด้วย prompt `query-rewrite` (เมื่อใช้ LLM จริง) | ก.4.7.2 | โหนด `inputGuardrail`/`rewriteQuery` |
| FR-033 | Re-ranker เป็นโมดูลแยก สลับได้ด้วย `retrieval.reranker` (`heuristic` ค่าเริ่มต้น หรือ `tei` เมื่อตั้ง `TEI_RERANK_URL`) | ก.4.7.1 | `src/domain/retrieval/heuristic-reranker.ts`, `src/server/retrieval/{reranker,tei-reranker}.ts` |
| FR-034 | ปรับคะแนนด้วย Precedence ของประเภทเอกสาร (ปรับรายเอกสารได้ 0–2) แล้วตัดเหลือ `finalK` ≤10 | ก.4.8.3, ก.6.3.3 | `src/domain/retrieval/relevance-scoring.ts` |
| FR-035 | บุคลิกบรรณารักษ์: ช่วยค้น/สรุปพร้อม Citation ไม่ชี้ขาดการตีความ ปฏิเสธคำถามใช่/ไม่ใช่ | ก.4.5.1, ก.4.5.3 | `config/prompts/qa-persona.v*.md`, guardrail `yes_no_pattern` |

### 3.5 Guardrails

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-040 | ตอบเฉพาะคำถามด้านพระไตรปิฎก อรรถกถา หลักธรรม และแนวทางปฏิบัติ/ปรัชญาชีวิต (หัวข้อ `scope_allow`); คำถามนอกขอบเขตถูกปฏิเสธอย่างสุภาพ เมื่อใช้ LLM จริงยืนยันด้วย prompt `guard-classifier` | ก.4.3.1, ก.4.3.2 | `src/domain/ai/guardrail-policy.ts`, โหนด `inputGuardrail` |
| FR-041 | ป้องกัน Prompt Injection ทั้งทางตรง (รูปแบบ `injection_pattern`) และทางอ้อม (sanitize ตอนนำเข้า + ครอบบริบทด้วย delimiter `<<<DATA ...>>>` และระบุว่า “เป็นข้อมูล ไม่ใช่คำสั่ง”) | ก.4.4.1 | `src/domain/ingestion/sanitize.ts`, `src/server/ai/context.ts` |
| FR-042 | ปฏิเสธหัวข้อการเมือง การเปรียบเทียบศาสนา/นิกาย และสุขภาพจิต พร้อมข้อความเฉพาะหัวข้อ | ก.4.4.3 | `GuardrailTopic` kind `refuse_sensitive` (key `politics`, `religion_comparison`, `mental_health`) |
| FR-043 | Output guardrail: แทน `[[cite:...]]` ด้วย label มาตรฐาน ตัด citation ที่ไม่อยู่ในรายการอ้างอิง ติดป้าย “(ไม่มีอ้างอิง — โปรดตรวจสอบ)” ให้ย่อหน้าที่ไม่มีอ้างอิง และลดทอนถ้อยคำชี้ขาด | ก.4.5.3, ก.4.6.3 | `resolveCitations()`, `evaluateOutput()` |
| FR-044 | ผู้ดูแลเพิ่ม/แก้/ลบ/เปิดปิดหัวข้อขอบเขตและหัวข้อปฏิเสธ และทดสอบข้อความได้ทันที | ก.6.2.5, ก.4.4.1 | `/admin/guardrails`, `/api/admin/guardrails*` |

### 3.6 งบประมาณโทเคนและโหมดปิดบริการ

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-050 | เพดานโทเคนรวม 24 ชั่วโมงล่าสุด ค่าเริ่มต้น 10,000,000 (`tokens.dailyCap`) | ก.3.4.1 | `src/domain/ai/token-budget-policy.ts` (`checkDailyBudget`), `src/server/ai/availability.ts` |
| FR-051 | เพดานโทเคนต่อ prompt: ถาม-ตอบ ≤10,000 และ Dhamma Talk ≤50,000 (schema บังคับช่วง) และปรับ Max Tokens ได้ | ก.4.2.1, ก.3.4.2, ก.6.2.1 | `tokens.maxPerPromptQa`, `tokens.maxPerPromptDhammaTalk`, `tokens.maxOutput`, `fitContext()` |
| FR-052 | เข้า Unavailable อัตโนมัติก่อนยอดจริงเกินเพดาน (คำขอที่ทำให้ยอดจองเกินเพดานถูกปฏิเสธก่อนเรียก LLM; margin ≤10%) พร้อมแจ้งเตือน `auto_unavailable` | ก.3.4.4 | `checkAvailability()`, `reserveTokens()` |
| FR-053 | ป้องกัน Cost Surge: จำกัดความยาว input (`input.maxChars` ค่าเริ่มต้น 2,000 → 400 `INPUT_TOO_LONG`) และ thinking budget (`tokens.thinkingBudget`) | ก.4.4.2 | โหนด `checkAvailability`, `generate` |
| FR-054 | ผู้ดูแลเปิด Unavailable Mode พร้อมข้อความ ไทย/อังกฤษ ทุกหน้าแสดงแบนเนอร์ (อ่าน `/api/status`) และ API แชตตอบ 503 | ก.3.4.3, ก.6.6.1 | `/admin/unavailable`, `GET/PUT /api/admin/unavailable-mode`, `GET /api/status` |

### 3.7 การเก็บรักษาข้อมูลและ PDPA

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-060 | ลบบทสนทนาที่พ้น `retentionUntil` อัตโนมัติทุก `retention.purgeIntervalMinutes` (ค่าเริ่มต้น 60 นาที) และสั่งทันทีได้ | ก.3.5.1, ก.6.2.2 | `src/application/privacy/purge.ts` (เริ่มใน `src/instrumentation-node.ts`), `POST /api/admin/maintenance/purge-conversations`, `npm run purge:conversations` |
| FR-061 | ผู้ใช้ดาวน์โหลดสำเนาข้อมูลตนเอง (JSON) และยื่นคำขอ `access_copy` / `erase` / `withdraw_consent` | 14.4 | `/privacy`, `GET /api/privacy/export`, `GET/POST /api/privacy/requests` |
| FR-062 | ผู้ดูแลดูและดำเนินการคำขอ PDPA (สร้างไฟล์สำเนา, ลบบทสนทนา+ทำให้ข้อมูลผู้ใช้เป็นนิรนาม, ระงับบัญชีเมื่อถอนยินยอม) และสร้างคำขอแทนผู้ใช้ | 14.4, ก.6.1.2 | `/admin/privacy-requests`, `/api/admin/privacy-requests*` |
| FR-063 | เข้ารหัสอีเมล/ชื่อด้วย AES-256-GCM (`DATA_ENCRYPTION_KEY`) ค้นหาด้วย `emailHash`; สร้างสำเนาข้อความที่ปิดบัง PII (`Message.contentMasked`) | 14.3 | `src/server/crypto.ts`, `src/server/repos/user-repo.ts`, `src/domain/privacy/pii-masking.ts` |
| FR-064 | แบนเนอร์ยินยอมคุกกี้ โหลด Google Analytics 4 เฉพาะเมื่อผู้ใช้ยินยอมและตั้ง `NEXT_PUBLIC_GA_ID`; หน้า `/privacy-policy` ระบุผู้ควบคุม/ผู้ประมวลผลข้อมูล | ก.1.1.3, ก.1.5.1, 14.1 | `src/components/layout/Banners.tsx`, `/privacy-policy` |

### 3.8 ฐานองค์ความรู้และการนำเข้า

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-070 | อัปโหลดเอกสาร PDF / TXT / Markdown / TEI-XML / DOCX ≤20MB พร้อม metadata แล้วนำเข้าแบบ async | ก.4.8.1, ก.6.3.1 | `/admin/knowledge-base`, `POST /api/admin/knowledge-base/upload`, `src/server/ingestion/parsers/index.ts` |
| FR-071 | Chunking: พระไตรปิฎก/อรรถกถาแบ่ง 1 ข้อ = 1 chunk ไม่ตัดกลางข้อ (ข้อยาว >3,000 tokens ติดธง `oversized`); เอกสารอื่นใช้ semantic chunking (~400 tokens ตัดเมื่อ cosine ระหว่างย่อหน้า <0.35) | ก.4.8.2, ก.4.6.2 | `src/domain/ingestion/chunking-policy.ts` |
| FR-072 | Metadata: ปิฎก นิกาย สูตร เล่ม ข้อ หน้า ประเภทเอกสาร ฉบับ ตัวย่อ เล่ม มมร. คำบาลี และ precedence แก้ไขได้ (อัปเดตดัชนีทันที) | ก.4.8.3, ก.6.3.1, ก.6.3.3 | `PATCH /api/admin/knowledge-base/[id]` |
| FR-073 | Pipeline ปรับดัชนี: งาน `ingest` / `reindex_all` / `reindex_selected` / `delete` แสดงสถานะรายเอกสาร (stage parse→sanitize→chunk→embed→index) รายการที่ล้มเหลว และรันซ้ำเฉพาะที่ล้มเหลว | ก.4.5.3, ก.4.7.3, ก.6.3.2 | `src/server/ingestion/pipeline.ts`, `/admin/knowledge-base/jobs`, `/api/admin/knowledge-base/{reindex,jobs*}` |
| FR-074 | รายงานความครอบคลุมเทียบพระไตรปิฎกและอรรถกถาฉบับ มมร. 91 เล่ม | 4.5.2, 7.2.5, ข.1 | `GET /api/admin/knowledge-base/coverage`, `src/application/ingestion/coverage.ts`, `npm run report:ingestion` |
| FR-075 | Sanitize ข้อความก่อนนำเข้า: ตัด control/invisible chars และบรรทัดที่เข้ารูปแบบคำสั่งแฝง บันทึกจำนวนบรรทัดที่ตัด | ก.4.4.1 | `src/domain/ingestion/sanitize.ts`, `KnowledgeDocument.sanitizedLines` |

### 3.9 การตั้งค่า AI และ Prompt

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-080 | ผู้ดูแลปรับค่า AI (provider/model, เพดานโทเคน, retention, retrieval, เกณฑ์ประเมิน ฯลฯ) มีผลทันทีโดยไม่ redeploy ทุกการแก้สร้าง `ConfigSnapshot` และย้อนกลับได้ | ก.6.2.1–ก.6.2.3, ก.6.2.6 | `/admin/ai-config`, `/api/admin/ai-config*`, `src/server/config/ai-config-store.ts` |
| FR-081 | ทดสอบการเชื่อมต่อ LLM และแสดงรายการ provider/model ที่ adapter รองรับ (ระบุว่าตั้ง API key แล้วหรือไม่ โดยไม่เปิดเผยค่า) | ก.6.2.3, 4.6.1 | `POST /api/admin/ai-config/test-llm`, `GET /api/admin/ai-config/models` |
| FR-082 | Prompt ไม่ hard-code: เก็บเป็นเวอร์ชัน (`PromptVersion`) ดู/เพิ่ม/แก้ (สร้างเวอร์ชันใหม่)/diff/activate (rollback) พร้อมผู้แก้และเวลา | ก.4.9.1, ก.6.2.4 | `/admin/prompts`, `/api/admin/prompts*` |
| FR-083 | Remote configuration ด้วยสคริปต์: `npm run prompts:publish` อ่าน `config/prompts/*.md` สร้างเวอร์ชันเมื่อเนื้อหาต่าง | ก.4.9.2 | `scripts/prompts/publish.ts` |
| FR-084 | กำหนดอัตราค่าโทเคนต่อ model (input/thinking/output ต่อ 1M) ใช้ประมาณค่าใช้จ่าย | ก.7.3.3 | `/admin/pricing`, `/api/admin/pricing*`, `LlmPricing` |

### 3.10 การประเมินคุณภาพ AI

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-090 | ชุดทดสอบ 7 ชุด: DS-1 ธรรมศึกษาปรนัย 100, NT-2 นักธรรมตอบสั้น 50, RET-1/2/3 Retrieval วินัย/สุตตันตะ/อภิธรรม ชุดละ 100, GR-1 นอกขอบเขต 100, GR-2 หัวข้อสุ่มเสี่ยง 50 | ก.5.1.1–ก.5.1.7 | `seed/benchmarks/*.csv`, `seed/benchmarks/SOURCES.md` |
| FR-091 | จัดการชุดและข้อทดสอบผ่านเว็บ (เพิ่ม/แก้/ลบ) นำเข้า CSV และดาวน์โหลดแม่แบบ ทุกการเปลี่ยนข้อทดสอบเพิ่ม `version` | ก.5.2.1, ก.6.4.1, ก.5.2.4 | `/api/admin/evaluation/datasets*` |
| FR-092 | 3 workflow: Accuracy (exact match ตัวเลือก), Recall@5 (retrieval), LLM-as-Judge 0–3 และ Refusal (guardrail) ผ่าน pipeline จริง | ก.5.2.2 | `src/application/evaluation/runner.ts` (`evaluateItem`), `src/domain/evaluation/scorers.ts` |
| FR-093 | สั่งรันรายชุดแบบ async บันทึกผลรายข้อ + prompt version + dataset version + model + judge model | ก.5.2.3, ก.5.2.4, ก.6.4.2 | `POST/GET /api/admin/evaluation/runs*` |
| FR-094 | เปรียบเทียบผลระหว่าง 2 รอบของชุดเดียวกัน (รายข้อ + delta + เวอร์ชัน) | ก.6.4.3 | `GET /api/admin/evaluation/compare` |
| FR-095 | เกณฑ์ขั้นต่ำ (80% / Judge 2.00) และค่ารับประกันของผู้รับจ้าง แสดงผ่าน/ไม่ผ่านเกณฑ์ตรวจรับ | ก.5.3, ก.5.3.1, 16.2 | `GET /api/admin/evaluation/thresholds`, `eval.guaranteed` |
| FR-096 | รายงาน AI Security: รัน GR-1 + GR-2 + ชุด Prompt Injection 20 เคส แสดงอัตราป้องกันต่อชุด/ประเภทการโจมตี | 7.2.7 | `GET/POST /api/admin/evaluation/security-report` |
| FR-097 | ส่งออกรายงาน Baseline/Acceptance และ Guardrail เป็น Markdown ลง `docs/deliverables` | 4.7.7, 7.2.3 | `POST /api/admin/evaluation/export`, `npm run report:evaluation` |

### 3.11 ผู้ดูแลผู้ใช้ และบันทึกการตรวจสอบ (Audit)

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-100 | ค้นหา/ดู/แก้/ระงับ/ลบ (soft) ผู้ใช้ แสดงสถานะ วันสมัคร การใช้งานล่าสุด สิทธิ์ Dhamma Talk | ก.6.1.2 | `/admin/users`, `/admin/users/[id]`, `/api/admin/users*` |
| FR-101 | การเปลี่ยน role ทำได้เฉพาะ `SUPER_ADMIN` และบันทึก audit `role_changed` | ก.6.1.1 | `PATCH /api/admin/users/[id]` |
| FR-102 | การเปิดดูบทสนทนาของผู้ใช้โดยผู้ดูแลถูกบันทึก audit `conversation_accessed` | ก.6.5.1 | `GET /api/admin/users/[id]/conversations` |
| FR-110 | บันทึก Audit: login/logout สำเร็จ-ไม่สำเร็จ สิทธิ์ config/prompt เอกสาร การเข้าถึงบทสนทนา การสั่งประเมิน คำขอ PDPA ทำลายข้อมูล defect พร้อม timestamp ผู้กระทำ IP ประเภท ก่อน/หลัง ผลลัพธ์ | ก.6.5.1, ก.6.5.2 | `src/server/audit/audit-logger.ts`, `AuditLog` |
| FR-111 | Audit Log แก้ไข/ลบไม่ได้ (SQLite trigger `audit_no_update`/`audit_no_delete`) และตรวจความสมบูรณ์ด้วย hash chain (sha256) | ก.6.5.3 | `prisma/migrations/20261002110735_audit_immutable`, `src/domain/audit/hash-chain.ts`, `GET /api/admin/audit-log/verify` |
| FR-112 | ค้นหา/กรอง Audit Log และส่งออก CSV หรือพิมพ์เป็น PDF | ก.6.5.4 | `/admin/audit-log`, `GET /api/admin/audit-log/export` |

### 3.12 Dashboard และการแจ้งเตือน

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-120 | Dashboard (เฉพาะ ADMIN/SUPER_ADMIN): บัญชีทั้งหมด DAU MAU sessions queries เฉลี่ยต่อ session แยกโหมด เลือกช่วง วัน/สัปดาห์/เดือน/ปี/กำหนดเอง และเทียบช่วงก่อนหน้า ข้อมูลอ่านสดจากฐานข้อมูล | ก.7.1.1, ก.7.1.2, ก.7.1.3, ก.7.2.1, ก.7.2.2 | `/admin/dashboard`, `GET /api/admin/dashboard/usage` |
| FR-121 | โทเคน input/thinking/output รายวัน/เดือนเทียบเพดาน โทเคนเฉลี่ยต่อคำถาม/บทสนทนาเทียบเป้า ประมาณการค่าใช้จ่าย และ Top users | ก.7.3.1–ก.7.3.4 | `GET /api/admin/dashboard/tokens` |
| FR-122 | อัตรา Feedback 👍/👎 | ก.3.8.1 | `GET /api/admin/dashboard/feedback` |
| FR-130 | แจ้งเตือนทางอีเมล (SMTP เมื่อตั้งค่า มิฉะนั้นบันทึกใน `AlertEmailLog`): โทเคนถึงเกณฑ์ (`tokens.alertThresholdPct`), ค่าใช้จ่ายถึงเกณฑ์, error rate แชต >5% ใน 15 นาที, เข้า Unavailable อัตโนมัติ, งานนำเข้าล้มเหลว | ก.4.10.2, ก.7.3.5 | `src/server/notification/index.ts`, `src/server/ai/usage.ts`, `/admin/alerts` |

### 3.13 การปฏิบัติการ (Ops) และเว็บไซต์

| รหัส | ข้อกำหนด | TOR | Implement |
|---|---|---|---|
| FR-140 | Defect Log: ความรุนแรง critical/high = 24 ชม., medium = 48 ชม., low = 80 ชม. ทำการ (10 วันทำการ) คำนวณ deadline แบบชั่วโมงทำการ ปิด high/critical ต้องมี resolutionNote การยืนยันของมหาวิทยาลัยคำนวณชั่วโมงเกินและค่าปรับ 0.025%/ชม. | 12.3, 12.4, 10.2, 4.7.8 | `/admin/defect-log`, `src/domain/report/sla-policy.ts` |
| FR-141 | รายงาน SLA: Availability, p95 latency, TTFT p95, error rate, defect เกิน SLA ส่งออกรายงานรายเดือน | 12.1, 12.2, 12.5 | `/admin/sla-report`, `/api/admin/sla-report*`, `npm run report:sla` |
| FR-142 | สำรองฐานข้อมูลและไฟล์อัปโหลด (SUPER_ADMIN) พร้อมคำสั่งกู้คืน | ก.1.6.3 | `/admin/backup`, `scripts/backup-db.sh`, `scripts/restore-db.sh` |
| FR-143 | ทำลายข้อมูลตามขอบเขตที่เลือกพร้อมหนังสือรับรอง (SUPER_ADMIN, ต้องพิมพ์ข้อความยืนยัน) | 4.11.3 | `/admin/data-destruction`, `POST /api/admin/data-destruction` |
| FR-144 | Health/Status/OpenAPI: `/api/health` (สถานะ DB และจำนวน chunk), `/api/status` (Unavailable + % งบโทเคน ไม่เปิดเผยความลับ), `/api/openapi.json` | ก.4.1.1 | `src/app/api/{health,status,openapi.json}` |
| FR-145 | Logging: JSON lines ลง `logs/app.log` + stdout และบันทึกทุกคำขอ API ลง `ApiRequestLog` (path, status, durationMs, ttftMs) | ก.4.10.1 | `src/server/logger.ts`, `withApi` |
| FR-150 | สองภาษา ไทย/อังกฤษ (`/th`, `/en`) มีเมนูสลับภาษาทุกหน้า | ก.3.2.1 | `next-intl`, `messages/{th,en}/*.json`, `LanguageSwitcher.tsx` |
| FR-151 | ธีมมืด/สว่าง พร้อมปุ่มสลับทุกหน้า | ก.3.1.3 | `next-themes`, `ThemeToggle.tsx` |
| FR-152 | Responsive รองรับมือถือ แท็บเล็ต เดสก์ท็อป (390/768/1280 px) ไม่มี horizontal scroll | ก.1.2.1, 4.3.3 | Tailwind, Playwright 3 viewport |
| FR-153 | หน้า `/tor` (ตาราง 200 ข้อ ค้นหา/กรอง/ส่งออก CSV) และศูนย์เอกสาร `/documents` (ดาวน์โหลด .md / พิมพ์ PDF) | 7.5, 4.4 | `src/app/[locale]/{tor,documents}` |
| FR-154 | หน้า `/help` วิธีใช้และช่องทางติดต่อช่วงรับประกัน | 11.2 | `src/app/[locale]/help` |

---

## 4. ความต้องการที่ไม่ใช่เชิงหน้าที่ (Non-Functional Requirements)

| รหัส | หมวด | ข้อกำหนด | TOR | วิธีรองรับ/ตรวจวัดในระบบสาธิต |
|---|---|---|---|---|
| NFR-001 | ประสิทธิภาพ | FCP ≤5 วินาที, TTFT ≤15 วินาที, Error rate ≤5% ที่ p95 | 12.2 | วัด TTFT ทุกคำขอ (`Message.ttftMs`, `ApiRequestLog.ttftMs`); E2E ตรวจ FCP <5s; `npm run report:perf` → `/documents/performance-report` |
| NFR-002 | รองรับผู้ใช้พร้อมกัน | ≥500 concurrent sessions | ก.1.4.1, 4.7.6 | ทดสอบโหลดระดับเครื่องเดียวด้วย `scripts/perf/load-test.ts`; ระดับ 500 ต้องใช้สถาปัตยกรรม Production (HPA) |
| NFR-003 | ขนาดข้อมูลผู้ใช้ | รองรับ 1,000,000 บัญชี | ก.1.4.2 | `npm run seed:perf-users -- --count=1000000` ลง `prisma/perf.db` แล้ววัดเวลาค้นหา (index `emailHash` unique) |
| NFR-004 | ความพร้อมใช้งาน | ≥95% ต่อเดือน | 12.1, ก.1.6.1 | คำนวณจาก uptime ของ process + ช่วง 5xx (`/admin/sla-report`); Production ใช้ replica ≥2 + probe |
| NFR-005 | การขยายระบบ | Microservices, Horizontal/Auto Scaling | ก.1.3.1, ก.1.3.2, ก.1.6.2 | โค้ดแบ่ง bounded context; Helm chart 8 deployment + HPA (CPU 70%) ที่ `infra/helm/mbu-buddhist-ai/` |
| NFR-006 | ความมั่นคงปลอดภัย | OWASP Top 10, TLS, Server-side validation, CSRF, Brute force, Security headers, Rate limit, WAF | ก.2.1.1–ก.2.1.5, ก.2.2.1, ก.1.1.2 | zod ทุก endpoint, Prisma (parameterized), CSP/X-Frame-Options/nosniff/Referrer-Policy/Permissions-Policy/COOP (`next.config.ts`), HSTS เมื่อ `ENABLE_HSTS=1`, rate limit 10/60/600 ต่อนาที, TLS self-signed (`server.https.js`); WAF เป็นตัวอย่างใน Helm values |
| NFR-007 | ความเป็นส่วนตัว | PDPA: minimisation, consent, เข้ารหัส in transit/at rest, ปิดบัง PII, สิทธิเจ้าของข้อมูล | 14.1–14.4, ก.1.1.3 | FR-001, FR-061–FR-064 |
| NFR-008 | การเข้าถึง | Contrast ≥4.5:1 (ข้อความปกติ) / ≥3:1 (ข้อความใหญ่) ทั้ง light/dark; มาตรฐานเว็บไซต์ภาครัฐ 3.0 | ก.3.1.1, ก.1.1.1 | `src/styles/tokens.ts` (`contrastPairs`) + `tests/unit/contrast.test.ts`; ประเมินตนเองใน `/documents/test-checklist` |
| NFR-009 | ความเข้ากันได้ | Chrome Android/Safari iOS, Chrome/Firefox/Safari/Edge | ก.1.2.1, ก.1.2.2 | ทดสอบอัตโนมัติด้วย Chromium 3 viewport (เบราว์เซอร์อื่นยังไม่ได้ทดสอบใน sandbox) |
| NFR-010 | การบำรุงรักษา | แยกชั้น, Unit coverage ≥80% (ไม่นับ config) | 4.7.3 | coverage gate lines/functions/statements ≥80%, branches ≥75% (`vitest.config.ts`) |
| NFR-011 | การติดตาม/วินิจฉัย | Log เพียงพอต่อการวินิจฉัย | ก.4.10.1 | FR-145, RAG Trace (FR-015), `x-request-id` ทุกคำขอ |
| NFR-012 | ความยืดหยุ่นของโมเดล | เปลี่ยน LLM/องค์ประกอบ AI โดยไม่กระทบระบบ | 4.4.6, 2.6 | `LlmGateway`, `SearchGateway`, `RerankerGateway`, `NotificationGateway` |
| NFR-013 | ความถูกต้องของ Audit | เก็บ ≥90 วัน แก้/ลบไม่ได้ | ก.6.5.3 | ไม่มีกลไกลบ AuditLog (trigger ห้าม UPDATE/DELETE; ทำลายข้อมูลไม่ลบ AuditLog) |
| NFR-014 | การสำรองและกู้คืน | คู่มือ + ทดสอบ restore ≥1 ครั้ง | ก.1.6.3 | `/documents/backup-restore-guide`, `npm run report:restore-test` → `/documents/restore-test-report` |
| NFR-015 | ความน่าเชื่อถือของคำตอบ | ทุกคำตอบอ้างอิงตรวจสอบย้อนกลับได้ ไม่ตัดตอนพระไตรปิฎก | 2.1, ก.4.6.2 | FR-012, FR-043, FR-071 |
| NFR-016 | ภาษาและวันที่ | วันที่แสดงเป็นพุทธศักราชเมื่อ locale = th | ก.3.2.1 | `fmtDate(d, locale)` |

---

## 5. สถาปัตยกรรมระบบ

### 5.1 แผนภาพสถาปัตยกรรมระบบสาธิต

```mermaid
flowchart TB
  U["ผู้ใช้ / ผู้ดูแล (เบราว์เซอร์)"] -->|HTTPS (server.https.js :3443) หรือ HTTP| MW["middleware.ts<br/>requestId · rate limit · CSRF · admin IP allowlist · locale"]
  MW --> PAGES["หน้าเว็บ src/app/[locale]/*"]
  MW --> API["Route Handlers src/app/api/* (withApi: RBAC · zod · audit · ApiRequestLog)"]
  API --> UC["Use cases src/application/*"]
  UC --> GRAPH["LangGraph StateGraph 8 โหนด<br/>src/server/ai/graph.ts"]
  GRAPH --> IDX["KnowledgeIndex (in-memory)<br/>BM25 + pseudo-embedding 512 + RRF"]
  GRAPH --> RR["Reranker: Heuristic | TEI"]
  GRAPH --> LLM["LlmGateway<br/>Typhoon (OpenAI-compatible) · OpenAI · Gemini · Claude · Mock"]
  UC --> DB[("SQLite ผ่าน Prisma<br/>prisma/dev.db")]
  GRAPH --> DB
  UC --> ING["Ingestion pipeline<br/>parse → sanitize → chunk → embed → index"]
  ING --> STORE[["storage/uploads"]]
  ING --> IDX
  UC --> NOTI["NotificationGateway<br/>AlertEmailLog | SMTP"]
  API --> LOG[["logs/app.log (JSON lines)"]]
  LLM -.->|HTTPS| TY["api.opentyphoon.ai/v1"]
```

### 5.2 การแมปสถาปัตยกรรม Production ↔ ระบบสาธิต

| Production (Solution Architecture) | ระบบสาธิต |
|---|---|
| 8 microservices (NestJS) บน Kubernetes: identity, chat, ai-orchestrator, retrieval, ingestion, evaluation, config, report | Next.js 15 แอปเดียว แบ่ง bounded context ใน `src/domain`, `src/application`, `src/server` + Helm chart 8 deployment ที่ `infra/helm/mbu-buddhist-ai/` |
| PostgreSQL 16 / Redis 7 (token budget, lockout, config pub/sub, BullMQ) | SQLite (Prisma) / ตาราง `TokenUsage`, `LoginAttempt`; config อ่านจาก DB ทุกคำขอ (hot reload); คิวงาน = `IngestionJob` ประมวลผล async ใน process เดียว |
| Elasticsearch BM25 + kNN (bge-m3) + RRF | BM25 in-memory (ตัดคำไทย `Intl.Segmenter`) + pseudo-embedding 512 มิติ + RRF (`SearchGateway`; มี `ElasticsearchSearchAdapter` เป็นโครง) |
| bge-reranker-v2-m3 (TEI) | `HeuristicReranker` (สลับเป็น `TeiRerankerAdapter` ได้เมื่อตั้ง `TEI_RERANK_URL`) |
| LangGraph.js | LangGraph.js จริง (`@langchain/langgraph`) |
| SMTP / S3 / cert-manager / Prometheus | `AlertEmailLog` (หรือ SMTP เมื่อตั้งค่า) / `storage/uploads` / self-signed TLS / `logs/app.log` + `ApiRequestLog` |
| GitHub Actions + Helm + HPA + WAF | ไฟล์ส่งมอบ `.github/workflows/ci-cd.yml`, `infra/helm/` (ยังไม่ได้ deploy จริง และยังไม่ได้รันบน GitHub Actions) |

### 5.3 Workflow: ถาม-ตอบผ่าน RAG pipeline 8 โหนด

ลำดับโหนดใน `src/server/ai/graph.ts`: `checkAvailability → inputGuardrail → rewriteQuery → hybridSearch → rerank → generate → outputGuardrail → persist` โดย refusal ที่โหนด 1–2 ข้ามไปโหนด `persist` ทันที (conditional edge) ก่อนเปิด SSE ระบบตรวจ availability ล่วงหน้าเพื่อให้ตอบ HTTP 503/400 เป็น JSON ได้

```mermaid
sequenceDiagram
  autonumber
  actor U as ผู้ใช้ (/chat)
  participant API as POST /api/chat/qa (withApi)
  participant UC as askStream (application/chat/ask.ts)
  participant G as ragGraph (LangGraph)
  participant IDX as KnowledgeIndex
  participant L as LlmGateway (Typhoon)
  participant DB as SQLite (Prisma)
  U->>API: { question, conversationId?, locale } + cookie + x-csrf-token
  API->>UC: ตรวจสิทธิ์ user + zod
  UC->>DB: โหลดบทสนทนา (ownership) + getAiConfig()
  UC->>UC: checkAvailability (unavailable / เพดาน 24 ชม. / input.maxChars)
  alt ไม่ผ่าน
    UC-->>U: HTTP 503 SERVICE_UNAVAILABLE หรือ 400 INPUT_TOO_LONG (บันทึก refusal)
  else ผ่าน
    UC-->>U: เปิด SSE (text/event-stream)
    UC->>G: runRagGraph(input) + reserveTokens()
    G->>G: 1 checkAvailability (trace)
    G->>DB: 2 inputGuardrail โหลด GuardrailTopic
    opt LLM จริง
      G->>L: guard-classifier + query-rewrite (ขนานกัน)
    end
    alt ถูกปฏิเสธ (scope/injection/sensitive/yes_no)
      G->>DB: 8 persist (Message.refusalReason)
      G-->>U: event: refusal
    else ผ่าน
      G->>G: 3 rewriteQuery (synonym ไทย-บาลี-อังกฤษ + LLM queries)
      G->>IDX: 4 hybridSearch BM25 + vector + RRF (candidateK=50)
      G->>G: 5 rerank (Heuristic) + precedence → finalK ≤10
      G-->>U: event: references
      G->>L: 6 generate (persona + DATA delimiters, fitContext ≤ maxPerPrompt)
      loop streaming
        L-->>G: delta
        G-->>U: event: delta (วัด TTFT)
      end
      G->>G: 7 outputGuardrail (resolveCitations, evaluateOutput)
      G->>DB: 8 persist (Message + contentMasked + Citation + TokenUsage + traceJson)
    end
    G->>DB: afterUsageRecorded (threshold / error-rate alerts)
    UC-->>U: event: done {conversationId, messageId, answer, references, usage, trace, ttftMs, latencyMs, model, promptVersion}
  end
  Note over UC,U: ข้อผิดพลาดระหว่างสร้างคำตอบ → event: error {code: GENERATION_FAILED}
```

**Dhamma Talk** (`POST /api/chat/dhamma-talk`) ใช้กราฟเดียวกันด้วย `mode = DHAMMA_TALK`: โหนด `rewriteQuery` เพิ่มหัวเรื่องของ artifact เป็นคำค้น โหนด `generate` ใช้ prompt `dhamma-talk` พร้อม artifact ฉบับที่ผู้ใช้แก้ล่าสุดครอบด้วย `<<<ARTIFACT>>>` เพดาน `tokens.maxPerPromptDhammaTalk` และโหนด `persist` บันทึก `Conversation.artifactMd`

### 5.4 Workflow: การนำเข้าเอกสาร (Ingestion)

```mermaid
sequenceDiagram
  autonumber
  actor A as ผู้ดูแล (/admin/knowledge-base)
  participant API as POST /api/admin/knowledge-base/upload
  participant FS as storage/uploads
  participant DB as SQLite
  participant Q as enqueueJob (job chain ใน process)
  participant P as pipeline.ts
  participant IDX as KnowledgeIndex
  A->>API: multipart: file (pdf/txt/md/xml/docx ≤20MB) + metadata
  API->>FS: บันทึกไฟล์
  API->>DB: KnowledgeDocument (pending) + IngestionJob(ingest) + IngestionJobItem + audit document_imported
  API-->>A: { document, jobId }
  API->>Q: enqueueJob(jobId)
  Q->>P: runJob
  P->>DB: item.stage = parse
  P->>P: parseFile (unpdf / mammoth / TEI→marker / txt) + stripFrontmatter
  P->>P: sanitize (ตัด control chars + บรรทัดคำสั่งแฝง)
  P->>P: chunk (ChunkingPolicy: ตามข้อ หรือ semantic)
  P->>P: embed (pseudo-embedding 512 มิติ)
  P->>DB: index: transaction ลบ/อัปเดต/สร้าง Chunk (คง id ของ chunk ที่เนื้อหาเดิม)
  P->>IDX: reindexDocument(documentId)
  alt สำเร็จทุก item
    P->>DB: job.status = completed
  else มีรายการล้มเหลว
    P->>DB: job.status = partial | failed + item.errorMsg
    P->>DB: AlertEmailLog (ingestion_failed)
  end
  A->>API: GET /api/admin/knowledge-base/jobs/[id] (ติดตามสถานะ) / POST .../retry-failed
```

### 5.5 Workflow: การประเมินคุณภาพ AI (Evaluation)

```mermaid
sequenceDiagram
  autonumber
  actor A as ผู้ดูแล (/admin/evaluation) หรือ CLI (scripts/eval/run-acceptance.ts)
  participant API as POST /api/admin/evaluation/runs
  participant R as runner.ts (createRun / executeRun)
  participant G as ragGraph
  participant J as Judge LLM
  participant DB as SQLite
  A->>API: { datasetId, label: baseline | acceptance | adhoc }
  API->>R: createRun (บันทึก datasetVersion, promptKey/promptVersion, model, judgeModel) + audit evaluation_run
  API-->>A: { run (queued) }
  R->>DB: status = running
  loop ทุกข้อในชุด
    alt accuracy (DS-1)
      R->>G: answerStyle = mcq, skipInputGuard → exactMatchScore
    else retrieval (RET-1/2/3)
      R->>G: stopAfter = rerank → recallAtK(top-5, expectedRefs)
    else guardrail (GR-1/GR-2/INJ)
      R->>G: stopAfter = guard → refusalScore
    else judge (NT-2)
      R->>G: ถาม-ตอบเต็ม pipeline
      R->>J: prompt judge → คะแนน 0–3 (mock: token overlap)
    end
    R->>DB: EvaluationResult (botAnswer, retrievedIds, score, passed, judgeReason, latencyMs)
  end
  R->>DB: EvaluationRun.score / passed (เทียบ threshold) / finishedAt
  A->>API: GET /runs/[id] · GET /compare?runA&runB · GET /thresholds · POST /export
```

### 5.6 การจัดการความลับ

- ไฟล์ `.env` ไม่อยู่ใน repository มีเพียง `.env.example` ที่ระบุชื่อคีย์ API key ของ LLM (เช่น `TYPHOON_API_KEY`) เป็นค่าที่มหาวิทยาลัยจัดหาและใส่เอง (TOR 4.6.1)
- ระบบไม่ส่งค่า API key ออกทาง API ใด (`/api/admin/ai-config/models` คืนเพียง `configured: true/false`)
- Production: เก็บใน Kubernetes Secret `mbu-buddhist-ai-secrets` (ดู `infra/helm/mbu-buddhist-ai/values.yaml` → `existingSecret`)

---

## 6. พจนานุกรมข้อมูล (Data Dictionary)

ตารางต่อไปนี้ **สร้างจากไฟล์ `prisma/schema.prisma` จริง** ด้วยสคริปต์แยกวิเคราะห์ (parse) ทุกบรรทัด `model { ... }` — ครบ **25 model / 295 field** (รวม field ความสัมพันธ์) คอลัมน์ “ข้อกำหนด/ค่าเริ่มต้น” คัดลอกจาก attribute ของ Prisma ตามตัวอักษร คอลัมน์ “คำอธิบาย” ใช้ความเห็น (comment) ใน schema เมื่อมี และสำหรับ field ที่ไม่มีความเห็น ผู้จัดทำเขียนคำอธิบายจากการใช้งานจริงในโค้ด

ข้อสังเกตทั่วไป:
- SQLite ไม่มีชนิด enum จึงเก็บค่าที่เป็นรายการเป็น `String` และตรวจค่าด้วย zod ในชั้น application/API (ค่าที่อนุญาตระบุในคำอธิบาย)
- ฟิลด์ JSON (เช่น `patterns`, `choices`, `expectedRefs`, `traceJson`, `before/after`, `embedding`) เก็บเป็น `String` ที่เป็น JSON
- ตาราง `AuditLog` มี trigger ระดับฐานข้อมูลห้าม UPDATE/DELETE (migration `20261002110735_audit_immutable`)
- คอลัมน์ `emailEnc` / `displayNameEnc` เข้ารหัส AES-256-GCM ชั้น repository เท่านั้นที่ถอดรหัส

#### User (กลุ่ม Identity)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `emailHash` | `String` | `@unique` | sha256(lowercase email) ใช้ค้นหา/ล็อกอิน |
| `emailEnc` | `String` | — | อีเมลเข้ารหัส AES-256-GCM (14.3) |
| `displayNameEnc` | `String` | — | ชื่อแสดง เข้ารหัส |
| `passwordHash` | `String?` | — | null สำหรับบัญชี OAuth |
| `provider` | `String` | `@default("local")` | local \| google \| facebook \| mock |
| `providerId` | `String?` | — | รหัสผู้ใช้ฝั่งผู้ให้บริการ OAuth |
| `role` | `String` | `@default("GENERAL_USER")` | SUPER_ADMIN \| ADMIN \| GENERAL_USER |
| `status` | `String` | `@default("active")` | active \| suspended \| deleted |
| `dhammaTalkEnabled` | `Boolean` | `@default(false)` | ก.3.3.3 default ปิด |
| `consentVersion` | `String?` | — | เวอร์ชันนโยบายที่ยินยอม |
| `consentAt` | `DateTime?` | — | วันเวลาที่ให้ความยินยอมตามนโยบายความเป็นส่วนตัว |
| `consentWithdrawnAt` | `DateTime?` | — | วันเวลาที่ถอนความยินยอม |
| `lastLoginAt` | `DateTime?` | — | วันเวลาที่เข้าสู่ระบบครั้งล่าสุด |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |
| `deletedAt` | `DateTime?` | — | วันเวลาที่ลบแบบ soft delete (null = ยังไม่ลบ) |
| `conversations` | `Conversation[]` | — | ความสัมพันธ์ไปยัง Conversation (หนึ่งต่อหลาย) |
| `feedbacks` | `Feedback[]` | — | ความสัมพันธ์ไปยัง Feedback (หนึ่งต่อหลาย) |
| `tokenUsages` | `TokenUsage[]` | — | ความสัมพันธ์ไปยัง TokenUsage (หนึ่งต่อหลาย) |
| `privacyRequests` | `PrivacyRequest[]` | — | ความสัมพันธ์ไปยัง PrivacyRequest (หนึ่งต่อหลาย) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@unique([provider, providerId])`, `@@index([role, status])`, `@@index([lastLoginAt])`

#### LoginAttempt (กลุ่ม Identity)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `emailHash` | `String` | — | sha256 ของอีเมลที่ใช้พยายามเข้าสู่ระบบ |
| `ip` | `String` | — | IP ของผู้พยายามเข้าสู่ระบบ |
| `success` | `Boolean` | — | ผลการเข้าสู่ระบบ (ใช้คำนวณ lockout) |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([emailHash, createdAt])`, `@@index([ip, createdAt])`

#### Conversation (กลุ่ม Chat)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `userId` | `String` | — | เจ้าของบทสนทนา (FK → User) |
| `user` | `User` | `@relation(fields: [userId], references: [id])` | ความสัมพันธ์ไปยัง User |
| `mode` | `String` | — | QA \| DHAMMA_TALK |
| `title` | `String` | — | ชื่อบทสนทนา (40 ตัวอักษรแรกของคำถามแรก) |
| `artifactMd` | `String?` | — | Dhamma Talk: artifact ล่าสุด (markdown) |
| `retentionUntil` | `DateTime` | — | createdAt + retentionDays (ก.3.5.1) |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |
| `messages` | `Message[]` | — | ความสัมพันธ์ไปยัง Message (หนึ่งต่อหลาย) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([userId, updatedAt])`, `@@index([retentionUntil])`

#### Message (กลุ่ม Chat)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `conversationId` | `String` | — | FK → Conversation |
| `conversation` | `Conversation` | `@relation(fields: [conversationId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง Conversation |
| `role` | `String` | — | user \| assistant \| system |
| `content` | `String` | — | เนื้อหาข้อความ (คำถาม/คำตอบ/ข้อความปฏิเสธ) |
| `contentMasked` | `String?` | — | สำเนาปิดบัง PII สำหรับใช้ปรับปรุงระบบ (14.3) |
| `refusalReason` | `String?` | — | scope \| injection \| sensitive_politics \| sensitive_religion \| sensitive_mental \| yes_no \| budget \| unavailable |
| `promptVersionId` | `String?` | — | PromptVersion ที่ใช้สร้างคำตอบ |
| `model` | `String?` | — | ชื่อโมเดล LLM ที่ตอบ |
| `traceJson` | `String?` | — | RAG trace (JSON) สำหรับ Trace panel |
| `latencyMs` | `Int?` | — | เวลาตอบรวม (มิลลิวินาที) |
| `ttftMs` | `Int?` | — | time to first token (12.2) |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `citations` | `Citation[]` | — | ความสัมพันธ์ไปยัง Citation (หนึ่งต่อหลาย) |
| `feedbacks` | `Feedback[]` | — | ความสัมพันธ์ไปยัง Feedback (หนึ่งต่อหลาย) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([conversationId, createdAt])`

#### Citation (กลุ่ม Chat)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `messageId` | `String` | — | FK → Message |
| `message` | `Message` | `@relation(fields: [messageId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง Message |
| `chunkId` | `String` | — | FK → Chunk ที่ถูกอ้างอิง |
| `chunk` | `Chunk` | `@relation(fields: [chunkId], references: [id])` | ความสัมพันธ์ไปยัง Chunk |
| `rank` | `Int` | — | 1..10 |
| `label` | `String` | — | "ข.ช. (ไทย) 27/87/87" |
| `score` | `Float` | — | คะแนนหลัง rerank และคูณ precedence |

#### Feedback (กลุ่ม Chat)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `messageId` | `String` | — | FK → Message ที่ให้ความเห็น |
| `message` | `Message` | `@relation(fields: [messageId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง Message |
| `userId` | `String` | — | FK → User ผู้ให้ความเห็น |
| `user` | `User` | `@relation(fields: [userId], references: [id])` | ความสัมพันธ์ไปยัง User |
| `value` | `Int` | — | 1 = thumb-up, -1 = thumb-down |
| `comment` | `String?` | — | ความเห็นเพิ่มเติม (ไม่บังคับ) |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@unique([messageId, userId])`

#### KnowledgeDocument (กลุ่ม Knowledge Base)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `title` | `String` | — | ชื่อเอกสาร |
| `docType` | `String` | — | tipitaka \| atthakatha \| tika \| contemporary \| teacher_dhamma  (ก.4.8.3(2)) |
| `edition` | `String?` | — | มมร. \| ไทย \| อื่น ๆ |
| `pitaka` | `String?` | — | vinaya \| sutta \| abhidhamma |
| `nikaya` | `String?` | — | นิกาย/วรรค |
| `sutta` | `String?` | — | ชื่อสูตร |
| `volume` | `Int?` | — | เล่ม (มมร. 1–91) |
| `abbreviation` | `String?` | — | ตัวย่อสำหรับ citation เช่น "ข.ช." "วิ.มหา." |
| `precedenceScore` | `Float` | `@default(1.0)` | ก.4.8.3(3) ปรับได้ใน admin |
| `sourceType` | `String` | — | upload \| seed \| online |
| `sourceFile` | `String?` | — | path ใน storage/uploads |
| `sourceUrl` | `String?` | — | URL ต้นทาง (ข้อมูลออนไลน์) |
| `mimeType` | `String?` | — | ชนิดไฟล์ต้นฉบับ |
| `status` | `String` | `@default("pending")` | pending \| indexed \| failed \| deleted |
| `chunkCount` | `Int` | `@default(0)` | จำนวน chunk ที่ทำดัชนีแล้ว |
| `sourceNote` | `String?` | — | หมายเหตุที่มา/ความถูกต้องของข้อความ |
| `mbuVolume` | `Int?` | — | เล่มในฉบับ มมร. 91 เล่ม (เมื่อทราบ) — ใช้คำนวณ coverage ภาคผนวก ข |
| `paliAliases` | `String?` | — | คำบาลี/อังกฤษของเอกสาร คั่นด้วย \| |
| `sanitizedLines` | `Int` | `@default(0)` | จำนวนบรรทัดที่ถูกตัดตอน sanitize |
| `createdBy` | `String?` | — | ผู้นำเข้าเอกสาร |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |
| `deletedAt` | `DateTime?` | — | วันเวลาที่ลบแบบ soft delete (null = ยังไม่ลบ) |
| `chunks` | `Chunk[]` | — | ความสัมพันธ์ไปยัง Chunk (หนึ่งต่อหลาย) |
| `jobItems` | `IngestionJobItem[]` | — | ความสัมพันธ์ไปยัง IngestionJobItem (หนึ่งต่อหลาย) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([docType, status])`

#### Chunk (กลุ่ม Knowledge Base)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `documentId` | `String` | — | FK → KnowledgeDocument |
| `document` | `KnowledgeDocument` | `@relation(fields: [documentId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง KnowledgeDocument |
| `ordinal` | `Int` | — | ลำดับ chunk ในเอกสาร (เริ่ม 0) |
| `content` | `String` | — | เนื้อหาเต็มของ chunk (1 ข้อ สำหรับพระไตรปิฎก/อรรถกถา) |
| `volume` | `Int?` | — | เล่ม |
| `item` | `Int?` | — | ข้อ |
| `page` | `Int?` | — | หน้า |
| `pageEnd` | `Int?` | — | หน้าสุดท้ายของข้อ (ข้อที่คร่อมหลายหน้า) |
| `heading` | `String?` | — | หัวข้อ/ชื่อสูตรของ chunk |
| `paliAliases` | `String?` | — | คำบาลีถอดอักษร คั่นด้วย \| |
| `tokenCount` | `Int` | — | จำนวนโทเคนโดยประมาณ (ตัวอักษร ÷ 3) |
| `oversized` | `Boolean` | `@default(false)` | ข้อยาวเกิน 3,000 tokens แต่ไม่ถูกแบ่ง (ก.4.6.2) |
| `embedding` | `String?` | — | JSON number[512] (pseudo-embedding) — cache |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `citations` | `Citation[]` | — | ความสัมพันธ์ไปยัง Citation (หนึ่งต่อหลาย) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([documentId, ordinal])`, `@@index([volume, item])`

#### IngestionJob (กลุ่ม Knowledge Base)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `type` | `String` | — | ingest \| reindex_all \| reindex_selected \| delete |
| `status` | `String` | `@default("queued")` | queued \| running \| completed \| failed \| partial |
| `requestedBy` | `String` | — | ผู้สั่งงาน (userId) |
| `totalItems` | `Int` | `@default(0)` | จำนวนรายการทั้งหมดในงาน |
| `successItems` | `Int` | `@default(0)` | จำนวนรายการที่สำเร็จ |
| `failedItems` | `Int` | `@default(0)` | จำนวนรายการที่ล้มเหลว |
| `startedAt` | `DateTime?` | — | เวลาเริ่มประมวลผล |
| `finishedAt` | `DateTime?` | — | เวลาประมวลผลเสร็จ |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `items` | `IngestionJobItem[]` | — | ความสัมพันธ์ไปยัง IngestionJobItem (หนึ่งต่อหลาย) |

#### IngestionJobItem (กลุ่ม Knowledge Base)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `jobId` | `String` | — | FK → IngestionJob |
| `job` | `IngestionJob` | `@relation(fields: [jobId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง IngestionJob |
| `documentId` | `String?` | — | FK → KnowledgeDocument |
| `document` | `KnowledgeDocument?` | `@relation(fields: [documentId], references: [id])` | ความสัมพันธ์ไปยัง KnowledgeDocument |
| `fileName` | `String?` | — | ชื่อไฟล์ที่นำเข้า |
| `stage` | `String` | `@default("queued")` | queued \| parse \| sanitize \| chunk \| embed \| index \| done \| failed |
| `status` | `String` | `@default("queued")` | queued | running | done | failed |
| `errorMsg` | `String?` | — | ข้อความผิดพลาดเมื่อล้มเหลว |
| `chunkCount` | `Int` | `@default(0)` | จำนวน chunk ที่ได้ |
| `durationMs` | `Int?` | — | เวลาที่ใช้ประมวลผล (มิลลิวินาที) |

#### PromptVersion (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `key` | `String` | — | qa-persona \| dhamma-talk \| guard-classifier \| judge \| query-rewrite |
| `version` | `Int` | — | หมายเลขเวอร์ชัน (เพิ่มทีละ 1 ต่อ key) |
| `content` | `String` | — | เนื้อหา prompt |
| `changelog` | `String?` | — | บันทึกการเปลี่ยนแปลง |
| `isActive` | `Boolean` | `@default(false)` | เวอร์ชันที่ใช้งานอยู่ (1 เวอร์ชันต่อ key) |
| `createdBy` | `String` | — | ผู้สร้างเวอร์ชัน |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@unique([key, version])`, `@@index([key, isActive])`

#### GuardrailTopic (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `kind` | `String` | — | scope_allow \| refuse_sensitive \| injection_pattern \| yes_no_pattern |
| `key` | `String?` | — | politics \| religion_comparison \| mental_health \| ... (ใช้จำแนก refusalReason) |
| `labelTh` | `String` | — | ชื่อหัวข้อภาษาไทย |
| `labelEn` | `String` | — | ชื่อหัวข้อภาษาอังกฤษ |
| `patterns` | `String` | — | JSON string[] (keyword/regex) |
| `refusalMsgTh` | `String?` | — | ข้อความปฏิเสธภาษาไทย |
| `refusalMsgEn` | `String?` | — | ข้อความปฏิเสธภาษาอังกฤษ |
| `enabled` | `Boolean` | `@default(true)` | เปิด/ปิดหัวข้อ |
| `sortOrder` | `Int` | `@default(0)` | ลำดับการแสดง/ประเมิน |
| `updatedBy` | `String?` | — | ผู้แก้ไขล่าสุด |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |

#### AiConfigSetting (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `key` | `String` | `@id` | ดูรายการ key ในข้อ 7.2 |
| `value` | `String` | — | JSON |
| `updatedBy` | `String?` | — | ผู้แก้ไขล่าสุด |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |

#### ConfigSnapshot (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `scope` | `String` | — | ai-config \| guardrails \| unavailable \| pricing |
| `snapshot` | `String` | — | JSON ของค่าทั้ง scope ก่อนเปลี่ยน |
| `reason` | `String?` | — | เหตุผลการเปลี่ยนแปลง |
| `createdBy` | `String` | — | ผู้เปลี่ยนแปลง |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([scope, createdAt])`

#### TokenUsage (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `userId` | `String?` | — | FK → User (null = งานระบบ เช่น ประเมินผล) |
| `user` | `User?` | `@relation(fields: [userId], references: [id])` | ความสัมพันธ์ไปยัง User |
| `conversationId` | `String?` | — | บทสนทนาที่เกี่ยวข้อง |
| `messageId` | `String?` | — | ข้อความที่เกี่ยวข้อง |
| `mode` | `String` | — | QA \| DHAMMA_TALK \| EVALUATION \| GUARD |
| `model` | `String` | — | ชื่อโมเดล |
| `inputTokens` | `Int` | — | โทเคนขาเข้า |
| `thinkingTokens` | `Int` | `@default(0)` | โทเคนการคิด (ไม่เกิน thinkingBudget) |
| `outputTokens` | `Int` | — | โทเคนขาออก |
| `totalTokens` | `Int` | — | ผลรวมโทเคน (ใช้คำนวณเพดาน 24 ชม.) |
| `estimatedCost` | `Float` | `@default(0)` | คำนวณจาก LlmPricing ณ เวลาบันทึก |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([createdAt])`, `@@index([userId, createdAt])`

#### LlmPricing (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `provider` | `String` | — | ผู้ให้บริการ LLM |
| `model` | `String` | — | ชื่อโมเดล |
| `inputPerMillion` | `Float` | — | USD หรือ THB ต่อ 1M tokens (currency ใน config) |
| `thinkingPerMillion` | `Float` | `@default(0)` | อัตราต่อ 1M thinking tokens |
| `outputPerMillion` | `Float` | — | อัตราต่อ 1M output tokens |
| `currency` | `String` | `@default("THB")` | สกุลเงิน |
| `updatedBy` | `String?` | — | ผู้แก้ไขล่าสุด |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@unique([provider, model])`

#### AlertEmailLog (กลุ่ม AI Config)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `type` | `String` | — | threshold_tokens \| threshold_cost \| auto_unavailable \| error_rate \| ingestion_failed \| system |
| `severity` | `String` | — | info \| warning \| critical |
| `to` | `String` | — | ผู้รับ |
| `subject` | `String` | — | หัวเรื่อง |
| `body` | `String` | — | เนื้อหาแจ้งเตือน |
| `delivery` | `String` | `@default("logged")` | logged \| sent \| failed |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([createdAt])`

#### EvaluationDataset (กลุ่ม Evaluation)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `name` | `String` | — | ชื่อชุดทดสอบ |
| `kind` | `String` | — | accuracy \| judge \| retrieval \| guardrail |
| `benchmarkCode` | `String` | — | DS-1 \| NT-2 \| RET-1 \| RET-2 \| RET-3 \| GR-1 \| GR-2 |
| `version` | `Int` | `@default(1)` | เพิ่มทุกครั้งที่ items เปลี่ยน (ก.5.2.4) |
| `threshold` | `Float` | — | เกณฑ์ขั้นต่ำ 0.8 / 2.0 |
| `guaranteed` | `Float` | — | ค่าที่ผู้รับจ้างรับประกัน (9.4/16.2) |
| `expectedCount` | `Int` | — | จำนวนข้อตาม TOR (100/50/...) |
| `createdBy` | `String` | — | ผู้สร้างชุด |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |
| `items` | `EvaluationItem[]` | — | ความสัมพันธ์ไปยัง EvaluationItem (หนึ่งต่อหลาย) |
| `runs` | `EvaluationRun[]` | — | ความสัมพันธ์ไปยัง EvaluationRun (หนึ่งต่อหลาย) |

#### EvaluationItem (กลุ่ม Evaluation)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `datasetId` | `String` | — | FK → EvaluationDataset |
| `dataset` | `EvaluationDataset` | `@relation(fields: [datasetId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง EvaluationDataset |
| `ordinal` | `Int` | — | เลขข้อ |
| `question` | `String` | — | คำถาม |
| `choices` | `String?` | — | JSON string[] สำหรับปรนัย |
| `expectedAnswer` | `String?` | — | เฉลย (choice label หรือข้อความ) |
| `expectedRefs` | `String?` | — | JSON [{volume,item}] หรือ chunkId[] สำหรับ Recall@5 |
| `expectRefusal` | `Boolean` | `@default(false)` | guardrail |
| `level` | `String?` | — | ตรี \| โท \| เอก |
| `results` | `EvaluationResult[]` | — | ความสัมพันธ์ไปยัง EvaluationResult (หนึ่งต่อหลาย) |

#### EvaluationRun (กลุ่ม Evaluation)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `datasetId` | `String` | — | FK → EvaluationDataset |
| `dataset` | `EvaluationDataset` | `@relation(fields: [datasetId], references: [id])` | ความสัมพันธ์ไปยัง EvaluationDataset |
| `datasetVersion` | `Int` | — | เวอร์ชันชุดทดสอบ ณ เวลารัน (ก.5.2.4) |
| `promptVersionId` | `String` | — | FK เชิงตรรกะ → PromptVersion |
| `promptKey` | `String` | — | key ของ prompt หลักของ workflow |
| `promptVersion` | `Int` | — | เวอร์ชัน prompt ณ เวลารัน (ก.5.2.4) |
| `model` | `String` | — | โมเดล LLM ที่ใช้ตอบ |
| `judgeModel` | `String?` | — | โมเดลที่ใช้เป็นผู้ตัดสิน (NT-2) |
| `label` | `String?` | — | baseline \| acceptance \| adhoc |
| `status` | `String` | `@default("queued")` | queued \| running \| completed \| failed |
| `score` | `Float?` | — | accuracy %, recall@5 %, judge avg, refusal % |
| `passed` | `Boolean?` | — | score ≥ threshold |
| `itemCount` | `Int` | `@default(0)` | จำนวนข้อที่ประเมิน |
| `startedAt` | `DateTime?` | — | เวลาเริ่ม |
| `finishedAt` | `DateTime?` | — | เวลาเสร็จ |
| `requestedBy` | `String` | — | ผู้สั่งรัน |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `results` | `EvaluationResult[]` | — | ความสัมพันธ์ไปยัง EvaluationResult (หนึ่งต่อหลาย) |

#### EvaluationResult (กลุ่ม Evaluation)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `runId` | `String` | — | FK → EvaluationRun |
| `run` | `EvaluationRun` | `@relation(fields: [runId], references: [id], onDelete: Cascade)` | ความสัมพันธ์ไปยัง EvaluationRun |
| `itemId` | `String` | — | FK → EvaluationItem |
| `item` | `EvaluationItem` | `@relation(fields: [itemId], references: [id])` | ความสัมพันธ์ไปยัง EvaluationItem |
| `botAnswer` | `String` | — | คำตอบของระบบ |
| `retrievedIds` | `String?` | — | JSON chunkId[] top-5 |
| `score` | `Float` | — | 0/1, 0/1, 0–3, 0/1 |
| `passed` | `Boolean` | — | ผ่าน/ไม่ผ่านรายข้อ |
| `judgeReason` | `String?` | — | เหตุผลของผู้ตัดสิน/ตัวให้คะแนน |
| `latencyMs` | `Int?` | — | เวลาที่ใช้ต่อข้อ (มิลลิวินาที) |

#### AuditLog (กลุ่ม Audit / Privacy / Ops)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `seq` | `Int` | `@id @default(autoincrement())` | SQLite อนุญาต autoincrement เฉพาะ primary key |
| `id` | `String` | `@unique @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `timestamp` | `DateTime` | `@default(now())` | วันเวลาที่เกิดเหตุการณ์ |
| `actorId` | `String?` | — | ผู้กระทำ (userId; null = ระบบ/ผู้ไม่ระบุตัวตน) |
| `actorEmailHash` | `String?` | — | sha256 อีเมลของผู้กระทำ (กรณี login ล้มเหลว) |
| `ip` | `String` | — | IP ของผู้กระทำ |
| `eventType` | `String` | — | login_success \| login_failed \| logout \| user_changed \| role_changed \| config_changed \| prompt_changed \| document_imported \| document_deleted \| conversation_accessed \| evaluation_run \| privacy_request \| data_destruction \| defect_changed |
| `targetType` | `String?` | — | ชนิดของสิ่งที่ถูกกระทำ |
| `targetId` | `String?` | — | รหัสของสิ่งที่ถูกกระทำ |
| `before` | `String?` | — | JSON |
| `after` | `String?` | — | JSON |
| `result` | `String` | — | success \| failure |
| `prevHash` | `String` | — | hash ของแถวก่อนหน้า (genesis = 0×64) |
| `hash` | `String` | — | sha256(prevHash + fields) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([timestamp])`, `@@index([eventType, timestamp])`, `@@index([actorId, timestamp])`

#### PrivacyRequest (กลุ่ม Audit / Privacy / Ops)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `userId` | `String` | — | เจ้าของข้อมูล (FK → User) |
| `user` | `User` | `@relation(fields: [userId], references: [id])` | ความสัมพันธ์ไปยัง User |
| `type` | `String` | — | access_copy \| erase \| withdraw_consent |
| `status` | `String` | `@default("pending")` | pending \| completed \| rejected |
| `note` | `String?` | — | หมายเหตุการดำเนินการ |
| `exportPath` | `String?` | — | ไฟล์ JSON สำเนาข้อมูล |
| `handledBy` | `String?` | — | ผู้ดูแลที่ดำเนินการ |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `completedAt` | `DateTime?` | — | วันเวลาที่ดำเนินการเสร็จ |

#### DefectLog (กลุ่ม Audit / Privacy / Ops)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `module` | `String` | — | โมดูลที่พบข้อบกพร่อง |
| `description` | `String` | — | รายละเอียดข้อบกพร่อง |
| `severity` | `String` | — | critical \| high \| medium \| low |
| `status` | `String` | `@default("open")` | open \| in_progress \| resolved \| closed |
| `reportedBy` | `String` | — | ผู้แจ้ง |
| `reportedAt` | `DateTime` | `@default(now())` | เวลาที่มหาวิทยาลัยแจ้ง (12.4 เริ่มนับ) |
| `assignedTo` | `String?` | — | ผู้รับผิดชอบแก้ไข |
| `foundInPhase` | `String?` | — | ระยะที่พบ (เช่น SIT/UAT) |
| `resolutionNote` | `String?` | — | วิธีแก้และมาตรการป้องกันซ้ำ (บังคับสำหรับ high/critical) |
| `resolvedAt` | `DateTime?` | — | วันเวลาที่แก้ไขเสร็จ |
| `confirmedAt` | `DateTime?` | — | มหาวิทยาลัยยืนยันผล (12.4 สิ้นสุดนับ) |
| `slaHours` | `Int` | — | 24 \| 48 \| 80 (10 วันทำการ×8 ชม.) |
| `slaDeadline` | `DateTime` | — | กำหนดเวลาแก้ไขตาม SLA (ชั่วโมงทำการ) |
| `overdueHours` | `Int?` | — | คำนวณเมื่อ confirm |
| `penaltyEstimate` | `Float?` | — | overdueHours × 0.025% × contractValue (10.2) |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |
| `updatedAt` | `DateTime` | `@updatedAt` | วันเวลาที่แก้ไขล่าสุด (Prisma อัปเดตอัตโนมัติ) |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([severity, status])`

#### ApiRequestLog (กลุ่ม Audit / Privacy / Ops)

| ฟิลด์ | ชนิด | ข้อกำหนด/ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `id` | `String` | `@id @default(uuid())` | รหัสอ้างอิงของระเบียน (UUID) |
| `requestId` | `String` | — | รหัสคำขอ (x-request-id) |
| `path` | `String` | — | route pattern ของ API |
| `method` | `String` | — | HTTP method |
| `statusCode` | `Int` | — | HTTP status ที่ตอบ |
| `durationMs` | `Int` | — | เวลาตอบสนอง (มิลลิวินาที) |
| `ttftMs` | `Int?` | — | Time to first token ของคำขอ SSE |
| `createdAt` | `DateTime` | `@default(now())` | วันเวลาที่สร้างระเบียน |

ดัชนี/ข้อจำกัดระดับตาราง: `@@index([createdAt])`

### 6.1 ความสัมพันธ์ระหว่างตารางหลัก

```mermaid
erDiagram
  User ||--o{ Conversation : owns
  User ||--o{ Feedback : gives
  User ||--o{ TokenUsage : consumes
  User ||--o{ PrivacyRequest : files
  Conversation ||--o{ Message : contains
  Message ||--o{ Citation : cites
  Message ||--o{ Feedback : receives
  Chunk ||--o{ Citation : referenced_by
  KnowledgeDocument ||--o{ Chunk : split_into
  KnowledgeDocument ||--o{ IngestionJobItem : processed_in
  IngestionJob ||--o{ IngestionJobItem : has
  EvaluationDataset ||--o{ EvaluationItem : has
  EvaluationDataset ||--o{ EvaluationRun : run_as
  EvaluationRun ||--o{ EvaluationResult : produces
  EvaluationItem ||--o{ EvaluationResult : scored_in
```

ตารางที่ไม่มีความสัมพันธ์แบบ foreign key: `LoginAttempt`, `PromptVersion`, `GuardrailTopic`, `AiConfigSetting`, `ConfigSnapshot`, `LlmPricing`, `AlertEmailLog`, `AuditLog`, `DefectLog`, `ApiRequestLog`

### 6.2 ค่าตั้งค่า AI (`AiConfigSetting.key`)

ค่าตั้งต้นจาก `AI_CONFIG_DEFAULTS` ใน `src/domain/config/ai-config.ts` (ผู้ดูแลแก้ได้ที่ `/admin/ai-config` ค่าที่ไม่ผ่าน schema ถูกละเว้นและใช้ค่าตั้งต้นแทน)

| key | ค่าตั้งต้น | ช่วงที่ schema ยอมรับ | TOR |
|---|---|---|---|
| `llm.provider` / `llm.model` | `typhoon` / `typhoon-v2.5-30b-a3b-instruct` | ข้อความ | ก.4.1.2, ก.6.2.3 |
| `llm.fallbackModels` | `[]` | ≤10 รายการ | — |
| `tokens.dailyCap` | 10,000,000 | 1 – 1,000,000,000 | ก.3.4.1 |
| `tokens.marginPct` | 10 | 0 – 50 | ก.3.4.4 |
| `tokens.alertThresholdPct` | 80 | 1 – 100 | ก.7.3.5 |
| `tokens.maxPerPromptQa` | 10,000 | 500 – 10,000 | ก.4.2.1 |
| `tokens.maxPerPromptDhammaTalk` | 50,000 | 500 – 50,000 | ก.4.2.1 |
| `tokens.thinkingBudget` | 2,000 | 0 – 32,000 | ก.4.4.2 |
| `tokens.maxOutput` | 1,500 | 64 – 16,000 | ก.3.4.2 |
| `input.maxChars` | 2,000 | 1 – 20,000 | ก.4.4.2 |
| `retention.conversationDays` | 90 | 1 – 3,650 | ก.3.5.1 |
| `retention.purgeIntervalMinutes` | 60 | 1 – 1,440 | ก.6.2.2 |
| `unavailable.enabled` / `unavailable.auto` | `false` / `false` | boolean | ก.3.4.3, ก.3.4.4 |
| `unavailable.messageTh` / `unavailable.messageEn` | ข้อความแจ้งปิดบริการ | 1 – 1,000 ตัวอักษร | ก.6.6.1 |
| `retrieval.candidateK` / `retrieval.finalK` / `retrieval.rrfK` | 50 / 10 / 60 | 1–200 / 1–10 / 1–1,000 | ก.3.6.2, ก.4.6.1 |
| `retrieval.reranker` | `heuristic` | ข้อความ (`heuristic` \| `tei`) | ก.4.7.1 |
| `eval.judgeModel` | `typhoon-v2.5-30b-a3b-instruct` | ข้อความ | ก.5.2.2 |
| `eval.guaranteed` | DS-1 0.85, NT-2 2.3, RET-1/2/3 0.85, GR-1/GR-2 0.9 | 0 – 3 ต่อชุด | 16.2 |
| `contract.value` | 5,000,000 | ≥0 | 10.2 |
| `pricing.currency` | `THB` | รหัส 3 ตัวอักษร | ก.7.3.3 |
| `privacy.policyVersion` | `1.0` | ข้อความ | 14 |

---

## 7. User Flow

### 7.1 ผู้ใช้ทั่วไป: สมัคร → ถาม-ตอบ → ตรวจอ้างอิง → ประวัติ

```mermaid
flowchart TD
  H["หน้าแรก /"] --> R{"มีบัญชีแล้ว?"}
  R -- ไม่มี --> REG["/register: อีเมล รหัสผ่าน ชื่อ + ยินยอม"] --> LOGIN
  R -- มี --> LOGIN["/login: อีเมล+รหัสผ่าน หรือ Gmail/Facebook"]
  LOGIN -->|ผิด 5 ครั้ง| LOCK["ล็อก 15 นาที"]
  LOGIN --> CHAT["/chat: พิมพ์คำถาม (เห็นคำเตือนใต้กล่อง)"]
  CHAT --> G{"Guardrail"}
  G -- ปฏิเสธ --> REF["การ์ดปฏิเสธพร้อมเหตุผล"] --> CHAT
  G -- ผ่าน --> REFS["รายการอ้างอิง 3 รายการ + Expand All"]
  REFS --> SUM["บทสรุปแบบ streaming + citation chip"]
  SUM --> MODAL["คลิก chip → Modal เนื้อหาเต็ม + metadata"]
  SUM --> FB["👍/👎 + ความเห็น"]
  SUM --> TRACE["RAG Trace 8 โหนด"]
  SUM --> HIST["/chat/history: เปิดต่อ / ลบ / ดูวันหมดอายุ"]
  HIST --> PRIV["/privacy: ดาวน์โหลดสำเนา / ขอลบ / ถอนยินยอม"]
```

### 7.2 Dhamma Talk

```mermaid
flowchart TD
  DT["/chat/dhamma-talk"] --> P{"dhammaTalkEnabled?"}
  P -- ไม่ --> ASK["หน้าอธิบายให้ขอสิทธิ์จากผู้ดูแล"]
  P -- ใช่ --> PROMPT["พิมพ์หัวข้อบทธรรมบรรยาย"] --> ART["Response Modal: artifact Markdown (แท็บแก้ไข/ตัวอย่าง)"]
  ART --> EDIT["ผู้ใช้แก้ไข → บันทึก (PATCH artifactMd)"]
  EDIT --> NEXT["สั่งรอบถัดไป → AI ใช้ฉบับที่แก้ล่าสุดเป็นบริบท"] --> ART
  ART --> DL["ดาวน์โหลด .md"]
```

### 7.3 ผู้ดูแลระบบ

```mermaid
flowchart LR
  AL["/login (บัญชี ADMIN/SUPER_ADMIN จาก IP ใน allowlist)"] --> DASH["/admin/dashboard"]
  DASH --> USERS["ผู้ใช้ / คำขอ PDPA / Audit Log"]
  DASH --> AI["ตั้งค่า AI / Prompt / Guardrails / Unavailable / ราคาโทเคน"]
  DASH --> KB["ฐานองค์ความรู้ / งานนำเข้า"]
  DASH --> EVAL["Evaluation / ชุดทดสอบ / เปรียบเทียบ / รายงานความปลอดภัย"]
  DASH --> OPS["Defect Log / SLA / Backup* / ทำลายข้อมูล*"]
```

\* เฉพาะ `SUPER_ADMIN`

Wireframe และ Prototype ของหน้าจอหลักอยู่ใน `/documents/ux-deliverables`

---

## 8. ข้อกำหนดส่วนต่อประสาน (API) — OpenAPI 3.0

- เอกสาร OpenAPI 3.0.3 ฉบับเครื่องอ่านได้: **`GET /api/openapi.json`** สร้างจาก registry ใน `src/server/api/openapi/index.ts` (`buildOpenApi`) ซึ่งรวมไฟล์กลุ่ม `core.ts`, `privacy.ts`, `admin-users.ts`, `admin-config.ts`, `admin-kb.ts`, `admin-eval.ts`, `admin-ops.ts`
- รูปแบบผลลัพธ์ทุก endpoint: `{ ok: true, data }` หรือ `{ ok: false, error: { code, message } }` ยกเว้น endpoint แบบ SSE (`text/event-stream`) และการดาวน์โหลด CSV/Markdown
- การยืนยันตัวตน: cookie `mbu_session` (securityScheme `sessionCookie`); คำขอที่แก้ไขข้อมูลต้องมี header `x-csrf-token`
- ชุดทดสอบ `tests/api/openapi-coverage.test.ts` ยืนยันว่าทุก path+method ใน registry มีกรณีทดสอบใน `tests/api` และเมื่อรัน `npm test` ระบบตรวจการเรียกจริงซ้ำอีกชั้น (regression ทุก endpoint — TOR 4.7.5; รายละเอียดใน `/documents/test-reports-index`)

### 8.1 สรุปกลุ่ม endpoint (รวม 107 operation ใน registry)

| กลุ่ม (tag) | จำนวน | สิทธิ์ | ตัวอย่าง |
|---|---:|---|---|
| System | 4 | public | `GET /api/health`, `GET /api/status`, `GET /api/openapi.json`, `GET /api/csrf` |
| Documents | 2 | public | `GET /api/documents`, `GET /api/documents/[slug]` |
| Auth | 10 | public / user | `POST /api/auth/register`, `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/oauth/[provider]/start`, `.../callback`, `GET /api/auth/oauth-status`, `POST /api/mock-oauth/token`, `GET /api/mock-oauth/userinfo`, `GET/PATCH /api/me` |
| Chat | 8 | user | `POST /api/chat/qa` (SSE), `POST /api/chat/dhamma-talk` (SSE), `GET /api/conversations`, `GET/PATCH/DELETE /api/conversations/[id]`, `GET /api/chunks/[id]`, `POST /api/messages/[id]/feedback` |
| Privacy | 3 | user | `GET /api/privacy/export`, `GET/POST /api/privacy/requests` |
| Admin — Users | 5 | ADMIN+ | `GET /api/admin/users`, `GET/PATCH/DELETE /api/admin/users/[id]`, `GET /api/admin/users/[id]/conversations` |
| Admin — Privacy Requests | 3 | ADMIN+ | `GET/POST /api/admin/privacy-requests`, `POST /api/admin/privacy-requests/[id]/complete` |
| Admin — Audit Log | 3 | ADMIN+ | `GET /api/admin/audit-log`, `GET .../export`, `GET .../verify` |
| Admin — AI Config | 6 | ADMIN+ | `GET/PUT /api/admin/ai-config`, `GET .../snapshots`, `POST .../revert/[snapshotId]`, `GET .../models`, `POST .../test-llm` |
| Admin — Prompts | 5 | ADMIN+ | `GET /api/admin/prompts`, `GET/POST /api/admin/prompts/[key]`, `POST .../activate/[version]`, `GET .../diff` |
| Admin — Guardrails | 5 | ADMIN+ | `GET/POST /api/admin/guardrails`, `PATCH/DELETE /api/admin/guardrails/[id]`, `POST .../test` |
| Admin — Unavailable | 2 | ADMIN+ | `GET/PUT /api/admin/unavailable-mode` |
| Admin — Pricing | 4 | ADMIN+ | `GET/POST /api/admin/pricing`, `PATCH/DELETE /api/admin/pricing/[id]` |
| Admin — Maintenance | 1 | ADMIN+ | `POST /api/admin/maintenance/purge-conversations` |
| Admin — Knowledge Base | 11 | ADMIN+ | `GET /api/admin/knowledge-base`, `POST .../upload`, `GET/PATCH/DELETE .../[id]`, `GET .../[id]/chunks`, `POST .../reindex`, `GET .../jobs`, `GET .../jobs/[id]`, `POST .../jobs/[id]/retry-failed`, `GET .../coverage` |
| Admin — Evaluation Datasets | 11 | ADMIN+ | CRUD `/api/admin/evaluation/datasets[/id]`, items CRUD, `POST .../items/import` (CSV), `GET .../items/template` |
| Admin — Evaluation Runs | 4 | ADMIN+ | `POST/GET /api/admin/evaluation/runs`, `GET .../runs/[id]`, `GET .../compare` |
| Admin — Evaluation Reports | 4 | ADMIN+ | `GET .../thresholds`, `GET/POST .../security-report`, `POST .../export` |
| Admin — Dashboard | 3 | ADMIN+ | `GET /api/admin/dashboard/{usage,tokens,feedback}` |
| Admin — Alerts | 2 | ADMIN+ | `GET /api/admin/alerts`, `POST /api/admin/alerts/test` |
| Admin — Ops | 11 | ADMIN+ / SUPER_ADMIN | Defect log CRUD + export, `GET /api/admin/sla-report` + export, `GET/POST /api/admin/data-destruction` (SUPER_ADMIN), `GET/POST /api/admin/backup` (SUPER_ADMIN) |

หมายเหตุ: route ภายใน `POST /api/internal/security-event` (ให้ middleware บันทึก audit เมื่อ IP นอก allowlist เข้าถึงส่วนผู้ดูแล ตรวจด้วย header ภายใน) ไม่ได้เปิดเป็น API สาธารณะ จึงไม่อยู่ใน OpenAPI registry

### 8.2 เหตุการณ์ SSE ของ `/api/chat/qa` และ `/api/chat/dhamma-talk`

| event | ข้อมูล | เวลาที่ส่ง |
|---|---|---|
| `references` | รายการอ้างอิง ≤10 (n, chunkId, label, title, snippet, score, volume, item ...) | หลังโหนด `rerank` |
| `delta` | ข้อความบางส่วนของคำตอบ | ระหว่างโหนด `generate` |
| `refusal` | `{ reason, message, topicLabel? }` | เมื่อถูกปฏิเสธ |
| `done` | `{ conversationId, messageId, answer, refusal, artifactMd, references, usage{input,thinking,output,total}, trace[], ttftMs, latencyMs, model, promptVersion }` | จบคำขอ |
| `error` | `{ code: "GENERATION_FAILED", message }` | เกิดข้อผิดพลาดระหว่างสร้างคำตอบ |

---

## 9. ข้อจำกัดและสมมติฐาน

1. ระบบสาธิตรันบนเครื่องเดียว (Node.js ≥22 + SQLite) ตัวเลขด้านประสิทธิภาพที่ระดับ 500 concurrent และ Availability รายเดือนต้องยืนยันบนโครงสร้างพื้นฐานจริงของมหาวิทยาลัย
2. ฐานองค์ความรู้ปัจจุบันเป็นพระไตรปิฎกฉบับออนไลน์บางส่วนตามที่ TOR ภาคผนวก ข อนุญาตในระยะแรก (ดู `/documents/data-ingestion-plan` และ `/documents/ingestion-report`)
3. เมื่อไม่ได้ตั้ง API key ของ provider ที่เลือก ระบบสลับไปใช้ `MockLlmAdapter` (deterministic) อัตโนมัติ และชุดทดสอบ vitest บังคับใช้ mock เสมอ
4. การทดสอบเจาะระบบไม่ได้ดำเนินการในรอบนี้ตามคำสั่งของผู้ว่าจ้าง (ดู `/documents/pentest-report`)
