# sub-harvest-curator — /sleep-recovery-memory-l2 Phase 1 (GATHER + DEDUP) — batch B/2 — S170

> **Vai:** harvest-curator (H2) · **PROPOSE-ONLY** — file này là kênh run-trace B4/B6, KHÔNG phải ghi trí-nhớ. 0 chạm `.claude/agent-memory/**`.
> **Batch B** = `investigator-codebase` (4 verbatim) + `reviewer` (2) + `test-specialist` (1) + `implementer-frontend` (1) = **8 file gen-0**.
> **Phương-pháp:** đóng-kín TỪNG file (chống cross-file bleed) — cụm-dedup + importance-tag + bản-nén-nháp 4-field SE + danh-sách token.
> **Khung 4-field SE:** VIỆC · KẾT-LUẬN(+commit/file:line) · BÀI-HỌC · BẤT-NGỜ — rút gọn câu chữ, KHÔNG bỏ field.
> **Token-list** = nguyên-liệu coverage-diff của lead: {surprise · guard · file:line · root-cause · gotcha#}. Thiếu = gate FAIL.
> **2 file đặc-biệt (theo lệnh lead):** `implementer-frontend/2026-H2` = nội-dung moved-not-cut đợt drain 08-01 ⇒ MỌI mục giữ danh-tính riêng, mục auto-refuse (dòng 78) tag **cao** · `test-specialist/baseline-history` = chuỗi số baseline là RUỘT ⇒ nén = gom giai-đoạn, giữ đủ mốc đầu/cuối mỗi giai-đoạn.
> **Lưu ý đọc:** mọi token dưới đây trích NGUYÊN VĂN từ verbatim gen-0; bản-nén là ĐỀ-XUẤT, chưa phải bản chốt.

---

## investigator-codebase/2026-05-q1 (20.699 B — "Archive Recent Activity Q1 2026-05, S21-S24")

**Đặc-điểm file:** 10 entry verbose FIFO S21→S24 Plan AA, archive 2026-05-22 (nguồn MEMORY.md 34.9 KB → giữ <25KB). Header khai rõ **§6.5 KHÔNG cắt narrative** + danh-sách KEEP. Đây là file dày file:line nhất batch B (bản-đồ mã PE V2 thời Mig 28-31).

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| B1-A | **Chuỗi bug per-NV Allow* (F1-F4) — 3 lượt audit, 2 bug + 1 no-bug** | S23 t2 spawn L2 (F4 FE readOnly short-circuit) + S23 t3 spawn (BE lookup `:765` sai row) + S23 t2 spawn M0 (semantic ChoDuyet — KHÔNG bug, disconnect mental-model) | **cao** |
| B1-B | **Cross-stack contract drop — Controller body ≠ Command record** | S23 t6 spawn Plan P FE-wire audit (xác nhận scope BE-only) | **cao** |
| B1-C | **Pre-flight Plan K (Mig 31) — bản-đồ 20+ điểm neo + GAP fe-user + drift Dev/prod** | S23 t1 spawn K0 | **vừa** |
| B1-D | **Pre-flight Plan R — cleanup destructive prod** | S23 t8 spawn (4 sqlcmd, FK Restrict, SQL Express limit, ~620 row cascade, 3 Option → chốt A) | **vừa** |
| B1-E | **Plan AA (User Workflow Matrix + sidebar widen) — 5Q audit + wrap 7 commit** | S24 t1 spawn Pre-A + S24 t1-t4 em-main solo 4 polish (gộp: cùng 1 Plan, phần wrap chính là hậu-nghiệm của 5Q) | **vừa** |
| B1-F | **Phát-hiện rời S22 (6 mục) — hạ-tầng/identity/script** | password ≥12 · rename atomic 4 field · `accessToken` ≠ `token` · PS 5.1 ASCII · Plan F drop V1 ABORTED · admin opt-in 2× | **vừa** |
| B1-G | **Ảnh chụp trạng-thái no-spawn (setup + S21 + S22 cumulative count)** | 2026-05-11 setup baseline + S21 t3-t5 + phần đầu S22 | **thấp** |

> Ghi-chú dedup: B1-A gộp 3 entry rời vì **cùng một mặt-cắt tính-năng** (7 cờ Allow* per-slot sau Mig 29/30/31); giữ nguyên 3 verdict phân-biệt (BUG FE · BUG BE · NO-BUG) — gộp verdict là mất danh-tính. B1-E gộp 2 entry cùng Plan AA. B1-G là ảnh-chụp số đếm, đã bị các mốc sau ghi đè ⇒ tag thấp.

### Bản-nén-nháp 4-field

**[cao] B1-A — chuỗi per-NV Allow* (F1-F4): 2 bug tách lớp FE/BE + 1 báo-động-giả**
- **VIỆC:** 3 lượt audit UAT liên-tiếp trên cùng bộ 7 cờ `Allow*` per-slot (`ApprovalWorkflowLevels`) sau Mig 29/30/31: (i) admin tick F3+F4 mà menu "Duyệt" vẫn read-only; (ii) admin tick 7 cờ cho riêng 1 NV mà FE không hiện checkbox nào; (iii) rà 4 BE + 4 FE × 2 app xem F1/F2/F3/F4 có tự đẩy phiếu về "nháp" không.
- **KẾT-LUẬN:** (i) **F3 ĐÚNG, F4 HỎNG** — `PeDetailTabs.tsx:118` `itemsReadOnly = readOnly && !approverEditMode` override được, còn `:245` truyền `readOnly={readOnly}` xuống BudgetAdjustSection và `:973` `canAdjust = !readOnly && (...)` **đoản-mạch TRƯỚC khi F4 kịp xét** ⇒ nút bút chì ẩn ở `:1030`; fix ~3-5 LOC 1 file = đảo vế thành `isAdmin || (!readOnly && isDrafter && isDrafterPhase) || isApproverChoDuyet` (đã land, thấy lại ở `:977`). (ii) **BE lấy nhầm hàng**: `PurchaseEvaluationFeatures.cs:765` `Levels.FirstOrDefault(l => l.Order == curLevelOrder)` — sau Mig 29 tách 1 Level-row PER ApproverUser thì `Order` **trùng nhau** trong cùng Cấp ⇒ EF trả row đầu theo PK; 2 token (admin + nv.test) gọi API ra **CÙNG** `currentLevelOptions` ⇒ handler mù currentUser; fix 1 dòng `&& l.ApproverUserId == currentUser.UserId` + fallback row-đầu cho admin. (iii) **KHÔNG bug**: F1.OneLevel `:285-312` / F1.OneStep `:314-333` / F1.Assignee `:335-360` / F2 `:483-524` / F3 `PurchaseEvaluationDetailFeatures.cs:42-99` / F4 `PurchaseEvaluationFeatures.cs:272-329` đều **giữ Phase=ChoDuyet**; chỉ F1.Drafter `:268-275` đặt `Phase=TraLai=98` — đúng thiết-kế.
- **BÀI-HỌC:** hai lớp cùng một tính-năng hỏng theo **hai kiểu khác nhau** ⇒ đọc verdict lớp này không suy ra lớp kia. Và: refactor cardinality (1-row-per-Cấp → 1-row-per-NV) làm **mọi phép `FirstOrDefault` theo khoá cũ** thành sai âm-thầm — phải rà toàn bộ site tra-cứu cùng khoá, không vá theo từng báo-cáo lỗi.
- **BẤT-NGỜ:** (a) bug BE **có từ ngày deploy Mig 29**, không phải hồi-quy S23 — nó ẩn suốt vì 13 user UAT cũ hầu hết nằm ở row đầu của Cấp và admin tick toàn FALSE ⇒ mọi row hành-xử giống nhau; chỉ lộ khi tick CHỌN LỌC per-NV. (b) route `/inbox` (`InboxPage.tsx`) đẩy sang DetailPage mobile mặc-định `readOnly=false`, chỉ màn Danh sách `?pendingMe=1` desktop 3-panel mới hard-code `readOnly={true}` ⇒ CÙNG phiếu, 2 lối vào, 2 hành-vi. (c) lượt (iii) chốt "code đã đúng" — cái sai là **mô-hình trong đầu người dùng**: chữ "Trả lại" bị gọi lẫn thành "draft", trong khi `TraLai=98` ≠ `DangSoanThao=1`.

**[cao] B1-B — Plan P: Controller record nuốt 3 field, ASP.NET im lặng**
- **VIỆC:** em-main giả-thiết Plan P chỉ đụng BE; investigator đối-chiếu FE × 2 app để xác nhận trước khi sửa.
- **KẾT-LUẬN:** FE `PeWorkflowPanel.tsx:113-124` `api.post(/transitions, body)` gửi ĐỦ (TargetPhase + Decision + Comment + ReturnMode + ReturnTargetUserId + SkipToFinal, object literal không typed-service); BE `PurchaseEvaluationsController.cs:267` record `TransitionPeBody` **chỉ 3 field** ⇒ model-binder **bỏ im lặng** 3 field còn lại. Verdict: Plan P = BE Controller ONLY ~6 LOC, không cần test mới (Mig 28/31 Domain test đã phủ handler).
- **BÀI-HỌC:** "FE gửi rồi" và "BE nhận được" là hai mệnh-đề khác nhau — điểm gãy nằm ở **ranh giới kiểu dữ-liệu** (record thiếu field = mất dữ-liệu không lỗi, không log). Đối-chiếu **đếm field body ↔ đếm field command** là phép thử rẻ.
- **BẤT-NGỜ:** một lượt audit FE thuần đã **chặn được em-main sửa mù xuyên tầng** — giá trị của spawn nằm ở chỗ nó BÁC BỎ giả-thiết, không phải xác nhận nó.

**[vừa] B1-C — Pre-flight Plan K (Mig 31): bản-đồ neo + GAP fe-user + lệch Dev/prod**
- **VIỆC:** rà trạng-thái trước khi chuyển `Users.AllowDrafterSkipToFinal` → `ApprovalWorkflowLevels.AllowApproverSkipToFinal` (đổi cả ngữ-nghĩa: Drafter-Nháp → Approver-ChoDuyet).
- **KẾT-LUẬN:** neo đủ 20+ điểm (`User.cs:38` · `ApprovalWorkflow.cs:86-105` 6 cờ · nhánh Drafter `PurchaseEvaluationWorkflowService.cs:119-161` (:125) · nhánh APPROVE `~:393-525` · `PurchaseEvaluationFeatures.cs:393-402` · `PurchaseEvaluationDtos.cs:86-92` · bundle DTO `:214`/`:217` · `UsersController` `:91-98`/`:105` · `UserFeatures.cs:332` · Designer `:873` nhãn slot + `:853-933` panel 6 checkbox · `UsersPage.tsx:306-318`/`:365-372`/`:181-186` · PeDetailTabs `:287-297` admin / `:294-304` user). Path migration thật = `Persistence/Migrations/` (KHÔNG phải `Migrations/`).
- **BÀI-HỌC:** **GAP fe-user** (không có UsersPage/Designer — quản-trị chỉ ở fe-admin) ⇒ "mirror 2 app" KHÔNG phải luật phổ-quát; phải xác định trước app nào có bề-mặt nào rồi mới khoanh phạm-vi UI.
- **BẤT-NGỜ:** Dev DB có **2 user / 0 flagged**, prod có **33 user / 4 flagged** — cả hai đều KHÔNG khớp con số spec (S22+2) ⇒ 4 user sẽ mất giá-trị khi DROP cột, chấp-nhận được vì ngữ-nghĩa mới làm nó vô-nghĩa.

**[vừa] B1-D — Pre-flight Plan R: xoá dữ-liệu prod, 3 phương-án**
- **VIỆC:** 4 truy-vấn sqlcmd đo trước khi xoá: 35 PE (28 active + 7 soft) · 17 V2 (15 chưa-ghim + 2 ghim) · 4 V1 (2 active + 2 inactive).
- **KẾT-LUẬN:** `PE.ApprovalWorkflowId` FK **Restrict** + `ApprovalWorkflow` kế-thừa `BaseEntity` (KHÔNG soft-delete) ⇒ buộc **hard-DELETE**; `LevelOpinion → ApprovalWorkflowLevel` Restrict chặn cascade; ước ~620 row con (446 PE-children + ~140 V2 + ~37 V1). Chốt Option A (hard-DELETE PE + bỏ V2 chưa-ghim + V1 inactive; **GIỮ** V2 ghim + V1 active).
- **BÀI-HỌC:** tiền-lệ Plan F được nhắc lại đúng lúc — **KHÔNG drop V1 active** vì còn phiếu pin vào, drop là BE crash lúc khởi-động.
- **BẤT-NGỜ:** hạ-tầng ra điều-kiện: SQL Express **không có COMPRESSION**, `RESTORE VERIFYONLY` đòi sysadmin, và index filtered (từ Mig 29) đòi `SET QUOTED_IDENTIFIER ON` mới chạy được sqlcmd.

**[vừa] B1-E — Plan AA: 5Q audit → 7 commit `a1a910f..ee0902a`**
- **VIỆC:** audit 5 câu hỏi mở đường (endpoint filter · menu seed · Designer · sidebar widen · enum) rồi em-main tự làm 4 vòng polish theo UAT.
- **KẾT-LUẬN:** `ApprovalWorkflowsV2Controller.cs:16-19` đã `[Authorize]` trần từ S18 (gotcha #44 vá vĩnh-viễn) ⇒ chỉ cần thêm param filter `IsUserSelectable`; menu seed `DbInitializer.cs:1429-1437` + quyền `:1541-1547` cho 7 role (Accounting KHÔNG có, admin cấp tay); Designer `ApprovalWorkflowsV2Page.tsx` 975 dòng, 13 field DTO / 7 cờ, nhãn VI `:892-948`; sidebar `Layout.tsx:325` (user) + `:218` (admin) `w-60 xl:w-72` → `w-72 xl:w-80` an-toàn @1280px, `w-80 xl:w-96` là ngưỡng rủi-ro; enum `ApprovalWorkflow.cs:45-50` {DuyetNcc=1, DuyetNccPhuongAn=2, Contract=3}. 7 commit: `ee776d5` BE+Layout · `c667802` FE Page · `ac2c859` docs · 4 polish `da218f1`/`4d60598`/`fbbd361`/`ee0902a`.
- **BÀI-HỌC:** **Tailwind JIT đòi chuỗi class ĐẦY ĐỦ trong mảng** — `bg-${color}-100` bị purge ⇒ bảng màu STEP_PALETTE/LEVEL_PALETTE phải liệt nguyên chuỗi (tái dùng được cho mọi cây menu phân-cấp). Bỏ `truncate` KHÔNG đủ để nhãn dài xuống dòng: phải đổi cả cấu-trúc NavLink (flex → block + inline) + `text-[12px] leading-snug`.
- **BẤT-NGỜ:** trí-nhớ dự-án SAI về chính UI của mình — `feedback_responsive_laptop_breakpoint` khai "PE Workspace 3-panel", đo lại là **2-panel** (`[260px_1fr] xl:[320px_1fr]`); và chiến-lược Order=2 cho "Luồng duyệt" buộc phải **UPDATE Order các leaf cũ**, không thể chỉ INSERT-if-not-exists; `Contract=3` khai trong enum nhưng FE chưa nối.

**[vừa] B1-F — 6 phát-hiện rời S22 (hạ-tầng/identity/script)**
- **VIỆC:** phiên em-main solo, investigator ghi lại các va-chạm khi seed 20 user role-based lên prod (13→33 user).
- **KẾT-LUẬN:** mật-khẩu Identity đòi **≥12 ký-tự** (`User@123456` 11 ký-tự FAIL 400, `TestUser@2026` PASS); đổi tên user là **4 trường nguyên-tử** (Email + NormalizedEmail + UserName + NormalizedUserName + FullName — xác nhận **gotcha #38**), sqlcmd cần `SET QUOTED_IDENTIFIER ON`; login trả `accessToken`/`refreshToken`/`user` — **KHÔNG có field `token`** (ví-dụ Bash trong spec dùng `.token` sẽ fail); script PS 5.1 phải ASCII-only (**gotcha #30**).
- **BÀI-HỌC:** khuôn "admin tick opt-in TỪNG slot" được củng-cố lần 2 (Mig 30 F4 lặp lại Mig 29 F1+F3) sau khi anh sửa lưng em-main: mặc-định là **KHÔNG mở rộng quyền**, muốn mở thì tick.
- **BẤT-NGỜ:** Plan F (drop V1) bị **HUỶ ngay ở pre-flight** vì sqlcmd lộ ra Contract còn HOÀN TOÀN V1 (chưa có cột `ApprovalWorkflowId`) + 4 PE V1-only + 19 PE lai V1+V2 ⇒ phạm-vi drop phải tính cả thực-thể liên-đới, không chỉ PE.

**[thấp] B1-G — ảnh-chụp trạng-thái no-spawn**
- **VIỆC:** 3 mốc không spawn: 2026-05-11 khởi-tạo agent · S21 t3-t5 · S22.
- **KẾT-LUẬN:** baseline 2026-05-11 = 44 gotcha + 14 memory + 6 skill + 27 mig + 81 test; S21 = 84 test / 29 mig / 45 gotcha / 19 memory; S22 = 104 test (+20) / 30 mig / 46 gotcha / ~146 endpoint (+3) / 33 user prod.
- **BÀI-HỌC:** entry loại "đếm cumulative" hết hạn nhanh — giá-trị còn lại chỉ là **mốc thời-gian**, không phải con số.
- **BẤT-NGỜ:** khai thẳng "Investigator KHÔNG spawn" mà vẫn ghi entry — nghĩa là diary vai này từng ghi cả **những phiên nó không làm gì**, đây là nguồn nhiễu khi đếm coverage sau này.

### Token-list (verbatim, investigator-codebase/2026-05-q1)

- **gotcha#:** `#45` (bug fix S21 t3) · `#38` (Identity rename atomic 4 field) · `#30` (PS 5.1 ASCII-only script discipline) · `#44` (class-level `[Authorize]` bare fixed permanent từ S18 2026-05-08)
- **surprise (nhãn nguyên-văn):** "Surprise: Inbox `/inbox` route (InboxPage.tsx) navigate sang `/purchase-evaluations/:id` (mobile DetailPage route, default readOnly=false)" · "**Surprise**: 'Trả lại' trong UI memory docs đôi khi gọi 'draft' colloquial — bro confuse 2 khái niệm" · "Surprise: bug PRESENT từ deploy Mig 29 (S21 t5 2026-05-13) — không phải regression S23" · "Surprises: (1) memory responsive breakpoint stale '3-panel' (2) Order strategy shift +1 (3) Contract=3 chưa wire FE" · "**Memory drift confirmed**: … S24 verify ACTUAL **2-panel**" · "**Plan AA color palette success** … Tailwind JIT yêu cầu full class strings array" · "**Drift Dev DB**: Total=2 user … NOT match 33-user prod seed" · "**Prod actual**: Total=33 / Flagged=4 (NOT 2 per S22+2 spec)" · "**GAP fe-user**: KHÔNG có UsersPage + ApprovalWorkflowsV2Page"
- **guard:** `PurchaseEvaluationDraftGuard.EnsureEditableForDetailsAsync` (`PurchaseEvaluationDetailFeatures.cs:42`, 8 callsite) · guard line 485 `ConflictException` non-admin + flag off (F2 skipToFinal) · `ApplyReturnModeAsync` gate per-slot per-mode `PurchaseEvaluationWorkflowService.cs:252-265` (Admin bypass :252) · entry guard `:760-762` (pointer null khi DaDuyet/TraLai/TuChoi) · BE budget-adjust `ConflictException` nếu `Allow=false` · FK **Restrict** `PE.ApprovalWorkflowId` + `LevelOpinion → ApprovalWorkflowLevel` · `SET QUOTED_IDENTIFIER ON` (filtered index Mig 29+) · class-level `[Authorize]` bare (gotcha #44) · Designer KHÔNG có `usePermission`/`PermissionGuard` (route guard là đủ)
- **root-cause:** ① "F4 = BROKEN at readOnly short-circuit" — `canAdjust = !readOnly && (…)` đoản-mạch TRƯỚC khi `isApproverChoDuyet` được xét (`PeDetailTabs.tsx:973`, nút ẩn `:1030`) ② "BE line 765 lookup semantic broken sau Mig 29" — `Level.Order` trùng nhau sau khi tách 1 row/ApproverUser ⇒ `FirstOrDefault(Order==X)` lấy row đầu theo PK (`PurchaseEvaluationFeatures.cs:765`) ③ Plan P — `TransitionPeBody` record 3 field ⇒ **ASP.NET silent DROP** 3 field còn lại (`PurchaseEvaluationsController.cs:267`) ④ Plan M0 — KHÔNG có root-cause code, "DISCONNECT bro mental model vs code reality post-Mig 28/29/30/31"
- **file:line:** `User.cs:38` · `ApprovalWorkflow.cs:86-105` · `ApprovalWorkflow.cs:45-50` · `PurchaseEvaluationWorkflowService.cs:119-161` (:125) · `~:393-525` · `:268-275` · `:285-312` · `:303-310` · `:314-333` · `:335-360` · `:364` · `:441-468` · `:483-524` (:485) · `:252-265` (:252) · `PurchaseEvaluationFeatures.cs:393-402` · `:272-329` (:283-290 / :291-323) · `:735-770` · `:765` · `:281-329` · `PurchaseEvaluationDtos.cs:86-92` · `PurchaseEvaluationDetailBundleDto` `:214` + `:217` · `UsersController` `:91-98` + `:105` · `UserFeatures.cs:332` · `PurchaseEvaluationDetailFeatures.cs:42` + `:42-99` (:49-51 / :54-94) · `PurchaseEvaluationsController.cs:267` · `ApprovalWorkflowsV2Controller.cs:16-19` · `DbInitializer.cs:1429-1437` + `:1541-1547` · `system/ApprovalWorkflowsV2Page.tsx:873` + `:853-933` + `:889-946` + `:892-948` (975 dòng) · `system/UsersPage.tsx:306-318` + `:365-372` + `:181-186` · `PeDetailTabs.tsx:113-118` + `:118` + `:224` + `:245` + `:957-973` + `:973` + `:977` + `:1030` + `:287-297`(admin) / `:294-304`(user) + `:109-115` + `:967-979` · `PeWorkflowPanel.tsx:51` + `:60-68` + `:113-124` + `:331-422` (:343-396, :343/:357/:371/:397) + `:425-442` (:425) · `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:256-261` · `fe-user Layout.tsx:325` + `fe-admin Layout.tsx:218` · `Persistence/Migrations/` (path thật) · mig `20260513160703_AddAllowApproverEditBudgetToLevels`
- **định-danh khác:** commit `a1a910f..ee0902a` (7): `ee776d5` · `c667802` · `ac2c859` · `da218f1` · `4d60598` · `fbbd361` · `ee0902a` · bug UAT prod `409a967` · phiếu `PE/2026/A/026` · WF `QT-DN-V2-001 v12` · user `nv.test@solutions.com.vn` · `TransitionAsync` param `skipToFinal` thứ 8 (position 47:47) · `w-60 xl:w-72` → `w-72 xl:w-80` (ngưỡng rủi-ro `w-80 xl:w-96`) · grid `[260px_1fr] xl:[320px_1fr]` · `STEP_PALETTE`/`LEVEL_PALETTE` 5 màu · token-cost 32k (Pre-A)

---

## investigator-codebase/2026-05-q2 (9.062 B — "Archive Recent Activity Q2 2026-05, S25-S29 verbose audit")

**Đặc-điểm file:** 4 entry verbose, archive 2026-05-26 S32 (MEMORY.md 27.7KB → ~22KB). Đây là file DUY NHẤT batch B có **0 token gotcha#** — mọi phát-hiện đều dạng schema-gap / nghiên-cứu ngoài repo.

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| B2-A | **2 bug UAT "Lịch sử thay đổi" trống — 2 root-cause KHÁC NHAU** | S25 t1 spawn 5Q (Bug1 Budget-Adjust: code ĐÚNG, nghi FE filter · Bug2 Return-mode: handler KHÔNG log) | **cao** |
| B2-B | **Pre-flight Plan B Contract V2 — 5Q + tái-chia 6 chunk** | 2026-05-22 (Q1-Q5 + Bonus re-chunk + 3 surprise) | **cao** |
| B2-C | **Plan AG — PE List cây thư-mục kiểu Outlook** | S26 spawn 5Q (scale · Project entity · 3-panel · GAP shadcn · đặt tên Mig 32) | **vừa** |
| B2-D | **Plan AI — nghiên-cứu phân-phối RAG, 4 case công-nghiệp** | S26 spawn 7Q (Cursor/Cline/Continue/Sourcegraph + multi-tenant + chi-phí + chốt Pattern C) | **vừa** |

> Ghi-chú dedup: 4 entry = 4 chủ-đề rời, KHÔNG có cặp trùng ⇒ dedup ở đây = giữ nguyên 4, chỉ nén câu chữ.

### Bản-nén-nháp 4-field

**[cao] B2-A — "Lịch sử thay đổi" trống: 1 bug hiển-thị + 1 bug thiếu-ghi**
- **VIỆC:** 2 báo lỗi UAT gộp 1 lượt audit 5Q: (1) chỉnh ngân-sách 2 lần → lịch-sử không hiện 2 dòng; (2) trả-lại theo mode "Người chỉ định" → lịch-sử không hiện.
- **KẾT-LUẬN:** Bug 1 **KHÔNG phải lỗi ghi**: `PurchaseEvaluationFeatures.cs:379-387` ĐÃ log changelog (EntityType=Header + Action=Update + diff) ⇒ nghi FE lọc bỏ Header, hoặc DB UAT cũ. Bug 2 **thiếu ghi thật**: `PurchaseEvaluationWorkflowService.cs:215-378 ApplyReturnModeAsync` **ZERO** dòng log — 4 nhánh chỉ đổi con-trỏ rồi trả summary, người gọi `TransitionAsync:100 LogTransitionAsync` chỉ log chuyển-phase, không log tác-dụng-phụ của mode. Query handler `ListPurchaseEvaluationChangelogsQueryHandler:1050-1064` **không lọc gì** ⇒ nếu mất thì mất ở FE. Đường vá: B2 trước (thêm `Changelogs.Add()` mỗi mode, 15-25 LOC 1 file), B1 sau (Option A thêm enum Kind 30 LOC / B mở EntityType 20 LOC / C nới filter FE 10 LOC).
- **BÀI-HỌC:** cùng một triệu-chứng "không thấy lịch-sử" nhưng **một cái là ghi-thiếu, một cái là đọc-thiếu** — phải tách hai trục ghi/đọc trước khi đặt giả-thiết chung; và `EntityType` quá thô (Header/Update dùng chung cho "sửa ngân sách" lẫn "sửa mục 2") nên không phân-biệt được ở tầng hiển-thị.
- **BẤT-NGỜ:** `EntityType.Workflow=5` tồn-tại trong enum nhưng **KHÔNG dòng code nào ghi vào** suốt 4+ migration — schema khai một khả-năng chưa bao giờ dùng (khai-mà-không-nối, dạng ghost-wire).

**[cao] B2-B — Plan B Contract V2: 80% schema dùng lại, chốt cùng-tồn V1+V2**
- **VIỆC:** pre-flight 5Q + tái-chia chunk cho việc nối Contract vào bộ máy duyệt V2 (đang chỉ có PE).
- **KẾT-LUẬN:** `Contract.cs:8-58` 25 field, sẵn V1 (`WorkflowDefinitionId:22`, `CurrentWorkflowStepIndex:39`, `Phase:12`, `RejectedAtStepIndex:40`) **thiếu** `ApprovalWorkflowId` + `CurrentApprovalLevelOrder` ⇒ Mig 32 chỉ ADD COLUMN vì Mig 22 (`20260508053749_AddApprovalWorkflowsV2.cs:14-127`) đã tạo 3 bảng DÙNG CHUNG cho cả PE lẫn Contract; `ContractPhase.cs:14-28` đã có ChoDuyet=10 / TraLai=98 / TuChoi=99 soi gương PE; `ContractWorkflowService.cs:23-219` 220 LOC **chỉ V1**, sinh mã HĐ `:148-155` ở bước cuối → `DaPhatHanh` (terminal KHÁC PE `DaDuyet`). Prod: 7 HĐ **100% pin V1**, 6 cái đang ở giữa quy-trình (Phase 2-8) ⇒ **BẮT BUỘC cùng-tồn, KHÔNG drop V1**. Mẫu clone = `ApproveV2Async` `PurchaseEvaluationWorkflowService.cs:446-634` (189 LOC: match actor `:484-495` · UPSERT opinion `:522-546` · advance `:605-633` · skipToFinal `:561-602`), điểm rẽ nhánh `:167-171`. Chia 6 chunk A1/A2/B/C/D/E theo tay-nghề (cookie-cutter → implementer, xuyên-tầng → em-main).
- **BÀI-HỌC:** khi schema đã tách sẵn dùng-chung, chi-phí thật của việc "nối module thứ 2" nằm ở **Service + terminal-state khác nhau**, không ở migration — 7 cờ `Allow*` trên `ApprovalWorkflowLevels:86-114` Contract **thừa-hưởng miễn phí**.
- **BẤT-NGỜ:** (a) prod có **0 row** `ApplicableType=3` ⇒ nối xong mà không seed thì dropdown rỗng, phải gài seed vào chunk A2 (đúng thứ sau này thành sự-cố Run #231 ở lane cicd); (b) `RejectedAtStepIndex` bên Contract `:40` đã chết mà chưa đánh dấu obsolete, lệch với PE `:50`; (c) 7 `ContractType` đối đầu 1 `ApplicableType=3` chung — có thể phải thêm `ContractTypes int[]` sau.

**[vừa] B2-C — Plan AG: PE List "đám rừng" → cây theo dự-án**
- **VIỆC:** anh gửi ảnh chụp phàn nàn danh sách phẳng; audit 5Q để chọn hướng.
- **KẾT-LUẬN:** `PurchaseEvaluationsListPage.tsx:133` 3-panel `lg:grid-cols-[340px_1fr_360px]`, thân phẳng `<ul><li>` `:199-252`, pageSize 50; `Master/Project.cs:5-14` thiếu MaxLength + thiếu navigation sang PE ⇒ sẵn sàng cho bảng ProjectPackage sau; **fe-user THIẾU Accordion/Collapsible/Tree/Card/Badge** (chỉ có Button/Dialog/Input/Label/Select/Textarea). Chốt Approach C: Phase 1 FE-only ~160 LOC × 2 app, Phase 2 schema hoãn tới sau UAT.
- **BÀI-HỌC:** thiếu component thư-viện KHÔNG chặn được việc — `<details>/<summary>` native + badge `<div>` là đường vòng đã kiểm-chứng ở Plan AA; chọn Phase-1-FE-only giữ được quyền đổi ý về schema.
- **BẤT-NGỜ:** không đo được quy-mô dữ-liệu thật (SSH prod fail auth) nên phải **ước** ~50-200 dự-án × 5-15 phiếu — quyết-định kiến-trúc đứng trên con số ước, có khai rõ.

**[vừa] B2-D — Plan AI: 4 case công-nghiệp cho hạ-tầng RAG 5 dự-án**
- **VIỆC:** nghiên-cứu 7Q cách chia-sẻ RAG cho 5 dự-án cùng máy.
- **KẾT-LUẬN:** 4 case — **Cursor** (Merkle tree + Turbopuffer + tái-dùng 92% similarity, khoá dẫn-xuất từ commit hash) · **Cline Memory Bank** (markdown + JIT + git-native) · **Continue.dev Hub** (YAML slug) · **Sourcegraph Cody** (phản-mẫu: BỎ embedding về Search API + graph IR cho 100K+ repo). 3 khuôn multi-tenant (pool / silo / hybrid base+delta). Chốt **Pattern C — MCP global cấp user, 1 server localhost phục-vụ 5 dự-án**, KHÔNG theo khuôn VPS-team.
- **BÀI-HỌC:** khuôn tốt nhất cho team 5 dev **KHÔNG** phải khuôn tốt nhất cho 1 người 5 dự-án — phải quy-chiếu về đúng hình-thái người dùng trước khi bê kiến-trúc về.
- **BẤT-NGỜ:** Voyage AI cho **200M token/tháng miễn phí**, đủ cho cả 5 dev ⇒ toàn bộ bài toán chi-phí thu về còn tiền VPS ($15/tháng), làm đổ luôn phép so "$20 vs Qdrant cloud $50".

### Token-list (verbatim, investigator-codebase/2026-05-q2)

- **gotcha#:** **0 token** (file không dẫn số gotcha nào — chỉ dẫn chéo "pattern memory: S23 t3 lookup bug")
- **surprise (nhãn nguyên-văn):** "Surprise: `EntityType.Workflow=5` enum value design-only, unused 4+ mig history" · "**Surprise:** Voyage AI **200M tokens/month free tier** cover 5 devs comfortably" · "**Surprises:** (a) ApprovalWorkflows Prod ZERO ApplicableType=3 (b) `RejectedAtStepIndex` Contract line 40 DEPRECATED chưa marked obsolete drift với PE line 50 (c) ContractType 7 variants vs ApprovalWorkflow generic ApplicableType=3" · "Q1-DB state Prod SSH fail (auth), fallback code inspect" (×2 entry) · "**MISSING MaxLength + navigation tới PE**" · "shadcn fe-user component GAP **THIẾU Accordion/Collapsible/Tree/Card/Badge**"
- **guard:** `ConflictException` (Contract V2 chưa có nhánh — nêu dạng thiếu) · nhánh rẽ `if (evaluation.ApprovalWorkflowId is Guid awId) ApproveV2Async else ApproveV1Legacy` (`:167-171`) · "MUST coexist V1+V2, KHÔNG drop V1" (rào tiền-lệ Plan F) · Auth JWT RS256 + role scope + document-level ACL (thiết-kế RAG) · Risk LOW / Risk MEDIUM (ContractType discriminator)
- **root-cause:** ① Bug1 — "FE filter skip Header updates OR Schema gap (Header.Update collision: budget vs section 2 edit)" ② Bug2 — "Handler **intentionally skip logging** (no companion audit table like PurchaseEvaluationApprovals for return history)"
- **file:line:** `PurchaseEvaluationFeatures.cs:379-387` · `PurchaseEvaluationWorkflowService.cs:215-378` (ApplyReturnModeAsync) · `TransitionAsync:100` (LogTransitionAsync) · `PurchaseEvaluationChangelog.cs` (EntityType=Workflow(5)) · `ListPurchaseEvaluationChangelogsQueryHandler:1050-1064` · `Master/Project.cs:5-14` · `PurchaseEvaluationsListPage.tsx:133` + `:199-252` · `Contract.cs:8-58` (:12 Phase, :22 WorkflowDefinitionId, :39 CurrentWorkflowStepIndex, :40 RejectedAtStepIndex) · `ContractPhase.cs:14-28` · `ContractWorkflowService.cs:23-219` (:113-126 match Dept+PositionLevel, :148-155 gen mã HĐ) · `20260508053749_AddApprovalWorkflowsV2.cs:14-127` · `20260508072821_AddApprovalWorkflowIdToPurchaseEvaluation.cs:14-31` · `20260508074937_AddCurrentApprovalLevelOrderToPe.cs:14-17` · `PurchaseEvaluationWorkflowService.cs:446-634` (:484-495, :522-546, :561-602, :605-633) + `:167-171` · `ApprovalWorkflowLevels` entity `:86-114` · PE `RejectedAtStepIndex:50`
- **định-danh khác:** tag schema `[audit, phase-9, contract]` · `source_path: solution_erp/audit/investigator-plan-b-contract-v2-preflight-2026-05-22` · token-cost 28k/30k/40k/25K · prod 35 PE·17 V2·4 V1 (entry q1) vs "7 Contracts 100% V1Pinned" (entry này) · Mig 32 tên đề-xuất `AddProjectPackageTable` (Plan AG) vs Mig 32 thực-dụng cho Contract (Plan B) — **2 nghĩa của "Mig 32" trong CÙNG file**

---

## investigator-codebase/2026-05-q3 (3.089 B — "Archive Q3 2026-05, S34 curate")

**Đặc-điểm file:** 4 mục ngắn, trong đó **2 mục là bản sao/ghi-chú housekeeping**. Mục S27 chứa **flag tự-nghi-ngờ về chính lịch-sử spawn của vai** — đắt nhất file, và là loại token mà harvest-curator phải giữ.

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| B3-A | **S27 — 2 bài: (i) pre-flight hạ-tầng BẮT BUỘC spawn (ii) NGHI-VẤN lịch-sử spawn của chính vai** | 2026-05-22 S27 wrap retrospective (2 đoạn) | **cao** |
| B3-B | **S25 wrap Plan AB — bản rút gọn của entry S25 ở q2 + kết-quả CI** | 2026-05-19 (root-cause 2 bug + 7 commit + run #215 FAIL → #216-#221 PASS + gotcha #48) | **vừa** |
| B3-C | **Housekeeping: bản-ghi phiên curate S29-era** | 2026-05-22 (archive 10 entry → q1, 34.9KB → ~20-22KB) | **thấp** |
| B3-D | **Trùng nguyên-văn: setup baseline 2026-05-11** | y-hệt đoạn mở đầu file q1 | **thấp** |

> Ghi-chú dedup: **B3-D trùng 100% với entry đầu của q1** (cùng 44 gotcha/14 memory/6 skill/27 mig/81 test) — đề-xuất nén còn 1 dòng trỏ, KHÔNG chép lại. B3-B là bản NGẮN của cùng sự-kiện với entry S25 ở q2 (bản q2 dài hơn, có 5Q); **KHÔNG gộp xuyên file** theo lệnh đóng-kín, chỉ FLAG cho lead ở §Ghi-chú liên-file cuối bài.

### Bản-nén-nháp 4-field

**[cao] B3-A — S27: hai bài học, một trong đó là tự nghi-ngờ hồ-sơ của chính mình**
- **VIỆC:** hồi-cố phiên S27 (em-main solo, không spawn) + rà lại các entry S20-S26 từng khai "spawn Investigator".
- **KẾT-LUẬN:** (i) **miss CRITICAL**: nếu có spawn pre-Plan A.3 thì đã bắt được "Qdrant Web UI static thiếu" TRƯỚC khi em-main viết link `localhost:6333/dashboard` vào `rag-dashboard.ps1` — anh phát-hiện qua trình duyệt rồi báo ngược. (ii) Trong phiên S27, spawn agent kiểu `investigator` **NOT FOUND** ⇒ registry chưa từng nạp trong phiên đó; nhưng diary S23 t1 K0 / S25 t1 / S26 Plan AG / Plan AI **đều đã ghi "spawn Investigator" kèm chi-phí 28-40K token**. Hai khả-năng: (a) phiên trước nạp được (bản Claude Code cũ / định-dạng khác / hot-reload sau khi mở UI `/agents`), hoặc (b) em-main quy-sai, thực ra spawn `general-purpose`. Kết: **tạm tin các entry cũ nhưng GẮN CỜ nghi-vấn** cho lần điều-tra sau.
- **BÀI-HỌC:** kiểm-tra phụ-thuộc-ngoài (binary ≠ web UI ở repo khác) phải là **việc của lượt pre-flight**, không phải việc của người dùng cuối; và khi tuyên "X hoạt động" trong tài-liệu thì phải có ai đó đã CHẠM vào X.
- **BẤT-NGỜ:** vai tự phát-hiện **hồ-sơ spawn của chính nó có thể sai** — chi-phí token đã ghi vào diary không chứng-minh được danh-tính agent đã chạy. Đây là ca "diary khai việc mà không khai bằng-chứng công-cụ", đúng loại rủi-ro Fidelity mà H2 phải soi.

**[vừa] B3-B — S25 wrap: chốt root-cause 2 bug + chuỗi CI hồi phục**
- **VIỆC:** khép phiên S25 sau audit ~28K: xác nhận nguyên-nhân 2 bug rồi em-main tự làm 6 kế-hoạch tiếp.
- **KẾT-LUẬN:** Bug 1 = handler ĐÃ log (Header+Update) nhưng **HistoryTab lọc cứng chỉ TraLai** nên loại mất; Bug 2 = `ApplyReturnModeAsync` 4 mode **không** `Changelog.Add()`, chỉ người gọi log chuyển-phase. Đường vá: BE thêm log + FE nới filter + tách badge Decision. 7 commit `e23f51c..506cada`; **run #215 FAIL** (test Plan M vỡ vì SQLite tie-break tái phát) → **#216-#221 PASS liền chuỗi**; **gotcha #48** còn nợ docs.
- **BÀI-HỌC:** "em-main solo từ Plan AC" vì chuỗi lý-luận xuyên tầng — implementer sẽ TỪ CHỐI, nên phân việc phải xét độ-xuyên-tầng trước khi xét khối-lượng.
- **BẤT-NGỜ:** bản này chốt Bug 1 là **lỗi bộ lọc FE** (giả-thiết ở lượt audit) — tức lượt audit trước đó đoán đúng nhưng **chưa cầm được bằng-chứng FE**, phải sang phiên sau mới xác nhận.

**[thấp] B3-C + B3-D — housekeeping & bản sao**
- **VIỆC:** ghi lại phiên curate S29-era + chép lại baseline khởi-tạo agent 2026-05-11.
- **KẾT-LUẬN:** curate: 10 entry S21→S24 chuyển sang `archive/2026-05-q1.md`, KEEP S25/S26/setup, MEMORY 34.9 KB → mục-tiêu 20-22 KB; baseline: 44 gotcha + 14 memory + 6 skill + 27 mig + 81 test, "No investigations performed yet".
- **BÀI-HỌC:** entry housekeeping nên nằm ở header file archive (đúng chỗ), không nên chiếm slot trong dòng thời-gian hoạt-động — chính nó gây trùng khi curate lần sau.
- **BẤT-NGỜ:** baseline 2026-05-11 xuất-hiện **2 lần trong 2 file archive khác nhau** của cùng một vai ⇒ bản sao được nhân lên bởi chính quy-trình curate, không phải bởi việc thật.

### Token-list (verbatim, investigator-codebase/2026-05-q3)

- **gotcha#:** `#48` (SQLite tie-break — "pending docs")
- **surprise (nhãn nguyên-văn):** không có nhãn "Surprise:" — token cùng loại: "Investigator pre-flight CRITICAL miss" · "spawn `investigator` agent type NOT FOUND trong session S27" · "**For now: trust prior memory entries but FLAG uncertainty cho future investigation.**" · "Possibility: (a) … hoặc (b) Em main misattribute - actually spawn `general-purpose` agent default" · "runs #215 FAIL Plan M tests SQLite tie-break re-emerge"
- **guard:** "**pre-flight infrastructure audit MUST spawn Investigator** trước khi em main claim 'X work' trong docs" · "cross-stack reasoning Implementer would REFUSE" · Rule §6.5 (trong header các file chị em; ở file này là "Patterns proven + Active workflow schemas foundation preserved untouched")
- **root-cause:** ① "Bug 1 Budget Adjust Handler ĐÃ log Changelog (Header+Update) nhưng FE HistoryTab filter strict TraLai-only loại" ② "Bug 2 ApplyReturnModeAsync 4 mode KHÔNG add Changelog.Add() — chỉ caller LogTransitionAsync log phase transition" ③ (meta) "registry chưa bao giờ load trong session này"
- **file:line:** **0 token file:line** trong toàn file (chỉ tên file `rag-dashboard.ps1` + URL `localhost:6333/dashboard`)
- **định-danh khác:** commit range `e23f51c..506cada` (7 commit) · CI run `#215` FAIL → `#216`-`#221` PASS · MEMORY 34.9 KB → 20-22 KB · baseline "44 gotchas + 14 memory entries + 6 skills + 27 mig + 81 test pass cumulative" (trùng q1) · token-cost ~28K

---

## investigator-codebase/2026-05-q4 (2.770 B — "Archive Q4 2026-05, S39-S40 era")

**Đặc-điểm file:** 3 bullet cô-đọng (đã ở dạng gần-nén sẵn) + **header mang một sự-kiện integrity**: file này từng được MEMORY.md trỏ tới nhưng **VẮNG trên đĩa**, nội-dung chỉ còn trong git `d2f52ba`, dựng lại ở S69. Đây là ca con-trỏ-chết đã được vá — giữ nguyên, đừng nén mất.

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| B4-A | **META: con-trỏ chết đã phục-hồi (file archive vắng đĩa, nội-dung git-only `d2f52ba`)** | header file (dựng S69) | **cao** |
| B4-B | **S40 STATE GROUNDING — 7 metric, 4/7 lệch docs** | 2026-05-29 | **cao** |
| B4-C | **P11-A WorkflowApps pre-flight — mirror Proposal + 2 mâu-thuẫn spec** | 2026-05-30 | **cao** |
| B4-D | **S39 BVAAU 7-agent extract (port khuôn từ dự-án khác)** | 2026-05-29 | **vừa** |

> Ghi-chú dedup: 3 bullet rời chủ-đề, không trùng nhau; cụm B4-A tách ra từ header vì nó là **sự-kiện về chính hồ-sơ**, không phải nội-dung việc.

### Bản-nén-nháp 4-field

**[cao] B4-A — con-trỏ chết: MEMORY trỏ file không tồn-tại**
- **VIỆC:** S69 phát-hiện `MEMORY.md` dẫn `archive/2026-05-q4.md` (kèm stub curate S40 + footer) mà **đĩa không có file**.
- **KẾT-LUẬN:** nội-dung thực nằm git-only ở `d2f52ba`; file được dựng lại, chứa 3 entry FIFO tháng 5 tràn ra, còn lô S29-S37 **vẫn nằm nguyên trong git `d2f52ba`** chứ chưa kéo về đĩa.
- **BÀI-HỌC:** trỏ-tới ≠ tồn-tại. Chỉ mục trí-nhớ phải được kiểm bằng phép **chạm đĩa**, vì đọc chỉ mục thấy "có mục" là cảm-giác đủ giả.
- **BẤT-NGỜ:** hai phần của cùng một lô archive kết-thúc ở **hai nơi khác nhau** (đĩa + git) mà chỉ mục chỉ khai một — nghĩa là còn một nửa dữ-liệu chỉ tra được nếu biết đúng commit.

**[cao] B4-B — S40: đo lại 7 con số, docs sai 4**
- **VIỆC:** đối-chiếu 7 chỉ-số trạng-thái giữa docs và mã nguồn.
- **KẾT-LUẬN:** khớp: Migrations=**40** (`.../Persistence/Migrations/*.cs`, cuối = `AddAttendances`) · Gotchas=**55** (đếm `### N.`) · git clean. Lệch: **DbSet=77 nhưng bảng SQL=84** (77 + 7 Identity — docs 84 ĐÚNG, phép đếm DbSet sai −7) · Endpoints=**211** (docs ~223) · FE pages **36+29=65** (docs 53, đếm thiếu) · Menu key=**53** hằng (docs 85, đếm thừa). 3 số tin được nhất = mig / gotcha / git.
- **BÀI-HỌC:** **đếm bảng phải đếm `.ToTable()` trong ModelSnapshot, KHÔNG đếm DbSet** — DbSet bỏ sót bảng Identity. Rộng hơn: mỗi chỉ-số cần khai kèm **cách đếm**, vì cùng một cái tên ("số bảng") có nhiều phép đếm cho ra nhiều số.
- **BẤT-NGỜ:** docs SAI theo **cả hai chiều** trong cùng một lượt đo (under-count FE pages 53<65, over-count menu key 85>53) ⇒ không thể suy "docs luôn lạc-hậu theo hướng thiếu".

**[cao] B4-C — P11-A: khuôn có sẵn, nhưng spec tự mâu-thuẫn 2 chỗ**
- **VIỆC:** pre-flight nối 4 module Leave/OT/Travel/Vehicle vào bộ máy duyệt.
- **KẾT-LUẬN:** schema **đã pin sẵn** từ Mig 39 (`Office/{Module}.cs` đều có `ApprovalWorkflowId?` + `CurrentApprovalLevelOrder?` + `WorkflowAppStatus` 5-state khớp ProposalStatus); phần code mới là SKELETON `Application/Office/WorkflowAppsFeatures.cs:11-15` (chỉ Create+List). Mẫu soi gương HOÀN HẢO = `ProposalFeatures.cs:403-486` (làm phẳng `Steps.OrderBy(Order).SelectMany(Levels.OrderBy(Order))` → chỉ số cấp toàn-cục → `allLevels[CurrentApprovalLevelOrder-1]` → khớp actor → UPSERT opinion → tăng con-trỏ/DaDuyet). Còn thiếu: 4 bảng `{Module}LevelOpinion` (Mig 41+), 3 route/controller, 4 seed WF, FE Detail+Opinion.
- **BÀI-HỌC:** khi có module anh em đã chạy thật, **khuôn tốt nhất là đọc thẳng handler của nó** rồi liệt phần thiếu — rẻ hơn thiết-kế lại từ spec.
- **BẤT-NGỜ:** 2 mâu-thuẫn được gắn cờ ngay: (a) `ApprovalWorkflow.cs:72` khai Level **KHÔNG OR-of-N** (1 ApproverUserId/Level) — **NGƯỢC** với trí-nhớ cũ "OR-of-N", đòi verify lại; (b) enum `ApplicableType` **THIẾU Travel** (có Leave=5/OT=6/Vehicle=7/ItTicket=8) và migration `ExtendApplicableTypeForWorkflowApps` có `Up/Down` **RỖNG** (enum-only, không đụng DB).

**[vừa] B4-D — S39: rút khuôn 7-agent từ BVAAU**
- **VIỆC:** đọc 8 file `.claude/agents/` của BVAAU (~22K) để tách trục vai.
- **KẾT-LUẬN:** 4→7 trục research(2)/implement(2)/quality(3); ranh giới: repo interface = domain, EF config = infra, test = test-specialist; cả 7 vai đều có 5 RAG MCP (+`search_code` BM25 +`store_memory` +`list_projects`). SOLUTION_ERP giữ 6 skill và chọn **split backend/frontend** (thay domain/infra) cho hợp 2 FE.
- **BÀI-HỌC:** port khuôn phải soi **hình-thái repo đích** — trục chia của dự-án nguồn không tự đúng ở đích.
- **BẤT-NGỜ:** BVAAU **Phase 0 codebase RỖNG** ⇒ toàn bộ khuôn 7-agent là bản vẽ chưa qua thực-chiến; và "VIPIX guide" được dẫn nhưng **không verify được vì file miss**.

### Token-list (verbatim, investigator-codebase/2026-05-q4)

- **gotcha#:** **0 token dạng `#N`** — chỉ có phép đếm "Gotchas=**55** (`### N.`)"
- **surprise (nhãn ⚠️/đối-lập nguyên-văn):** "⚠️ `ApprovalWorkflow.cs:72` nói Level **KHÔNG OR-of-N** (1 ApproverUserId/Level) — KHÁC memory cũ 'OR-of-N', verify lại" · "⚠️ enum `ApplicableType` THIẾU Travel (có Leave=5/OT=6/Vehicle=7/ItTicket=8); `ExtendApplicableTypeForWorkflowApps` mig empty Up/Down (enum-only)" · "DbSet=77 nhưng **SQL tables=84** … DbSet count sai −7" · "Endpoints=**211** (docs ~223)" · "FE pages … =**65** (docs 53 under-count)" · "Menu keys=**53** const (docs 85 over-count)" · "BVAAU Phase 0 codebase RỖNG → aspirational template chưa battle-test" · "VIPIX guide claim KHÔNG verify được (file miss)" · header: "the file was absent on disk (content was git-only `d2f52ba`)"
- **guard:** "Boundary: repo interface=domain, EF config=infra, test=test-specialist" · "Verbatim — byte-exact, no reflow" (rào chép archive) · "3 số tin cậy nhất = mig/gotcha/git" (rào tin-cậy chỉ-số)
- **root-cause:** "tables phải count ToTable KHÔNG DbSet" (phép đếm sai nguồn ⇒ lệch −7 bảng Identity)
- **file:line:** `Application/Office/WorkflowAppsFeatures.cs:11-15` · `ProposalFeatures.cs:403-486` · `ApprovalWorkflow.cs:72` · `Office/{Module}.cs` (không số dòng) · `.../Persistence/Migrations/*.cs` · `.claude/agents/` (8 file ~22K)
- **định-danh khác:** commit `d2f52ba` (nguồn git-only) · Mig 39 · Mig 41+ (dự-kiến) · mig `AddAttendances` (mới nhất) · mig `ExtendApplicableTypeForWorkflowApps` (Up/Down rỗng) · `WorkflowAppStatus` 5-state ↔ `ProposalStatus` · tag `[cross-project, bvaau, port]` · `[state-grounding, docs-drift, s40]` · `[pre-flight, p11-a, workflowapps]`

---

## reviewer/2026-05-q1 (18.707 B — "Archive Recent Activity Q1 2026-05, S21-S24")

**Đặc-điểm file:** 7 entry, archive 2026-05-22 (MEMORY.md 34.5 KB). Ba entry là **self-review em-main không spawn** (chế-độ UAT), bốn entry là review thật. Xuyên suốt có 1 sợi chỉ: **"Smart Friend guard active"** lặp ở cuối mọi entry — dấu hiệu vai tự nhắc chuẩn không được hạ.

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| B5-A | **Plan K1+K2 — PASS kèm 2 Major: "khuôn sentinel tạm" đẻ endpoint zombie** | S23 t1 spawn | **cao** |
| B5-B | **Plan N→U (8 kế-hoạch self-review) — 3 phản-mẫu, gồm "vá theo báo-cáo lỗi" và vòng lặp re-seed** | S23 t4-t11 | **cao** |
| B5-C | **Plan M — PASS, phạm-vi hẹp có chủ-đích + lệch chữ xuyên module** | S23 t3 spawn | **vừa** |
| B5-D | **Plan AA — pre-commit PASS 0 issue + hậu-nghiệm ROI spawn** | S24 pre-commit spawn + S24 post-wrap (gộp: cùng Plan, phần sau là kết-luận chi-phí của phần trước) | **vừa** |
| B5-E | **Chế-độ UAT không spawn (S21/S22) + 3 phản-mẫu + gotcha #47 treo** | S21 t3-t5 + S22 | **vừa** |

> Ghi-chú dedup: B5-D gộp 2 entry cùng Plan AA (một trước commit, một sau khi anh UAT xong 4 vòng polish) vì kết-luận thật nằm ở chỗ nối hai phần: **spawn 1 lần cho chunk nặng thì lãi, spawn cho polish thì lỗ**.

### Bản-nén-nháp 4-field

**[cao] B5-A — Plan K: PASS nhưng để lại endpoint xác sống**
- **VIỆC:** review đối-kháng trước K3 cho Mig 31 (đổi chỗ + đổi nghĩa cờ skip-to-final) — 11 file BE, +4093/−83 LOC (Designer migration chiếm 3938 dòng), 3 commit `eb106f2..56868bf..db66253..364aef6`.
- **KẾT-LUẬN:** **PASS với 2 Major + 2 Minor**. Wire đúng: nhánh Approver F2 đặt ở `:477` — SAU khi UPSERT opinion `:441-468`, TRƯỚC khi đẩy con-trỏ `:502` ⇒ chữ ký người duyệt được ghi trước khi nhảy terminal; guard `skipToFinal` cho V1 đặt ở **người gọi** (`:147-149` trong TransitionAsync) nên V1 giữ nguyên chữ ký cũ. sqlcmd Dev xác nhận 7 cột `Allow*` + `Users.AllowDrafterSkipToFinal` đã drop (count=0), snapshot regen sạch, `HasDefaultValue(false)` có nối, Mig 31 `Up()` đảo tay ADD→DROP đúng khuôn. Major-1: **endpoint zombie** `PATCH /api/users/{id}/allow-skip-final` còn sống nhưng thân rỗng (`Task.CompletedTask`) — admin tick, BE nuốt im ⇒ đòi chunk K5 dọn. Major-2: comment cũ `ApprovalWorkflow.cs:78` vẫn nói cờ "đã move sang Users…" trong khi `:107-113` là prop mới. Minor: `PurchaseEvaluationFeatures.cs:401` còn chữ "Drafter"; `ApprovalWorkflowConfiguration.cs:22` chưa ghi Mig 31.
- **BÀI-HỌC:** đặt **sentinel tạm kèm lời hứa "chunk sau dọn"** là bẫy — chunk sau đổi phạm-vi thì sentinel hoá xác sống. Muốn dùng thì phải có **chunk dọn được đặt tên trước**, đứng trước cả chunk sửa test.
- **BẤT-NGỜ:** cùng một lượt review vừa xác nhận wire ĐÚNG ở chỗ khó (thứ tự UPSERT-trước-skip) vừa bắt được lỗi ở chỗ dễ (endpoint quên xoá) — rủi-ro không tập trung ở phần phức-tạp mà ở **phần bị coi là đã xong**.

**[cao] B5-B — Plan N→U: 8 kế-hoạch, 3 phản-mẫu đắt**
- **VIỆC:** em-main tự review liên-tiếp 8 kế-hoạch (N sửa lookup per-NV, O cascade 4 site, P Controller body, Q CSS, R+S+T5 xoá prod, T cờ DemoSeed, U sidebar).
- **KẾT-LUẬN:** Plan N vá `PurchaseEvaluationFeatures.cs:765`; Plan O phát-hiện **4 site còn lại cùng khuôn** (`EnsureCanRejectV2Async:201` · `ApplyReturnModeAsync:248` · `EnsureEditableForDetailsAsync:72` · `AdjustBudgetCommandHandler:311`) + 3 test hồi-quy `PurchaseEvaluationPerNvLookupRegressionTests.cs`; Plan P thêm 3 field vào `TransitionPeBody:267` — **đây là gốc của 2 ngày lỗi prod F1+F2**; Plan R+S+T5 xoá ~720 row prod mà BE vẫn khoẻ nhờ giữ V1 active; Plan T thêm cờ `DemoSeed:Disabled` tắt 5 hàm seed demo. Tổng: 111 test (+7 so với 104), 31 mig, 47 gotcha.
- **BÀI-HỌC:** **vá theo báo-cáo lỗi = vá 1/5**; refactor đổi hình-dạng khoá thì phải **liệt kê TOÀN BỘ site tra-cứu cùng khuôn** rồi vá một lượt. Và bản ghi record ở Controller **phải soi gương record Command** — lệch field là mất dữ-liệu câm.
- **BẤT-NGỜ:** dọn prod xong thì **IIS recycle làm seeder chạy lại** ⇒ dữ-liệu demo mọc lại thành vòng lặp; gốc không nằm ở lệnh xoá mà ở chỗ seeder không có công-tắc — Plan T mới thật sự đóng.

**[vừa] B5-C — Plan M: PASS, hẹp có chủ-đích, còn lệch chữ xuyên module**
- **VIỆC:** review trước push 3 commit `c2afef2..508b17a..4dd6f9c` — sửa ca biên "đang ở Bước 1" + đổi nhãn hiển-thị Phase=TraLai.
- **KẾT-LUẬN:** **PASS**. Hot path `Service.cs:287-333`: thay "đặt TraLai + xoá con-trỏ + return" bằng "đặt con-trỏ (0,1) + summary 'không lùi được' → rơi tiếp xuống reset SLA `:364` → **giữ ChoDuyet**"; người gọi `:94-100` truyền Phase mới vào log ⇒ ContextNote khớp assert của test. Drafter mode `:268-275` giữ nguyên nghĩa; Assignee `:335-360` vẫn ném nếu không khớp; F2/F3/F4 KHÔNG đụng. Build 0 err 2 warn (DocxRenderer có sẵn), **test 106/106** (58 Domain + 48 Infra, +2), npm build 2 app sạch TS, không `--no-verify`. 2 test mới phủ đúng 2 ca biên.
- **BÀI-HỌC:** phân-biệt **động-từ hành-động** ("Trả lại" trên nút) với **nhãn kết-quả** ("Cần chỉnh sửa lại" cho Phase=98) — đổi tên phải chọn đúng lớp, đổi nhầm lớp thì UI mâu-thuẫn.
- **BẤT-NGỜ:** phạm-vi hẹp để lại **8 chỗ chữ "Trả lại" chưa đổi** và 2 module anh em (`contracts.ts:29`, `budget.ts:20`) vẫn `98:'Trả lại'` ⇒ nếu deploy chung thì người dùng thấy 3 module gọi khác nhau cho cùng một trạng-thái.

**[vừa] B5-D — Plan AA: PASS sạch + bài toán ROI của việc spawn**
- **VIỆC:** review trước commit chunk A (BE+Layout) + chunk B (FE trang ma-trận, implementer làm), rồi ghi nhận 4 vòng polish sau đó KHÔNG spawn.
- **KẾT-LUẬN:** **PASS 0 critical / 0 major / 0 minor**; wire xuyên suốt Controller `:21-26` → Handler `:114-117` (`request.IsUserSelectable is bool ius → Where(...)`) → FE `:29-33` gọi thật (không mock) → route `App.tsx:38` + `Layout.tsx resolvePath:81-86`; 0 migration (cột có từ Mig 25); seed menu idempotent (PK trên Key ⇒ ToDictionary an toàn); `[Authorize]` trần `:18` **được giữ** (gotcha #44); quyền 7 role qua `SeedPurchaseEvaluationPermissionDefaultsAsync:1573`. Ghi chú rủi-ro thấp: người không phải admin có thể truyền `isUserSelectable=false` để xem workflow chưa ghim — dữ-liệu không nhạy-cảm nên **không chặn commit**. Plan AA tổng ~620 LOC / 7 commit `a1a910f..ee0902a`, spawn 1 lần ~25K.
- **BÀI-HỌC:** **gom lại review một lượt cho chunk nặng (wire BE / migration / xuyên tầng), bỏ qua với việc polish <30 phút (CSS/màu/layout)** — 4 lần spawn polish ≈ 100K token đổi lấy gần như không phát-hiện.
- **BẤT-NGỜ:** "Smart Friend guard" được khai là **vẫn bật** ngay trong lượt PASS-0-issue, kèm 3 ghi-chú rủi-ro treo lại — tức PASS ở đây không có nghĩa "không còn gì", mà là "không có gì đáng chặn"; và bất đối-xứng cấu-trúc giữa 2 app (fe-user có StaticLeaf "Hộp thư", fe-admin không) **được chấp-nhận** thay vì ép mirror.

**[vừa] B5-E — chế-độ UAT không spawn (S21/S22): 3 phản-mẫu + 1 gotcha treo**
- **VIỆC:** 2 phiên em-main tự review theo `feedback_uat_skip_verify` (bỏ `dotnet test` mỗi chunk nhưng vẫn build + npm build 2 app); 11-12 commit cộng dồn.
- **KẾT-LUẬN:** 4 điểm được xác nhận: phân quyền V2 siết theo `actor.UserId` (bỏ mệnh-đề lỏng `|| ApprovalWorkflowId != null`); helper `EnsureCanRejectV2Async` chặn request giả mạo `PATCH /transitions decision=Reject` (**phòng-thủ 2 lớp FE+BE**); handler chỉnh ngân-sách 3 tầng (Drafter Nháp/Trả-lại · Approver ChoDuyet · Admin) rồi siết lại theo cờ `AllowApproverEditBudget`; mật-khẩu ≥12 ký-tự. Trạng-thái: 104 test / 30 mig / 46 gotcha / ~146 endpoint / 33 user.
- **BÀI-HỌC:** 3 phản-mẫu — (a) **mở rộng quyền theo mặc-định** (S22+4) phải sửa thành admin-tick-từng-slot (S22+5); (b) **giả-định field tồn-tại** khi render (`PeDetailBundle.changelogs`) → TS2339, phải xem kiểu trước; (c) script PS 5.1 có dấu tiếng Việt → parser lỗi (**gotcha #30**).
- **BẤT-NGỜ:** vai reviewer tự khai **không spawn** trong 2 phiên liên-tiếp mà vẫn viết entry đầy-đủ, và ghi nhận **CICD Monitor đứng thay vai reviewer** sau deploy; đồng thời phát-hiện **gotcha #47** (`paths-ignore` bỏ sót `agent-memory`) nhưng để **treo chờ anh quyết**, không tự thêm vào docs.

### Token-list (verbatim, reviewer/2026-05-q1)

- **gotcha#:** `#45` (fix + 3 test hồi-quy, test-before §7) · `#44` (class-level `[Authorize]` bare — "protect", nhắc 2 lần) · `#47` ("paths-ignore agent-memory gap … pending bro decide" — **New gotcha discovered**) · `#30` (PS 5.1 Vietnamese diacritics)
- **surprise (nhãn nguyên-văn dạng Anti-pattern/Pattern caught/Discovery):** "**Pattern caught: 'Transient sentinel pattern'** — đặt sentinel + comment commit chunk khác cleanup nhưng chunk đó scope SHIFT → zombie state" · "**Anti-patterns observed:** (a) Default scope expansion mistake S22+4 → S22+5 fix (b) History display field assumption … FAIL TS2339 (c) PS 5.1 Vietnamese diacritics gotcha #30 reinforced" · "**Plan N point 9 chỉ catch 1/5 sites**" · "**Plan O caveat #1 surfaced Plan P pre-existing bug**" · "**DbInitializer auto re-seed** loop — Plan R+S clean → IIS recycle → re-seed loop" · "**Anti-pattern observed S24 polish chunks**: UI/UX iteration thuần CSS/layout KHÔNG cần Reviewer spawn mỗi chunk" · "**Discovery #3 anomaly re-tested S24** … KHÔNG reinforce docs-only anomaly hypothesis" · "**Low note S24**: BE filter `IsUserSelectable=false` non-admin leak workflow chưa ghim" · "Non-admin pass `isUserSelectable=false` BE không block → workflow chưa ghim leak risk — Low severity"
- **guard:** **"Smart Friend guard"** (bật/nhắc ở 6/7 entry; "KHÔNG lower bar") · `EnsureCanRejectV2Async` (BE helper chặn forge Reject, defense-in-depth FE+BE) · entry guard `:75-81` (đòi ChoDuyet mới Reject) · Reject guard `:81` · Admin bypass `:252-265` · anti-fiddle audit (scope drift 0%) · "No `--no-verify` bypass" (×2) · class-level `[Authorize]` bare `:18` giữ nguyên · `HasDefaultValue(false)` EF config · `MenuItemConfiguration HasKey(Key)` PK unique ⇒ ToDictionary an toàn · `PermissionGuard`/`usePermission` KHÔNG có (route guard đủ) · Cat-checklist 5 mục (Wire BE + Schema + Security + Code quality + Test) · `feedback_uat_skip_verify` (rào chế-độ) · `feedback_ef_migration_backfill_reorder` (rào ADD→DROP)
- **root-cause:** ① "Transient sentinel pattern" ⇒ orphan endpoint `PATCH /api/users/{id}/allow-skip-final` silent NoOp (`Task.CompletedTask`) ② "Plan P Controller `TransitionPeBody:267` record +3 fields mirror `TransitionPurchaseEvaluationCommand` schema — **root cause 2 ngày prod bug F1+F2 wire fail**" ③ "DbInitializer auto re-seed loop … Plan T flag fix root cause" ④ "Plan N point 9 chỉ catch 1/5 sites → Plan O cascade fix"
- **file:line:** `ApprovalWorkflow.cs:78` + `:107-113` · `PurchaseEvaluationFeatures.cs:401` · `:765` · `ApprovalWorkflowConfiguration.cs:22` · K2 F2 branch `:477` / UPSERT `:441-468` / advance `:502` / caller guard `:147-149` · test ref `:253` (K7-pending) · `Service.cs:287-333` · `:364` · `:268-275` · `:335-360` · caller `:94-100` · entry guard `:75-81` · `:81` · `:252-265` · `:105-132` · `EnsureCanRejectV2Async:201` · `ApplyReturnModeAsync:248` · `EnsureEditableForDetailsAsync:72` · `AdjustBudgetCommandHandler:311` · `TransitionPeBody:267` · Controller `:21-26` · Handler `:114-117` · `WorkflowMatrixViewPage :29-33` · `App.tsx:38` · `Layout.tsx resolvePath:81-86` · `SeedPurchaseEvaluationPermissionDefaultsAsync:1573` · `[Authorize]` bare `:18` · `PeListPanel.tsx:112` · `PeWorkflowPanel.tsx:270` / `:272` / `:315` / `:342` · `PeDetailTabs.tsx:152` / `:287` · `ApprovalWorkflowsV2Page.tsx:639` · `contracts.ts:29` · `budget.ts:20` · file test `PurchaseEvaluationPerNvLookupRegressionTests.cs`
- **định-danh khác:** commit `eb106f2..56868bf..db66253..364aef6` · `c2afef2..508b17a..4dd6f9c` · `a1a910f..ee0902a` (`da218f1`/`4d60598`/`fbbd361`/`ee0902a`) · Run #210 sha=`ac2c859` (4/4 wire + bundle 2 app rotate) · diff 11 file BE +4093/−83 (Designer 3938 dòng) · test 84 → 104 → **106/106** (58 Domain + 48 Infra) → **111** · mig 29 → 30 → 31 · gotcha 45 → 46 → 47 · ~146 endpoint · 33 user · 2 test mới `ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet` + `ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet` · fe-admin npm build 1926 modules / 740ms / 0 TS err · ~720 row prod bị xoá (Plan R+S+T5) · spawn-cost ~25K/lượt vs ~100K nếu spawn 4 polish

---

## reviewer/2026-05-q2 (3.526 B — "Archive Q2 2026-05, S34 curate")

**Đặc-điểm file:** 3 mục ngắn, mật-độ bài-học cao/byte. Mục S28 là **bài về ranh-giới thẩm-quyền** — sinh ra hẳn một hạng-mục kiểm mới (Cat 6), và tự khai **một luật cũ bị BỎ**.

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| B6-A | **S28 — tự-cấp-phép luật xuyên dự-án ⇒ sinh Cat 6 "authority boundary" + huỷ luật cũ** | 2026-05-22 | **cao** |
| B6-B | **S25 Plan AB — gotcha #48 (SQLite đồng-hồ đóng băng) + bổ checklist Cat 5** | 2026-05-19 | **vừa** |
| B6-C | **S26 Plan AG — 12 phép kiểm đối-kháng PASS + củng-cố luật ROI spawn** | 2026-05-21 | **vừa** |

### Bản-nén-nháp 4-field

**[cao] B6-A — S28: "chú ý X" bị đọc thành "BẮT BUỘC X"**
- **VIỆC:** ghi lại quỹ-đạo phiên S28 dưới góc reviewer (thực tế em-main solo, không có việc review nào).
- **KẾT-LUẬN:** t1 kết-luận ROI của RAG "ngắn hạn không đáng, dài hạn đổi chất" → t2 em-main **tự ban luật xuyên dự-án "ghi RAG mọi tương tác" khi chưa có đồng-ý của anh** → t4 anh bắt lỗi, thu phạm-vi về kỷ-luật nội-bộ SOLUTION_ERP → t5 chốt quản-trị 3 lớp (4 nhóm mặc-định + danh-sách bỏ qua + **tag schema bắt buộc** `[lesson, phase-<N>, <bc>]` với phase ∈ {phase-9, phase-9plus, phase-10} và BC ∈ {contract, pe, budget, workflow, identity, form, infra}). Luật t2 **ABANDONED**.
- **BÀI-HỌC:** thêm **Cat 6 — kiểm ranh-giới thẩm-quyền**: phân-biệt "anh gợi ý phương án X" (tham-khảo) với "anh chốt X" (mệnh-lệnh); gắn cờ mọi thứ dạng "BẮT BUỘC … xuyên dự-án" mà nguồn là em-main tự quyết. 5 nhóm kiểm cũ giữ nguyên, Cat 6 cộng thêm từ S28.
- **BẤT-NGỜ:** sai-phạm không nằm ở kỹ-thuật mà ở **phép đọc ngôn-ngữ**: một chữ "chú ý" biến thành chính sách; và vai reviewer phát-hiện được nó **theo lối hồi-cố**, tức lúc xảy ra thì không có ai chặn.

**[vừa] B6-B — S25 Plan AB: đồng-hồ đóng băng của SQLite**
- **VIỆC:** ghi từ-khoá đúc kết sau đợt vá changelog (`cdfd542` — PE chỉnh ngân-sách + trả-lại Người-chỉ-định có log).
- **KẾT-LUẬN:** **gotcha #48** — nhiều `Changelog.Add()` trong CÙNG một `SaveChangesAsync` nhận **cùng dấu thời-gian** ⇒ `OrderByDescending(CreatedAt).FirstAsync()` cho kết-quả **không tất-định** trên SQLite; kèm rủi-ro lặp lại của chế-độ UAT bỏ `dotnet test` khi refactor BE > 100 LOC.
- **BÀI-HỌC:** bổ vào Cat 5: **bộ lọc trong test phải phân-biệt bằng thứ khác ngoài mốc thời-gian** (EntityType + từ khoá trong Summary).
- **BẤT-NGỜ:** một bài về *test* lại bắt nguồn từ *đặc-tính đồng-hồ của engine DB dùng trong test* — thứ không xuất-hiện ở prod (SQL Server), nên chỉ lộ ra khi test đỏ ngẫu-nhiên.

**[vừa] B6-C — S26 Plan AG: PASS 0 issue, và bài ROI lặp lại**
- **VIỆC:** review trước commit 3 chunk A+B+C (~25K token, 12 phép kiểm đối-kháng).
- **KẾT-LUẬN:** **PASS 0 issue**; commit `0bf6c7e` 2 file +346/−116 LOC, hai bản mirror **SHA256 IDENTICAL** (`21001E90...`); wire = useMemo gom nhóm lồng + `<details>/<summary>` 2 tầng + lưu tập mở/đóng vào localStorage; 0 migration. 5 vòng polish AG2-AG6 sau đó em-main tự verify (SHA256 giống nhau + npm build 2 app + `dotnet test 111/111`), **không spawn lại**.
- **BÀI-HỌC:** củng-cố luật phân-bổ: spawn reviewer cho việc nặng xuyên tầng (~370 LOC + 4 sub-agent phối hợp), em-main tự lo vòng polish 50-100 LOC.
- **BẤT-NGỜ:** phép chứng "2 app giống nhau" được nâng lên **so SHA256 nguyên file** thay vì đọc diff — rẻ và không cãi được; đồng thời cả 6 commit của S26 giữ nguyên baseline 111 test, tức thay đổi UI thuần **không mua thêm rủi-ro hồi-quy**.

### Token-list (verbatim, reviewer/2026-05-q2)

- **gotcha#:** `#48` (SQLite **frozen clock** tie-break — mô-tả đầy đủ: "Multi-Changelog.Add same SaveChangesAsync transaction non-deterministic `OrderByDescending(CreatedAt).FirstAsync()`") · dải nền "Foundation gotchas **#17-#48**" (header)
- **surprise (nhãn nguyên-văn):** "**Rule cũ ABANDONED**: 'RAG ghi mọi tương tác mandatory' S28 t2 over-reach" · "t4 bro caught mistake scope-down về SOLUTION_ERP self-discipline" · "em main t2 implicit interpret 'chú ý X' (bro suggestion) AS 'MANDATORY X' (em policy decision)" · "Reviewer perspective về S28 trajectory (em main solo, **KHÔNG actual review work**)" · "UAT skip `dotnet test` recurring risk khi BE refactor > 100 LOC"
- **guard:** **Smart Friend guard** ("active S28+ cho Plan B Contract V2 wire pre-commit spawn (mandatory heavy diff > 50 LOC cross-stack)") · **Cat 6 — Authority boundary check** (MỚI) · Cat 5 bổ-sung "test filter discriminator beyond timestamp (EntityType + Summary keyword)" · 5-category checklist baseline (Wire BE + Schema + Security + Code quality + Test) UNCHANGED · tag schema **mandatory** `[lesson, phase-<N>, <bc>]` · "Cross-module security validation mirror pattern preserved" (header)
- **root-cause:** "scope creep từ project-local → cross-project KHÔNG bro consent là **authority boundary violation**" ⇐ "implicit consent ('chú ý' / 'có thể') KHÔNG = explicit mandate ('BẮT BUỘC' / 'mandatory')"
- **file:line:** **0 token file:line** (file này chỉ dẫn tên hàm/commit: `ApplyReturnModeAsync`, `cdfd542`)
- **định-danh khác:** commit `0bf6c7e` (2 file +346/−116) · SHA256 mirror `21001E90...` · commit `cdfd542` · `dotnet test 111/111` (baseline 111 preserved, 6 commit S26, 0 prod regression) · spawn ~25K · phase enum {phase-9, phase-9plus, phase-10} · BC enum {contract, pe, budget, workflow, identity, form, infra}

---

## test-specialist/baseline-history (32.069 B — "Baseline count history (verbatim) + aged FIFO")

**Đặc-điểm file:** file LỚN nhất batch B, có frontmatter YAML (`name: baseline-history` · `type: project`), 1 chuỗi số một dòng + **25 entry** newest-first (S155 → S54). Tách khỏi `MEMORY.md` 2026-06-26 vì chỉ mục vượt trần đọc 24KB.
**⚠️ Ràng buộc nén (lệnh lead):** chuỗi số là RUỘT ⇒ **giữ đủ mốc đầu/cuối mỗi giai-đoạn**, chỉ được gom giai-đoạn, KHÔNG được rút gọn thành "254 → 561".

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| TS-A | **CHUỖI SỐ BASELINE (ruột) — chuỗi gốc + hậu-chuỗi rải trong entry + 4 lỗ + 2 mâu-thuẫn nội-tại** | dòng "Count progression" + mọi mốc `X→Y` trong 25 entry | **cao** |
| TS-B | **S155 — 1 FAIL GIỮ ĐỎ = bug prod thật + fixture đầu tiên wire `AuditingInterceptor`** | S155 (+10 → 561) | **cao** |
| TS-C | **S153 — fault-inject máy `mind-check` (KHÔNG phải test C#), bắt 1 GAP + bài fixture-hỏng-cho-xanh** | S153 (18/18, baseline C# không đổi) | **cao** |
| TS-D | **S133 budget-freeze RED→GREEN (INV-A T0 + INV-C T1-T9) + va-chạm sub-file 2 invocation** | S133 INV-A + S133 INV-C (gộp: cùng feature, 1 đỏ 1 xanh) | **cao** |
| TS-E | **S134 — lũy kế TẠM TÍNH: khuôn "2 hàm cùng seed, khớp số CHÍNH XÁC"** | S134 (+8 → 528) | **cao** |
| TS-F | **Cụm SECURITY/authz regression (reflection harness Pattern 10 + contrast chống vacuous)** | S118b (+4→509) · S118 (+4→505) · S92 (+3→434) · S69 Office (+6) · S54 ItTicket (+13) | **vừa** |
| TS-G | **Cụm FINANCIAL / critical-algo (giá chốt, số âm, ngưỡng, multi-winner)** | S117 (+5→500) · S116 (+9→495) · S72 (+28) · S69b (+14) · D2 (+6→419) · S87 (+7) | **cao** |
| TS-H | **Cụm Supplier Excel-import (case-collation + 1 BUG NRE báo-không-sửa)** | D3 (+10→450) · S113 (+19→477) | **vừa** |
| TS-I | **Cụm bug-fix / spec-change / compile-fix arity** | D4 (+8→458) · S97 (+0) · S86 (+3) · S85b (+3) · S85 (+5) · S77c (rewrite 12→11) · S77 (+7) · S76 (+5) · S74 (+5) | **vừa** |
| TS-J | **2 ghi-chú kỹ-thuật lẻ giữ lại (S57bis / S56)** | mục "S57bis / S56 (notes kept)" | **thấp** |

> Ghi-chú dedup: TS-D gộp 2 entry S133 vì là **hai nửa của một chu-trình test-before** (INV-A dựng đỏ, INV-C đẩy xanh) — gộp KHÔNG mất danh-tính vì giữ đủ 2 verdict. TS-F/TS-G/TS-H/TS-I gộp theo **loại rủi-ro** (security / financial / import / bug-fix) chứ không theo thời-gian, mỗi entry trong cụm vẫn giữ 1 dòng riêng có số delta.

### Bản-nén-nháp 4-field

**[cao] TS-A — chuỗi baseline: giữ nguyên mốc, gắn cờ 4 lỗ + 2 mâu-thuẫn**
- **VIỆC:** duy-trì một chuỗi số test-pass xuyên ~20 phiên làm bằng-chứng "không hồi-quy".
- **KẾT-LUẬN (giữ NGUYÊN VĂN mốc):** **254 (S60) → 263 (S61: +22 PeWorkItemBudget, −14 BudgetPolicy Domain drop) → 286 (S67 +23 HRM) → 292 (S69 +6 Office) → 306 (S69b +14 PE) → 334 (S72 +28 Mig54) → 339 (S74 +5 CcmNote) → 344 (S76 +5 PRO-split) → 351 (S77 +7 suggested-price NOTE) → 354 (S77b symmetric, superseded) → 366 (S78-S84, MEMORY chưa ghi từng delta) → 371 (S85 +5) → 374 (S85b +3) → 377-395 (S86-S86d) → 402 (S87 +7) → 419 (D2 +6 Mig58 multi-winner + translation-smoke)**. Hậu-chuỗi nằm rải trong entry (chuỗi một-dòng KHÔNG được cập nhật tiếp): **431→434 (S92 +3)** · **440 (S97, +0 extend in-place)** · **440→450 (D3 +10)** · **450→458 (D4 +8)** · **458→477 (S113 +19)** · **486→495 (S116 +9)** · **495→500 (S117 +5)** · **501→505 (S118 +4)** · **505→509 (S118b +4)** · **→519 (S133, Domain 45 + Infra 474)** · **→528 (S134, 45D+483I)** · **→561 (S155 +10; 560 PASS / 1 FAIL; 45D + 516I)**.
- **BÀI-HỌC:** chuỗi này chỉ có giá-trị khi **mỗi mốc kèm delta + lý-do**; đoạn "366 (S78-S84, MEMORY chưa ghi từng delta)" và "377-395 (S86-S86d)" cho thấy chỗ nào bỏ ghi thì chỗ đó vĩnh-viễn không truy được nữa. Khi nén, mốc đầu/cuối mỗi giai-đoạn là thứ **không được phép mất**.
- **BẤT-NGỜ / CỜ CHO LEAD (4 lỗ + 2 mâu-thuẫn nội-tại, trích nguyên văn):** ① lỗ **419 → 431** (+12 không có mốc) · ② lỗ **434 → 440** (+6) · ③ lỗ **477 → 486** (+9) · ④ lỗ **500 → 501** (+1). Mâu-thuẫn (a): S133 tiêu-đề ghi "**+10 -> 519**" nhưng thân ghi "**+9 test vào PeBudgetFreezeTests**" — *hoà-giải KHẢ DĨ, chưa xác-minh:* T0 của INV-A (+1) + T1-T9 của INV-C (+9) = +10, khớp "464+10". Mâu-thuẫn (b): S153 (2026-07-26) ghi "**baseline 509 KHÔNG đổi**" trong khi S134 (2026-07-17, TRƯỚC đó) đã ghi **528** và S155 (2026-07-27, NGAY SAU) ghi 551→561 ⇒ con số 509 ở S153 **không nằm cùng thang** với hai mốc kẹp nó. **Tôi CHỈ đọc file, KHÔNG chạy `dotnet test`** — đây là mâu-thuẫn *trong verbatim*, không phải phán-quyết về baseline thật.

**[cao] TS-B — S155: giữ nguyên 1 test ĐỎ vì đó là bug prod**
- **VIỆC:** đợt 2 tính-năng "xoá phiếu khi đang duyệt" + màn "Đã xoá" — +10 test `PeDeleteByApproverTests.cs` (T21 lũy-kế 4 hướng · T22/b/c màn Đã xoá · T23/T24/b cờ+lượt · T25 authz reflection · T25b chiều thuận · T26 changelog).
- **KẾT-LUẬN:** **561 = 560 PASS / 1 FAIL** (45 Domain + 516 Infra), 0 hồi-quy (506 Infra cũ nguyên vẹn). **T26 GIỮ ĐỎ** vì là **bug prod thật**: `PeSoftDeleteFeatures.cs` thêm changelog ở `:126` rồi `Remove(pe)` ở `:142`; `CascadeDeleteTiming.Immediate` cascade **ngay tại `Remove`**, thực-thể con đang ở trạng-thái `Added` bị đẩy sang **`Detached`** (không phải `Deleted`) ⇒ **0 row**; `AuditingInterceptor` chạy ở `SavingChanges` = **SAU** điểm mất nên không cứu được. Đo bằng chẩn-đoán `ChangeTracker` (`Added→Detached`, `rowsInDb=0`), KHÔNG suy đoán. Grep cùng lớp: **đây là site DUY NHẤT** có hình "Add con → Remove cha → 1 SaveChanges". Báo lead, KHÔNG tự sửa.
- **BÀI-HỌC:** hạ-tầng mới `SoftDeleteFixture` (~90 LOC, ngay trong file test) là **fixture ĐẦU TIÊN của repo** có wire `AuditingInterceptor` — bắt buộc khi đo `Remove()` nghĩa là xoá-mềm, vì `TestApplicationDbContext` không wire ⇒ mặc-định **xoá cứng** ⇒ màn "Đã xoá" rỗng ⇒ assert đúng ở **CẢ HAI** thế-giới = đúng bẫy vacuous.
- **BẤT-NGỜ:** số học T21 phải seed thêm phiếu hàng-xóm (DaDuyet + TraLai) để hai đại-lượng "**không đổi**" (PrevSelected, PendingSubmitted) khác 0 ở cả hai phía — nếu để 0 thì "bất động" và "rỗng" nhìn giống hệt nhau.

**[cao] TS-C — S153: tiêm lỗi vào máy `mind-check`, và bắt được chính fixture của mình nói dối**
- **VIỆC:** fault-inject #8-#17 + #21 lên `scripts/session_ctx.py mind-check` (KHÔNG phải test C#), mọi đột-biến ở cây tạm qua seam `--root`, repo read-only.
- **KẾT-LUẬN:** **18/18 PASS 0 FAIL**, **baseline C# KHÔNG đổi**. Bắt **1 GAP THẬT**: ở chế-độ closed, **xoá con-trỏ → cờ/exit 0** trong khi **giữ con-trỏ đang-chờ một cách trung thực → trượt/exit 1** ⇒ đường rẻ nhất để xanh là đường SAI (chữ luật cấm, nhưng gradient thưởng). Báo em-main, KHÔNG tự sửa.
- **BÀI-HỌC:** fixture `draft-ts` **MALFORMED vẫn cho exit 0** — `inject_A` neo vào `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find` = −1 ⇒ chèn **GIỮA** heading ⇒ `ts:` không nằm đầu dòng ⇒ máy ĐÚNG mà vẫn xanh. **Fixture-hỏng-cho-xanh trông Y HỆT máy-đúng-cho-xanh** ⇒ phải soi RUỘT fixture (`cat -A` / `repr` từng dòng), không đọc mỗi exit code.
- **BẤT-NGỜ:** 2 kỹ-thuật đem dùng lại — (i) **cặp CÙNG-RUỘT khác đúng 1 biến** (cap=32 vs cap=1 trên cùng tệp = chứng đọc config LIVE; `_end` giả `_pause=9` vs không = chứng nguồn đếm là ĐĨA, dùng `diff` chứng 2 output BYTE-IDENTICAL); (ii) **đo wall-clock phải đi nhánh ĐẮT** — lệnh literal rơi vào nhánh thoát-sớm (repo chưa có `_mind`) là tự đo nhánh rẻ rồi gọi đó là chi-phí thật; đo 55-64ms/lượt so với ước 140-300ms ⇒ **lệch 2.2-5.4×**, trả lead điền lại.

**[cao] TS-D — S133 budget-freeze: dựng ĐỎ rồi đẩy XANH, và 2 lượt cùng vai đè file của nhau**
- **VIỆC:** INV-A dựng test tái-hiện lỗi (test-before) → INV-C viết T1-T9 phủ tính-năng đóng băng ngân-sách phiếu đã duyệt.
- **KẾT-LUẬN:** INV-A **RED thật** `Failed 1/1` — kỳ vọng ProInitial 100tr, thấy 999tr @`PeBudgetFreezeTests.cs:184`; luồng chuẩn = đưa phiếu tới DaDuyet **QUA `svc.TransitionAsync`** (V2 1-Bước-1-Cấp `AllowApproverFinalize=true` + giá chốt), **KHÔNG seed thẳng Phase** (vì sau khi vá thì snapshot chỉ được đặt qua transition); đổi cặp giá trị qua `UpdatePeBudgetPro/Ccm` (2 handler không chặn theo phase = đúng cơ-chế lỗi); **2 sanity-assert chống vacuous** (Phase==DaDuyet + bản ghi live đổi thật) đặt TRƯỚC assert chính. INV-C +9 test (`:426` T1 đủ-11-cột · `:480` T2 CCM-threshold · `:524` T3 all-steps · `:555` T4 V1-legacy · `:638` T5 guard CCM+Admin · `:687` T6 AdjustBudget · `:730` T7 live-vs-frozen · `:783` T8 null-safe · `:821` T9 overwrite+refresh) → lead chạy full exit-0: **Domain 45 + Infra 474 = 519**.
- **BÀI-HỌC:** **INV-C ghi đè `sub-test-specialist-0.md` của INV-A** — cùng vai + cùng chỉ-số task qua 2 lần gọi trong cùng run-folder = **va-chạm tên sub-file**; nội-dung INV-A sống sót nhờ journal + diary. Nhiều lần gọi trong một run **phải có nhãn/chỉ-số phân biệt**.
- **BẤT-NGỜ:** spec ghi **sai tên truy-vấn** — truy-vấn thật là `GetPurchaseEvaluationQuery` (bundle `.BudgetSummary`), spec ghi `GetPurchaseEvaluationDetailQuery`; test-specialist bắt được và lead sửa spec (test soi ra lỗi tài-liệu, không phải lỗi mã).

**[cao] TS-E — S134: hai hàm, một seed, khớp số CHÍNH XÁC**
- **VIỆC:** +8 test `PePendingAccumulationTests.cs` (C1-C8) cho lũy-kế TẠM TÍNH (mirror seed `PeBudgetFreezeTests:277-336`).
- **KẾT-LUẬN:** C1 ChoDuyet+winner → delta2+PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 nháp chỉ-liệt-kê / TraLai delta1 · C4 cửa-sổ `CreatedAt` · C5 delta âm có dấu · **C6 bổ-sung-không-đếm-hai-lần: gọi CẢ `ComputeAsync` + `ComputePendingAsync` trên CÙNG seed, assert SỐ CHÍNH XÁC hai phía** (PrevSelected 90tr/1 bất động ⟂ PendingSelected 50tr/1) · C7 frozen→0/null qua query handler · C8 changelog D4 dương + đối-chứng (lọc `ContextNote.Contains("[S134 pending-prior]")`, **gotcha #48**). Full **528/0** (45D+483I).
- **BÀI-HỌC:** (1) gọi thẳng accumulator thì chỉ cần `curPeId` tổng-hợp + `CreatedAt` muộn, **không cần row phiếu đang xét** (chỉ lọc `Id!=` / `CreatedAt<`); (2) phép chống hồi-quy mạnh nhất cho đại-lượng "bất động" là **kiểu C6 — số chính xác, 2 hàm, 1 seed**; (3) FluentAssertions `ContainInOrder(params T[])` đụng overload với chuỗi `because` khi T=Guid ⇒ bọc `new[]{...}`.
- **BẤT-NGỜ:** helper D4 **không gọi SaveChanges** ⇒ row chỉ thực sự nằm trong DB **sau `TransitionAsync`** — nếu assert sớm sẽ thấy "không có row" và kết luận sai về tính-năng.

**[vừa] TS-F — cụm SECURITY/authz: khuôn reflection + đối-chứng chống vacuous**
- **VIỆC:** 5 đợt khoá quyền bằng test hồi-quy: S118b Reports (+4 → **509**) · S118 Procurement master-access (+4 → **505**) · S92 admin-only revoke (+3 → **434**) · S69 Office golive seed (+6) · S54 ItTicket reassign (+13).
- **KẾT-LUẬN:** S118b mở rộng `Api/AuthorizePolicyRegressionTests.cs` (Pattern 10 reflection, file 15→19 [Fact]): `Dashboard` + `ExportContracts` nay `[Authorize(Policy="Reports.Read")]`, còn `MyDashboard` **cố ý để trần** — T4 assert `(attr?.Policy).Should().BeNull()` để **cấm mọi policy** (thêm policy = khoá luôn dashboard cá-nhân của role không-Admin); csproj đã ref Api từ S45 nên không vướng. S118 `ProcurementMasterAccessSeedTests.cs` 4 [Fact], **T2 ⭐ScopeIsolation** là lõi: DeptManager đứng riêng, pre-grant → revoke → seed ⇒ 9 key VẪN all-false (row còn giữ vết) = cấp quyền cho role này **không rò sang role khác**, kèm **đối-chứng** Procurement thật sự được nâng. S92 `AdminOnlyModulesRevokeTests.cs` 3 test giữ bất-biến "PE KHÔNG bị revoke". S69 Office khác HRM ở chỗ cấp cả Read AND Create trên 16 key. S54 ItTicket = authz theo dữ-liệu (2 phòng ban + ma-trận role/dept).
- **BÀI-HỌC:** khuôn chung = **reflection vào private static + seed FK đúng thứ tự (MenuItem → Role → Permission) + luôn có nhánh đối-chứng dương** để test không "xanh vì chẳng có gì xảy ra"; và authz theo dữ-liệu (handler) khác hẳn authz tĩnh (`[Authorize]` đọc bằng reflection) — hai điểm thực-thi khác nhau, phải test khác nhau.
- **BẤT-NGỜ:** S92 — `OfficeModulePermissionSeedTests` + `HrmProfilePermissionSeedTests` **VẪN xanh dù prod đã TẮT 2 lời gọi cấp quyền**, vì test gọi thẳng method cô-lập qua reflection, không chạy chuỗi `SeedAsync` ⇒ **test xanh không chứng minh đường chạy thật còn sống**; đã thêm một dòng ghi chú đầu mỗi file.

**[cao] TS-G — cụm FINANCIAL / critical-algo: giá chốt, số âm, ngưỡng, đa người thắng**
- **VIỆC:** 6 đợt test cho phần dính tiền: S117 ProMinMax (+5 → **500**) · S116 số âm (+9 → **495**) · S72 Mig 54 (+28 → **334**) · S69b ngưỡng CCM + urgent (+14 → **306**) · D2 multi-winner (+6 → **419**) · S87 CCM budget-period (+7 → **402**).
- **KẾT-LUẬN:** S117 khoá bất-biến `Max != null ⟺ Source=="ProMinMax"` + ca biên **min==max PHẢI OK** (revert `>` thành `>=` là đỏ). S116 đổi guard nộp phiếu từ `Sum()<=0` sang `Count==0` / `All(x==0)` để cho **báo giá ÂM** (phát-sinh-giảm/hoàn tiền) đi qua; ranh-giới quyết-định là ca **net==0 nhưng từng dòng ≠ 0** `[+500k, −500k] → ChoDuyet`, và ngưỡng CCM tính trên **NET** (`[+1.2tỷ, −0.3tỷ]=0.9tỷ` không chạm ngưỡng ⟂ đối-chứng `[+1.2tỷ]` một mình thì chạm). S72 chuyển CCM-finalize từ TỰ-ĐỘNG sang **CHỌN-THAM-GIA** + thứ tự fail-closed (threshold-null → Conflict / sai role → Forbidden / vượt ngưỡng → Conflict). D2 chuyển 1→N hợp-đồng cho N đơn-vị thắng, `GiaTri` per-winner = tổng ThanhTien báo giá của ĐÚNG winner (140/260) ⟂ ca single dùng **legacy** `ThanhTienNganSach` (800).
- **BÀI-HỌC:** với phần tiền, **đối-chứng ngược luôn đi kèm**: mỗi assert "cho qua" phải có một seed "chặn lại" cạnh bên, nếu không thì không phân-biệt được "luật đúng" với "luật không chạy".
- **BẤT-NGỜ:** ① S117 — đổi method prod sang 5 tham số khiến `MethodInfo.Invoke` **không tự bind optional param** ⇒ giữ 4 arg là `TargetParameterCountException` phá 3 test cũ (tự gây, tự tránh). ② S72 — nhánh `isSystem` **KHÔNG với tới được** qua `ApproveV2Async` công-khai (PE không có job SLA) ⇒ phòng-thủ/chết, phải test bằng unit + reflection, và ghi rõ đó là OBSERVATION. ③ D2 — `PurchaseEvaluationQuote.PESupplierId` có **FK CỨNG → Supplier Restrict** (`cfg:135`) nên Guid ngẫu-nhiên làm SQLite ném lỗi 19, phải seed row thật; trong khi `Contract.SupplierId/ProjectId` **không có FK vật lý** — hai quy-ước ngược nhau trong cùng một luồng. ④ S87 — `Changelog.UserId` là `Guid?` **không FK** nên Guid ngẫu-nhiên vô-hại.

**[vừa] TS-H — cụm Supplier import: lỗi so-sánh hoa/thường và một NRE báo-mà-không-sửa**
- **VIỆC:** D3 (+10 → **450**) test-before cho import Excel; S113 (+19 → **477**) test-after cho dedup v2 + guard công-bố.
- **KẾT-LUẬN:** **Lỗi lõi = collation**: dedup nằm ở **SERVICE** với `OrdinalIgnoreCase`, không dựa DB ⇒ fixture SQLite (BINARY) **không bắt** trùng khác-hoa-thường, còn prod SQL Server (CI) thì bắt — nên `truonggiang` vs `TRUONGGIANG` ra Inserted=0/Updated=1; nếu service dùng `Ordinal` thì prod nổ 500 cả lô. Thứ tự ưu-tiên dedup: existing-MST → existing-Code → batch-MST → batch-Code → New, và **MST thắng, không âm thầm ghi đè** (FillNulls bỏ qua Code). Guard công-bố: `PublishSupplierCommand` (KHÔNG đi qua UpdateSupplier — **#73**), chặn Code rỗng, chặn trùng chính-xác.
- **BÀI-HỌC:** khuôn dựng header xlsx **qua reflection từ `ExpectedHeaderTokens`** ⇒ test tự đồng-bộ khi hằng đổi; ClosedXML in-memory gán `.Value = string` **giữ nguyên chữ** (0.105 không bị ép về số ⇒ mã có số 0 đứng đầu vẫn an toàn).
- **BẤT-NGỜ:** S113 tìm ra **BUG thật và KHÔNG sửa**: `NewSupplier` `Code = r.Code!.Trim()` → **NRE khi `row.Code == NULL`** (đường R4 nháp: parser sinh null cho ô rỗng, preview phân loại New không báo lỗi ⇒ tới confirm mới nổ ⇒ **500 cả lô, dbCount=0**), trong khi chuỗi rỗng `""` thì OK; viết **characterization test chốt đúng hành-vi NRE hiện tại** để khi vá null-safe thì test tự lật sang NotThrow. Và D3 khai `ExpectedHeaderTokens` chỉ là **PHỎNG ĐOÁN** ≠ 30 token thật ở dòng 4 ⇒ Case 9 so hai chiều động, hiện `LayoutValid=false`, sẽ tự lật true khi hằng được sửa.

**[vừa] TS-I — cụm bug-fix / spec-change / compile-fix arity**
- **VIỆC:** 9 đợt nhỏ: D4 sign-off A1 (+8 → **458**) · S97 cờ runtime (+0) · S86 loại người tạo khỏi chuông (+3) · S85b giữ workflow khi lưu nháp (+3) · S85 người khai tự gỡ cờ gấp (+5) · S77c đối-xứng → **BẤT** đối-xứng (12→11) · S77 note giá đề-xuất (+7) · S76 tách cột PRO (+5) · S74 CcmNote (+5).
- **KẾT-LUẬN:** D4 đổi `ApplyDrafterBypassOnSubmitAsync` sang A1 — bypass CHỈ các cấp dưới thuần (Order < minOwn), con-trỏ **DỪNG** tại minOwn để người soạn ký TAY; 4 test cũ được **đổi tên** vì tên cũ gây hiểu nhầm. S97 = spec-change: đọc thẳng cờ `EndedByLevelFinalize` thay vì suy từ config, test cũ thêm param + **ca tái-hiện mới** `peFinButWentCeo` (cùng config nhưng cờ FALSE). S86 chứng RED-trên-code-cũ **bằng `git stash`** rồi khôi phục nguyên trạng. S77c là **đảo chiều luật giữa chừng**: SET = đủ role chức-năng / UNSET = role AND DeptManager.
- **BÀI-HỌC:** đổi arity của record command thì **compile-fix là một phần của việc test** (S76 vá 5 call-site + đổi assert `.ProEstimateAmount` → `.ProInitialAmount`; S74 vá 3 call-site thêm `null`) — bỏ qua thì không phải "test đỏ" mà là "không build được".
- **BẤT-NGỜ:** S86 — phép loại-trừ theo **actor** KHÔNG che được bug (vì actor ở Cấp 1 còn người tạo ở Cấp 2) nên test mới thật sự có răng; S85 — nhánh `isDrafter` **ưu tiên và độc lập** với `isProTp`, nên người vừa là Drafter vừa là Procurement-không-DM vẫn gỡ được **cả 2 cờ**; S77c ghi thẳng "test theo CODE" (không đụng prod).

**[thấp] TS-J — 2 ghi-chú kỹ-thuật lẻ**
- **VIỆC:** giữ lại 2 quan-sát không gắn với đợt test nào.
- **KẾT-LUẬN:** S57bis — `NotEmpty()` trên `Guid?` **KHÔNG bắt** `Guid.Empty` (FluentValidation 7.2 coi `default(Guid?)` == null) ⇒ dựa vào FK-guard ở create-handler (phòng-thủ nhiều lớp, không phải bug prod). S56 — `ExecuteUpdateAsync` **đi vòng qua change tracker** ⇒ bản đọc đang tracked bị cũ, phải `.AsNoTracking()` đọc lại; transaction đã ship ở mức `IsolationLevel.Serializable`.
- **BÀI-HỌC:** hai ca đều thuộc loại "**API trông như có kiểm mà không kiểm**" — validator im lặng và lệnh update im lặng; phải biết trước thì mới viết được assert đúng.
- **BẤT-NGỜ:** cả hai ghi-chú **sống sót qua mọi vòng curate** dù không kèm test nào — tức giá-trị của chúng nằm ở chỗ **chặn một giả-định sai**, không ở số lượng test.

### Token-list (verbatim, test-specialist/baseline-history)

- **gotcha#:** `#48` (S134 — lọc `ContextNote.Contains("[S134 pending-prior]")`) · `#73` (S87 "null=clear (pre-seed 500tr, gotcha #73)"; S113 "`PublishSupplierCommand` (KHÔNG UpdateSupplier #73)") · **các ⚠️/"Gotcha" không đánh số:** S117 "⚠️ Gotcha (self-inflicted-avoided)" · S92 "**Gotcha:**" (test xanh dù prod tắt) · S56 "gotcha:" (`ExecuteUpdateAsync`)
- **surprise (nhãn nguyên-văn):** "Surprise: D4 no-SaveChanges-in-helper → row chỉ persist sau TransitionAsync" (S134) · "⚠️ Lesson mới: INV-C **ghi đè** `sub-test-specialist-0.md` của INV-A … sub-file collision" (S133) · "**Bài học mới quan trọng nhất:** fixture `draft-ts` MALFORMED ra exit 0 … **Fixture-hỏng cho-xanh trông Y HỆT máy-đúng cho-xanh**" (S153) · "đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×" (S153) · "**1 FAIL = T26 GIỮ ĐỎ = BUG PROD**" (S155) · "🔴 1 BUG-FOUND (REPORTED em-main, NOT fixed)" (S113) · "**Spec-drift FLAGGED (không phải bug)**: `ExpectedHeaderTokens` = BEST-GUESS ≠ 30 real row-4 token" (D3) · "⚠️**SEED-FK: PurchaseEvaluationQuote HARD FK PESupplierId→Supplier Restrict (cfg:135)**" (D2) · "⚠️OBSERVATION: isSystem KHÔNG reachable qua public ApproveV2Async … defensive/dead" (S72) · "⚠️`ApprovalWorkflowId` FK Restrict → PE seed PHẢI ref WF thật" (S85b) · "⚠️**actor-exclusion KHÔNG che bug**" (S86) · "⚠️Changelog.UserId Guid? no FK User → random-Guid an toàn" (S87) · "⚠️GUARD-FIRST: bypass-test PHẢI dựng PE đủ 4 ĐK" (S60) · "Bắt được lỗi spec: query THẬT = `GetPurchaseEvaluationQuery` … spec ghi nhầm" (S133 INV-A) · "[superseded by S77c for urgent SET/UNSET asymmetry]" (S69b) · "(1 FAIL GIU DO = bug prod)" (tiêu-đề S155) · "**baseline 509 KHÔNG đổi**" (S153 — xem mâu-thuẫn ở TS-A)
- **guard:** `SoftDeleteFixture` (fixture ĐẦU TIÊN wire `AuditingInterceptor`) · "2 sanity-assert chống vacuous (Phase==DaDuyet + liveRec đổi thật) TRƯỚC assert chính" (S133) · "+CONTRAST assert … (chống vacuous-pass)" (S118) · "CONTROL/đối-chứng" ở S116 (`[+1.2tỷ]` alone) · "Forbidden red-able BY-CONTRAST (non-IT vs IT-staff identical-setup)" (S54) · submit-guard `:202-208` (S116) · publish-guard (S113) · "guard R2-revised" (S116) · "wire-bug-guard" (S117) · "fail-closed order (832-851)" (S72) · "role-gate 152-157 TRƯỚC side-effect" (S74) · "GUARD-FIRST" (S60) · `PurchaseEvaluationDraftGuard`-tương-đương: "T5 guard CCM+Admin Conflict + ChoDuyet-allow" (S133 `:638`) · "authz-capability … CanReassign flag + Forbidden/Conflict guard + empty-staff 0-leak + no-mutation" (S54) · "Pattern 10 reflection static `[Authorize]`" (S118b) · "T4 `(attr?.Policy).Should().BeNull()`" (S118b — rào NGƯỢC: cấm thêm policy) · "invariant T6"-tương-đương: "Pe-module-NOT-revoked invariant" (S92) · "REPORT lead, KHÔNG tự sửa" (S155, S153) · "repo read-only … mọi mutation trong cây tạm qua seam `--root`" (S153) · "No prod touch — verify qua stash, restore exact" (S86) · "KHÔNG touch prod" (S113/S116/S117/S118/S118b)
- **root-cause:** ① S155 — "`Remove(pe)` (:142) → `CascadeDeleteTiming.Immediate` cascade **ngay tại Remove**; dependent đang `Added` bị cascade ⇒ **Detached** ⇒ 0 row; `AuditingInterceptor` chạy ở SavingChanges = SAU điểm mất" ② S153 — "`inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find`=-1 ⇒ chèn GIỮA heading ⇒ `ts:` không ở đầu dòng" ③ S113 — "`NewSupplier` `Code=r.Code!.Trim()` → NRE khi `row.Code==NULL`" ④ D3/S113 — "dedup ở SERVICE OrdinalIgnoreCase, KHÔNG dựa DB — SQLite fixture BINARY collation KHÔNG bắt CI-dup, prod SQL Server CI thì có" ⑤ S117 — "optional param KHÔNG auto-bind qua `MethodInfo.Invoke`" ⑥ S92 — "test invoke method-CÔ-LẬP qua reflection … KHÔNG chạy SeedAsync chain"
- **file:line:** `PeSoftDeleteFeatures.cs:126` + `:142` · `PeBudgetFreezeTests.cs:184` · `:426` `:480` `:524` `:555` `:638` `:687` `:730` `:783` `:821` + seed `:277-336` · `CreateContractFromEvaluationFeatures.cs:196` · `PurchaseEvaluationFeatures.cs:285` (UpdateDraft) + `:849-864` (FullAmount) · `PurchaseEvaluationWorkflowService.LogTransitionAsync (~:1083-1135)` · submit-guard `:202-208` · `ApproveV2Async 816-854` (strict-`<` @838) + `832-851` · role-gate `152-157` · Quote FK `cfg:135` · call-site `PeWorkItemBudgetTests 388/407/421` · `PurchaseEvaluationUrgentFeatures.cs:49-55` · `Api/AuthorizePolicyRegressionTests.cs` (15→19 [Fact]) · csproj `line 25` · `scripts/session_ctx.py mind-check`
- **tên file test (định-danh):** `PeDeleteByApproverTests.cs` · `PePendingAccumulationTests.cs` · `PeBudgetFreezeTests.cs` · `SupplierImportV2DedupTests.cs` · `SupplierPublishAndListTests.cs` · `Services/SupplierExcelImportServiceTests.cs` · `ProcurementMasterAccessSeedTests.cs` · `AdminOnlyModulesRevokeTests.cs` · `OfficeModulePermissionSeedTests.cs` · `HrmProfilePermissionSeedTests.cs` · `PeSubmitGuardAndBypassTests.cs` · `PeCcmThresholdFinalizeTests.cs` · `PeApprovedPriceFinalizeTests.cs` · `PeSuggestedPriceSetterAuthzTests.cs` · `PeUrgentToggleAuthzTests.cs` · `PeCcmBudgetPeriodSetterAuthzTests.cs` · `PeApproverNotifyExcludesDrafterTests.cs` · `PeUpdateDraftWorkflowPreserveTests.cs` · `PeMultiWinnerTests` · `PeWorkItemBudgetTests` · `PeFinalizeProjectionTests` · `PeApproverFinalizeTests` · `CreateContractFromEvaluationMultiWinnerTests.cs` · `PeListWinnerNamesProjectionTests.cs` · `ItTicketReassignAuthzTests.cs` · `SqliteDbFixture` / `TestApplicationDbContext` / `IdentityFixture` / `CapturingNotificationService`
- **định-danh run/engine:** `run 2026-07-27-S155-pe-delete-approver · sub-d2-test-4` (diary 13.9KB) · `run 2026-07-26-S153-ctx-adopt` `[engine: opus-5]` · `wf_97d6a582-b7e` `[engine: opus-worker]` (S134) · `wf_75c6bfd3-754` (S133 INV-C, **#53 return-fail lần 3**) · `wf_d9639faa-97a` (S133 INV-A, return sạch) · Mig 54/55/56/57/58/59/60/61/65/66/67/68 được dẫn rải rác

---

## implementer-frontend/2026-H2 (22.215 B — "verbatim L2, dựng @S166 2026-08-01")

**Đặc-điểm file:** **TOÀN BỘ file là nội-dung moved-not-cut** của đợt drain 08-01 (header: "Chuyển verbatim từ `MEMORY.md` khi L1 chạm trần đọc. Con-trỏ 1 dòng ở `MEMORY.md`; Ctrl-F tag để nhảy"), thứ tự mới→cũ, **14 mục**, mỗi mục có **tag Ctrl-F riêng** = danh-tính tra-cứu.
**⚠️ Ràng buộc nén (lệnh lead):** mọi mục moved = **load-bearing**, tag tối-thiểu {cao} cho mục auto-refuse (dòng 78) và **KHÔNG được nén mất danh-tính** ⇒ đề-xuất dưới đây **giữ 1 khối riêng cho từng mục**, tuyệt đối không gộp 2 mục thành 1 dòng, và **giữ nguyên chuỗi tag Ctrl-F** (nếu mất tag thì con-trỏ ở `MEMORY.md` trỏ vào hư-không).

### Cụm-dedup

| # | Cụm | Thành-phần gộp | Tag |
|---|---|---|---|
| FE-A | **S152 R1 — 2 ranh AUTO-REFUSE** `[s152, auto-refuse-trivial-30min, no-lower-bar-smart-friend]` (dòng 78) | 1 mục, **KHÔNG gộp với gì** | **cao** |
| FE-B | **S166 K4b — harness đo nhầm cái render bị React vứt** `[s166, khkk-k4b-group-wire, harness-do-nham-render-bi-vut]` | 1 mục | **cao** |
| FE-C | **S166 K4a — regex 42 leaf + single-source nhãn nhóm** `[s166, khkk-k4a-regex-42-leaf, tien-de-lead-sai-x2]` | 1 mục | **cao** |
| FE-D | **S166 K5 — cây toàn-trình: forward-provision = đổi HÌNH DẠNG, không bịa field** `[s166, khkk-k5-8-sub-folder]` | 1 mục | **cao** |
| FE-E | **S153 T2+T5 — `writeGuard` là 2 ĐIỂM, chỉ `:235` có RĂNG** `[s153, t5-writeguard-2-diem-235-co-rang]` | 1 mục | **cao** |
| FE-F | **S166 K4c — lọc WfView theo nhóm; rỗng-vì-lọc ⟂ rỗng-vì-chưa-có** `[s166, khkk-k4c-wfview-loc-nhom, rong-vi-loc-khac-rong-that]` | 1 mục | **vừa** |
| FE-G | **S166 K6 — thu gọn menu admin: dựng lại cây thật rồi chạy bộ lọc đã ship** `[s166, k6-admin-thu-gon, …]` | 1 mục | **vừa** |
| FE-H | **S164/S165 K2 — tiền-đề lead sai, việc thật ≈ 1/4; `colSpan` lỗi CÂM** `[s164, khkk-nhom-duyet, …]` | 1 mục | **vừa** |
| FE-I | **S161 ARC W1+W2+W3đ1 — containment 2 lớp, types-theo-ENTITY, hợp-đồng phải LEAD định-nghĩa** `[s161, …]` | 1 mục (3 lượt gộp sẵn trong verbatim) | **vừa** |
| FE-J1..J5 | **5 mục micro-lesson FE, mỗi mục GIỮ khối riêng:** S134 slot-prop `sub?: ReactNode` · S133 anchor-by-visible-text · S117 spread-trước-cast · S116 sign-toggle mất-dấu-thầm-lặng · S113 publish-not-update | 5 mục — chỉ **xếp chung nhóm để tiện đọc**, KHÔNG hợp nhất nội-dung | **vừa** (S116: **cao**) |
| FE-M | **[CHỈ-MỤC, không thay thế mục nào] Sợi chỉ "TIỀN-ĐỀ ĐỀ-BÀI/LEAD SAI ⇒ đo đĩa TRƯỚC khi gõ" — xuất-hiện 5 lần liên-tiếp** | K2 · K4a (×2) · K4b (lần 3) · K5 · K6 | **cao** |

> Ghi-chú dedup: file này **gần như không có trùng-lặp để gộp** — mỗi mục là 1 lượt việc riêng có tag riêng. Giá-trị dedup thật nằm ở **FE-M**: cùng một bài học lặp 5 lần trong 3 phiên ⇒ lead có thể viết **1 dòng luật** ở L1 và để 5 mục chi-tiết ở L2, thay vì lặp bài học 5 lần.

### Bản-nén-nháp 4-field

**[cao] FE-A — S152 R1: hai ranh giới auto-refuse (mục dòng 78, giữ nguyên danh-tính)**
- **VIỆC:** ghi chú lưu-giữ do lead seed on-behalf (M9, hàng đợi memory-ops) — chốt 2 ranh giới vai implementer-frontend tự áp cho chính mình.
- **KẾT-LUẬN:** **(a) Việc < 30 phút / vặt** (sửa 1 dòng CSS, chỉnh nhãn) = em-main tự làm còn nhanh hơn chi-phí spawn ⇒ **TỪ CHỐI nhận, đúng vai, ghi vào role-file**; nhận việc vặt là đốt spawn vô ích. **(b) KHÔNG hạ chuẩn kiểu "Smart-Friend nể-nang"**: dù bị giục "làm nhanh" vẫn giữ sàn **mirror-SHA × 2 app + `npm build` × 2 PASS**; thiếu 1 trong 2 thì **trả lại, không ship**.
- **BÀI-HỌC:** hai ranh này bảo vệ hai thứ khác nhau — (a) bảo vệ **ngân-sách token**, (b) bảo vệ **sàn chất-lượng**; và cả hai đều là quyền **tự từ chối** của vai, không cần xin phép từng lần.
- **BẤT-NGỜ:** một ghi-chú "không làm gì cả" lại là mục đáng giữ nhất nhóm — vì nó định-nghĩa **cái vai này KHÔNG nhận**, thứ mà mọi mục còn lại (toàn là việc đã làm) không nói được.

**[cao] FE-B — S166 K4b: phép đo bắt đúng cái render đã bị React vứt đi**
- **VIỆC:** nối `?group=` vào trang danh sách + trang tạo, kèm vá-3/5/6. Artifact `…/sub-implfe-k4b.md`.
- **KẾT-LUẬN:** **BÀI TO NHẤT — suýt kết-luận NGƯỢC**: gọi `setState` **trong lúc render** khiến React **huỷ render đó và render lại với state mới TRƯỚC khi commit**; harness bản 1 chỉ chạy 1 lượt nên đọc ra `phase=3` (giá-trị của render đã bị vứt) — **và control-âm cũng ra 3 ⇒ hai nhánh giống hệt nhau ⇒ phép đo mất khả-năng phân-biệt, trông y như PASS**. Vá = lặp tới khi state đứng yên → A ra `phase=2`, control-âm giữ `3`. Giải bài chính: **override 3 trạng-thái** `chipPhase: T|null|undefined` (`undefined` = "nghe URL" ≠ `null` = "user chọn Tất cả") + reset **trong render theo `navKey`**, KHÔNG dùng `useEffect` (effect vẽ một lượt dữ-liệu cũ rồi mới sửa = nháy).
- **BÀI-HỌC:** mô-phỏng hành-vi framework thì phải mô-phỏng **CẢ vòng đời**, không chỉ một lượt gọi hàm. Và tiền-đề đề-bài sai **lần thứ 3 liên-tiếp**: làm đúng nguyên văn ("derive `phase` từ searchParams thay `useState`") sẽ **giết 6 chip lọc** (`KpiCard onClick setPhase :153`) — hỏng CÂM, build vẫn xanh.
- **BẤT-NGỜ:** BE `/deleted` **thiếu tham số `group`** (`Controller:61-66` so với `List:42`) ⇒ không lọc được ở client (server đếm `total` trên tập chưa lọc ⇒ **số trang nói dối**) ⇒ xử bằng cách **không cho UI tuyên nhóm ở màn đã-xoá** (đổi sai-thầm-lặng thành thiếu-nhìn-thấy-được) + gắn cờ cho lane BE; và staticMap lệch **có chủ đích** (fe-user 6/6 mang `group=1`, fe-admin 5/6 vì WfView admin trỏ Designer).

**[cao] FE-C — S166 K4a: hai tiền-đề lead sai trong CÙNG một đề bài**
- **VIỆC:** nới regex cho 42 leaf KHKK + gom nhãn nhóm về một nguồn. Artifact `…/sub-implfe-k4a.md`.
- **KẾT-LUẬN:** ① đề bài bảo export `KHKK_GROUP_LABELS` **mới**, nhưng đĩa **đã có** `KHKK_APPROVAL_GROUP_LABELS` (`types/khkk.ts:129`, land từ K2, **5 call-site**) ⇒ đẻ map thứ 2 chính là **phá đúng cái acceptance-5 (single-source) mà nó phục vụ**; đo thêm thấy nhãn BE ≡ `` `${n}. ${short}` `` cho **8/8** ⇒ giải bằng **DERIVE `khkkGroupMenuLabel(n)`**, 0 call-site cũ bị đụng (hardcode lần 2 sẽ ra "N2 - 2. …" đánh số kép ở select). ② comment vá-8 bảo "phải nới regex trước vì sắp gỡ `KeHoachKyKet` khỏi ẩn-admin", nhưng `isAdminHidden:194` = `key.startsWith('Ct_')` ⇒ **KHKK chưa bao giờ bị ẩn** ⇒ viết comment theo ĐĨA + gắn cờ "7 nhóm rỗng" (`MenuGroup` chỉ xét `children.length>0`, không xét con có resolve được ⇒ header hiện, thân rỗng).
- **BÀI-HỌC:** **chứng regex bằng `new Function()` chạy CHÍNH khối text đã ship** (`indexOf('const khkkMatch')` → slice → eval), key sinh từ `KhkkGroupNumbers`/`KhkkLeafActions` **đọc thẳng `MenuKeys.cs`** ⇒ 42/42 resolve, 0 drop, **+3 control-âm** (G9→null · action rác→null · `Khkk_List` nhóm-1-không-infix→null vì bị chặn ở staticMap trước). Nhãn cũng kèm control-âm (chèn 'x' vào N4 ⇒ phép so phải FLAG) — **0-hit trần là vô nghĩa**.
- **BẤT-NGỜ:** `Khkk_G1` + 6 leaf **KHÔNG có infix** (S159, cấm đổi kẻo mồ côi permission-row) ⇒ 42 leaf infix chỉ thuộc nhóm **2..8**; giữ `[1-8]` rộng hơn tập seed một nấc là **cố ý**; và Layout 2 app **cố ý KHÔNG mirror** (user +34 dòng regex · admin +13 dòng comment).

**[cao] FE-D — S166 K5: "chuẩn bị sẵn cho sau" nghĩa là đổi hình dạng, không phải bịa field**
- **VIỆC:** mở 8 ngăn nhóm ở GĐ2 của cây toàn-trình. Artifact `…/sub-implfe-k5.md`.
- **KẾT-LUẬN:** đề bài đòi GĐ3/4 đi qua `lines[].contractId ∪ pe.contractId`; đo **4 nơi đều ÂM** (`PeListItem` 0 hit chữ `lines` · `PurchaseEvaluation.cs:31` chỉ có `Guid? ContractId` đơn · `ContractListItem` không có `purchaseEvaluationId` · `Contract.cs` 0 khoá ngược). Bằng-chứng đắt nhất là **BE tự khai**: `CreateContractFromEvaluationFeatures.cs:143-145` `pe.ContractId = contracts[0].Id` + comment "N HĐ liên-danh truy qua changelog" — endpoint TRẢ `contractIds[]` (`PeDetailTabs.tsx:2186`) nhưng **chỉ HĐ đầu được ghi ngược ⇒ HĐ thứ 2 trở đi VÔ HÌNH**. ⇒ chỉ wire vế làm được: `contractIdsOfPackage = new Set<string>()` (hợp) thay vì đọc 1 ô, đánh dấu sẵn chỗ cắm nguồn-2 + trích nguyên văn BE; FLAG 2 đường cho lead.
- **BÀI-HỌC:** `stage.leaves` giữ vai **danh sách PHẲNG canonical (nguồn đếm duy nhất)**, `groups` chỉ **chia lại chính nó** ⇒ bất-biến tổng là chuyện **CẤU TRÚC**, không phải kỷ-luật nhớ-cộng-cho-khớp. Hiện đủ 8 ngăn kể cả rỗng = `.map` trên hằng chứ **không `.filter`**; ngăn "(chưa phân nhóm)" **chỉ mọc khi thật có phiếu lạc** (8 nhóm là khung nghiệp-vụ cố định ⟂ ngăn kia là chỗ chứa bất thường).
- **BẤT-NGỜ:** **CA4 control-âm tiêm lỗi nhân-đôi ⇒ `console.warn` NỔ** — không có CA4 thì "0 cảnh báo" ở CA1-3 vô nghĩa (**im-vì-đúng ⟂ im-vì-câm**); và **2 hash ĐỨNG YÊN** (`PipelineTreePanel` `d70ac315`, `PePipelineStrip` `6fb7337e`) được dùng làm **đối chứng dương** cho phép đo SHA.

**[cao] FE-E — S153 T5: `writeGuard` là 2 điểm, chỉ một điểm có răng**
- **VIỆC:** T2 dựng template `session-mind-template.md` + T5 sửa `hmw.js`. Lead seed on-behalf @closeout M9 (**T2 return mất schema-fail — #53 +1**, phải derive-from-body; template VẪN LAND 8.819 B).
- **KẾT-LUẬN:** `VALID_ROLES` 20→21 (sau T10/T11 = **23**); 🔴 **`writeGuard` nằm ở 2 CHỖ**: `:228` là comment JS (tài-liệu) **và** `:235` là template-literal **ĐI VÀO PROMPT của sub**. Chỉ sửa `:235` ⇒ tài-liệu lệch trong chính file đó; **chỉ sửa `:228` ⇒ prompt runtime KHÔNG đổi ⇒ vai read-only bị hướng-dẫn như vai được ghi ⇒ Bash-write MD ⇒ mojibake G-009**. `:235` = **điểm có RĂNG**. Vùng cấm đo bằng diff: `WIDTH_CAP` 0 hit · `TIER2_EXPECTED_FULL_ID` 0 hit.
- **BÀI-HỌC:** T2 — `ts` sống trong HEADING sau em-dash nên **0 dòng key `ts:`** (không phạm rào-2); placeholder `<k>` trong schema-comment để parser không khớp nhầm; kiểm `_SECRET_RES` để ví-dụ của rào-3 không tự khớp mẫu của chính nó.
- **BẤT-NGỜ:** 2 kẽ báo lead — ① **va tên cùng-vai 2 lần gọi** (chỉ số `i` reset mỗi lần gọi ⇒ đợt 2 suýt đè `sub-implementer-frontend-1.md` của đợt 1) = kẽ CÒN LẠI sau S134; ② `node --check` trên file thô **LUÔN** SyntaxError (top-level await) ⇒ acceptance phải là **parse-sau-khi-bọc + đối chứng HEAD**.

**[vừa] FE-F — S166 K4c: rỗng-vì-lọc phải khác câu với rỗng-vì-chưa-có-gì**
- **VIỆC:** lọc trang WfView theo nhóm (đóng FLAG-3 của K4b: `?group=` chưa có ai tiêu-thụ ⇒ 8 leaf mở ra 8 trang y hệt). fe-user ONLY, 1 file (+68/−5), `git diff -- fe-admin` **RỖNG**.
- **KẾT-LUẬN:** đo trước 4 thứ, không đoán cái nào — ① `AwDefinitionDto.code` là tên field THẬT (`types/approvalWorkflowV2.ts:41`, đang render `:165`) ② trang tải trọn bộ theo type rồi đọc `summary.history` ⇒ **lọc CLIENT-side, KHÔNG thêm trục `group` vào queryKey** (thêm = 8 lần gọi mạng cho cùng payload + 8 ô cache trùng) ③ `DbInitializer.cs:609` `code = $"KHKK-N{n}"` + `:623` `IsUserSelectable = true` ⇒ 8 workflow **thực sự lọt** bộ lọc của trang (seed `false` thì lọc kiểu gì cũng trắng trang) ④ `PageHeader` bản đang import CÓ `actions` (repo có ≥2 bản khác chữ ký — **lặp bài S37**).
- **BÀI-HỌC:** chốt chặn **KÉP** `typeInt === 10 && /^[1-8]$/` — `group` là khái-niệm riêng của KHKK, type khác hoặc rác thì **bỏ qua hoàn toàn** (thà hiện đủ còn hơn lọc theo số vô nghĩa ra trang trắng); regex trên chuỗi bắt được `"03"` mà `Number()` sẽ nuốt; lọc giữ **đủ VERSION cùng code** (nghĩa là "quy trình của nhóm", không phải "1 bản ghi").
- **BẤT-NGỜ:** dùng chung một câu cho hai loại rỗng thì user nhóm N5 tưởng **cả module chưa cấu hình** trong khi 7 nhóm kia đang chạy — cùng họ bài "sai-thầm-lặng → thiếu-nhìn-thấy-được"; chứng 21/21 kèm **2 control-âm** (gỡ vị-ngữ lọc ⇒ ra 8 = phép đo phân-biệt được · helper sai khuôn `KHKK_N3` ⇒ 0 wf, không âm thầm khớp).

**[vừa] FE-G — S166 K6: đếm bằng cách DỰNG LẠI CÂY THẬT rồi chạy chính bộ lọc đã ship**
- **VIỆC:** thu gọn menu admin về đúng vai "nơi cấu-hình". fe-admin ONLY, **1 file** (+52/−19), fe-user 0-diff.
- **KẾT-LUẬN:** spec treo ẩn-số "N = 7 hay 8 tuỳ `AwV2_Contract` có tồn tại"; đo ra **0 HIT toàn repo** ⇒ 2 seed leaf + 8 designer ⇒ chốt **N = 1 Dashboard + 4 System-leaf + 2 AwV2 + 8 designer = 15** (spec **tự quên cộng 8 designer** vào công thức của chính nó). Phép đếm: parse 72 tuple literal từ `DbInitializer` + sinh 128 key từ vòng lặp (hằng đọc thẳng `MenuKeys.cs`) = 200 node → chạy `ADMIN_HIDDEN_KEYS` + `isAdminHidden` + `filterForAdmin` **cắt từ chính file** → **166 lá → 15 lá**. **CONTROL-ÂM:** bỏ `'KeHoachKyKet'` khỏi Set ⇒ 15→**63** (+48 = đúng 8 nhóm × 6 leaf).
- **BÀI-HỌC:** `filterForAdmin` lọc **ĐỆ QUY** ⇒ ẩn 1 ROOT là cả cây con chết theo ⇒ tập chỉ cần liệt 12 ROOT (thêm `Workflows`/`PeWorkflows` vì chúng là CON của `System`); liệt từng leaf = thừa và phải bảo trì mỗi lần BE thêm key. Vá-14 có **2 lần đảo** (S29 ẩn → S57 bỏ ẩn → QĐ7 S164 ẩn lại rộng hơn) ⇒ comment cũ đang nói NGƯỢC luật đang chạy, phải gỡ và viết lại kèm câu "cả hai đều từng đúng, mốc sau thắng mốc trước".
- **BẤT-NGỜ:** **cây dựng lại tự kiểm chính nó** — đối chứng "113 key trong `MenuKeys.All` → 0 key thiếu menu-row (invariant T6)", dựng sai thì chỉ số này vỡ ngay; và **bẫy suýt lẫn**: `resolvePath:173` CÓ nhánh nhận code `'Contract'` nhưng **route-resolver có nhánh ≠ menu-row tồn tại**.

**[vừa] FE-H — S164/S165 K2: tiền-đề lead sai, việc thật chỉ ≈ 1/4 mô tả**
- **VIỆC:** relaunch p2 sau khi p1 chết vì #53 — nhóm duyệt N1-N8 cho KHKK.
- **KẾT-LUẬN:** lead khai CreatePage "mid-wire, 8 khai báo chưa vào JSX" (nghĩa là TS6133 sẽ đỏ); **đo `git diff` trước khi gõ** thì **8/8 ĐÃ có call-site**, Detail cũng đã land ⇒ **việc thật ≈ 1/4 mô tả**, chỉ ListPage còn dở. p2 sửa đúng 3 điểm: badge `N{g}` **nhét vào ô "Mã kế hoạch"** (không đẻ cột — bảng đã cuộn ngang @1366 do cây GĐ 19rem) · thêm cột "Tổng duyệt" (BE trả **0** cho phiếu chưa duyệt ⇒ ép hiển thị `—` kẻo đọc nhầm "đã duyệt 0 đ") · **`colSpan` 7→8 ở hai chỗ** (loading + empty row).
- **BÀI-HỌC:** khuôn "WRITE-lane garble ≠ mất code" (S114) đúng thêm một lần: lead đọc transcript dở-dang nên kết-luận theo **Ý ĐỊNH đang chạy**, không theo đĩa ⇒ **viết lại phần đã đúng chính là phá**. `npm run build` = `tsc -b && vite build` với `noUnusedLocals:true` ⇒ **build LÀ phép thử TS6133 thật**, không cần tin IDE (#68).
- **BẤT-NGỜ:** quên `colSpan` là **lỗi CÂM cho tới lúc bảng rỗng** — thứ chỉ lộ ở trạng-thái ít ai test; và SHA-pair **5/5 IDENTICAL** với Panel không sửa (hash `2B64144EF6E8` BẰNG baseline) làm đối-chứng dương.

**[vừa] FE-I — S161 ARC W1+W2+W3đ1: containment 2 lớp và hợp-đồng types**
- **VIỆC:** 3 lượt FE cho KHKK (Designer type-10 · 3 page + types · panel workflow). **Mọi lượt đều #53, đĩa cứu trọn.**
- **KẾT-LUẬN:** W1 — containment phải **2 LỚP render + payload** (`levelFlagsPayload(e, lock)` ép false ở **2/2** call-site; chỉ dùng `disabled` là **containment GIẢ** vì `copyFromDefinition` tuồn state ra); 2 tiền-đề đề-bài sai tự bẻ (FE không có bảng nhãn — nhãn đến từ BE `applicableTypeLabel`; khuôn type-9 **không tồn tại** ⇒ soi gương `Contract:3`). W2 — types **theo ENTITY** khi BE chạy song song; header types = **HỢP-ĐỒNG 12 endpoint** (reviewer đối chiếu ra **6 chỗ đứt** → lead vá). W3đ1 — `KhkkWorkflowPanel` soi gương PE-panel 1063 dòng theo **bảng GIỮ/BỎ tường minh**; cây workflow THÔ nên **FE tự suy ✓/●/○ từ con-trỏ đôi** (StepIndex 0-based vs `Level.Order` là giá-trị).
- **BÀI-HỌC:** hợp-đồng giữa 2 lane phải do **LEAD định-nghĩa literal** — để mỗi lane tự suy thì đứt ở chỗ không ai nhìn; và "place-3 menuKeys SKIP-có-khai (0 consumer)" cho thấy **bỏ qua có khai** là hành-vi hợp lệ, khác hẳn bỏ sót.
- **BẤT-NGỜ:** W2 khai **thiếu `approverUserId`** trong opinion, tới W3đ1 mới bổ — lỗ hổng hợp-đồng chỉ lộ ra ở lượt tiêu-thụ, không lộ ở lượt định-nghĩa.

**[vừa/cao] FE-J1..J5 — 5 mục micro-lesson (giữ khối riêng từng mục)**
- **VIỆC:** 5 lượt FE 2-app: J1 S134 lũy-kế tạm tính · J2 S133 badge budget-frozen · J3 S117 radio thứ 5 ProMinMax · J4 S116 cho nhập số ÂM (**cao**) · J5 S113 import v2 + publish.
- **KẾT-LUẬN:** **J1** `sub?: ReactNode` cắm vào `BudgetSharedNumCell:1184` + `BudgetColValue:1197` ⇒ chèn sub-dòng amber vào N chỗ render **không nhân bản markup**; gate `showPending = !bs.budgetFrozen && (...)`; hmw đánh chỉ-số sub-file **GLOBAL theo taskList** ⇒ hết va tên khi 2 vai KHÁC nhau cùng một lần gọi. **J2** badge `🔒` ở `PeBudgetSummaryTable :1350-1359`, 🔴 className header **lặp 2 chỗ** (`:1350` budget và `:1931` winners) ⇒ anchor của Edit **phải kèm text nhìn thấy được** ("Tổng hợp ngân sách trình ký") mới unique. **J3** entry mảng có điều-kiện phải **spread TRƯỚC `as T[]`** (né suy-diễn sai); `?? undefined` = cách bỏ key chuẩn của axios; Max **không cần state riêng** (derive từ selectedCandidate) ⇒ 0 chỗ phải reset. **J4** 🔴 2 dialog POST **commit thẳng `form.thanhTien`** (`:2247`/`:2906`, không có nút "Lưu") ⇒ nút đổi dấu **phải ghi giá CÓ DẤU NGAY**, chỉ lật cờ là **MẤT DẤU thầm lặng**; `parseVnd/formatVndInput :51-52` là hàm dùng chung (chia với ngân sách) nên **không sửa**, bọc `Math.abs`; gate đổi thành `!== 0` (không phải `> 0`) và **2 gate FE cùng đổi, không phải 1** (`missingForApproval:212` + `submitChecklist:243` → helper `hasRealSelectedQuote`). **J5** nút "Công bố/Ẩn" gọi `POST /suppliers/{id}/publish` — **KHÔNG qua UpdateSupplier (#73)**; badge dùng full-class-literal (P14).
- **BÀI-HỌC:** ba khuôn tái dùng được: **slot-prop** (J1) để mở rộng cell dùng chung, **anchor theo chữ nhìn thấy** (J2) khi className lặp, **spread trước cast** (J3). Và J4 dạy cái đắt nhất: khi form **không có nút Lưu** thì mọi thay đổi hiển-thị phải commit ngay, nếu không thì UI và payload lệch nhau trong im lặng.
- **BẤT-NGỜ:** J4 — báo giá nằm ở `details[].quotes` **chứ không phải** `suppliers[].quotes` (đi nhầm nhánh là đo nhầm), và công thức cũ SUM `winnerQuoteTotal` chặn oan ca net-zero `+100/−100=0`; J5 — mục "3.d disabled" hoá ra **đã đúng sẵn ⇒ NO change**, kèm một **DEVIATION có khai** ("Tải file mẫu" chỉ đặt trong dialog trong khi parent 2.d có nhắc thêm nút ở page).

**[cao] FE-M — [CHỈ-MỤC] sợi chỉ "tiền-đề sai ⇒ đo đĩa trước khi gõ" ×5**
- **VIỆC:** đánh dấu chỗ lặp để lead viết **1 dòng luật** thay vì 5 lần kể lại.
- **KẾT-LUẬN:** K2 (lead khai mid-wire, thật ra 8/8 đã land — việc thật ≈1/4) · K4a (**2 tiền-đề sai trong 1 đề bài**: "chưa có map" thật ra có 5 call-site · "sắp gỡ ẩn" thật ra chưa từng bị ẩn) · K4b (**lần 3 liên-tiếp**, làm đúng nguyên văn sẽ giết 6 chip) · K5 (đề bài đòi đường dữ-liệu **không tồn tại**, đo 4 nơi đều âm) · K6 (spec **tự quên** 8 designer trong công thức của chính nó).
- **BÀI-HỌC:** luật rút gọn: **đo đĩa trước khi gõ, và khi đĩa cãi đề bài thì đĩa thắng — nhưng phải KHAI ra chỗ cãi** (cả 5 lượt đều khai, nên lead sửa được spec).
- **BẤT-NGỜ:** 5 lần sai đều **không phải lỗi cẩu thả**, mà là lead đọc transcript dở-dang hoặc spec viết trước khi code land ⇒ nguyên-nhân nằm ở **độ trễ giữa lúc viết đề bài và lúc đọc đĩa**, không nằm ở người viết đề bài.

### Token-list (verbatim, implementer-frontend/2026-H2)

- **gotcha#:** `#82` ("**Display-only #82**: route+quyền còn nguyên, deep-link vẫn 200, thu gọn = bớt lối đi menu KHÔNG phải thu hồi quyền") · `#68` ("build LÀ phép thử TS6133 thật, không cần tin IDE (#68)") · `#73` ("`POST /suppliers/{id}/publish` (**KHÔNG qua UpdateSupplier** #73)") · `#53` (×4: "Relaunch p2 sau p1 chết #53" · "mọi lượt #53, đĩa cứu trọn" · "T2 return mất schema-fail (#53 +1)" · hàm ý ở S161) · `G-009` (mojibake — mã governance, không phải gotcha số) · `P14` (badge full-class-literal) · `403 S89` (N9 KHÔNG link)
- **surprise (nhãn 🔴/BÀI/BẪY nguyên-văn):** "🔴 **BÀI TO NHẤT — HARNESS ĐO NHẦM CÁI RENDER BỊ REACT VỨT, suýt kết luận NGƯỢC**" · "**control-âm cũng ra 3 ⇒ 2 nhánh giống nhau = phép đo mất khả năng phân biệt, trông y như PASS**" · "🔴 **Tiền-đề đề-bài sai lần 3 liên tiếp** (S164→K4a→K4b)" · "🔴 **Khuôn S164 'đo đĩa TRƯỚC khi gõ' ăn tiếp 2 lần trong CÙNG 1 đề bài**" · "🔴 **BÀI CHÍNH — tiền-đề lead SAI, đo `git diff` TRƯỚC khi gõ** … Việc thật ≈ **1/4** mô tả … **Viết lại phần đã đúng = phá**" · "🔴 **BÀI: forward-provision đúng nghĩa = đổi HÌNH DẠNG, KHÔNG bịa field**" · "🔴 **Bẫy suýt lẫn: `resolvePath:173` CÓ nhánh nhận code `'Contract'` — nhưng route-resolver có nhánh ≠ menu-row tồn tại**" · "(spec tự quên cộng 8 designer vào công thức của chính nó)" · "🔴 **Rỗng-vì-lọc ⟂ rỗng-vì-chưa-có-gì phải khác câu**" · "🔴 **writeGuard là 2 ĐIỂM không phải 1**" · "🔴 2 dialog POST commit `form.thanhTien` THẲNG … chỉ flip cờ = **MẤT DẤU thầm lặng**" · "🔴 GOTCHA quote ở `details[].quotes` KHÔNG `suppliers[].quotes`" · "🔴 Gotcha: className header LẶP 2 chỗ … Edit anchor PHẢI kèm text visible" · "**2 hash đứng yên = đối chứng dương**" (K5) · "Panel không sửa = đối chứng dương" (K2) · "types `7a99b865` không đổi = đối chứng dương" (K4b) · "**im-vì-đúng ⟂ im-vì-câm**" (CA4) · "**0-hit trần vô nghĩa**" (K4a) · "chỉ `disabled` = containment GIẢ" (W1) · "DEVIATION flagged" (S113) · "3.d disabled ĐÃ đúng SẴN → NO change" (S113) · "**2 kẽ báo lead**" (S153) · "reviewer đối chiếu ra 6 đứt → lead vá" (W2)
- **guard:** `writeGuard` `hmw.js:228` (doc) + **`:235` (prompt runtime — điểm có RĂNG)** · containment **2-LỚP** `levelFlagsPayload(e, lock)` ép false 2/2 call-site · chốt chặn **KÉP** `typeInt === 10 && /^[1-8]$/` · `ADMIN_HIDDEN_KEYS` + `isAdminHidden:194` (`key.startsWith('Ct_')`) + `filterForAdmin` (lọc ĐỆ QUY) · `noUnusedLocals:true` (⇒ build = phép thử TS6133) · gate `showPending = !bs.budgetFrozen && (...)` · gate `!== 0` (thay `> 0`) + 2 gate `missingForApproval:212` / `submitChecklist:243` → `hasRealSelectedQuote` · `preventDefault` + `stopPropagation` trong `<summary>` · `TRANSIENT_QUERY_KEYS:311` (group/filter/view **∉** ⇒ giữ active-state) · `MenuGroup` chỉ xét `children.length>0` · **CONTROL-ÂM** (7 ca: K4a ×3 + nhãn ×1 · K4c ×2 · K5 CA4 · K6 15→63 · K4b control-âm giữ 3) · **SHA-pair ×2 app** + `npm build ×2 PASS` (sàn tự áp, S152) · "REFUSE việc <30 min/trivial" (rào nhận việc) · `_SECRET_RES` (rào-3 tự-không-khớp-mẫu) · placeholder `<k>` chống parser false-match · "0 dòng key `ts:`" (không phạm rào-2) · `WIDTH_CAP` 0 hit + `TIER2_EXPECTED_FULL_ID` 0 hit (vùng CẤM đo bằng diff)
- **root-cause:** ① K4b — "`setState` gọi **trong lúc render** ⇒ React huỷ render đó + render lại ngay với state mới TRƯỚC khi commit; harness bản-1 chỉ chạy 1 pass" ② K5 — "`pe.ContractId = contracts[0].Id` … chỉ HĐ ĐẦU được ghi ngược ⇒ HĐ 2+ vô hình" (`CreateContractFromEvaluationFeatures.cs:143-145`) ③ S116 — "QuoteDialog `setForm({thanhTien})` thay CẢ object → nuốt neg" + "2 dialog POST commit `form.thanhTien` THẲNG (:2247/:2906, no nút 'Lưu')" ④ S153 — "chỉ sửa `:228` ⇒ **prompt runtime KHÔNG đổi** ⇒ vai read-only bị hướng-dẫn như write-sub ⇒ Bash-write MD ⇒ mojibake G-009" ⑤ K2 — "lead đọc transcript dở-dang → kết luận theo Ý ĐỊNH đang chạy, không theo đĩa" ⑥ K4b — BE `/deleted` thiếu `group` ⇒ "server đếm `total` trên tập chưa lọc → số trang nói dối" ⑦ S153 — "index `i` reset mỗi invocation ⇒ đợt-2 suýt đè `sub-implementer-frontend-1.md` của đợt-1" ⑧ S153 — "`node --check` file thô **luôn** SyntaxError (top-level await)"
- **file:line:** `resolvePath:173` · `types/approvalWorkflowV2.ts:41` (+ render `:165`) · `DbInitializer.cs:609` + `:623` · `PeDetailTabs.tsx:2186` · `CreateContractFromEvaluationFeatures.cs:143-145` · `PurchaseEvaluation.cs:31` · `KpiCard onClick setPhase :153` · `Controller:61-66` vs `List:42` · `types/khkk.ts:129` + comment `:125-127` · `isAdminHidden:194` · `TRANSIENT_QUERY_KEYS:311` · `hmw.js:228` + `:235` · `BudgetSharedNumCell:1184` + `BudgetColValue:1197` · types `fe-user :333` / `fe-admin :331` · `PeBudgetSummaryTable :1350-1359` + `:1931` · `PeDetailBundle L433` (anchor admin `:488` / user `:490`) · `AddSupplier :2379` + `QuoteDialog :2948` · POST `:2247` / `:2906` · `parseVnd/formatVndInput :51-52` · `missingForApproval:212` · `submitChecklist:243` · `MenuKeys.cs` (`MenuKeys.All` 113 key) · `Contract.cs` (0 khoá ngược) · `PeListItem` / `ContractListItem` (0 hit)
- **SHA/hash (đối chứng đo):** `usePipelineStages 7144eeb7→edff9931` · `PipelineStageFolders d449cc8d→bed52f39` · `PipelineTreePanel d70ac315` KHÔNG ĐỔI · `PePipelineStrip 6fb7337e` KHÔNG ĐỔI · List `80ebd79d→fc3944df` · Create `677f4ab8→545b4c2f` · types `7a99b865` không đổi · types `eba649cc→7a99b865` · `5534addb→d28eaa16` · `a83a58e1→bebc0ada` · `011ee8cf` · `617b0aa0` · `3e16dba5` · `78403bd8` · `9947b121` · `2B64144EF6E8` (baseline Panel) · build: `index-B0OrpamE` 1961mod 620ms · `index-DBtbmn5s` 1950mod 531ms · `index-CZUcTUUg` 19.8s / `index-BZWvx0Y8` 1961mod 10.9s · `index-C4F7kHKT` 3.6s / `index-kv90BBYQ` 0.9s · `index-B2v0kn5F` 1950mod 20.8s / `index-84zDBHE-` 1961mod 8.2s
- **định-danh run/artifact:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k6.md` · `…/sub-implfe-k4c.md` · `…/sub-implfe-k5.md` · `…/sub-implfe-k4b.md` · `…/sub-implfe-k4a.md` · `…/sub-implfe-k2.md` (§1-§5) · `runs/2026-07-29-S161-khkk-*/` · `.claude/templates/session-mind-template.md` (8.819 B) · `wf_58302bd5-b9f INV-2.B` (S134, engine opus-worker) · chứng: **21/21** (K4c) · **15/15** (K5) · **42/42 resolve + 3 control-âm** (K4a) · `VALID_ROLES` 20→21→**23**

---

## Ghi-chú liên-file (FLAG-only cho lead — KHÔNG gộp, giữ nguyên tắc đóng-kín từng file)

> Phần này KHÔNG phải bản nén. Đây là các quan-hệ **giữa** file mà lead cần biết trước khi hợp nhất, để không đếm 2 lần hoặc nén mất một bên.

1. **Bản sao nguyên-văn cùng vai:** `investigator-codebase/2026-05-q1` (mục "2026-05-11 (setup)") và `…/2026-05-q3` (mục "2026-05-11 — Setup baseline") có nội-dung **trùng** ("44 gotchas + 14 memory entries + 6 skills + 27 mig + 81 test pass cumulative … Awaiting first SendMessage"). Đề-xuất: giữ 1 bản, bản kia thành con-trỏ.
2. **Cùng sự-kiện, 2 độ dài:** audit 2 bug changelog S25 có bản DÀI (q2, 5Q đầy đủ) và bản NGẮN (q3, wrap + kết quả CI `#215` FAIL → `#216-#221` PASS). Nén mất bản dài thì mất file:line; nén mất bản ngắn thì mất kết-cục CI.
3. **Cùng sự-kiện, 2 VAI (rủi-ro đếm đôi khi hợp nhất):** ① bug lookup per-NV — `investigator-q1` B1-A (chẩn-đoán `:765`) ↔ `reviewer-q1` B5-B (Plan N vá 1/5 site → Plan O cascade 4 site). ② Plan P — `investigator-q1` B1-B (phát-hiện body 3 field) ↔ `reviewer-q1` B5-B ("root cause 2 ngày prod bug F1+F2"). ③ Plan AA — `investigator-q1` B1-E ↔ `reviewer-q1` B5-D. Đề-xuất: giữ **cả hai** nhưng ghi rõ vai, vì góc nhìn khác nhau (chẩn-đoán ⟂ nghiệm-thu).
4. **Một tên, hai nghĩa:** trong `investigator-q2`, "**Mig 32**" xuất-hiện với 2 nghĩa (Plan AG đề-xuất `AddProjectPackageTable` · Plan B thực-tế `AddApprovalWorkflowToContract`). Nén ẩu sẽ tạo mâu-thuẫn giả.
5. **Một gotcha, hai cách chú-giải:** `#48` được chú "SQLite frozen clock tie-break" ở `reviewer-q2` + `investigator-q3` + `test-specialist` (dùng làm lý-do lọc theo `ContextNote`), nhưng ở batch A (`sub-harvest-curator-sleep-batchA-S170.md:43`) `#48` được chú là "refactor >100 LOC Service chạm changelog/audit". Hai chú-giải nằm cạnh nhau trong cùng dòng từ-khoá gốc của `reviewer-q2` — **đề-nghị lead chốt 1 chú-giải canonical** khi hợp nhất (đây là quan-sát, KHÔNG phải phán một bên sai).
6. **3 mục liên-quan trực-tiếp tới sức-khoẻ HARVEST (H2 lưu ý riêng):** ① `investigator-q4` header — MEMORY trỏ file **vắng đĩa**, nội-dung git-only `d2f52ba`, và **lô S29-S37 tới nay vẫn khai là chỉ nằm trong git** (nếu lead muốn kéo về đĩa thì đây là đầu mối; tôi **chưa kiểm** trạng-thái hiện tại). ② `test-specialist` S133 — **2 lần gọi cùng vai trong 1 run-folder ghi đè sub-file của nhau**, nội-dung sống nhờ journal + diary (rủi-ro mất nguyên-liệu harvest tại nguồn). ③ `investigator-q3` S27 — vai **tự nghi-ngờ lịch-sử spawn của chính mình** ("agent type NOT FOUND", "trust prior memory entries but FLAG uncertainty"); đây là token loại Fidelity, **giữ nguyên, không tự phán**.
7. **Ràng buộc nén đã tuân:** `test-specialist` giữ **đủ 16 mốc chuỗi gốc + 12 mốc hậu-chuỗi** (xem TS-A) và gắn cờ **4 lỗ + 2 mâu-thuẫn nội-tại**; `implementer-frontend` giữ **14/14 mục moved-not-cut ở khối riêng, kèm nguyên chuỗi tag Ctrl-F**, mục auto-refuse (dòng 78) tag **cao**.

---

## Tự-kiểm (harvest-curator, propose-only)

- **Phạm-vi ghi:** DUY NHẤT file này. 0 chạm `.claude/agent-memory/**`, 0 `store_memory`, 0 sửa canonical. Viết bằng Write/Edit tool (KHÔNG Bash-append) theo rào chống mojibake của chính vai.
- **Anti-#53:** file được bồi **5 lượt ghi tăng dần** (header+§1 → §2-3 → §4-5 → §6-7 → §8+kết), mỗi lượt tự đứng được nếu lượt sau chết.
- **Tự bắt 1 lỗi trong lúc làm:** lượt ghi §1 lỡ kéo `:379-387` (thuộc q2) vào token-list của q1 ⇒ đã gỡ ngay bằng Edit — đúng loại **cross-file bleed** mà lệnh đóng-kín nhắm chặn.
- **Giới hạn hiệu-lực:** mọi con số/verdict ở trên là **trích từ verbatim**, KHÔNG phải phép đo mới của tôi. Cụ-thể: tôi **không chạy `dotnet test`**, **không chạy `git log/show`** để xác-minh commit, **không mở** các artifact `runs/…/sub-implfe-*.md` được dẫn. Mâu-thuẫn nêu ở TS-A là mâu-thuẫn **nội-tại của văn bản**.
- **Trạng-thái:** Phase 1 (GATHER + DEDUP) — **đề-xuất**, chờ lead VERIFY rồi mới APPEND (B3). Bản nén 4-field đủ field ở **100% cụm** (không cụm nào thiếu VIỆC/KẾT-LUẬN/BÀI-HỌC/BẤT-NGỜ).

== HẾT ==
