# sub-reviewer (lane 3) — LĂNG KÍNH: LOGIC WAVE + ACCEPTANCE — S160

> Run: `2026-07-29-S160-khkk-dryrun-plan` · Vai: reviewer (adversarial, READ-only) · Ngày: 2026-07-29
> Đích soi sâu: `plan-cha-dry-run-toan-trinh-29-07-2026.md` + `spec-wave-w1-schema-designer` + `spec-wave-w3-duyet-3-tram`; liếc đối chiếu đồ thị: w2/w5/w6/w7/w8 + `sub-investigator-codebase-1.md` §F-10.
> NOTE tên file: TASK lượt-1 chỉ định tên này; boilerplate RUN-TRACE ghi `sub-reviewer-3.md` — ghi theo TASK, khai lệch trong return.

**VERDICT (dòng-1): SUA-TRUOC-THI-CONG** — 4 HIGH (2 chặn E2E im lặng: W1 grant no-op trên prod · W7 danh sách GĐ4 rỗng với đúng vai; 2 rơi bước: workflow HĐ 3 trạm không ai dựng · b.14-17 rơi khỏi bảng default). W2/W3/W5/W6 = GO-WITH-FIXES. Bộ plan **grounding tốt bất thường**: 26/28 neo tôi chấm lại trên đĩa là ĐÚNG.

---

## 0. Nhật ký đo (ghi TRONG lúc làm — chống #53)

- [t1] scaffold file này.
- [t2] Đọc: plan-cha (92 dòng) · w1 (39) · w3 (37) · w5 (30) · w6 (32) · w2 (35) · w7 (35) · w8 (33) · sub-investigator-codebase-1 (215).
- [t3] Grep `ApproveV2Async` toàn `src/Backend` ⇒ **CÓ** `ContractWorkflowService.cs:217`. Nghi-vấn "W3 copy sai file" (CLAUDE.md §S158 ghi ApproveV2Async ở `ContractFeatures.cs`): **BÁC BỎ** — CLAUDE.md mới là chỗ mô-tả lệch, spec đúng.
- [t4-t8] Chấm 28 neo trên đĩa (bảng §6). Bắt 4 HIGH + 5 MED + 5 LOW.
- [t9] Viết file này TRƯỚC khi return.

---

## 1. Đồ thị phụ thuộc (plan-cha §2 `:30-39`)

**Cạnh ĐÚNG (verify được):**
- `W1→W2→W3` — W2 §②-2 sinh `MaKeHoach` bằng CodeGen của W1, W3 §③-A neo bảng `Approvals`/`Changelogs` của W1 ⇒ thứ tự bắt buộc, đúng.
- `W3→W5` — W5 §②-2a đọc `plan Phase==DaDuyet`; trạng thái đó chỉ tồn tại sau W3. Đúng.
- **`W6 trước W5` (về VẬN HÀNH) = ĐÚNG và có căn cứ đĩa**: không có W6 thì HĐ pin V2 vào `ChoDuyet=10` là *vô hình* — `GetEligiblePhases` (`ContractFeatures.cs:334-351`) **0 entry cho ChoDuyet lẫn DaPhatHanh**, `PhaseActorRoles` (`:421-430`) 100% phase legacy. Đồng thời **W5 KHÔNG bị W6 chặn về mặt acceptance**: người trình là Drafter tự tạo ⇒ qua guard `:73-78`, và `GET /{id}` qua được nhờ nhánh `isDrafter` (`:485-489`). Tức plan gọi đây là ràng buộc *vận hành* chứ không phải *biên dịch* — mô tả CHÍNH XÁC.
- `W7 build song song, VERIFY sau W5+W6` — đúng về nguyên tắc, nhưng thiếu 2 cạnh (dưới).

