# sub-plan-review-fable — STAGE C plan-review TỔNG THỂ + TỔNG HỢP PLAN wave — YC-023 lượt 4/4

> Run `2026-08-10-S187-duyet-hd-tu-khkk` · vai `reviewer` (adversarial, propose-only) · ENGINE `/fable-real` (H21 lệnh-A) · 2026-08-10.
> Đề bài 8 mục: (1) review tổng thể 2 invest + 7 flag style, claim load-bearing MỞ CODE verify lại; (2) FLAG-2 wave BE-DTO mồ côi — định vị; (3) FLAG-1 chốt F3 verify-only; (4) FLAG-3 nhánh điều-kiện trục 7-type⟂8-nhóm; (5) FLAG-4 quyết họ key `AwV2_Hd*`; (6) FLAG-5/6/7 disposition; (7) TỔNG HỢP PLAN wave W0..Wn; (8) hợp nhất 12 câu CHỜ-ANH.
> Ghi TỪNG MỤC trong lúc làm (chống #53). Return chỉ là tóm — file này là chân lý.

## §G0 — Ground-truth độc lập (đo tươi, không chép)

- **Mig trên đĩa = 72** (`ls Migrations | grep -c Designer.cs` = 72; cuối `20260806070242_AddMaHangMucToContractSigningPlanLines`) — KHỚP invest-BE §0 (brief "71" sai, invest tự sửa đúng).
- STATUS.md:6 mốc SỐNG S185: test **668** · Policies **452** · Menu keys **113** — khớp invest-BE §0 trích STATUS:6.
- 4 file PIN đọc trọn: run.md (46 dòng) · sub-invest-be (171 dòng, `== HẾT ==`) · sub-invest-fe (116 dòng, `== HẾT ==`) · sub-style-review (145 dòng, `== HẾT ==`).

---

## §1 — REVIEW TỔNG THỂ: verify code THẬT từng claim load-bearing (mục 1 đề bài)

### 1a. Bảng verify — mọi claim đắt đều MỞ FILE đọc lại, không tin chép

| # | Claim (nguồn) | Đo phiên này | Verdict |
|---|---|---|---|
| 1 | Bridge K7 251 dòng; guard pin 4 vế KHÔNG so ContractType; ContractType chọn TAY (BE §1b/§3a) | `CreateContractFromSigningPlanFeatures.cs` = 251 dòng đúng; `:53` `ContractType` trong request; `:134-148` đúng 4 vế tồn-tại/type/IsActive/IsUserSelectable, 0 vế so loại; `:23-25` comment tự khai "KHÔNG suy được từ ContractType" | ✅ KHỚP |
| 2 | `ApplicableType=3` = 1 giá trị cho cả 7 loại HĐ (BE §1b) | `ApprovalWorkflow.cs:57` `Contract = 3, // HĐ general (any ContractType)`; `:70` `ContractSigningPlan = 10` slot riêng | ✅ KHỚP |
| 3 | Inbox V2-aware — đính chính S156 STALE (BE §1c ⟂ FE §3) | `ContractFeatures.cs:532-534` resolve v2InboxIds; `:536-538` thoát-sớm xét CẢ 2 nguồn; `:543` `\|\| v2InboxIds.Contains(c.Id)`; `:559-593` `ResolveV2InboxIdsAsync` in-memory join, `:588-589` OR-of-N | ✅ BE ĐÚNG — FLAG-1 chốt §3 |
| 4 | Create pin OPTIONAL + guard thiếu 2 vế (BE §1c-1/2) | `ContractFeatures.cs:37` `Guid? ApprovalWorkflowId = null`; `:79-87` chỉ exists+type — drift 1-luật-2-site với bridge `:141-148` THẬT | ✅ KHỚP |
| 5 | UpdateDraft KHÔNG re-pin (BE §1c-3) | `ContractFeatures.cs:136-145` record 9 field, 0 field `ApprovalWorkflowId` | ✅ KHỚP |
| 6 | Bom 1 deactivate per-TYPE (BE §2c) | `ApprovalWorkflowV2AdminFeatures.cs:370-377` ternary `typeEnum == ContractSigningPlan` — type-3 rơi nhánh per-TYPE `:374-376`; comment `:363` "bất biến per-TYPE của các loại khác"; R-6 `:365-368` CẤM regex | ✅ KHỚP — FLAG-5 nit đúng |
| 7 | Bom 2 overview Active-đơn (BE §2c) | `:205-215` `versions.FirstOrDefault(v => v.IsActive)` per type — 7 active cùng type sẽ hiện 1 giấu 6 | ✅ KHỚP |
| 8 | DTO-gap `currentApproval`/`approvalFlow` (FE §7 + FLAG-2) | grep Application/Contracts = **0 hit**; **control dương** PE: `PurchaseEvaluationDtos.cs:158/:186/:309-310` + Features `:1046-1047` CÓ | ✅ KHỚP — FLAG-2 THẬT |
| 9 | Khuôn KHKK 8 key `AwV2_KhkkN*` + 32 policy (FLAG-4) | `MenuKeys.cs:127-134` 8 const; comment `:123-126`: vào `All` ⇒ 32 policy + 2 row STATUS đổi + **invariant T6: vào All BẮT BUỘC có menu-row, thiếu = Permission FK nổ lúc boot** | ✅ KHỚP — §5 quyết |
| 10 | `KhkkWorkflowPanel` = khuôn màn duyệt mới nhất; owner-trim bỏ Từ chối @S174 (FE §2-bis) | `KhkkWorkflowPanel.tsx:482` 🧊 comment "Nút TỪ CHỐI ĐÃ GỠ — anh chốt 'vậy thì bỏ luôn từ chối'"; header `:10-32` cấu trúc 7 khối | ✅ KHỚP |
| 11 | Mig mốc 72 (BE §0, brief ghi 71 SAI) | đo đĩa: 72 file Designer; cuối `20260806070242_AddMaHangMucToContractSigningPlanLines` | ✅ KHỚP |
| 12 | fe-user regex 6-leaf `Ct_` / fe-admin ẩn root Contracts (FE §1) | `fe-user/Layout.tsx:143` regex `^Ct_([^_]+)_(List\|Create\|Pending\|WfView\|Approved\|Deleted)$`; `fe-admin/Layout.tsx:227-230` `ADMIN_HIDDEN_KEYS` chứa `'Contracts'` | ✅ KHỚP + phát hiện mới 1c-③ |

### 1b. 🔴 PHÁT HIỆN MỚI của plan-review (3 điểm KHÔNG có trong cả 3 bản trước)

**① Khuôn DTO cho màn duyệt: FE invest đề xuất SAI KHUÔN so với panel nó chọn mirror — phân xử BẰNG CODE.**
- FE §7/F2 đề xuất BE thêm cặp `currentApproval` + `approvalFlow` (cookie-cutter DTO **PE** S17 — BE precompute status Done/Current/Pending). Style-review FLAG-2 cũng đi theo hướng đó.
- Nhưng panel được chọn mirror là **`KhkkWorkflowPanel`** — và chính header nó khai (`:19-25`): *"🔴 KHÁC PE Ở CHỖ ĐẮT NHẤT: PE nhận `approvalFlow` đã có `status` do BE precompute. **KHKK BE trả CÂY THÔ — panel TỰ SUY** từ cặp con-trỏ `currentWorkflowStepIndex` (INDEX 0-based) + `currentApprovalLevelOrder`"*. Khuôn KHKK trên đĩa: detail DTO mang **ĐỦ CẶP con-trỏ + cây thô inline** (`ContractSigningPlanFeatures.cs:194` `CurrentWorkflowStepIndex` + `:205` `List<...WorkflowStepDto> WorkflowSteps`; FE `khkk.ts:381`).
- Contract DTO hiện tại (`ContractDtos.cs:27-64`): CÓ `ApprovalWorkflowId` + `CurrentApprovalLevelOrder` + `LevelOpinions`, **THIẾU `CurrentWorkflowStepIndex` + THIẾU cây steps** (comment `:61`: Section 5 read-only đang fetch shape qua endpoint AwV2 riêng).
- ⇒ **Phân xử: wave BE-DTO nên theo khuôn KHKK (cây thô + đủ cặp con-trỏ), KHÔNG mirror cặp DTO PE.** Lý do đo được: (i) panel mirror tiêu thụ ĐÚNG shape KHKK — chọn shape PE là phải VIẾT LẠI panel, hết "mirror"; (ii) KHKK là khuôn mới nhất qua 8 đợt UAT S179; (iii) delta BE nhỏ hơn (2 mảnh vs 2 DTO + logic precompute). Đây là quyết KỸ THUẬT — 2 tiền lệ đều sống, chọn cái khớp UI-khuôn đã chọn — KHÔNG cần owner (vào mục "tự trả lời được" §8).

**② fe-admin ẩn `Ct_*` bằng 2 TẦNG, không phải 1:** `fe-admin/Layout.tsx:248` `return ADMIN_HIDDEN_KEYS.has(key) || key.startsWith('Ct_')` — ngoài ẩn root đệ-quy còn PREFIX-RULE riêng cho `Ct_*`. FE invest §6 chỉ kê ADMIN_HIDDEN_KEYS. Wave admin-visibility (F5) nếu chạy phải gỡ **CẢ 2 chỗ** + nới regex `:117` — thiếu 1 = leaf drop im lặng (#50).

**③ Đường fetch cây workflow ĐÃ CÓ SẴN:** `ContractDtos.cs:61` comment khai FE Section 5 fetch flow qua `/api/approval-workflows-v2/{id}` — nghĩa là dù chưa sửa DTO, cây đã lấy được. Chọn inline (khuôn KHKK, 1 request, panel mirror nguyên) vẫn đúng hơn — nhưng implementer cần biết đường cũ để KHÔNG đẻ bản fetch thứ 3.

### 1c. Verdict tổng thể 3 bản

- **Invest BE:** mọi claim load-bearing tao re-verify độc lập đều đúng (12/12 bảng 1a + đối chiếu style-review §A3 100%). Wave-list W0-W5 hợp lý nhưng **thiếu wave DTO** (FLAG-2 đúng) và W3 gộp nhiều việc (tách ở §7).
- **Invest FE:** đúng ở hiện trạng FE + gap DTO; sai 1 tiền-đề S156 (tự khai xin re-verify — FLAG-1); đề xuất DTO-khuôn-PE lệch với panel-khuôn-KHKK nó tự chọn (1b-① — không ai trong 3 bản bắt được).
- **Style-review:** 7 flag đều ĐỨNG sau khi tao re-verify bằng code; FLAG-4 verdict đúng cả chi tiết invariant T6 (menu-row bắt buộc khi key vào All).

---

## §2 — FLAG-2 (wave MỒ CÔI — nặng nhất): định vị wave BE-DTO (mục 2 đề bài)

**Xác nhận gap bằng đo:** grep `CurrentApprovalDto|ApprovalFlowDto` trong `Application/Contracts` = 0 hit (control dương PE có — bảng 1a-#8). `ContractDetailDto` thiếu `CurrentWorkflowStepIndex` + cây steps + mọi trường nguồn-gốc KHKK. `Contract.cs` không mang con-trỏ ngược plan (link ở `ContractSigningPlanLine.ContractId`).

**Wave mới: W2 "BE-DTO nền hiển thị" — nội dung chốt theo phân xử §1b-①:**
1. **(a) Nguồn-gốc KHKK (nuôi F1):** `ContractDetailDto` + `ContractSourceDto?` — reverse-join `ContractSigningPlanLines WHERE ContractId = id` → plan (`MaKeHoach`, `PlanId`, `ApprovalGroup`) + lines (`TenHangMuc[]`, Σ`ApprovedAmount`) + `PurchaseEvaluationId` (chuỗi sâu GĐ1). Null-path: HĐ cũ 0 dòng nối → field null, FE không render card.
2. **(b) Con-trỏ + cây V2 (nuôi F2):** +`CurrentWorkflowStepIndex` (int?, đọc thẳng entity — `ResolveV2InboxIdsAsync:567` chứng field sống) + `List<ContractWorkflowStepDto>? WorkflowSteps` cây thô mirror `ContractSigningPlanFeatures.cs:194/:205` (steps + levels + tên NV + cờ per-level). **KHÔNG** sinh cặp DTO PE-precompute.
3. Positional record: append field CÓ DEFAULT null ở cuối — wire cũ không vỡ; types/contracts.ts cập nhật ×2 app cùng wave.

**Vị trí trong thứ tự:** W2 chạy được **NGAY** — 0 phụ thuộc câu owner nào (nguồn-gốc + con-trỏ là bất biến mọi nhánh trục/PA; đường đọc qua Line là hiện trạng đã ship). Chặn-bởi-nó: F1 (card nguồn gốc) + F2 (màn duyệt) — cả 2 wave FE đứng sau W2 đúng 1 nhịp. Acceptance đo được: HĐ dry-run K8 (`ZZTEST-K8/...`) GET detail trả `source.maKeHoach = "KHKK/2026/00x"` đúng + HĐ tạo tay trả null; test DTO mới (baseline 668 tăng); build + cicd 3-chân.

---

## §3 — FLAG-1: chốt F3 = VERIFY-ONLY (mục 3 đề bài)

**Chốt hẳn bằng code (bảng 1a-#3):** `/contracts/inbox` ĐÃ V2-aware trên đĩa từ W6 S161 — `ContractFeatures.cs:530-593` đủ 3 mảnh (resolve đúng Cấp-đang-chờ + OR-of-N + thoát-sớm xét cả 2 nguồn). Chẩn đoán S156 "inbox 100% V1-LEGACY" **HẾT HIỆU LỰC**; FE tự khai xin re-verify là đúng kỷ luật, và re-verify ĐÃ XONG tại đây.

**Disposition trong plan:** F3 **KHÔNG còn là wave code** — hạ thành **hạng mục VERIFY trong wave dry-run cuối** (W10 §7): tạo HĐ V2 từ bridge → assert xuất hiện ở `/inbox?type=N` của approver Cấp-1, user ngoài Cấp không thấy. FE 0-đổi (đúng nhánh rủi ro FE tự viết). Việc kèm: diary investigator đính chính S156-stale (BE Surprise #2 đã ghi — lead nhắc vai FE cùng đính chính; 1 dòng, không wave).

⚠️ Ranh giới để KHÔNG đọc rộng verdict này: inbox V2-aware ≠ "màn duyệt xong" — cái thiếu THẬT cho GĐ3 vẫn là DTO (W2) + UI trạm (F2/W6); và per-type server-filter của inbox vẫn client-side (FE F4 đúng).

---

## §4 — FLAG-3: nhánh điều-kiện trục 7-type ⟂ 8-nhóm (mục 4 đề bài)

FE §5 + bảng F1-F5 viết CỨNG trục 7-type trong khi Q1 (BE#1) còn treo — plan phải mang nhánh điều-kiện, KHÔNG hardcode trước. Cách gắn vào plan §7:

| Mảnh việc | Nếu Q1 = **7-type** (default) | Nếu Q1 = **8-nhóm** | Nếu Q1 = **lai** |
|---|---|---|---|
| Discriminator phiếu | `Contract.Type` CÓ SẴN — 0-mig | +1 AddColumn `Contract.ApprovalGroup` (khuôn Mig 71, backfill đọc ngược `Line→plan.ApprovalGroup`) — mig điều-kiện (b) của BE §4 | như 8-nhóm + luật chọn trạm theo ngưỡng — cần owner vẽ ma trận |
| Họ workflow W3-seed | 7 Code `HD-{TYPE}` | 8 Code `HD-N{n}` (mirror sát `KHKK-N{n}` hơn) | theo ma trận owner |
| Guard khớp W4 | so `Contract.Type` ↔ token Code | so `Contract.ApprovalGroup` ↔ token Code | 2 vế |
| Menu/param FE | GIỮ NGUYÊN 42 leaf `Ct_*` + `?type=N` — FE §5 đúng nguyên trạng, 0 key mới phía menu phiếu | menu phiếu theo nhóm = vẽ lại theo khuôn `Khkk_G*` (49 key S164-S169) — **đắt, đổi cả sidebar đã fan-out 7 loại anh đã thấy prod** | giữ `Ct_*` (định danh menu theo loại) + badge nhóm trong phiếu |
| Key Designer §5 | +7 `AwV2_Hd*` | +8 `AwV2_HdN*` | theo số họ |
| W1 (gỡ bom) · W2 (DTO) · F4 · F1-card | **KHÔNG ĐỔI ở mọi nhánh** — deactivate per-Code + overview family + nguồn-gốc + con-trỏ V2 là bất biến trục | ← | ← |

**Điểm nghiêng đo được (không quyết thay owner):** trục 7-type khớp (i) chữ YC-023, (ii) 42 leaf `Ct_*` đã seed + sidebar 7 mục anh xác nhận hình prod, (iii) `Contract.Type` sẵn = 0-mig. Trục 8-nhóm khớp chuỗi ký nháy SP-002 (dữ kiện thật: `InitialsChain`/`SignerRole` đi theo nhóm, không theo loại). Chi phí đổi-ý sau khi build: nhánh 7-type → 8-nhóm đắt ở W3+W4+FE-menu; W1/W2 vô cảm. ⇒ thứ tự wave §7 xếp W1/W2 (bất biến) TRƯỚC, W3/W4 SAU câu Q1 — plan không phải đoán.

---

## §5 — FLAG-4: QUYẾT TƯỜNG MINH họ key `AwV2_Hd*` (mục 5 đề bài)

**Quyết: CÓ sinh — trong nhánh PA-A (Q2), mirror TRỌN khuôn K3-N1, đặt trong W3 (cùng wave seed workflow).** Đây là quyết mức-lead (tiền lệ: 8 key KHKK vào `All` là "lệnh lead K3-N1" — `MenuKeys.cs:123` — không phải quyết owner), owner chỉ cần biết hệ quả số.

**Trade-off từng nhánh:**
- **Nhánh SINH (+7 key `AwV2_Hd{Code}`, đề xuất):** admin sidebar có mục Designer per-loại (đúng UX KHKK đang chạy — fe-admin KHÔNG ẩn root `ApprovalWorkflowsV2`, họ `AwV2_KhkkN[1-8]` đang sống ở `fe-admin/Layout.tsx:188`); grant per-loại được (chỉ cho CCM sửa quy trình Thầu phụ). Giá: Menu keys 113→120, Policies 452→480 (DERIVED |All|×4) — **2 row canonical STATUS đổi CÙNG COMMIT** + test T2 khoá drift + **invariant T6: key vào `All` BẮT BUỘC có menu-row trong `SeedMenuTreeAsync`, thiếu = Permission FK nổ lúc boot** (`MenuKeys.cs:125-126` — bẫy nổ NGAY lần deploy đầu, không âm thầm).
- **Nhánh KHÔNG SINH (chỉ Bom-2 family-panel):** rẻ hơn (0 key, 2 row STATUS giữ), Designer vào qua panel type-3 family. Mất: mục sidebar per-loại + grant per-loại. **Lệch khuôn đã deploy** — KHKK có CẢ HAI tầng (key sidebar + panel family), bỏ 1 tầng phải có lý do mà chưa ai nêu được lý do ngoài "tiết kiệm" ⇒ theo LUẬT LÕI không chọn.
- **Phụ thuộc:** số key theo đáp án Q1 (7 hay 8 — bảng §4); nếu Q2 = PA-B thì Designer per-cột có thể thay đổi hình key ⇒ dòng key này thi công SAU khi Q1+Q2 chốt (đúng vị trí W3). KHÔNG phụ thuộc Q6 (admin-visibility chỉ bàn 4 root pipeline; root `ApprovalWorkflowsV2` không nằm trong `ADMIN_HIDDEN_KEYS`).
- **Sửa lời FE invest:** câu "0 key mới" của FE §5 vẫn ĐÚNG cho **menu phiếu** (42 leaf `Ct_*` đủ) — chỉ SAI khi đọc thành "0 key toàn cục". Plan tách 2 mệnh đề này để không ai đọc nhầm lần nữa.

---

## §6 — FLAG-5/6/7: disposition từng nit (mục 6 đề bài)

| Flag | Nội dung | Disposition | Lý do |
|---|---|---|---|
| FLAG-5 🟡 | W1 predicate deactivate: giữ R-6 CẤM regex (tập-type tường minh) + sửa comment `:363` CÙNG edit | **NHẬN — vào acceptance W1** | Đo lại `:365-368` R-6 nguyên văn CẤM regex vì lý do sống (Code ngoài họ rơi nhầm nhánh nuke); comment `:363` không sửa = đẻ đúng bug-class 1-luật-2-site mà Surprise #6 BE đang tố. Cả 2 rẻ (1 edit), phòng bug đắt |
| FLAG-6 🟡 | W3-seed mã họ ASCII UPPER + hằng 1-nhà riêng, KHÔNG đụng nhà KHKK | **NHẬN — vào acceptance W3** | Validator Designer `^[A-Za-z0-9._-]+$` (`:274-276`) — seeder không qua validator nhưng admin tạo version thì QUA ⇒ seed mã có "Đ" là kẹt vòng đời THẬT. Token họ mới cần nhà `GroupCodePattern` RIÊNG (guards KHKK `:44-45` tự khai "chép lần nữa = 3 bản chuỗi rời nhau") |
| FLAG-7 🟡 | W4 "bổ 2 vế" cho Create bằng NHÀ CHUNG `public static`, 4 cửa gọi CÙNG method | **NHẬN — nâng thành yêu cầu kiến trúc W4 + fault-inject** | Đã có 2 bản inline (`ContractFeatures.cs:79-87` + bridge `:134-148`); inline lần 3 = nhân bản luật lần nữa. Acceptance: fault-inject 1 cửa quên gọi phải ĐỎ (khuôn test-before W1b S182) |

Cả 3 NHẬN — 0 bác, 0 hoãn. Chúng là nit thi-công đúng chỗ, chi phí ~0 khi biết trước, đắt khi phát hiện sau merge.

---

## §7 — TỔNG HỢP PLAN WAVE (mục 7 đề bài) — 11 wave W0..W10

> **Gate CHUẨN mọi wave code (nếp KHKK S161):** `dotnet build` 0-err + `npm run build` ×2 app 0-err (trọng tài chạy SAU agent dừng tay — #68) + `dotnet test` baseline `docs/STATUS.md` (668 @S185) GIỮ-hoặc-TĂNG theo scope + reviewer diff-review + cicd 3-chân (deploy xanh · marker-dương bundle/DLL · **control-âm ĐỘC QUYỀN** — chuỗi chỉ tồn tại trong bản mới) + SHA-pair `git hash-object` từng cặp file mirror cho wave FE. Wave BE nào 0-mig thì acceptance kèm "0 file Migrations đổi".

| W | Tên · phạm vi | File/khu vực đích | Phụ thuộc | Chờ câu nào (§8) | Chạy NGAY? |
|---|---|---|---|---|---|
| **W0** | **Chốt trục + roster** (0 code): trình danh sách §8, thu đáp án tối thiểu Q1/Q2/Q4 (Q3/Q5/Q6/Q9 lấy được cùng lúc càng tốt); ghi sổ YC mã vĩnh viễn | `docs/governance/so-yeu-cau-chu-du-an.md` | — | sinh ra đáp án | ✅ (việc của lead+owner) |
| **W1** | **Gỡ 2 bom per-type** (BE, 0-mig): mở deactivate per-Code cho type-3 (tập-type tường minh — R-6 CẤM regex, FLAG-5) + sửa comment `:363` CÙNG edit + overview family type-3 (Bom-2) | `ApprovalWorkflowV2AdminFeatures.cs:205-215/:356-377` + test-before RED→GREEN (khuôn W3-KHKK): (i) POST version code loại X → IsActive code loại khác GIỮ (đếm SQL trước/sau); (ii) overview type-3 trả ĐỦ N active | — (đúng ở MỌI nhánh Q1/Q2 — §4 hàng cuối) | 0 | ✅ **NGAY** |
| **W2** | **BE-DTO nền hiển thị** (BE, 0-mig — wave MỚI đóng FLAG-2, nội dung §2): (a) `ContractSourceDto` reverse-join nguồn KHKK; (b) +`CurrentWorkflowStepIndex` + `WorkflowSteps` cây thô **khuôn KHKK** (§1b-① — KHÔNG cặp DTO PE) | `ContractDtos.cs` + `ContractFeatures.cs` GetContract + `types/contracts.ts` ×2 app; mirror `ContractSigningPlanFeatures.cs:194/:205` | — (bất biến mọi nhánh) | 0 | ✅ **NGAY** |
| **W3** | **Seed họ workflow type-3 + key Designer** (BE, 0-mig data-op): seeder per-Code idempotent mirror `SeedKhkkGroupWorkflowsAsync` (thiếu user → bỏ slot + warning, KHÔNG throw boot) + mã họ ASCII UPPER hằng 1-nhà riêng (FLAG-6) + **+7 key `AwV2_Hd*` vào `All` + menu-row + 2 row STATUS + T2/T6** (§5) | `DbInitializer.cs` (khuôn `:540-548/:607-625`) + `MenuKeys.cs` + `SeedMenuTreeAsync` + `docs/STATUS.md` 2 row | W1 (bom phải gỡ trước khi 7 active cùng type sống) + W0 | **Q1** (7 hay 8 Code) · **Q2** (PA-A/B) · **Q4** (roster — HARD: tên người thật, #51 seed UNGATED chạy thẳng prod lần deploy đầu) | ❌ |
| **W4** | **Guard 4 cửa pin + re-pin** (BE, 0-mig): nhà chung `public static EnsureContractWorkflow*` (FLAG-7) gọi từ 4 cửa — Create (bổ 2 vế `IsActive∧IsUserSelectable`, xoá drift §1c-2) + bridge K7 (auto-resolve theo loại) + **cửa re-pin MỚI** trên UpdateDraft (+field `ApprovalWorkflowId` — §1c-3) + cửa TRÌNH trong `ContractWorkflowService` | `ContractFeatures.cs:79-87/:136-145` + `CreateContractFromSigningPlanFeatures.cs:132-148` + `ContractWorkflowService.cs` + file guards mới mirror `ContractSigningPlanWorkflowGuards` | W3 (guard so token họ đã seed) | **Q1/Q2** (so cái gì) · **Q9** (dialog còn dropdown?) | ❌ |
| **W5** | **FE card "Nguồn gốc KHKK"** (F1): trong `ContractDetailContent` — mã KHKK link `/khkk/:planId?group=n` + badge N{n} + hạng mục + NCC + giá chốt; đối xứng chiều ngược "Đã có HĐ" | `components/contracts/` ×2 app SHA-pair; null-path HĐ cũ KHÔNG render | **W2(a)** | 0 | sau W2 |
| **W6** | **FE màn duyệt V2-trạm** (F2): mirror `KhkkWorkflowPanel` (tự-suy từ con-trỏ + cây thô W2(b)) — banner lượt-ai + gating nút + stepper Bước→Cấp; GIỮ nhánh V1 2-nút cũ cho HĐ không pin (dual-render sẵn); bộ nút theo Q5; **kèm Đ2-guard nhánh Reject BE nếu Q3 duyệt** (màn mới wire nút Reject mà API còn thủng #82 thì gating FE = trang trí) | `components/contracts/` ×2 app + `ContractWorkflowService.cs:48-66` (phần Đ2) | **W2(b)** | **Q5** (2/3 nút) · **Q3** (vế Đ2) | ❌ |
| **W7** | **FE per-type server-filter** (F4): `MyContractsPage`/`InboxPage` gửi `type` lên BE thay client-filter (param BE CÓ SẴN — style-review §E verify `ContractsController.cs:23-27`) | `MyContractsPage.tsx:42-48` + `InboxPage.tsx:91` ×2 app; đổi queryKey → invalidate cache cũ | — | 0 | ✅ **NGAY** |
| **W8** | **Authz diện rộng** (điều kiện Q3 = Đ1/Đ3): ~24 per-action policy `ContractsController` + grant seeder code-first (#84 CẤM SQL tay) + extend `AuthorizePolicyRegressionTests` + FE gate `Contracts.*` (#85) | `ContractsController.cs` + `DbInitializer` + tests (khuôn `KhkkBridgeAuthzTests` 245 dòng) | W6 (biết bộ nút/luồng trước khi siết) | **Q3** (+Q10 nếu per-loại) | ❌ |
| **W9** | **Admin-visibility GĐ3** (điều kiện Q6 = admin thấy): gỡ `'Contracts'` khỏi `ADMIN_HIDDEN_KEYS` **+ gỡ prefix-rule `key.startsWith('Ct_')` `:248`** (§1b-② — 2 TẦNG, thiếu 1 = drop câm #50) + nới regex admin `:117` thêm 3 leaf + `Ct_*_WfView` admin trỏ Designer | `fe-admin/Layout.tsx:117/:227-248` | — | **Q6** (giữ-ẩn = HỦY wave) | ❌ |
| **W10** | **Dry-run toàn trình + nghiệm thu** (khuôn K8 S168): 2 nhánh 2 loại HĐ 2 quy trình khác nhau, trọn PE→KHKK→bridge→duyệt V2 từng trạm→`DaPhatHanh`→SealedCopy trên Dev; **verify F3 inbox tại đây** (§3 — HĐ V2 ChoDuyet hiện đúng inbox approver Cấp-1, user ngoài 0 thấy); probe authz #87-aware (415/411 = PRE-AUTH); sổ ID phiếu + seq cháy (bài K8: phiếu test lệch số phiếu thật — tiền lệ KHKK/2026 001-003 đều test) | Dev DB + sổ trong run-folder; lưu ý Surprise #7 BE: Dev 0 workflow type-1/2 → tạo workflow PE trước | W3+W4+W5+W6 | 0 (chạy sau khi các câu đã chốt) | ❌ |

**Đường tới hạn:** W0 → W3 → W4 → W6 → W10 (mọi wave khác song song/chèn xen kẽ). **3 wave chạy NGAY hôm nay không cần owner: W1 + W2 + W7** — đủ việc cho 1 nhịp fan-out đầu trong lúc W0 thu đáp án. Nhịp ship theo khuôn S159-⑦ {đợt nhỏ → ship → anh nhìn prod → vẽ tiếp}.

**Khác biệt so wave-list 2 invest (khai để lead đối chiếu):** +W2 MỚI (FLAG-2); W3-BE cũ tách thành W3-seed ⟂ W4-guard (2 mối chặn khác nhau: W3 chặn bởi roster, W4 chặn bởi UX-dialog — gộp là wave nghẽn kép); F3 hạ khỏi wave-list vào W10-verify (FLAG-1); F5 thành W9 điều-kiện-huỷ-được; key Designer (FLAG-4) nhét W3 cùng nhịp seed.

---

## §8 — HỢP NHẤT 12 CÂU CHỜ-ANH → 9 câu hỏi owner + 3 mục tự-trả-lời (mục 8 đề bài)

> Gộp: BE#5 ≡ FE#2 + FE#5 → **Q3** (1 cụm authz — style-review C4 map đúng, tao nhập luôn FE#5 vì cùng 1 quyết định posture). FE#4 → **Q3b** (phụ đề trong cụm, phụ thuộc Q1). BE#7 → **Q7**. Còn lại 1-1. Mã gốc ghi kèm để truy vết.

### 8a. Danh sách hỏi owner (9 câu — mỗi câu: phát biểu · đề xuất default · wave bị chặn)

1. **Q1 [BE#1] — Quy trình duyệt HĐ chia theo TRỤC nào?** 7 **loại hợp đồng** (Thầu phụ, Giao khoán, NCC, Dịch vụ, Mua bán, 2 loại Nguyên tắc — như menu 7 mục anh đang thấy) hay 8 **nhóm duyệt N1-N8** của danh mục SP-002 (như bên Kế hoạch ký kết), hay lai (trạm theo nhóm + người ký theo ngưỡng)? → **Đề xuất: 7 loại HĐ** (khớp lời anh trong YC-023 + menu đã dựng; đổi sang nhóm sau vẫn được nhưng đắt ở seed+guard+menu — bảng §4). **Chặn W3, W4** (+hình dạng key §5).
2. **Q2 [BE#2] — Cách gắn quy trình vào loại:** làm y hệt cách Kế hoạch ký kết đang chạy (mỗi loại 1 mã quy trình riêng, 0 đổi cấu trúc DB — PA-A) hay thêm 1 cột thật trong bảng quy trình (1 migration, "chuẩn" hơn về lâu dài — PA-B)? → **Đề xuất: PA-A trước** (khuôn đã qua UAT, style-review chấm khớp nhất; PA-B để dành làm bước gộp sau nếu cần). **Chặn W3, W4.**
3. **Q3 [BE#5 ≡ FE#2 + FE#5] — Khoá quyền module HĐ tới mức nào?** Hiện MỌI người đăng nhập đều Từ-chối/Trả-lại được bất kỳ HĐ nào (lỗ S156 anh treo *"chỗ hợp đồng cứ từ từ"*) — nhưng YC-023 chính là mở màn duyệt HĐ cho nhiều người dùng thật. 3 mức: **Đ2** chỉ vá đúng lỗ nguy hiểm (chặn Từ-chối sai lượt/sai vai) · **Đ1** khoá đủ bộ ~24 endpoint như bên KHKK · **Đ3** = Đ1 + rà trọn 2 tầng menu/API. Menu HĐ đang mở 13/13 vai ("hiển thị hết để góp ý") — giữ hay thu? → **Đề xuất: Đ2 làm NGAY trong W6** (màn duyệt mới mà API thủng thì nút khoá chỉ là trang trí) **+ Đ1 thành W8 riêng sau khi anh nhìn màn mới; menu giữ public như anh đã chốt.** *(Q3b [FE#4]: có ai chỉ được duyệt MỘT loại HĐ không? Nếu không — giữ quyền chung `Contracts.*`, đỡ 1 tầng phức tạp.)* **Chặn W8 + vế Đ2 của W6.**
4. **Q4 [BE#6] — Roster người duyệt cho 7 quy trình HĐ:** mỗi loại HĐ đi qua NHỮNG AI, thứ tự trạm nào? (Bên KHKK sao từ `QT-DN-V2-001` + 11 email cứng; SP-002 chỉ có pattern ký nháy per-NHÓM — KHÔNG tự suy ra per-loại được.) → **Không có default — cần anh cho danh sách** (bảng: Loại HĐ → trạm 1..n → tên người). **Chặn CỨNG W3** (seed chạy thẳng prod lần deploy đầu — #51, sai roster là sai trên prod).
5. **Q5 [FE#1] — Bộ nút màn duyệt HĐ:** 2 nút **Duyệt / Trả lại** (như KHKK sau khi anh trim *"bỏ luôn từ chối"*) hay 3 nút thêm **Từ chối** (như Duyệt NCC)? → **Đề xuất: 2 nút theo KHKK** (khuôn mới nhất chính tay anh trim; máy BE Reject vẫn còn nếu sau cần). **Chặn phần dialog W6.**
6. **Q6 [FE#3] — Trang quản trị (admin) có cần THẤY menu GĐ3 không**, hay giữ như hiện nay (admin ẩn cả 4 mục pipeline, nghiệp vụ nằm bên eoffice)? → **Đề xuất: giữ ẩn** (đúng posture KHKK đang chạy; trang vẫn tồn tại, vào bằng URL được). Q6 = giữ ẩn ⇒ **HỦY W9** (0 công). **Chặn W9.**
7. **Q7 [BE#7] — Dialog "Đưa vào HĐ":** khi đã có quy trình đúng loại, máy tự chọn — còn cho người bấm ĐỔI TAY quy trình khác không? → **Đề xuất: tự chọn + KHÔNG cho đổi chéo loại** (mirror KHKK; pin nhầm sửa ở phiếu Nháp qua cửa re-pin W4). **Ảnh hưởng W4** (không chặn cứng — làm theo default được, đổi sau 1 edit nhỏ).
8. **Q8 [BE#3] — Danh mục SP-002 có nên thêm cột "gợi ý loại HĐ"** để dialog tự điền loại (đỡ chọn tay/chọn nhầm)? → **Đề xuất: CHƯA** — giữ chọn tay (hiện trạng); thêm sau = 1 migration nhỏ + cập nhật seed 86 dòng (mig điều-kiện (c) BE §4). **Không chặn wave nào.**
9. **Q9 [BE#4] — Ruột HĐ ở GĐ3 có cần bảng hạng mục chi tiết** (khối lượng / đơn giá / thành tiền — phụ lục 3.5 SP-002) hay chỉ cần điều khoản + tổng giá trị (hiện trạng: tên hạng mục ghép vào tên HĐ)? → **Đề xuất: CHƯA** (spec-memory: GĐ3 "bàn điều khoản, không bàn hạng mục"); nếu cần = wave line-level MỚI sau này. **Không chặn wave nào.**

**Tối thiểu để mở khoá đường tới hạn: Q1 + Q2 + Q4** (mở W3/W4). Q3/Q5 cần trước W6. Q6-Q9 trả lúc nào cũng được.

### 8b. Tự-trả-lời-được (KHÔNG hỏi owner — bằng chứng kèm)

- **T1 — Inbox "Đang duyệt" có cần wave BE không? KHÔNG.** Code đã V2-aware từ W6 S161 (`ContractFeatures.cs:530-593` — bảng 1a-#3); F3 = verify trong W10. (Đóng luôn tiền-đề S156-stale; diary FE đính chính.)
- **T2 — Khuôn DTO màn duyệt: cây-thô KHKK, không phải cặp DTO PE.** Panel mirror (`KhkkWorkflowPanel:19-25`) tiêu thụ đúng shape con-trỏ + cây thô (`ContractSigningPlanFeatures.cs:194/:205`); chọn shape PE = viết lại panel, hết mirror (§1b-①). Kỹ thuật thuần — 2 tiền lệ đều sống, chọn cái khớp UI đã chọn.
- **T3 — Sinh key Designer `AwV2_Hd*` (trong nhánh PA-A): CÓ.** Tiền lệ K3-N1 là lệnh-lead không phải quyết-owner (`MenuKeys.cs:123`); bỏ mảnh khuôn phải có lý do — chưa ai nêu được (§5). Owner chỉ cần biết 2 số canonical đổi (113→120 · 452→480, theo đáp án Q1).

== HẾT ==
