# sub-review-b2-lens2 — LENS-2 FIDELITY-KHUÔN + SCOPE

> Reviewer lane-2/3 (ensemble /fable-clone). Lăng-kính: độ trung-thành với lời anh (YC-029 + 3 ảnh) + khuôn S176 "đã-deploy = CHUẨN" + scope-risk + mirror 2 app + wave order.
> Mọi claim dưới đây ĐO từ đĩa tại HEAD `1c677fb7` (working tree). Không có claim nào chép lại từ `sub-invest-b1-fable.md` mà không mở file gốc.

## Verdict

**PASS_WITH_FIXES — 12 findings** (4 CRITICAL · 4 MAJOR · 4 minor). Spec phủ 5/5 điều chỉnh về mặt ĐỀ-BÀI, nhưng **4 câu implement trong §2 sẽ đẻ lỗi CÂM nếu implementer làm đúng chữ**, W2 (cây sang trái) thiếu 2 mitigation mà chính khuôn KHKK tự khai là BẮT BUỘC, và **W5 mirror NHẦM module** — khuôn KHKK mà anh chỉ định lại CẤM đúng cái W5 định mở.

---

## Findings (F-1..)

### F-1 · CRITICAL — §2 Đ2+Đ3: "bỏ `type=3` khỏi URL, trang tự suy applicableType theo path" — cái "khuôn alias-path khkk" ĐÓ KHÔNG TỒN TẠI, và trang rơi về **type 1 (Duyệt NCC)** chứ không phải 3

Spec `spec-…md:37-38` chỉ đạo: `Layout.tsx:151` WfView → `/contracts/workflow-matrix?ct=N` (**BỎ `type=3`**) vì "trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn".