**CẠNH THIẾU (F-01, F-02, F-03 — chi tiết §7):**
1. **`W7 → (view-guard cho vai KHÔNG-approver)`**: W7 liệt HĐ `phase=9`. Bộ lọc IDOR ở `ContractFeatures.cs:302` = `DrafterUserId==userId || eligiblePhases.Contains(Phase)`; `GetEligiblePhases` **không có DaPhatHanh(9)** ⇒ HRA (đóng dấu b.20) và PB/BCH (phát hành b.21) thấy **danh sách rỗng**, `GET /{id}` 403. W6 chỉ mở cho **approver V2 của chính HĐ đó** (W6 §②-3) — HRA/BCH thường KHÔNG nằm trong workflow ⇒ không wave nào cấp. Cạnh này KHÔNG có trong đồ thị.
2. **`W5/W6 → (dựng ApprovalWorkflow V2 type=3 ba trạm)`**: E2E `:39` hứa "3 trạm HĐ". Đĩa chỉ có seed `QT-HD-V2-001` = **1 Bước × 1 Cấp** (`DbInitializer.cs:239-265`, approver duy nhất `binh.le@` CCM), và seeder tự tắt nếu đã tồn tại workflow type-3 (`:226-228`). Không wave nào, không mục nào của §4 "chiến lược data" giao việc dựng workflow HĐ 3 trạm PMH/PRO→CCM→CEO. Đây là cạnh chuẩn-bị-dữ-liệu bắt buộc.
3. **`W8 → W7` (thứ tự nguy hiểm)**: W8 gắn `Contracts.Update` cho **mọi** HttpPost/Put/Delete của `ContractsController` — bao gồm `POST {id}/attachments` mà W7 dùng để upload 3 purpose. Danh sách role được grant của W8 (§②-3) = Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin — **KHÔNG có HrAdmin (HRA)**, chính là người làm b.20 đóng dấu (F-10 `:123`). W8 chạy trước W7-UAT ⇒ HRA 403 khi upload.

**Thứ tự đề nghị (giữ tinh thần plan, thêm 2 nút):** `W1 → W2 → W3` ∥ `W6` → **[dựng WF V2 type=3 3 trạm]** → `W5` → `W7(+view-guard vai cứng)` → `W8(+HrAdmin vào grant)`.

---

## 2. Mỗi wave có THẬT deploy-độc-lập không? ("người dùng thấy gì" đứng vững?)

| Wave | Người dùng thấy gì khi đẩy riêng | Đứng vững? |
|---|---|---|
| W1 | 0 thay đổi cho user thường (menu giữ, leaf vẫn `/coming-soon`); **admin** dựng được quy trình type-10 | ✅ đứng — nhưng xem F-04 (grant có thể no-op ⇒ "thấy" mà không "gọi" được ở W2) |
| W2 | 6 leaf `Khkk_*` hết ComingSoon → trang thật, tạo/sửa/xoá phiếu nháp | ✅ đứng |
| W3 | Phiếu trình được, 3 trạm duyệt, chốt giá | ✅ đứng (phụ thuộc W2, khai rõ) |
| W5 | Nút "Tạo Hợp đồng" trên phiếu KHKK DaDuyet | ✅ đứng |
| W6 | Approver V2 **thấy + mở + duyệt** HĐ đang chờ mình | ✅ đứng — và tôi xác nhận đường tạo HĐ-tay để test W6 trước W5 là KHẢ THI thật: `ContractCreatePage.tsx:340` lấy bucket `applicableType===3`, `ContractFeatures.cs:109` nhận `ApprovalWorkflowId` ⇒ pin V2 được từ UI. (Điểm cộng cho spec.) |
| W7 | 7 leaf `Hdc_*` thành trang theo dõi bản cứng | ⚠️ **KHÔNG đứng vững cho đúng vai** — F-01 (list rỗng/403 với HRA, BCH, CCM) + F-05 (filter `type` không tồn tại ⇒ 7 leaf hiển thị y hệt nhau) |
| W8 | Thu hẹp ai bấm được | ✅ đứng (acceptance prod 201/403 là phép đo thật) — trừ F-03 (thiếu HrAdmin) |

---

## 3. E2E tới GĐ4 có ĐỦ qua W1+W2+W3+W6+W5+W7? — đối chiếu bảng b.13→21 (`sub-investigator-codebase-1.md:114-124`)

