690608:0012 ADR-035-135 #08
CI / CD Pipeline / build (push) Successful in 5m5s
CI / CD Pipeline / deploy (push) Successful in 3m48s

This commit is contained in:
2026-06-08 00:12:31 +07:00
parent a0f77ad121
commit 52b96d01de
48 changed files with 1590 additions and 1266 deletions
+17
View File
@@ -0,0 +1,17 @@
# Recent Rollouts
| วันที่ | Version | รายการ | สถานะ |
| ---------- | ------- | ---------------------------------------------------------------------------------------------------- | ----------------------------- |
| 2026-05-23 | v1.9.6 | Specs reorganization (`100/200/300-*` folders), AGENTS.md v1.9.6 update | ✅ Complete |
| 2026-05-23 | v1.9.6 | N8N Workflow v2 (`n8n.workflow.v2.json`) — ADR-023A compliant, ลบ Ollama direct | ⏳ Pending import to n8n UI |
| 2026-05-24 | v1.9.6 | AGENTS.md Project Memory Override rule (Devin / Antigravity / Codex) | ✅ Complete |
| 2026-05-25 | v1.9.6 | Migration Queue attachment UUID fix — DTO + Service + n8n.workflow.v2.json (Session 3) | ✅ Complete (tsc verified) |
| 2026-05-25 | v1.9.6 | Migration error normalization + `job_id` logging — workflow + backend + SQL/delta (Session 4) | ✅ Complete |
| 2026-05-25 | v1.9.6 | PaddleOCR Sidecar บน Desk-5439 — FastAPI `/ocr`+`/normalize`, CIFS mount, path remapping (Session 7) | ✅ Running |
| 2026-05-27 | v1.9.7 | Context-Aware Prompt Templates & DB CC Whitespace Cleanup (ADR-030) (Session 9) | ✅ Complete (v1.9.7 main) |
| 2026-05-30 | v1.9.7 | OCR Engine Migration — PaddleOCR → Tesseract (Session 8) | ✅ Complete (pending rebuild) |
| 2026-05-30 | v1.9.7 | OCR Sandbox Two-Step Flow (ADR-030/231) — Step 1 OCR-only → Step 2 AI Extraction (Session 10) | ✅ Complete |
| 2026-05-30 | v1.9.7 | Typhoon OCR & LLM Integration (ADR-032) — Dynamic model switching + VRAM Monitor (Session 11) | ✅ Complete |
| 2026-06-03 | v1.9.8 | Thai-Optimized AI Model Stack (ADR-034) — typhoon2.5-np-dms:latest + typhoon-np-dms-ocr:latest | ✅ Complete |
| 2026-06-05 | v1.9.8 | RAG Pipeline Enhancements (Spec 234 / ADR-035) — BGE-M3 + BGE-Reranker + Hybrid Qdrant (Session 14/15) | ✅ Complete |
| 2026-06-06 | v1.9.9 | LLM JSON Parse Failure & VRAM Fix (ADR-035-135) — retry logic + keep_alive=0 + ESLint heap fix | ✅ Complete |
@@ -0,0 +1,45 @@
# Session 2 — 2026-05-23 (N8N Workflow Refactor)
## Decisions ที่ Lock แล้ว (จาก QuizMe Session)
| # | Decision |
| ------- | ------------------------------------------------------------------------------------ |
| S1 | **n8n = Migration only** — New Correspondence ใช้ Backend pipeline (BullMQ) |
| S2 | New Correspondence → BullMQ `ai-realtime` (ไม่ผ่าน n8n) |
| S3 | n8n call `POST /api/ai/jobs` (Backend) แทน Ollama direct (ADR-023A) |
| PA | Excel metadata (`docNumber`, `title`, `sender`, etc.) ส่งไปพร้อม AI job เป็น context |
| PB-Tags | Tag suggestion ทาง C — แนะนำ existing + สร้าง tag ใหม่ได้ถ้าไม่มี (`isNew` flag) |
| PB-UX | Editable form pre-filled ด้วย AI suggestions — user approve/edit ก่อน submit |
## Endpoint ที่ Verified แล้ว
- `POST /api/ai/jobs` (`type: migrate-document`) — **พร้อมใช้งานแล้ว** (verified 2026-05-23)
- `GET /api/ai/jobs/:jobId` — polling endpoint พร้อม
- `POST /api/storage/upload` — two-phase upload พร้อม
## ไฟล์ที่สร้าง/แก้ไข
| ไฟล์ | การเปลี่ยนแปลง |
| -------------------------------------------------------------- | -------------------------------------------------------------- |
| `specs/03-Data-and-Storage/CONTEXT-N8N-Refactor.md` | ✅ สร้างใหม่ — context doc สำหรับ implement |
| `specs/03-Data-and-Storage/n8n.workflow.v2.json` | ✅ สร้างใหม่ — ADR-023A compliant workflow |
| `specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md` | ✅ อัพเดต v1.9.0 — ลบ Ollama direct, เพิ่มขั้นตอน update token |
| `specs/03-Data-and-Storage/03-06-migration-business-scope.md` | ✅ อัพเดต — Gate #1 blocker → Verified 2026-05-23 |
## สาระสำคัญของ n8n.workflow.v2.json
**Nodes ที่ลบ (ADR-023A violations):** `Ollama AI Analysis`, `Build AI Prompt`, `Extract PDF Text` (Tika), `Check/Update Fallback State`, `Import to Backend`, `Upsert Tags`, `Link Tags`
**Nodes ใหม่:** `Validate Token``Upload PDF to Backend``Build AI Job Payload``Submit AI Job``Poll AI Job Status`
**Flow สรุป:**
```
Form Trigger → Set Config → Health/Token Check → Fetch Master Data
→ File Mount Check → Read Excel → Read Checkpoint
→ [Per Record: File Validate → Upload PDF → Submit AI Job → Poll → Parse/Route]
→ Auto/Flagged → migration_review_queue
→ Rejected → CSV Log
→ Error → CSV + DB Log
→ Save Checkpoint → Delay → Loop
```
@@ -0,0 +1,14 @@
# Session 1 — 2026-05-23 (Specs Reorganization)
## งานที่ทำ
- Reorganize โครงสร้างโฟลเดอร์ `specs/` สำเร็จ (`100-Infrastructures`, `200-fullstacks`, `300-others`)
- อัปเดตกฎ `AGENTS.md` และ `GEMINI.md` ให้ตรงกับมาตรฐานใหม่
- ริเริ่มระบบ `memory/agent-memory.md`
## ไฟล์ที่แก้ไข
- `specs/` folder structure reorganization
- `AGENTS.md` update
- `GEMINI.md` update
- `memory/agent-memory.md` initial creation
@@ -0,0 +1,26 @@
# Session 3 — 2026-05-24 (Migration Queue Attachment UUID Bug Fix)
## ปัญหาที่พบ (Root Cause)
ไฟล์ `n8n.workflow.v2.json` (โหนด `Insert Review Queue`) ส่งค่า `tempAttachmentId` โดยใช้ `{{parseInt($json.attachmentId)}}` ซึ่งพยายามแปลง UUID string เป็นตัวเลข ผลลัพธ์คือค่า `NaN` หรือตัวเลขที่ผิดพลาด (เช่น `"0195..."``19`) ทำให้คอลัมน์ `temp_attachment_id` ใน `migration_review_queue` เป็น `NULL` เสมอ — ละเมิด ADR-019 Tier 1 Blocker
## การแก้ไข (Fix)
| ไฟล์ | การเปลี่ยนแปลง |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `backend/src/modules/ai/dto/migration-checkpoint.dto.ts` | ปรับ `tempAttachmentId` เป็น `@IsOptional()` รองรับทั้ง UUID string และ Integer PK |
| `backend/src/modules/ai/ai-migration-checkpoint.service.ts` | เพิ่ม UUID→INT resolution: `SELECT id FROM attachments WHERE uuid = ? LIMIT 1` |
| `specs/03-Data-and-Storage/n8n.workflow.v2.json` | เปลี่ยนส่ง `temp_attachment_public_id` (UUID string) แทน `parseInt(...)` ที่ผิด |
## Pattern ที่ตกลง (Locked)
```
n8n ส่ง: { tempAttachmentId: "019505a1-7c3e-7000-..." } ← UUID string
Backend รับ: ตรวจสอบประเภท → ถ้าเป็น string → query DB → ได้ INT id จริง
DB บันทึก: migration_review_queue.temp_attachment_id = <INT> ← ถูกต้อง
```
## Verification
- `npx tsc --noEmit` — ✅ ผ่าน ไม่มี type error
- ตรวจสอบ logic ใน Service แล้ว ไม่มีการเขียนทับ `tempAttachmentId` ด้วย `undefined` (guard check แล้ว)
@@ -0,0 +1,46 @@
# Session 4 — 2026-05-25 (Migration Error Normalization ตาม AGENTS.md)
## ปัญหาที่พบ (Root Cause)
- `Log Error to CSV` และ `Log Error to DB` ใน `n8n.workflow.v2.json` ส่ง `error_type` บางค่าไม่ตรง enum ของ `migration_errors`
- ค่าที่พบจริงและต้อง normalize: `AI_JOB_FAILED`, `PARSE_ERROR`, `TOKEN_EXPIRED`
- backend `AiMigrationCheckpointService.logError()` เดิม insert ค่า `dto.errorType` ตรง ๆ ทำให้เสี่ยง DB enum reject
- ตาราง `migration_errors` เดิมไม่มี `job_id` แม้ workflow/DTO จะมี `jobId` อยู่แล้ว ทำให้ trace กลับไป BullMQ job ไม่ครบ
## การแก้ไข (Fix)
| ไฟล์ | การเปลี่ยนแปลย |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `specs/03-Data-and-Storage/n8n.workflow.v2.json` | normalize `error_type`, `document_number`, `error`, `job_id` ก่อนเขียน CSV/DB |
| `backend/src/modules/ai/ai-migration-checkpoint.service.ts` | map/validate `errorType` ซ้ำก่อน insert และเพิ่ม `job_id` ใน SQL insert |
| `backend/src/modules/migration/entities/migration-error.entity.ts` | เพิ่ม field `jobId?: string` |
| `specs/03-Data-and-Storage/lcbp3-v1.9.0-migration.sql` | เพิ่มคอลัมน์ `job_id VARCHAR(100) NULL` และ index |
| `specs/03-Data-and-Storage/deltas/2026-05-22-drop-migration-tables.rollback.sql` | อัปเดต table definition ของ `migration_errors` ให้มี `job_id` |
| `specs/03-Data-and-Storage/deltas/2026-05-24-add-migration-errors-job-id.sql` | เพิ่ม delta สำหรับ add `job_id` |
| `specs/03-Data-and-Storage/deltas/2026-05-24-add-migration-errors-job-id.rollback.sql` | เพิ่ม rollback สำหรับ drop `job_id` |
| `backend/src/modules/ai/ai-migration-checkpoint.service.spec.ts` | เพิ่ม regression tests สำหรับ error normalization + `job_id` |
## Mapping ที่ Lock แล้ว
```
AI_JOB_FAILED -> API_ERROR
PARSE_ERROR -> AI_PARSE_ERROR
TOKEN_EXPIRED -> API_ERROR
unsupported value -> UNKNOWN
```
## กฎใช้งานต่อไป
- ให้ถือ enum ของ `migration_errors.error_type` เป็น source of truth เสมอ
- workflow ต้อง normalize ก่อนส่งเข้า backend และ backend ต้อง normalize ซ้ำอีกชั้น
- ห้ามพึ่ง DB enum reject เป็น validation mechanism
- การเพิ่มคอลัมน์ `job_id` ต้องทำผ่าน SQL/delta ตาม ADR-009 เท่านั้น
## Verification
- workflow normalization assertion — ✅ ผ่าน
- `pnpm --filter backend build` — ✅ ผ่าน
- `pnpm --filter backend test -- --runTestsByPath src/modules/ai/ai-migration-checkpoint.service.spec.ts` — ✅ ผ่าน
- regression seam ที่เพิ่มยืนยัน:
- `AI_JOB_FAILED` map เป็น `API_ERROR`
- unsupported error type fallback เป็น `UNKNOWN`
@@ -0,0 +1,40 @@
# Session 5 — 2026-05-25 (N8N Submit AI Job Debug + Upload Dedup)
## ปัญหาที่พบ (Root Cause)
**Bug 1: `Submit AI Job` → 400 Bad Request**
- n8n HTTP Request node `typeVersion: 4.1` เมื่อ `specifyBody: "json"` และ `jsonBody` เป็น expression ที่ return **object** → n8n ส่ง body เป็น `"[object Object]"` แทน JSON string
- แก้ด้วย `JSON.stringify($json.submit_payload)`
**Bug 2: `Submit AI Job` → 403 Forbidden**
- `migration_bot` (user_id=5, role_id=1/Superadmin) ไม่มี `ai.suggest` ใน `role_permissions`
- Root cause: Seed script `INSERT INTO role_permissions SELECT 1, permission_id FROM permissions WHERE is_active = 1` รันก่อน `ai.*` permissions (id 181-186) ถูก insert เข้า `permissions` table
- แก้ด้วย delta SQL grant ai.\* ให้ role_id=1
**Bug 3: Upload ซ้ำเมื่อ n8n retry**
- `FileStorageService.upload()` เดิมไม่มี dedup → ทุก retry สร้าง orphan temp attachment ใหม่
- แก้ด้วย checksum-based dedup: query หา temp record ที่มี checksum+userId เดิมและยังไม่หมดอายุ → คืน record เดิมแทน
## การแก้ไข (Fix)
| ไฟล์ | การเปลี่ยนแปลย |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `specs/03-Data-and-Storage/n8n.workflow.v2.json` | `jsonBody` เปลี่ยนเป็น `JSON.stringify($json.submit_payload)` |
| `specs/03-Data-and-Storage/deltas/2026-05-25-grant-ai-permissions-to-superadmin.sql` | INSERT IGNORE ai.\* permissions สำหรับ role_id=1 (Superadmin) |
| `specs/03-Data-and-Storage/deltas/2026-05-25-grant-ai-permissions-to-superadmin.rollback.sql` | Rollback DELETE สำหรับ delta ข้างบน |
| `backend/src/common/file-storage/file-storage.service.ts` | เพิ่ม checksum dedup ใน `upload()` ก่อน write file |
## กฎที่ Lock แล้ว
- `jsonBody` ใน n8n HTTP Request `typeVersion >= 4.1` ต้องใช้ `JSON.stringify(...)` เมื่อ `specifyBody: "json"` และค่าเป็น object
- ทุกครั้งที่เพิ่ม permission ใหม่ใน `permissions` table ต้อง grant ให้ Superadmin (role_id=1) ด้วยทันที — ห้ามปล่อยให้ขาดหาย
- `FileStorageService.upload()` เป็น idempotent ผ่าน SHA-256 checksum + userId + expiresAt
## Verification ที่ยังต้องทำ
- รัน delta SQL ใน MariaDB (ถ้ายังไม่รัน): `2026-05-25-grant-ai-permissions-to-superadmin.sql`
- Import `n8n.workflow.v2.json` ใหม่เข้า n8n UI
- `pnpm --filter backend test -- file-storage` — ยืนยัน checksum dedup
@@ -0,0 +1,51 @@
# Session 7 — 2026-05-25 (PaddleOCR Sidecar Setup)
## สิ่งที่ทำ
- แก้ `AggregateError` (empty message) ใน `ocr.service.ts` — wrap เป็น Error พร้อม context ที่ชัดเจน
- สร้าง PaddleOCR + PyThaiNLP FastAPI sidecar รันบน Desk-5439 (Windows 10/11, Docker Desktop WSL2)
- เพิ่ม path remapping `remapPath()` ใน `OcrService` — แปลง `/app/uploads/...``/mnt/uploads/...`
## ไฟล์ที่สร้าง/แก้ไข
| ไฟล์ | การเปลี่ยนแปลย |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/Dockerfile` | ✅ สร้างใหม่ — Python 3.10 slim, ลบ pre-download step (segfault) |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` | ✅ สร้างใหม่ — FastAPI: `/health`, `/ocr` (PaddleOCR), `/normalize` (PyThaiNLP) |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/requirements.txt` | ✅ สร้างใหม่ — `numpy<2.0` ต้องอยู่ก่อน paddlepaddle (ABI fix) |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/docker-compose.yml` | ✅ สร้างใหม่ — CIFS volume mount + named volume สำหรับ model cache |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml` | เพิ่ม `OCR_API_URL`, `OCR_CHAR_THRESHOLD`, `OCR_SIDECAR_UPLOAD_BASE` |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/.env.example` | เพิ่ม `OCR_API_URL`, `OCR_CHAR_THRESHOLD`, `OCR_SIDECAR_UPLOAD_BASE` |
| `backend/src/modules/ai/services/ocr.service.ts` | เพิ่ม `remapPath()`, AggregateError fix |
## Known Issues / Fixes
- `numpy<2.0` ต้องอยู่ก่อน `paddlepaddle` — ABI mismatch กับ cv2 (numpy 2.x)
- Docker Desktop WSL2 ไม่รองรับ bind mount จาก network drive (Z:\) → ใช้ CIFS volume แทน
- Pre-download model ใน Dockerfile ทำให้ segfault (exit 139) → ลบออก download ตอน runtime
- `OLLAMA_RAG_MODEL` → เปลี่ยนเป็น `OLLAMA_MODEL_MAIN=gemma4:e2b` ตาม ADR-023A
## CIFS Volume Config
```yaml
volumes:
qnap_uploads:
driver: local
driver_opts:
type: cifs
o: 'username=${QNAP_USER},password=${QNAP_PASS},vers=3.0,uid=0,gid=0'
device: '//192.168.10.8/np-dms-as/data/uploads'
```
## Path Remapping
```
backend: /app/uploads/temp/xxx.pdf → sidecar: /mnt/uploads/temp/xxx.pdf
OCR_SIDECAR_UPLOAD_BASE=/mnt/uploads (env var)
```
## Verification
- `curl http://localhost:8765/health``{"status":"ok","engine":"paddleocr"}`
- `POST /ocr` ทดสอบกับไฟล์จริงใน `/mnt/uploads/temp/` → ได้ text กลับ ✅
- 78 test suites, 672 tests ผ่านทั้งหมด ✅
@@ -0,0 +1,226 @@
# Session 9 — 2026-05-26 (System Memories Consolidation)
## QNAP SSH Key Authentication & CI/CD Deployment
### Infrastructure
- QNAP `192.168.10.8` — target deploy server (runs Gitea + app containers)
- ASUSTOR `192.168.10.9` — Gitea runner
### SSH Key Setup (Persistent)
- Private key: `/etc/config/ssh/gitea-runner`
- Public key: `/etc/config/ssh/gitea-runner.pub`
- Fingerprint: `SHA256:OhPbRe9vi4aWTyzBqCQ6T3MLl+JK9lFtH5bPrx+ICPw`
- Authorized keys: `/etc/config/ssh/authorized_keys` (symlinked from `/root/.ssh/`)
- QNAP SSH config: `/etc/config/ssh/sshd_config` (persistent — ใช้อันนี้เท่านั้น ไม่ใช่ `/etc/ssh/sshd_config`)
### Critical Fix: AuthorizedKeysFile
```
AuthorizedKeysFile /etc/config/ssh/authorized_keys
```
ต้องใช้ **absolute path** — ถ้าใช้ `.ssh/authorized_keys` จะ resolve ไปที่ `/share/homes/admin/.ssh/` ซึ่งผิด (admin home = `/share/homes/admin` แต่ symlink อยู่ที่ `/root/.ssh`)
### Reload QNAP SSH daemon
```bash
kill -HUP $(ps | grep "/usr/sbin/sshd -f /etc/config" | grep -v grep | awk '{print $1}')
```
ไม่มี `pgrep` และไม่มี `systemctl` บน QNAP
### Gitea Secrets
| Secret | Value |
|--------|-------|
| HOST | `192.168.10.8` |
| PORT | `22` |
| USERNAME | `admin` |
| SSH_KEY | private key content from `/etc/config/ssh/gitea-runner` |
### deploy.sh Fix
```bash
# scripts/deploy.sh line 10 — correct path:
COMPOSE_FILE="$SOURCE_DIR/specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml"
```
ไม่ใช่ `...04-00-docker-compose/docker-compose-app.yml` (ขาด `QNAP/app/`)
### Root Causes (ทั้งหมด)
1. `authorized_keys` เสียหาย — 2 keys บรรทัดเดียว
2. SSH key pair หายหลัง reboot — QNAP `/` เป็น RAM, ต้องเก็บใน `/etc/config/`
3. `AuthorizedKeysFile` ใช้ relative path — resolve ผิด directory
4. HOST secret ชี้ไปผิด server (Go SSH) — แก้เป็น `192.168.10.8:22`
5. `deploy.sh` COMPOSE_FILE path ผิด — ขาด `QNAP/app/` subdirectory
## Backend TransformInterceptor Double Registration Bug
### Issue
API responses were double-wrapped `{ data: { data: actualData } }` causing frontend detail pages to fail loading data.
### Root Cause
TransformInterceptor registered in TWO places:
1. `backend/src/main.ts`: `app.useGlobalInterceptors(new TransformInterceptor())`
2. `backend/src/common/common.module.ts`: `{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor }`
### Fix
Removed duplicate registration from `main.ts` (keep only APP_INTERCEPTOR in CommonModule).
### Why list page still worked
Paginated responses were re-detected as paginated by second interceptor, preventing double-nesting. Non-paginated (detail) endpoints were affected.
### Verification
`curl http://localhost:3001/api/correspondences/{uuid}` now returns single-wrapped `{ data: {...} }` instead of double-wrapped.
### Pattern to Avoid
Never register global interceptors/filters in both `main.ts` AND via `APP_INTERCEPTOR`/`APP_FILTER` providers.
## ADR-021 Integration: Transmittals & Circulation
### Summary
Successfully integrated ADR-021 (Integrated Workflow Context & Step-specific Attachments) into Transmittals and Circulation modules. All backend services, frontend pages, and tests are wired to the Unified Workflow Engine.
### Backend Changes (B1-B9)
- **WorkflowEngineService**: Added `getInstanceByEntity(entityType, entityId)` for polymorphic workflow instance lookup
- **TransmittalService**:
- Expose `workflowInstanceId`, `workflowState`, `availableActions` in `findOneByUuid()`
- Added purpose filter to `findAll()`
- Added `submit()` with EC-RFA-004 validation (prevents submission if any item correspondence is DRAFT)
- Starts workflow instance `TRANSMITTAL_FLOW_V1` and updates CorrespondenceRevision status
- **TransmittalController**: Added `POST /:uuid/submit` endpoint with RBAC and Audit
- **TransmittalModule**: Imported `WorkflowEngineModule` and `CorrespondenceRevision`
- **CirculationService**:
- Expose workflow fields in `findOneByUuid()`
- Added `reassignRouting()` (EC-CIRC-001) for PENDING routing reassignment
- Added `forceClose()` (EC-CIRC-002) with transactional rollback and reason validation
- **CirculationController**: Added `PATCH /:uuid/routing/:routingId/reassign` and `POST /:uuid/force-close`
- **Circulation Entity**: Added `deadlineDate` column for EC-CIRC-003 Overdue badge
- **Schema Delta**: `05-add-circulation-deadline.sql` per ADR-009 (no migrations)
### Frontend Changes (F1-F7)
- **Types**: Extended `Transmittal` and `Circulation` interfaces with workflow fields; added `deadlineDate` to Circulation
- **Hooks**: Created `useTransmittal()` and extended `useCirculation()` hooks with TanStack Query
- **Detail Pages**:
- Both wired with `IntegratedBanner` and `WorkflowLifecycle` using live workflow data
- Circulation page includes EC-CIRC-003 Overdue badge logic (`isOverdue()`)
- **List Page**: Added purpose filter dropdown to `transmittals/page.tsx`
### Tests (T1-T2): 19/19 Passing
- **TransmittalService**: 7 tests covering EC-RFA-004 validation, workflow instance creation, and error cases
- **CirculationService**: 12 tests covering EC-CIRC-001 (reassign), EC-CIRC-002 (forceClose), EC-CIRC-003 (deadlineDate exposure)
### Key Technical Decisions
- Followed ADR-019 UUID handling (no parseInt, use string UUIDs)
- Used ADR-009 direct schema edits (no TypeORM migrations)
- Enforced RBAC with CASL guards and Audit decorators
- Implemented transactional force-close with proper rollback
- Maintained existing patterns for error handling and service architecture
### Remaining Work
- I1: i18n keys for new workflow actions (low priority)
## Correspondence Detail Display Fixes
### Issue
`/correspondences/[uuid]` detail display inconsistency
### Fix
Made backend `findOneByUuid` query deterministic with explicit relation joins and revision ordering (rev.revisionNumber DESC, rev.createdAt DESC), and normalized recipient_type values in frontend detail page before TO/CC filtering to handle whitespace variants per schema (e.g., 'CC ').
### Files Modified
- `backend/src/modules/correspondence/correspondence.service.ts`
- `frontend/components/correspondences/detail.tsx`
## Correspondence Create Permission Bypass
### Issue
Users without primaryOrganizationId could not create documents even with system.manage_all permission
### Fix
In backend CorrespondenceService.create flow, users without primaryOrganizationId can still create when they have system.manage_all and provide originatorId. Validation now resolves originator organization under that permission instead of immediately throwing 'User must belong to an organization to create documents'. Added regression test in correspondence.service.spec.ts.
### Extension
Applied same pattern to RFA, Transmittal, and Circulation create endpoints — they now accept optional originatorId and allow creation for users with system.manage_all even when primaryOrganizationId is null. Added permission-gated impersonation checks in their services to prevent unauthorized cross-organization creation.
## Playwright E2E Testing Setup
### Test Stack
- **Backend**: Jest (Unit + Integration + E2E)
- **Frontend**: Vitest (Unit) + Playwright (E2E)
### MCP Server Setup (Devin)
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"]
}
}
}
```
### Devin Cascade Tools
- `browser_navigate` - เปิด URL
- `browser_click` - คลิก element
- `browser_type` - พิมพ์ข้อความ
- `browser_take_screenshot` - ถ่าย screenshot
- `browser_evaluate` - รัน JavaScript
### Run E2E Tests
```bash
cd frontend
npx playwright test # Run all
npx playwright test --ui # Debug mode
npx playwright test --headed # See browser
npx playwright show-report # Generate report
```
### E2E Script Location
`frontend/e2e/workflow-adr021.spec.ts`
## Tag Creation and Contract UUID Fixes
### Issue 1
`/admin/doc-control/reference/tags` needed a list-level Project dropdown filter and Tag creation could fail due to TypeORM Tag entity column-name mismatches.
### Fix
Added selectedProjectId filter in frontend tags page and mapped backend Tag entity fields to schema names (project_id, tag_name, color_code, created_by, created_at, updated_at, deleted_at).
### Issue 2
Frontend contract detail page typecheck failure — `contract.project?.id` vs `contract.project?.publicId`
### Fix
In `frontend/app/(admin)/admin/doc-control/contracts/page.tsx`, handleEdit must read nested project UUID from contract.project?.id (not project?.publicId) because Contract.project is typed and returned as { id: string; projectCode; projectName }.
@@ -0,0 +1,34 @@
# Session 9 — 2026-05-27 (Context-Aware Prompt Templates & Database Typo CC Cleanup)
## Summary
ดำเนินการอิมพลีเมนต์ ADR-030 (Context-Aware Prompt Templates สำหรับการสกัดข้อมูลเอกสาร) และทำการแก้ไขบัคช่องว่างประเภทผู้รับ `'CC '` ในฐานข้อมูล
## Backend Changes (B1-B6)
- **AiPrompt Entity**: เพิ่มการแมปคอลัมน์ `contextConfig` ไปยัง JSON ฟิลด์ `context_config` ในฐานข้อมูลเพื่อควบคุม master data resolution
- **CreateAiPromptDto / Response DTO**: ปรับแต่งให้รองรับการรับและส่งออกคอลัมน์ `contextConfig`
- **AiPromptsService**:
- อิมพลีเมนต์เมธอด `resolveContext()` สำหรับการดึงข้อมูล Master Data ดำเนินการคัดกรองข้อมูลอ้างอิงโครงการ (Projects, Organizations, Disciplines, CorrespondenceTypes, Tags) สอดคล้องกับ dynamic config filter
- ติดตั้ง **Gatekeeper Rule** (ตัวกรองความปลอดภัย) โยน `ForbiddenException` ทันทีเมื่อมีการร้องขอ override project UUID ข้ามอาณาเขตโครงการที่กำหนดใน template เพื่อป้องกัน Cross-project data leak
- **AiBatchProcessor**:
- ปรับปรุงโครงสร้าง `MigrateDocumentMetadata` interface, sandbox extraction, และ migration process ให้ดึงข้อมูลและแมป master data context-aware
- สกัดและจำแนกผู้รับเอกสาร (recipients) ภายใต้โครงสร้าง JSON แบบใหม่ในรูป Object Array: `recipients: Array<{ organizationPublicId: string, recipientType: 'TO' | 'CC' }>` เพื่อความเสถียรและทนทานของข้อมูล
- **Unit Tests**:
- เพิ่มชุดการทดสอบ `resolveContext` ใน `ai-prompts.service.spec.ts` ครอบคลุมการจำลอง master data resolution, การโยน `NotFoundException` และการล็อคสิทธิ์ความปลอดภัยด้วย `ForbiddenException` เมื่อ override โครงการข้าม boundary
- แก้ไข mock dependencies ของ `AiPromptsService` ใน `ai-batch.processor.spec.ts` ป้องกันปัญหา `TypeError: getActive is not a function` ทำให้ผ่าน unit tests 100%
## Database & Schema Changes (ADR-009)
- **schema-02-tables.sql**: แก้ไข line 338 ปรับปรุง `ENUM('TO', 'CC ')` เป็น `ENUM('TO', 'CC')`
- **SQL Delta**: สร้าง `2026-05-27-add-context-aware-prompts-and-cleanup.sql` ดำเนินการ `UPDATE` ข้อมูลเก่าที่เป็น `'CC '` ให้เป็น `'CC'` เพื่อล้างช่องว่าง จากนั้นสั่ง `ALTER TABLE` ปรับปรุงฟิลด์ enum และ Seed template ภาษาไทยเวอร์ชัน 2
- **Rollback SQL**: สร้างไฟล์ย้อนกลับ `2026-05-27-add-context-aware-prompts-and-cleanup.rollback.sql` เรียบร้อย
## Frontend Changes
- **detail.tsx**: ตรวจสอบการใช้งาน `normalizeRecipientType` ซึ่งครอบคลุมการล้างช่องว่างและการกรองผู้รับ TO/CC ได้อย่างทนทาน
## Verification
- `pnpm --filter backend build` — ✅ Compile ผ่านแบบ Strict Mode
- unit tests AI module & backend suites — ✅ ผ่านทั้งหมด 60 suites / 521 tests
@@ -0,0 +1,41 @@
# Session 8 — 2026-05-30 (OCR Engine Migration — PaddleOCR → Tesseract)
## ปัญหาที่พบ (Root Cause)
**Bug 1: PaddleOCR SIGILL (Illegal Instruction)**
- PaddleOCR 2.6.2 ต้องการ AVX instruction set ซึ่ง CPU บน Desk-5439 ไม่รองรับ
- ลองลดรุ่นเป็น 2.5.2 → ยังมี dependency conflict กับ paddleocr 2.7.3
- ลองใช้ paddlepaddle-cpu → ยังคงมีปัญหา dependency
**Bug 2: OCR ภาษาไทยผิด**
- PaddleOCR ตั้งค่า `lang="en"` ทำให้ข้อความภาษาไทยถูกแปลงเป็นอักษรละตินผิดๆ
- เช่น: "10 กุมภาพันธ์ 2568" → "10 qunnwus 2568"
## การแก้ไข (Fix)
เปลี่ยนจาก PaddleOCR เป็น Tesseract OCR เพื่อ:
1. แก้ปัญหา SIGILL บน CPU เก่าที่ไม่รองรับ AVX
2. รองรับภาษาไทยด้วย `tha+eng` language code
| ไฟล์ | การเปลี่ยนแปลย |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/requirements.txt` | ลบ paddlepaddle/paddleocr, เพิ่ม pytesseract, Pillow |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` | เปลี่ยนใช้ pytesseract, OCR_LANG เป็น `tha+eng`, ลบ PaddleOCR initialization |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/Dockerfile` | ติดตั้ง tesseract-ocr, tesseract-ocr-tha, tesseract-ocr-eng |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/docker-compose.yml` | OCR_LANG เป็น `tha+eng`, ลบ paddleocr_models volume |
| `backend/src/modules/ai/services/ocr.service.ts` | เปลี่ยน comment/error message จาก PaddleOCR เป็น Tesseract |
| `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` | เปลี่ยน Badge text จาก PaddleOCR เป็น Tesseract |
## กฎที่ Lock แล้ว
- Tesseract OCR ไม่ต้องการ AVX instruction set → ทำงานได้บน CPU ทุกรุ่น
- Tesseract รองรับภาษาไทยด้วย `tha+eng` language code
- API contract ยังเหมือนเดิม (`POST /ocr` คืน `{ text, ocrUsed }`) → backend/frontend ไม่ต้องเปลี่ยน logic
## Verification ที่ต้องทำ
- Rebuild container บน Desk-5439: `docker compose down && docker compose up -d --build`
- ทดสอบ OCR ภาษาไทย: "10 กุมภาพันธ์ 2568" ควรออกมาถูกต้อง
@@ -0,0 +1,67 @@
# Session 10 — 2026-05-30 (OCR Sandbox Two-Step Flow)
## Summary
แยก OCR Sandbox เป็น 2 steps ตาม spec 231: Step 1 OCR-only → Step 2 AI Extraction เพื่อให้ admin ตรวจคุณภาพ OCR ก่อนทดสอบ AI prompt
## Backend Changes (B1-B5)
- **AiBatchJobType**: เพิ่ม `sandbox-ocr-only` และ `sandbox-ai-extract` job types
- **AiBatchProcessor**:
- เพิ่ม `processSandboxOcrOnly()` — รัน OCR เท่านั้น, cache OCR text ใน Redis key `ai:sandbox:ocr:{idempotencyKey}` (TTL 3600s)
- เพิ่ม `processSandboxAiExtract()` — ดึง OCR text จาก cache, resolve prompt version (active หรือ version ที่ระบุ), replace {{ocr_text}} และ {{master_data_context}}, run LLM
- **AiPromptsService**: เพิ่ม `findByVersion(promptType, versionNumber)` method สำหรับดึง prompt version ที่ระบุ
- **AiController**:
- เพิ่ม `POST /ai/admin/sandbox/ocr` — Step 1 endpoint (รับ file multipart/form-data)
- เพิ่ม `POST /ai/admin/sandbox/ai-extract` — Step 2 endpoint (รับ requestPublicId + optional promptVersion)
- **AiQueueService**: อัปเดต `enqueueSandboxJob()` รองรับ job types ใหม่และ extraPayload สำหรับส่ง promptVersion
## Frontend Changes (F1-F3)
- **adminAiService**: เพิ่ม `submitSandboxOcr(file)` และ `submitSandboxAiExtract(requestPublicId, promptVersion)` methods
- **OcrSandboxPromptManager.tsx**:
- เพิ่ม states: `sandboxStep` ('ocr'|'ai'), `ocrResult` (requestPublicId, ocrText, ocrUsed), `selectedPromptVersion`
- เพิ่ม handlers: `handleStep1Ocr()` (poll OCR result), `handleStep2AiExtract()` (poll AI result), `handleResetSandbox()`
- Refactor UI: Step 1 (upload + Run OCR button) → Step 2 (prompt version dropdown + Run AI Extraction button + Reset button)
- แสดง OCR Raw Text card หลัง Step 1 เสร็จ (สีน้ำเงิน, badge บอก PaddleOCR/Fast Path)
- แสดง AI Extraction result หลัง Step 2 เสร็จ (สีเขียว, badge บอก promptVersionUsed)
## Schema Fix (ADR-009 + ADR-019)
- **Delta SQL**: สร้าง `2026-05-30-add-ai-prompts-publicId.sql` เพิ่ม `publicId CHAR(36) UNIQUE` column ใน `ai_prompts` table
- **Root Cause**: Entity มี `publicId` field แต่ DB table ไม่มี column นี้ → QueryFailedError: "Unknown column 'AiPrompt.publicId' in 'SELECT'"
- **Fix**: ใช้ CHAR(36) แทน UUID type (MariaDB compatible), generate UUID สำหรับ existing records ด้วย `UUID()` function
## Verification
- Backend TypeScript: ✅ ผ่าน (`npx tsc --noEmit`)
- Frontend TypeScript: ✅ ผ่าน (`npx tsc --noEmit`)
- ESLint: ✅ ผ่าน (แก้ unused `user` parameter ใน `submitSandboxAiExtract`)
## Data Flow
```
Step 1: Upload PDF → POST /ai/admin/sandbox/ocr
BullMQ: sandbox-ocr-only job
OCR Service → Cache OCR text (ai:sandbox:ocr:{id})
Frontend displays OCR Raw Text
Step 2: Select prompt version → POST /ai/admin/sandbox/ai-extract
BullMQ: sandbox-ai-extract job
Retrieve OCR text from cache (ai:sandbox:ocr:{id})
Replace {{ocr_text}} → LLM → JSON result
Frontend displays AI Extraction result
```
## Pending
- Run delta SQL `2026-05-30-add-ai-prompts-publicId.sql` ใน production database
- Restart backend service หลัง apply delta
- Test 2-step flow จริงใน production environment
@@ -0,0 +1,30 @@
# Session 11 — 2026-05-30 (Typhoon OCR & LLM Integration)
## Summary
ออกแบบและพัฒนาการใช้งานโมเดลภาษาไทยผสมอังกฤษ Typhoon OCR-3B ร่วมกับ Tesseract OCR แบบ Dynamic พร้อมระบบ caching 24 ชม., VRAM Monitor ป้องกัน GPU OOM และระบบ fallback 5s เมื่อโมเดลมีปัญหา และการสลับและบริหารจัดการ LLM โมเดลหลักแบบ Dynamic ในระบบ AI Model Management ของ Next.js frontend ตามข้อกำหนด ADR-032
## Backend Changes (B1-B5)
- **OcrService**:
- เพิ่ม dynamic OCR engine selection (`getOcrEngines()`, `selectOcrEngine()`, `getActiveEngineId()`) จัดเก็บสถานะหลักใน DB `system_settings` (`OCR_ACTIVE_ENGINE`) พร้อม cache ใน Redis 30s ป้องกันคิวรีซ้ำซ้อน
- ปรับปรุง `detectAndExtract()` ให้เลือกใช้ engine ที่เหมาะสม หากผู้ใช้เลือก Typhoon OCR-3B จะทำงานผ่าน `processWithTyphoon()` ร่วมกับ `OcrCacheService` (24-hour Redis caching) และ `VramMonitorService` (ตรวจสอบ VRAM capacity > 4GB ก่อนโหลดโมเดล)
- พัฒนาระบบ **Graceful Fallback** ไปยัง Tesseract OCR ในเวลา 5 วินาทีหาก Typhoon ขัดข้องหรือ VRAM ไม่เพียงพอ โดยมีการบันทึกรายละเอียดและ error ลง `ai_audit_logs`
- **AiService**:
- เพิ่ม endpoints สำหรับ AI Model Management: `GET /models`, `POST /models` (Superadmin), `PATCH /models/:modelId/activate` และ `GET /vram/status` (Used/Free VRAM และ Active models บน GPU) ร่วมกับ `AiSettingsService` และ `VramMonitorService`
- ตรวจสอบความปลอดภัย VRAM ก่อนอนุญาตให้สลับโมเดลหลัก หากเหลือพื้นที่หน่วยความจำ GPU ไม่พอ จะโยน `BusinessException` แจ้งเตือนภาษาไทยพร้อมบันทึกลง Audit Log และระงับการเปลี่ยนโมเดลทันที
- แก้ไขข้อผิดพลาด build error ใน `ai.service.ts` โดยการนำเข้า `OllamaService` และ `AiQdrantService` ที่ขาดหายไปใน constructor
## Frontend Changes (F1-F3)
- **admin-ai.service.ts**: เพิ่ม interface `LoadedModelInfo` และ `VramStatusResponse` และเพิ่ม methods `getVramStatus()`, `getAvailableModels()`, `setActiveModel()`, และ `addModel()` โดยใช้ dynamic path ที่อ้างอิง UUIDv7 (`modelId`) และส่ง idempotency headers ตาม ADR-019/ADR-016
- **admin/ai/page.tsx**: อัปเดตหน้า AI Admin page โดยเพิ่ม **VRAM GPU Monitor Card** แบบ realtime (ดึงและสลับรีเฟรชผ่าน React Query ทุก 15s) แสดง Free/Used VRAM และ active models บน GPU และปรับปรุง UI ส่วน AI Model Management ให้สลับโมเดลหลักผ่าน UUIDv7 และแสดง VRAM requirements ของแต่ละโมเดลอย่างสวยงามพรีเมียม
## ADRs Update (ADR-023/023A)
- อัปเดต `ADR-023-unified-ai-architecture.md` (v1.2) และ `ADR-023A-unified-ai-architecture.md` (v1.3) เพื่อรับรองสถาปัตยกรรม dynamic Thai specialized models (Typhoon OCR & LLM) ภายใต้การควบคุมของ VRAM Monitor
## Verification
- Backend NestJS Build: ✅ Compile สำเร็จ 100% ปราศจาก Error (`npm run build` ใน backend)
- Frontend Next.js Build: ✅ Compile สำเร็จ 100% ปราศจาก Error (`npm run build` ใน frontend)
@@ -0,0 +1,51 @@
# Session 15 — 2026-06-05 (Feature 234 RAG Pipeline Enhancements — สมบูรณ์)
## Summary
Implement RAG Pipeline Enhancements ตาม Spec 234 / ADR-035 ครบทุก Functional Requirement (FR-001 → FR-015), ผ่าน speckit-analyze → speckit-implement → speckit-tester → speckit-validate; ปิด Gap ทั้ง 2 รายการ
## งานที่ทำในเซสชั่นนี้
| งาน | ไฟล์หลัก | สถานะ |
|-----|----------|-------|
| Sidecar `/embed` (BGE-M3 Dense+Sparse) | `ocr-sidecar/app.py`, `requirements.txt` | ✅ |
| Sidecar `/rerank` (BGE-Reranker-Large) | `ocr-sidecar/app.py` | ✅ |
| `OcrService.embedViaSidecar()` + `rerankViaSidecar()` | `ocr.service.ts` | ✅ |
| Hybrid Qdrant schema (1024 dims, drop+recreate) | `qdrant.service.ts` | ✅ |
| Payload indexes (project_public_id tenant, doc_public_id, status_code, doc_type) | `qdrant.service.ts` | ✅ |
| `EmbeddingService.embedDocument()` — Semantic Chunking + fallback | `embedding.service.ts` | ✅ |
| `semanticChunkTextWithFallback()``parseChunkTags()``fixedSizeChunk()` | `embedding.service.ts` | ✅ |
| `AiBatchProcessor` case `rag-prepare``processRagPrepare()` | `ai-batch.processor.ts` | ✅ |
| `AiQueueService.enqueueRagPrepare()` + jobId dedup | `ai-queue.service.ts` | ✅ |
| `CorrespondenceWorkflowService.syncStatus()``triggerRagPrepare()` | `correspondence-workflow.service.ts` | ✅ |
| `AiRagService.processQuery()` — Hybrid Search + Reranker | `ai-rag.service.ts` | ✅ |
| SQL delta `rag_chunking` prompt | `deltas/2026-06-05-add-rag-chunking-prompt.sql` | ✅ |
| Integration test (9 tests) | `ai-rag-pipeline.integration.spec.ts` | ✅ |
| Validation report | `specs/200-fullstacks/234-rag-pipeline-enhancements/validation-report.md` | ✅ |
## Verification
- `npx jest` (6 suites): **24/24 tests PASS**
- `npx tsc --noEmit`: **0 errors**
- speckit-validate: **15/15 FR covered, 6/6 SC verifiable, 0 Gaps remaining**
## Architecture Summary (ADR-035)
```
CorrespondenceWorkflowService.syncStatus()
└── triggerRagPrepare() [fire-and-forget]
└── AiQueueService.enqueueRagPrepare() [jobId dedup]
└── BullMQ ai-batch queue
└── AiBatchProcessor.processRagPrepare()
├── OcrService.detectAndExtract() [if no cached text]
└── EmbeddingService.embedDocument()
├── semanticChunkTextWithFallback() → typhoon2.5 / fixed-size fallback
├── OcrService.embedViaSidecar() → BGE-M3 [dense 1024 + sparse]
└── AiQdrantService.upsert() → lcbp3_vectors [delete-before-upsert]
AiRagService.processQuery()
├── OcrService.embedViaSidecar(question) → BGE-M3
├── AiQdrantService.searchByProject(dense, sparse, projectPublicId, 15) → RRF Fusion
├── OcrService.rerankViaSidecar(question, chunks) → BGE-Reranker top-5
└── Ollama typhoon2.5-np-dms:latest → answer + citations
```
@@ -0,0 +1,44 @@
# Session 14 — 2026-06-05 (RAG Pipeline Enhancements / Spec 234 / ADR-035)
## Summary
ปรับโค้ดให้สอดคล้องกับ feature `234-rag-pipeline-enhancements` และ `ADR-035-ai-pipeline-flow-architecture` โดยปิด compile blockers ของ RAG pipeline ใหม่, บังคับ tenant scope ผ่าน `projectPublicId` ใน vector deletion path, ย้าย dashboard ไปใช้ `/ai/rag/*`, และ retire runtime surface ของ legacy `/rag/*`
## Backend Changes (B1-B6)
- **Compile blockers fixed (4 จุด):**
- ปรับ `ai-batch.processor.ts` ให้ legacy `embed-document` path ส่งข้อมูลตาม signature ใหม่ของ `EmbeddingService.embedDocument(...)`
- เพิ่ม `projectPublicId` ใน `AiVectorDeletionJobPayload` และ propagate ผ่าน `AiQueueService` + `vector-deletion.processor.ts`
- ปรับ typing ใน `src/modules/ai/qdrant.service.ts` ให้รองรับ hybrid upsert contract ของ Qdrant client
- แก้ nullability ใน `correspondence-workflow.service.ts` สำหรับ trigger `enqueueRagPrepare()`
- **Legacy delete path hardening:** `backend/src/modules/rag/rag.service.ts` เปลี่ยน `deleteVectors()` ให้ resolve `projectPublicId` จาก `DocumentChunk`/server-side context ก่อน ไม่เชื่อ query param จาก client เป็นหลัก
- **Legacy runtime deprecation:** mark `backend/src/modules/rag/rag.controller.ts` และ endpoint summaries เป็น deprecated ชั่วคราวระหว่าง audit
- **Legacy runtime removal:** หลัง consumer audit ใน repo ไม่พบ caller ของ `/rag/status`, `/rag/ingest`, `/rag/vectors`, `/rag/admin/init-collection`, จึงถอด `RagModule` ออกจาก `backend/src/app.module.ts` และลบ `backend/src/modules/rag/rag.controller.ts` + `rag.module.ts`
## Frontend Changes (F1-F2)
- **Dashboard RAG migration:** `frontend/app/(dashboard)/rag/page.tsx` เปลี่ยนมาใช้ `RagChatWidget` ซึ่งวิ่งผ่าน `POST /ai/rag/query` และ `GET /ai/rag/jobs/:requestPublicId`
- **Legacy frontend cleanup:** ลบ `frontend/hooks/use-rag.ts` และ `frontend/components/rag/*` ที่ไม่ถูกใช้งานแล้ว
## Architecture / Documentation Updates
- **ADR-035:** เพิ่มและอัปเดต Legacy Compatibility Note ให้สะท้อนสถานะจริงของ migration ไป `/ai/rag/*`
- **Current runtime state:** `/rag/*` ไม่ถูก mount ใน `AppModule` แล้ว; flow หลักของระบบใช้ `AiModule` + `/ai/rag/*`
## Verification
- `pnpm --filter backend build` — ✅ ผ่าน
- `pnpm --filter backend lint:ci` — ✅ ผ่าน
- `pnpm --filter backend test -- --runTestsByPath src/modules/ai/processors/ai-batch.processor.spec.ts src/modules/ai/services/embedding.service.spec.ts` — ✅ ผ่าน
- `pnpm --filter backend test -- --runTestsByPath src/modules/rag/__tests__/rag.service.spec.ts` — ✅ ผ่าน
- `pnpm --filter lcbp3-frontend lint` — ✅ ผ่าน
- `pnpm exec tsc --noEmit -p frontend/tsconfig.json` — ✅ ผ่าน
## Follow-up Completed (Session 14+)
- ✅ แก้ `backend/src/modules/ai/qdrant.service.ts``ensureCollection()` ตรวจ schema ก่อน delete: ถ้าเป็น Hybrid 1024 dims แล้ว skip recreation
- ✅ ย้าย `rag-prepare` enqueue ใน `CorrespondenceWorkflowService.syncStatus()` ไปหลัง commit — after-commit pattern ด้วย `triggerRagPrepare()`
- ✅ ลบ `backend/src/modules/rag/` ทั้งหมด (audit ไม่พบ import จาก backend/src)
- ✅ สร้าง `backend/src/modules/ai/ai-qdrant.service.spec.ts` — regression test สำหรับ `deleteByDocumentPublicId()` (4/4 ผ่าน)
- ✅ Code Review Fixes (Session 14+): try-catch after-commit, `createPayloadIndexes()` private, defensive vector size check, `skipRagPrepare` flag
- ✅ F5 (Session 15): เพิ่ม `ai-rag-pipeline.integration.spec.ts` — 9 integration tests ครอบคลุม SC-002, SC-003, SC-006, FR-005, jobId dedup