Đo:
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:57,61`:
  ```
  const rawType = Number(searchParams.get('type'))
  const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1
  ```
  Bỏ `type` khỏi URL ⇒ `Number(null)` = 0 ⇒ **fallback `: 1`** ⇒ fetch `applicableType=1` = quy trình **Duyệt NCC**. Trang KHÔNG đọc `pathname` để suy type: `pathname` chỉ dùng cho link "xem tất cả" — chính comment `:54-56` khai vậy ("link *xem tất cả* phải bám đường dẫn ĐANG đứng").
- "Khuôn alias-path khkk" là **phản-chứng của chính spec**: leaf KHKK `Layout.tsx:216` = `/khkk/workflow-matrix?type=10&group=${group}` — KHKK **GIỮ `type=` trên URL**, không suy theo path. Route `/khkk/workflow-matrix` (`App.tsx:83`) chỉ là alias pathname, KHÔNG mang ngữ nghĩa type.

⇒ Làm đúng chữ spec = trang "Luồng duyệt — HĐ Thầu phụ" hiển thị **quy trình Duyệt NCC**, không lỗi đỏ, không 404. Đây là lệch khuôn S176 (KHKK đã-deploy giữ `type=`), lại là dạng hỏng CÂM.
**Sửa:** GIỮ `?type=3&ct=N` (đúng khuôn KHKK) và chữa bug A ở **accordion** (`Layout.tsx:470-485`) — vốn đã nằm sẵn trong spec như "phòng thủ 2 lớp". Lớp phòng thủ đó chính là toàn bộ liều thuốc; vế "bỏ type=3" là phần TỰ CHẾ và là phần gây hại. Nếu vẫn muốn bỏ `type=3` thì spec phải kèm 1 câu bắt buộc: thêm nhánh suy type từ `pathname` TRƯỚC, và acceptance phải có ca "URL không có type ⇒ vẫn ra HD-*".

### F-2 · CRITICAL — §2 Đ4-W3: "BE param `pendingMe` sẵn" là SAI cho Contract ⇒ `/contracts?pendingMe=1` bị model-binding NUỐT, list ra TOÀN BỘ HĐ mà không báo lỗi

Spec `:45-46` (và invest `:114`) viết: *"`Ct_*_Pending` → `/my-contracts?type=N&pendingMe=1` (khuôn PE `:176`; **BE param `pendingMe` sẵn**) + `MyContractsPage` đọc `pendingMe` **gửi server**"*.

Đo:
- `grep -rn "PendingMe" src/Backend --include=*.cs` = **2 hit, CẢ HAI thuộc KHKK** (`ContractSigningPlansController.cs:42,50` + `ContractSigningPlanFeatures.cs:1219,1255`). Contract: **0 hit**.
- `ContractsController.cs:17-27` — `List(...)` nhận `page,pageSize,search,sortDesc,phase,supplierId,projectId,type`. **Không có `pendingMe`**. Query gửi thừa param ⇒ ASP.NET bỏ qua im lặng ⇒ trả full list.
- Khuôn PE THẬT (không phải "param BE"): `PurchaseEvaluationsListPage.tsx:91-97` — `if (pendingMe)` thì **ĐỔI ENDPOINT** sang `/purchase-evaluations/inbox` (kèm `type`). Còn `/contracts/inbox` **không nhận tham số nào** (`ContractsController.cs:41-43` `GetMyInboxQuery()` 0-arg) ⇒ mirror cho HĐ phải lọc `type` client-side (đúng như `InboxPage.tsx` đang làm) hoặc nới BE.

⇒ W3 đang được spec dán nhãn "FE nhỏ" trên một tiền-đề sai. Hoặc sửa câu implement thành "pendingMe ⇒ gọi `/contracts/inbox` + lọc type client (khuôn PE)", hoặc khai W3 có delta BE. **Acceptance W3 hiện KHÔNG bắt được lỗi này**: (i) grep `pendingMe=1` vẫn xanh; (ii) "list khớp inbox cũ" — người kiểm dễ chấp nhận vì list *có* dữ liệu (chỉ là nhiều hơn đúng). Đề nghị siết acceptance thành **so số dòng bằng nhau + 1 HĐ KHÔNG-đến-lượt phải VẮNG**.

### F-3 · CRITICAL — W2 bỏ mất 2 mitigation mà chính khuôn KHKK khai là **BẮT BUỘC**, và cây GĐ3 về mặt cấu trúc **không chứa HĐ tạo trực tiếp**

Header `fe-user/src/components/khkk/KhkkListPanel.tsx:9-24` khai nguyên văn: *"Chọn HÌNH ≠ chấp-nhận VỠ ⇒ 4 mitigation dưới đây là BẮT BUỘC, không phải tuỳ chọn"* — M-1 (`!canPe` ⇒ fallback danh-sách phẳng), M-2 (`onSelect` thay navigate), B1 (`stage2Content` phải kèm `stage2Count`), R-11 (cây chỉ 200 dòng ⇒ tìm/lọc đi SERVER).

Spec `:29-35` chỉ port **M-2** (`onOpenContract`) + nửa R-11 ("flat-list fallback server-side"). Thiếu:
- **M-1 analog:** nguồn của cây là index PE, `enabled: canPe` (`usePipelineStages.ts:83,102`). User chỉ có quyền HĐ (drafter HĐ, không có key Duyệt NCC) ⇒ **panel trái TRỐNG** trong khi `/contracts` vẫn trả dữ liệu ⇒ đúng mặt còn lại gotcha #82. Hôm nay `/my-contracts` không có bệnh này vì panel-1 là list thuần.
- **Cấu trúc:** `usePipelineStages.ts:333-359` — stage-3 leaves duyệt trên `contractIdsOfPackage` = hợp của `pe.contractId` + contract-id trên dòng KHKK. **HĐ tạo thẳng từ `/contracts/new?type=N`** (chính là leaf "Thao tác" và nút "+ Tạo HĐ mới" `MyContractsPage.tsx:145`) **không thuộc gói nào ⇒ 0 leaf trên cây**. Cây làm panel mặc định ⇒ nhóm HĐ này **không còn đường mặc định để thấy**; chỉ tìm ra nếu user gõ đúng từ khoá. Ở KHKK không có lỗ này vì mọi phiếu KHKK đều sinh từ PE.
⇒ W2 phải khai điều-kiện fallback ĐẦY ĐỦ: `!canPe` **HOẶC** search/lọc **HOẶC** (đề nghị) mặc định vẫn phẳng khi tập "HĐ ngoài gói" > 0. Và acceptance W2 phải có ca **"tạo 1 HĐ trực tiếp (không qua KHKK) → nó phải xuất hiện được ở panel trái"**.

### F-4 · MAJOR — W2 ⟂ Đ2 mâu thuẫn CHÉO: cây toàn trình **không lọc theo ContractType**, trong khi cả đợt này là để "mỗi loại đúng danh tính"

`usePipelineStages.ts:356-379` dựng stage-3 leaf từ tập id của gói, **0 vế lọc `c.type`** (đối chứng: stage-4 có đọc `c.type` nhưng chỉ để dựng URL `:389`). Cây gốc còn đứng ở trục Năm → Dự án → Gói thầu (ảnh-1), tức trục **gói thầu**, không phải trục **loại HĐ**.

⇒ Sau W2, `/my-contracts?type=1` (leaf "01. HĐ Thầu phụ → Danh sách") sẽ hiện panel trái chứa HĐ **của cả 7 loại**. Điều đó phá đúng thứ mà Đ2/Đ3 (W1) vừa dựng, và header trang vẫn ghi "HĐ của tôi · HĐ Thầu phụ (n)" ⇒ nhãn nói dối nội dung.
Khuôn KHKK **có xử vế này**: panel-1 giữ `Select` nhóm ngay trên cây (`KhkkListPanel.tsx:19-20` khai trade-off "folder GĐ2 hiện CARD PHIẾU thay 8 ngăn N1-N8; **lối vào nhóm giữ ở `Select` nhóm ngay bên trên**"). Spec cho HĐ **không có** vế tương ứng.
⇒ Cần chốt 1 trong 2 và ghi vào spec: (a) lọc stage-3 leaf theo `type` khi host truyền `typeFilter`; hoặc (b) giữ cây full + bắt buộc có ô chọn loại/`Select` như KHKK. **Đây là câu nên hỏi anh** (xem §Đề nghị, Q2).

### F-5 · MAJOR — Đọc-ý Thứ-1 chốt một chiều: mũi tên là "dời cây sang trái", nhưng "cột danh-sách biến thành fallback" là **suy diễn của đội**, không phải lời anh

`anh-mo-ta.md:26-30` ghi trung thực: mũi tên đỏ từ khung CÂY (phải) chỉ SANG khung danh-sách (trái) + lời anh *"Bỏ phần này sang bên này nhé"*; và tự đánh dấu *"Danh-sách HĐ đi đâu … = lane invest đối chiếu khuôn KHKK rồi đề xuất"*. Invest chọn "flat-list = fallback trong CÙNG panel" bằng lập luận khuôn KHKK — hợp lý, **nhưng nó XÓA một khối UI mà anh không nói xóa**, trong khi cách đọc thứ hai (**HOÁN ĐỔI**: cây sang trái, danh-sách sang phải/giữa) cũng khớp y hệt mũi tên và **không mất gì**.
Cộng với F-3 (HĐ ngoài gói vô hình) + F-4 (cây không theo loại), phương án "list thành fallback" là phương án **mất chức năng nhiều nhất** trong 3 cách đọc. ⇒ **Phải hỏi anh trước khi khoá** (Q1), hoặc chọn mặc định an toàn: dời cây sang trái, **giữ danh-sách** ở panel giữa/phải và để panel-3 dashed khi chưa chọn.

### F-6 · MAJOR — Đ4 bị THU HẸP: lời anh *"Phần thao tác, duyệt, xóa **...** → Cấu trúc tương tự như KHKK"* bị spec cắt còn 3 món, 3 món KHKK-có/HĐ-thiếu bị đẩy ra "ngoài wave" **không đánh số**

Invest `:91` đo được 3 món cố-ý-không-port: ① ô-tích "Cấp này KẾT THÚC" (`skipToFinal`) ② badge "⚑ Duyệt thay CEO" ③ banner kết-thúc-sớm + meta `{code} v{NN}` — cả 3 đều thuộc đúng chữ **"duyệt"** trong câu của anh, và đều là **hành-vi màn duyệt KHKK trên prod** (chuẩn S176). Spec đẩy xuống dòng cuối `:100-103` dưới nhãn "Ngoài wave — CHỜ ANH (không tự làm)" — nhưng **không có câu hỏi nào được đánh số để anh trả lời**, nên thực tế nó sẽ rơi im lặng đúng như bài `feedback_absence_looks_like_clean`.
Thêm: ảnh-3 mà anh gửi có đúng ô **"Duyệt là KẾT THÚC quy trình (không trình tiếp CEO)"** (chữ xanh) và ô **"Cho phép duyệt thẳng Cấp cuối"** trong dialog Sửa-tại-chỗ (`anh-mo-ta.md:57-58`) ⇒ anh **đang nhìn thấy** 2 cờ đó trong Admin. Cờ bật được ở Designer mà màn duyệt HĐ không đọc = cấu hình câm (đúng lớp `feedback_guard_built_but_never_called`). ⇒ Nâng thành **Q3 đánh số**.

### F-7 · CRITICAL — W5 mirror NHẦM MODULE: khuôn KHKK (thứ anh chỉ định) **CẤM approver xóa phiếu đang duyệt**; cờ `AllowApproverDelete` là của PE, KHKK chưa từng dùng

Đo BE, không suy:
- `ContractSigningPlanFeatures.cs:1763-1766` — chỉ **Drafter hoặc Admin** được xoá (`ForbiddenException`), không có nhánh approver.
- `:1768-1783` — allow-list **{DangSoanThao, TuChoi, TraLai}**, và comment tự khai nguyên văn: *"**Xoá phiếu ĐANG duyệt vẫn cấm** (cần rào đúng-lượt như PE)"*, kèm dấu tích S177 *"anh: Trạng thái trả lại cho xóa đc luôn nhé"*.
- `grep -rn "AllowApproverDelete" src/Backend` = 27 hit, **0 hit trong `ContractSigningPlans/`** (hit nằm ở PE + `ApprovalWorkflowV2AdminFeatures` + Domain/Infra/migration). Control dương: PE có đủ cả 2 đầu (`PeSoftDeleteFeatures.cs:110-114` + `PeWorkflowPanel.tsx:129-139` + endpoint `/purchase-evaluations/{id}/by-approver` `:297`).

⇒ Spec `:53` viết "mirror khuôn PE prod" cho một điều chỉnh mà **lời anh nói "tương tự như KHKK"**. Theo luật S176 (đã-deploy = CHUẨN) + đúng chữ của anh, đích đồng-khuôn là **KHKK**: xoá được ở **Nháp / Trả lại / Từ chối**, **cấm** xoá khi đang duyệt. Mở thêm đường approver-delete = **nhập tính năng của PE**, vượt cả khuôn được trích lẫn câu của anh, và nó đụng **đường xoá dữ liệu** ⇒ phải có chữ "đồng ý" của anh trước.
🟢 Vế còn lại của W5 thì **đúng và cần**: `ContractFeatures.cs:896` so `Phase >= DangInKy` với `DangInKy = 5` (`ContractPhase.cs:20`) trong khi V2 dùng `ChoDuyet=10 · TraLai=98 · TuChoi=99` (`:25-27`) ⇒ hiện **cả 3 trạng thái V2 đều bị chặn xoá**, kể cả phiếu vừa bị Trả lại — lệch đúng cái mà anh đã chốt cho KHKK ở S177.

### F-12 · CRITICAL — Bằng chứng "KHKK có nút Xóa ở màn duyệt" trỏ vào **CODE CHẾT**; anchor `:118` thật ra là dòng khai ĐÃ GỠ nút đó

Invest `:90` (và spec `:60-61`) căn cứ: *"KHKK panel mục 4 `KhkkWorkflowPanel.tsx:12-14,118,233`"*. Mở ra:
- `:117-118` nguyên văn: *"🧊 [S175] `isDraftLike` / `canSubmit` / `canDeletePlan` **ĐÃ GỠ** cùng 2 nút *Gửi duyệt* + *Xóa phiếu* — panel này nay chỉ lo màn ĐANG DUYỆT (Duyệt / Trả lại)."*
- `:438-440`: *"*Gửi duyệt* và *Xóa phiếu* **ĐÃ RỜI khỏi đây** → nay nằm ở THANH NÚT ĐÁY"*.
- `:12-14` là **mục lục comment đầu file chưa cập nhật** (vẫn liệt "4 Nút … + nút Xóa phiếu") — cite đúng dòng nhưng dòng đó nói dối.
- Dialog `confirmDelete` còn ở `:742-750` nhưng **`setConfirmDelete(true)` = 0 hit toàn file** (chỉ có `useState(false)` `:107` và 3 chỗ set `false`) ⇒ **không có đường bấm tới**. Nút Xoá thật của KHKK nằm ở host: `KhkkDetailContent.tsx:1077-1096` (thanh nút đáy, `window.confirm`, hướng tới người soạn).

⇒ Câu "KHKK CÓ mà GĐ3 THIẾU" là **so sánh với một khối code không chạy**. Điều đúng còn lại: `ContractWorkflowPanel.tsx` **0 hit** mọi biến thể "Xóa/Delete" — HĐ thiếu **đường xoá phía người soạn ở màn 3-panel**, và đó mới là món nên port (từ `KhkkDetailContent`, không phải từ `KhkkWorkflowPanel`).
📌 Bài chung: đây là ca `feedback_stale_anchor_born_dead` + `dry-run bắt lời-hứa-chưa-land` — spec đang mang 1 anchor tự-vô-hiệu vào wave.

### F-7b · minor — Nếu port đường xoá theo KHKK thì phải port cả **vết audit**, spec chưa nhắc

KHKK ghi `ContractSigningPlanChangelog` (Action=Delete, PhaseAtChange, Reason) và **bắt buộc thứ tự `Remove()` TRƯỚC, `Add(changelog)` SAU** (`:1787-1803`, bug T26 S155 — làm ngược thì cascade `Immediate` DETACH row changelog ⇒ ghi 0 dòng, **im lặng**). Contract hiện xoá trần: `ContractFeatures.cs:891-901` — 0 changelog, 0 `Reason`. Wave nào nới đường xoá cũng phải mang theo vế này, kèm cùng cái bẫy thứ-tự.
🟢 **Câu hỏi treo ② của lead (soft-vs-hard delete) TRẢ LỜI ĐƯỢC NGAY**: `Contract : AuditableEntity` (`Contract.cs:8`) + `AuditingInterceptor.cs:54-63` (đổi `EntityState.Deleted` → `Modified` + `IsDeleted/DeletedAt/DeletedBy`) ⇒ `db.Contracts.Remove(entity)` (`:899`) **đang là SOFT-DELETE**, khớp màn `/contracts/deleted`. Chốt vào spec để wave khỏi mở lại — và để không ai "sửa" `Remove` thành đường khác.

### F-8 · minor — Acceptance W1(iv) đo được, control-dương đã kiểm

`grep -rn "workflow-matrix?type=3" fe-user/src` hôm nay = **1 hit** (`Layout.tsx:151`) ⇒ phép đo có răng (không phải 0-hit sẵn). Control dương `workflow-matrix?ct=` cũng sẽ ≥1 sau sửa. ✅ Nhưng lưu ý: nếu theo F-1 mà GIỮ `type=3`, thì **acceptance (iv) phải viết lại** — bằng không nó ép implementer làm đúng cái sai.

### F-9 · minor — Đếm "17 leaf" của W4 ĐÚNG (đã truy `AwV2_Contract`)

Nhánh `code === 'Contract'` trong `fe-admin/src/components/Layout.tsx:178` là **nhánh chết**: `grep -rn "AwV2_Contract" src/Backend` = **0 hit** (không có const trong `MenuKeys.cs`, không seed) ⇒ không có menu-row ⇒ sidebar sau W4 = 2 + 8 + 7 = **17**. Acceptance W4(i) giữ nguyên được.
Cơ chế W4 cũng khớp: menu-key `AwV2_Hd{token}` (`MenuKeys.cs:151`, seed `DbInitializer.cs:2303-2310` Order 11..17) ⟂ `contractWfTypeCode('HD-TP') = 'HdTP'` (`contractWorkflowFamily.ts:69,77-80`) ⇒ `findContractWfByTypeCode(code)` khớp đúng phần sau `AwV2_`. **W4 là wave lành nhất — nên chạy TRƯỚC/song song thật sự.**

### F-10 · minor — "flat-list fallback **server-side**" là delta ngầm so hiện trạng (nhưng khả thi FE-only)

`MyContractsPage.tsx:73-88` đang tìm **client-side** trên 100 dòng đã tải. Muốn đúng khuôn R-11 (KHKK tìm đi server) thì phải nối `q` → param `search` của `/contracts` — param **có sẵn** (`ContractsController.cs:19` + `ContractFeatures.cs:358`), nên vẫn FE-only. Spec nên nói thẳng "đổi ô tìm sang server-side" thay vì để chữ "server-side" trôi trong ngoặc, kẻo implementer giữ nguyên client-side rồi vẫn tick ✅ acceptance (iii) "gõ search → flatMode".

### F-11 · minor — Mirror 2 app: 3 cặp file ĐANG identical ở baseline ⇒ acceptance SHA có nghĩa

Đo `sha256sum` (16 ký tự đầu): `usePipelineStages.ts` `1b9b5c61…` (user ≡ admin) · `PipelineTreePanel.tsx` `4ffc96be…` (≡) · `ContractWorkflowPanel.tsx` `9cdf9ffe…` (≡). `MyContractsPage.tsx` **chỉ có ở fe-user** — đúng như spec khai.
2 chỗ spec nói "KHÔNG mirror" cũng đúng: `contractWorkflowFamily.ts` tự khai fe-admin-ONLY (`:16-18`) và fe-admin ẩn `Ct_*` (`Layout.tsx:246-248` `key.startsWith('Ct_')`). ✅ Không có finding — ghi lại để lượt sau khỏi đo lại.

---

## Đề nghị sửa spec

**Sửa BẮT BUỘC trước khi mở wave (3):**
1. **§2 Đ2+Đ3** — bỏ chỉ đạo "bỏ `type=3` khỏi URL"; giữ `?type=3&ct=N` (khuôn KHKK `Layout.tsx:216`), chữa bug A bằng nhánh accordion (ưu tiên `ct` + loại trừ pathname `/contracts/workflow-matrix`). Sửa kèm acceptance W1(iv). [F-1]
2. **§2 Đ4-W3** — thay "BE param `pendingMe` sẵn" bằng khuôn PE thật: `pendingMe=1` ⇒ gọi `/contracts/inbox` (0-param) + lọc `type` client-side; hoặc khai W3 CÓ delta BE. Siết acceptance: 1 HĐ không-đến-lượt phải VẮNG. [F-2]
3. **§2 Đ1-W2** — bổ sung M-1 (`!canPe` ⇒ phẳng) + ca "HĐ tạo trực tiếp, ngoài gói" vào cả cách-làm lẫn acceptance; nói rõ ô tìm chuyển server-side. [F-3, F-10]
4. **§2 W5** — viết lại theo khuôn **KHKK** chứ không phải PE: (a) nới guard `ContractFeatures.cs:896` thành allow-list **{DangSoanThao, TraLai, TuChoi}** cho **Drafter/Admin** (đúng `ContractSigningPlanFeatures.cs:1763-1783`), (b) port nút Xoá vào chỗ tương đương `KhkkDetailContent.tsx:1077-1096` — **KHÔNG** lấy `KhkkWorkflowPanel` làm khuôn (code chết), (c) kèm changelog + thứ-tự `Remove` trước `Add`, (d) **tách hẳn** phần approver-delete + `AllowApproverDelete` ra thành mục chờ anh duyệt (Q4). Acceptance phải có ca **"ChoDuyet ⇒ VẪN CẤM xoá"** — đúng chiều khuôn KHKK, ngược hẳn acceptance hiện tại `:94-96`. [F-7, F-12, F-7b]

**3 câu hỏi ĐÁNH SỐ nên trình anh (thay cho mục "ngoài wave" hiện đang không có số):**
- **Q1 (Thứ-1):** Ý anh là **hoán đổi 2 khung** (cây sang trái, danh-sách chuyển sang giữa/phải) hay **bỏ hẳn khung danh-sách**, để danh-sách chỉ hiện khi tìm/lọc như trang KHKK? *(Cây hiện chỉ chứa HĐ sinh từ Duyệt NCC/KHKK — HĐ tạo trực tiếp sẽ không nằm trên cây.)*
- **Q2 (Thứ-1 × Thứ-2):** Khi anh bấm "01. HĐ Thầu phụ → Danh sách", khung trái nên **chỉ hiện HĐ Thầu phụ**, hay hiện cây đầy đủ mọi loại (như cây hiện nay) và thêm ô chọn loại giống trang KHKK?
- **Q3 (Thứ-4):** Màn duyệt HĐ có cần **3 món KHKK đang có** không: ô "Cấp này KẾT THÚC quy trình", nhãn "Duyệt thay CEO", và dòng báo "kết thúc sớm ở Bước X"? *(Hai cờ này anh đã thấy trong dialog Sửa-tại-chỗ ở Ảnh 3; bật ở Admin mà màn duyệt HĐ chưa đọc.)*
- *(Giữ nguyên câu ① của spec — thay/gỡ ghim `QT-HD-V2-001` — nhưng cũng nên đánh số Q4: nó là điều-kiện để lời "mỗi loại 1 quy trình" đúng tuyệt đối trên DỮ LIỆU, không chỉ trên UI.)*

**Thứ tự wave — đề nghị đổi:**
`W4` (5 dòng, rủi ro thấp nhất, đã verify khớp cơ chế) → `W1` (bug anh đau nhất) → **chốt Q1/Q2 rồi mới** `W2` → `W3` (phụ thuộc panel của W2: sau khi panel-1 thành cây thì "Đang duyệt" hiển thị ở đâu phải được định nghĩa) → `W5` (sau Q3, gộp cùng lượt nếu anh duyệt 3 món để khỏi mở `ContractDetail` DTO hai lần).
Lý do đổi: W2 là wave DUY NHẤT có quyết-định-của-anh chưa chốt (Q1/Q2) — để nó ở giữa chuỗi sẽ chặn W3.