| b | Việc (QT) | Wave phủ | Verdict |
|---|---|---|---|
| 13 | Lựa chọn NTP/NCC | GĐ1 PE + W2 (winners→Lines) | phủ gián tiếp; plan không khai ánh xạ — LOW |
| 14 | Soạn thảo HĐ (7 ngày) | W5 tạo HĐ nháp; **soạn nội dung** = `PUT /contracts/{id}` sẵn có | ⚠️ **RƠI khỏi bảng default** (F-06) |
| 15 | Góp ý 6 phòng (7 ngày) | không wave nào; F-19 `:202` đề "comment thread" | ⚠️ RƠI (F-06) |
| 16 | Đàm phán (7 ngày) | không có máy | ⚠️ RƠI (F-06) |
| 17 | In HĐ + **số HĐ** + ký nháy + cover khối ĐỀ XUẤT | số HĐ: default #10 ✓ (verify `ContractFeatures.cs:113-118` gen tại create — ĐÚNG); **ký nháy + cover: chỉ được HIỂN THỊ ở W7, KHÔNG có đường UPLOAD nào trong GĐ3** | ⚠️ RƠI một nửa (F-07) |
| 18 | CCM kiểm tra + ký nháy + cover khối CCM | trạm V2 — nhưng workflow 3 trạm chưa ai dựng (F-02) | ⚠️ phụ thuộc F-02 |
| 19 | BOD/NĐUQ ký duyệt | W7 (upload `ScannedSigned`) | ✅ — nhưng TRÙNG trạm CEO của V2 (F-08 semantics) |
| 20 | Đóng dấu HRA/ISO | W7 khai **gộp vào b.21** (`spec-w7:13`) | ✅-with-note (khai thật, chấp nhận) — nhưng F-03 chặn HRA upload nếu W8 chạy trước |
| 21 | Phát hành + lưu (scan, 01 bản gốc CCM, Filing) | W7 (`SealedCopy` + note) | ⚠️ **scan cover FO-002.01 không có purpose** (F-09) |

**Kết luận (3):** chuỗi wave đủ để *máy chạy* từ PE→KHKK→HĐ→bản-cứng, nhưng **4 bước nghiệp vụ (b.14/15/16 + nửa b.17) không có ô nào trong bảng default §5 và không wave nào nhận**, còn b.18-21 vướng F-02/F-03/F-09. Nói cách khác: E2E hiện *đo được* ở tầng phase-machine, KHÔNG đo được ở tầng "9 bước ISO".

---

## 4. Defer W4/W9 có phá E2E?

- **W9 defer: KHÔNG phá** ✅ (đúng như F-19 `:200`).
- **W4 defer: KHÔNG phá luồng**, nhưng nhãn default sai một nửa (F-10): plan §2 `:23` ghi W4-defer ⇒ dry-run chạy "comment tự do **+ cảnh báo mềm**, 0 dev". F-18 `:184` chỉ chứng minh **comment tự do = 0 dev**; "cảnh báo mềm khi thiếu căn cứ TvgsDuyet" (default #4, §5 `:66`) **không wave nào code** — W3 §② không có bước nào đọc `DossierItems` lúc trình. Vậy hành vi thật = **không kiểm gì cả**, không phải "cảnh báo mềm". Default đang tự khen là có lưới trong khi lưới rỗng.
- Hệ quả kèm: luật (c) FO-002.01 (cover phải lưu kèm bộ gốc) mà F-18.4 `:187` đề thành **acceptance GĐ4** — W4 defer + F-09 ⇒ không chỗ nào giữ.

---

## 5. Acceptance: cái nào 0-bit / không đo được / literal tự-lão-hoá

| # | Chỗ | Loại | Bằng chứng |
|---|---|---|---|
| A1 | W1 `:33` "`dotnet test` → `AuthorizePolicyRegressionTests` XANH (policy `KeHoachKyKet.*` hợp lệ — All đã chứa key)" | **0-BIT** | Test tại `AuthorizePolicyRegressionTests.cs:120-143` chỉ soi **`ApprovalWorkflowsV2Controller`** (+ HrmConfigs/Employees ở dưới). Ở W1 **chưa có controller nào dùng policy `KeHoachKyKet.*`** (controller ra ở W2) ⇒ test XANH **bất kể** có thêm key vào `All` hay không. Phép đo không thể trượt ⇒ 0 bit. |
| A2 | W1 `:35` "Admin tạo quy trình type-10 …; **xoá 1 Level CHƯA chữ ký** → OK" | **0-bit với rủi ro đã nêu** | Chính W1 §②-10 cảnh báo 3 site `ApprovalWorkflowV2AdminFeatures.cs` (`:911/:929`, `:971/:992`, `:1044/:1056`) — cả 3 chỉ chạy khi Level **CÓ** chữ ký. Ca duy nhất acceptance kiểm là ca KHÔNG chạm 3 site đó. Ở W1 chưa thể có chữ ký type-10 ⇒ rủi ro tự khai chỉ lộ ở W3. |
| A3 | W7 `:32` "`GET /api/contracts?type=1&phase=9` → 200, mọi item `phase==9`" | **0-BIT trên trục `type`** | `ListContractsQuery` = `(Phase, SupplierId, ProjectId)` — **KHÔNG có Type** (`ContractFeatures.cs:282-285`); controller List `:16-24` cũng không nhận `type`. Query-param lạ bị bỏ im lặng ⇒ assertion "mọi item phase==9" PASS **dù filter loại chết**. |
| A4 | W1 `:37` "sqlcmd đếm Permission rows key `KeHoachKyKet` = **số role đích × đúng cờ**" | **không đo được** | không có số kỳ vọng, không có tên cờ ⇒ mọi kết quả đều "khớp". So sánh: W8 `:30` ("user đã-cấp POST→201; chưa-cấp→403") là phép đo THẬT — cùng bộ spec mà 2 chuẩn khác nhau. |
| A5 | W1 `:32` "`sys.tables` **89 → 96**" · `:38` "menu KHÔNG đổi (**142** giữ)" · W7 `:23` "142 giữ" | literal tự-lão-hoá | plan §7 `:82` tự đặt luật "neo TÍNH-CHẤT, CẤM literal" cho test-count nhưng lại literal cho bảng/menu. Rủi ro thấp (1 mig) nhưng nên viết "= số-đo-ngay-trước + 7" / "= số-đo-ngay-trước". |
| A6 | Mã phiếu `KHKK/{YYYY}/{Seq:D3}` (default #6, W1 §②-9) | **không có acceptance nào** | W1 §③ không có dòng nào chạm CodeGen; W2 §③ chỉ "POST → 201". Một quyết-định owner-visible (format mã in trên phiếu) đi qua 2 wave mà 0 phép đo. |
| A7 | W3 `:30` "`Lines WHERE PlanId=@id AND ApprovedAmount IS NULL` = 0" | ✅ TỐT | có `AND PlanId=@id` (chống bẫy S134 đếm-toàn-bảng) — giữ nguyên. |
| A8 | W3 `:28` OR-of-N 4 vế (2 phiếu song sinh + ca ÂM + 1 row + không-đòi-cả-hai) | ✅ TỐT NHẤT bộ | đúng lưới cho rủi ro "copy nhầm `ProposalFeatures.cs:427-439` mất OR-of-N âm thầm". |

---

## 6. Gate 5-anchor: đủ 5? có thật? (chấm lại 28 neo trên đĩa)

| Wave | Neo | Trên đĩa | Verdict |
|---|---|---|---|
| W1 | `ApprovalWorkflow.cs:53-67` slot 10 trống | enum dừng `TravelRequest = 9` (`:66`), `}` `:67` | ✅ ĐÚNG |
| W1 | `MenuKeys.cs:41` const + comment tái dùng | `:41 public const string KeHoachKyKet` + comment `:37-40` "Wave-1 GĐ2 … phải TÁI DÙNG" | ✅ ĐÚNG |
| W1 | `MenuKeys.cs:163+` All chưa có KeHoachKyKet | `All` `:163-181`, không có | ✅ ĐÚNG |
| W1 | `AuthorizePolicyRegressionTests.cs:138-139` | `MenuKeys.All.Should().Contain(parts[0], …)` đúng 2 dòng đó | ✅ ĐÚNG (nhưng xem A1) |
| W1 | `DbInitializer.cs:1779-1786` Khkk_* 7 key | `:1779 Khkk_G1` + `:1781-1786` 6 leaf | ✅ ĐÚNG |
| W2 | `ProposalFeatures.cs:427-439` bẫy flatten · `:258-267` guard type · `CreateContractFromEvaluationFeatures.cs:53-62` | (2 cái Proposal không chấm — ngoài ngân sách lượt; cái thứ 3 ✅ `:53-62` đúng guard+SUM) | ⚪ 1/3 chấm |
| W2 | `DbInitializer.cs:2207` "khu **staticMap** comment Khkk_*" | `:2207` là comment trong khối **grant CanRead** (`reviewKeys` `:2202-2208`), KHÔNG phải staticMap. staticMap thật ở `fe-user/src/components/Layout.tsx:59-62,79-80` | ⚠️ **NHÃN SAI** (F-11) |
| W3 | `ContractWorkflowService.cs:217-394` nguồn copy | `ApproveV2Async` `:217`, kết thúc `:394` | ✅ ĐÚNG (kể cả cận trên) |
| W3 | `:259-260` OR-of-N · `:292-316` UPSERT · (`:246` GroupBy, `:320-352` skipToFinal) | `:259 allowedUserIds` `:260 Contains` ✓ · `:292-316` UPSERT ContractLevelOpinions ✓ · `:246` GroupBy ✓ · `:320` if(skipToFinal) … `:352` ✓ | ✅ 4/4 ĐÚNG (chính xác đến dòng) |
| W3 | `ApprovalWorkflow.cs:95` class Level + `:116-125` 4 return-mode | class `:95` ✓; `:116/119/122/125` = 4 cờ ✓ (cảnh báo "class nằm TRONG ApprovalWorkflow.cs, không có file riêng" ĐÚNG) | ✅ ĐÚNG |
| W3 | `PurchaseEvaluationWorkflowService.cs:1008` khuôn RULE choke-point — spec mô tả *"comment nêu LUẬT + **lệnh grep**, CẤM số-đếm site"* | `:1008` = `// RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay TRƯỚC assignment (**reviewer grep-check 4 site**).` — **không có lệnh grep** và **CÓ số-đếm "4 site"** | ⚠️ **MÔ TẢ NGƯỢC** (F-12) |
| W5 | `:53-60` guard · `:68-71` pin V1 · `:88-90` SUM · `:145` idempotency-write · `ContractWorkflowService.cs:115-116` Conflict | 5/5 khớp từng dòng | ✅ 5/5 |
| W6 | `ContractFeatures.cs:334-351` · `:421-430` · `:488-489` · `ContractWorkflowService.cs:73-78` · `ContractsController.cs:38-40` | 5/5 khớp (inbox endpoint đúng `:38-40`) | ✅ 5/5 |
| W7 | `ContractAttachment.cs:5-11` enum · `DbInitializer.cs:1862-1867` loop 7 leaf · `Layout.tsx:63-69` 7 dòng Hdc_ · `MyContractsPage.tsx:29-46` | 4/4 khớp (Layout `:63-69` đúng CHÍNH XÁC 7 dòng) | ✅ 4/4 |
| W7 | `ContractsController.cs:16-24` "list params **type**/phase" | `:16-24` có `phase/supplierId/projectId`, **KHÔNG có `type`**; `type` chỉ ở `/deleted` `:31` | ❌ **SAI** (F-05) |
| W8 | `ContractsController.cs:13` class-trần · `:28-29` tiền lệ policy · `MenuKeys.cs:163-181` Contracts∈All (`:168`) · `ContractWorkflowService.cs:49-66` Reject | 4/4 khớp (`SuppliersController.cs:67` không chấm) | ✅ 4/4 chấm |

**Đếm:** 7/7 wave có đủ 5 mục anchor về HÌNH THỨC. **Về THỰC CHẤT: 26 đúng / 2 sai-hoặc-nhãn-sai** (W7 `type`, W2 "staticMap") **+ 1 mô tả ngược** (W3 khuôn `:1008`).
**Khiếm khuyết cấu trúc (F-13, LOW):** W3 đếm 5 neo nhưng 3 trong số đó **lồng nhau** (`:259-260` ⊂ `:292-316` ⊂ `:217-394`) ⇒ chỉ 3 sự-kiện độc lập; W1 có 2 neo cùng file `MenuKeys.cs`; W6 3/5 cùng `ContractFeatures.cs`. "5 anchor" đang đếm KHOẢNG-DÒNG chứ không đếm mệnh-đề độc lập.

---

## 7. FLAG (F-01 → F-16) + SEV

### HIGH

**F-04 (W1) — grant `KeHoachKyKet` 4-action nhiều khả năng NO-OP trên prod (403 im lặng, gotcha #44 + #84).**
Trên đĩa, key này **đã có row Permission cho mọi role** qua nhánh read-only: `DbInitializer.cs:2204-2206` `.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })` (comment: "CanRead-only mọi role … Prod đã INSERT trực tiếp"). Ngay dưới, `:2211-2212` cảnh báo nguyên văn: *"nhánh grant key-thường là **skip-existing** ⇒ trên prod row false **KHÔNG được nâng** ở đây"*. ⇒ W1 §②-8(a) thêm Create/Update/Delete theo đường seeder thường sẽ **bị bỏ qua vì row đã tồn tại**; W2/W3 deploy xong thì user dry-run POST → **403 im lặng**, và acceptance A4 (đếm ROW, không đếm CỜ) không bắt được.
*Acceptance đề nghị:* dùng đúng khuôn W8 `:30` — sau deploy, user role đích gọi `POST /api/contract-signing-plans` phải 201 và user không-cấp phải 403; cộng thêm phép đọc CỜ (`CanCreate/CanUpdate/CanDelete`) chứ không đếm row.

**F-01 (W7, kèm W6) — GĐ4 không vận hành được cho đúng vai: danh sách rỗng + 403.**
`ContractFeatures.cs:302` lọc `DrafterUserId==userId || eligiblePhases.Contains(Phase)`; `GetEligiblePhases` `:342-349` **không chứa `DaPhatHanh(9)`** (cũng không chứa `ChoDuyet(10)`). ⇒ HRA (b.20), PB/BCH + CCM (b.21), BOD (b.19) nếu không phải Drafter/Admin sẽ thấy **0 dòng** ở mọi leaf `Hdc_*` và 403 khi mở chi tiết. W6 §②-3 chỉ mở cho **approver V2 của chính HĐ đó**, và W6 acceptance chỉ đo `inbox` + `GET /{id}` — **không wave nào đo LIST**. W7 §③ prod-check lại không nói ai gọi (admin gọi thì luôn xanh vì `:298`).
*Acceptance đề nghị:* thêm ca "user vai HRA/BCH KHÔNG-admin KHÔNG-drafter gọi `GET /api/contracts?phase=9` ≥1 dòng" + ca ÂM giữ 403 cho người ngoài.

**F-02 (plan §4 + W5/W6) — không ai dựng ApprovalWorkflow V2 `type=3` ba trạm; seed chỉ có 1 trạm.**
`DbInitializer.cs:226-228` bỏ qua nếu đã có workflow type-3; `:239-265` seed `QT-HD-V2-001` = **1 Bước (CCM) × 1 Cấp**, approver duy nhất `binh.le@solutions.com.vn`. E2E `:39` hứa "3 trạm HĐ"; W6 §① hứa "3 trạm PRO→CCM→CEO". §4 "chiến lược data" chỉ liệt ZZTEST + NCC + PE — **thiếu hẳn mục dựng workflow HĐ**. Nếu chạy bằng seed: GĐ3 chỉ 1 trạm CCM, **b.19 (BOD ký) không có trạm web**, và default #1 "CEO ký thật" không thể xảy ra.
*Acceptance đề nghị:* thêm bước-0 của W5/W6 "admin tạo WF type-3 ≥3 Bước, `IsUserSelectable=true`, approver = user dry-run thật" + acceptance đếm `Steps>=3` trước khi bấm E2E.

**F-03 (W8×W7) — grant W8 thiếu `HrAdmin` ⇒ khoá đúng người làm b.20.**
W8 §②-2 gắn policy cho MỌI HttpPost/Put/Delete của `ContractsController` (bao gồm `POST {id}/attachments`); §②-3 grant cho Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin — **không có HrAdmin**, trong khi b.20 đóng dấu là HRA/ISO (`sub-investigator-codebase-1.md:123`) và W7 yêu cầu HRA upload bằng chứng. Cùng lớp với F-04 (403 im lặng giết chính dry-run — W8 tự nêu rủi ro này ở §③-C mà lại rơi đúng vào nó).

### MEDIUM

**F-05 (W7) — `type` filter không tồn tại ở endpoint list ⇒ 7 leaf hiển thị TRÙNG nhau; anchor sai; acceptance 0-bit.**
`ContractsController.cs:16-24` + `ListContractsQuery` `:282-285` không có `Type`. Khuôn W7 chọn (`MyContractsPage.tsx:41-46,:60`) lách bằng **lọc client-side sau khi lấy `pageSize:100`** ⇒ nếu copy nguyên, trang bản-cứng phân trang SAI (server phân trang trước khi lọc loại) và trần 100. Cần hoặc thêm `Type` vào query BE (nhỏ, 0-mig) hoặc khai rõ lọc client + bỏ phân trang.

**F-06 (plan §5 + W5) — b.14/15/16 rơi khỏi bảng 12 default.**
`sub-investigator-codebase-1.md:202` có default rõ ("Nháp + comment thread + update draft; muốn trạm thật thì thêm Bước trong workflow, 0 code") nhưng plan-cha KHÔNG chép vào §5 và không wave nào nhận. E2E `:39` nhảy thẳng "HĐ per-winner pin V2 → 3 trạm HĐ" ⇒ 21 ngày SLA nghiệp vụ (7+7+7) biến mất khỏi kế hoạch mà không có dòng khai.

**F-07 (W7 §① + F-11 lane-1) — ký nháy/cover b.17-18 chỉ có đường ĐỌC, không có đường GHI.**
W7 khai "readonly từ GĐ3 … hiển thị từ attachments (`InitialSigned` mới)" nhưng chính W7 mới là chỗ thêm enum `InitialSigned`, và panel upload của W7 nằm ở trang GĐ4 (sau `DaPhatHanh`). ⇒ hoặc GĐ3 không có nút upload ký-nháy (dữ liệu luôn rỗng, khối readonly vô nghĩa), hoặc phải upload ở GĐ4 rồi gọi là "của b.17" (nghịch thời gian). Cần chốt 1 trong 2 và khai.

**F-09 (W7) — scan cover FO-002.01 không có purpose để gắn.**
`AttachmentPurpose` = `{DraftExport=1, ScannedSigned=2, SealedCopy=3, Other=99}` (`ContractAttachment.cs:5-11`); W7 chỉ thêm `InitialSigned=4`. §① lại đòi "upload scan cover FO-002.01 (luật (c))" và §③ chỉ đo "3 purpose". ⇒ cover rơi vào `Other` hoặc rơi hẳn; luật (c) (bắt buộc lưu kèm bộ gốc) không có phép đo nào.

**F-10 (plan §2 `:23` + §5 #4) — default "cảnh báo mềm" không có chủ.**
W4 deferred = 0 dev, W3 §② không có bước kiểm căn cứ ⇒ hành vi thật là **không kiểm gì**. Nhãn "cảnh báo mềm" đang mô tả một lưới không tồn tại (lớp Goodhart: default trông như đã che rủi ro).

### LOW

- **F-08 (semantics, cross-wave):** terminal V2 của HĐ = `DaPhatHanh=9` "Đã duyệt (gen mã + phát hành) — terminal thành công" (`ContractPhase.cs:24`, set tại `ContractWorkflowService.cs:379`). Nhưng "phát hành" theo QT là **b.21**, tức trạng thái hệ thống nói "đã phát hành" **trước khi** GĐ4 làm b.19/20/21. W7 §① có ý thức ("GĐ4 nằm SAU terminal") nhưng plan-cha không khai mâu thuẫn NHÃN. Rẻ: đổi nhãn hiển thị GĐ3-terminal thành "Đã duyệt HĐ" ở UI HĐ.
- **F-11 (W2 anchor):** `DbInitializer.cs:2207` bị gọi là "khu staticMap" — thực tế là khối grant CanRead; site staticMap thật (`fe-user Layout.tsx:59-62,79-80` + mirror fe-admin) **không nằm trong gate-5 của W2** dù W2 phải sửa đúng chỗ đó.
- **F-12 (W3):** spec mô tả khuôn `PurchaseEvaluationWorkflowService.cs:1008` là "LUẬT + lệnh grep, CẤM số-đếm" — đĩa cho thấy ngược: có "(reviewer grep-check **4 site**)", không có lệnh grep. Worker copy khuôn sẽ copy đúng cái con-số mà spec đang cấm.
- **F-13:** gate-5-anchor đếm khoảng-dòng lồng nhau (W3: `:259-260` ⊂ `:292-316` ⊂ `:217-394`) ⇒ "5" phóng đại độ phủ; đề nghị đếm mệnh-đề độc lập.
- **F-14 (W2 vs W5):** tên field FK lệch giữa 2 spec — W2 `p.PeId` (`spec-w2:13`) vs W5 `Plans.Where(PurchaseEvaluationId==peId)` (`spec-w5:13`); W1 §②-2 không chốt tên cột. Chốt 1 tên ở W1 để W5 không phải sửa lại.
- **F-15 (W1):** thêm `KeHoachKyKet` vào `All` **buộc** cập nhật 2 số canonical — chính mã dặn: `MenuKeys.cs:150-151` *"Thêm vào `All` sẽ đổi 2 số canonical `Menu keys`/`Policies` ở `docs/STATUS.md` (Policies = |All| × |Actions|, DERIVED)"*. Checklist W1 không có dòng cập nhật `docs/STATUS.md` (B1 drift).
- **F-16 (plan §4.4, xác nhận ĐÚNG):** "HĐ ≥ phase 5 không xoá được" — `ContractFeatures.cs:632-633` `Phase >= DangInKy` ✓; đường thoát admin-override `ContractWorkflowService.cs:191-198` ✓ tồn tại thật. Lưu ý phụ: `ChoDuyet=10`/`TraLai=98`/`TuChoi=99` đều ≥5 ⇒ **mọi phiếu HĐ đã trình đều không xoá được** bằng API — plan chỉ nói "≥ phase 5", đúng nhưng dễ đọc nhầm là chỉ HĐ cuối luồng.

---

## 8. Điểm ĐỨNG VỮNG trước soi (ghi để không chỉ kể lỗi)

- 26/28 neo chấm lại **đúng đến dòng**, kể cả các cận trên khó (`:217-394`, `Layout.tsx:63-69` đúng 7 dòng, `DbInitializer.cs:1779-1786` đúng 7 key).
- W3 §③-B (OR-of-N 4 vế + ca ÂM + `AND PlanId=@id`) là bộ acceptance mạnh nhất bộ plan — đủ răng để bắt đúng rủi ro tự khai ("copy nhầm Proposal mất OR-of-N âm thầm").
- W6 §③ có **ca ÂM tường minh** ("user ngoài workflow vẫn 403") — đúng bài chống mở-toang.
- Đường tạo HĐ-tay để test W6 trước W5 là khả thi thật (`ContractCreatePage.tsx:340` + `ContractFeatures.cs:109`) — không phải hứa suông.
- Nghi vấn nặng nhất tôi mang vào (W3 copy sai file vì CLAUDE.md nói `ApproveV2Async` ở `ContractFeatures.cs`) đã bị đĩa BÁC BỎ: spec đúng, CLAUDE.md lệch.

<!-- END sub-reviewer lane-3 · 16 FLAG = HIGH 4 (F-01,02,03,04) / MED 5 (F-05,06,07,09,10) / LOW 7 (F-08,11,12,13,14,15,16) · 28 neo chấm lại (26 đúng, 2 sai/nhãn-sai) · verdict SUA-TRUOC-THI-CONG -->
