# LĂNG KÍNH 1/4 — WAVE-PLAN + ACCEPTANCE (reviewer đối kháng)

> Đích chấm: `spec-ke-hoach-ky-ket-hd-28-07-2026.md` §③ (dòng 208-340).
> Đối chứng: `runs/2026-07-27-S155-pe-delete-approver/run.md` (tiền lệ chia 2 đợt đã chạy THẬT).
> 4 trục: (a) acceptance đo được thật? (b) wave deploy độc lập thật? (c) thứ tự? (d) cờ CHẶN sai?
> Ký hiệu chứng: `spec:NNN` = dòng trong file spec; `run.md:NNN` = tiền lệ S155; còn lại là đường dẫn thật trong repo.

## Verdict

**SUA-TRUOC-W1** — khung 5 wave đúng hướng và phần lớn acceptance có hình dạng đo được, **nhưng W1 và W2 chưa
deploy độc lập được như bảng khai**, và 4 phép đo trụ cột (policy · OR-of-N · test-count · dark-launch) hiện
**không thể làm cho trượt** kể cả khi implement sai. 19 điểm: **6 HIGH · 8 MED · 5 LOW** (+ 9 điểm củng cố).
Sửa 6 HIGH là spec đủ răng để phóng W1.

---

## Điểm HIGH

### H1 — W2 đẩy riêng lên prod thì **không tạo được phiếu nào**: phần wiring Designer type-10 không nằm trong wave nào

- **Chứng:** `spec:141` lệnh Create nhận `ApprovalWorkflowId` bắt buộc; `spec:143-144` validator đòi workflow có
  `ApplicableType == ContractSigningPlan` (=10). `spec:192-194` khai phải sửa **Designer admin nhận type 10**
  (`fe-admin/.../ApprovalWorkflowsV2Page.tsx:143` + `ApprovalWorkflowV2AdminFeatures.cs`, và phải grep lại đường
  `fe-user` matrix). Nhưng danh sách file của **W1** (`spec:227-229`), **W2** (`spec:245-246`), **W3**
  (`spec:265-266`) đều **không chứa 2 file đó**.
- **Hệ quả đo được:** sau khi deploy W2, admin không dựng nổi 1 `ApprovalWorkflow` type 10 ⇒ mọi `POST` đều rơi
  validator ⇒ **acceptance W2#1 (`spec:248`, "→ 201") không thể đạt** trên chính prod mà nó vừa lên.
- **Đây đúng lớp lỗi tiền lệ:** `run.md:125` — *"F.0 chứng minh A-E không chạy nếu thiếu F"*, tức đợt-1-một-mình
  vô dụng. Spec trích đúng bài học đó ở `spec:210-211` rồi lặp lại nó ở W2.
- **Đề xuất:** đưa wiring type-10 (BE `ApprovalWorkflowV2AdminFeatures.cs` + fe-admin Designer + grep lại fe-user)
  vào **W1**, và thêm acceptance W1: *admin tạo + lưu 1 quy trình type 10 →
  `sqlcmd -Q "SELECT COUNT(*) FROM ApprovalWorkflows WHERE ApplicableType=10"` trả về `1`*. Việc này đồng thời
  biến W1 từ "0 giá trị" thành "nền + dựng được quy trình" (xem C1 ở mục thứ tự wave).

### H2 — Tên policy trong spec **không khớp** menu key của chính spec ⇒ test hiện có sẽ ĐỎ, và trên prod là **500** chứ không phải 403

- **Chứng lệch:** `spec:181` chốt 4 key = `Csp` / `Csp_List` / `Csp_Create` / `Csp_Inbox`. `spec:173-174` và
  `spec:234` lại dùng policy = `ContractSigningPlans.{Read|Create|Update|Delete}`. Chính spec khai luật sinh
  policy là `{menuKey}.{action}` (`spec:175`, foreach `All × Actions`) ⇒ với key `Csp` thì policy hợp lệ là
  `Csp.Read`, **không tồn tại** `ContractSigningPlans.Read`.
- **Repo đã có lưới bắt đúng ca này:**
  `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs:138-139` —
  `MenuKeys.All.Should().Contain(parts[0], $"policy \"{policy}\" của {action} trỏ menu key KHÔNG có trong MenuKeys.All ⇒ Program.cs không sinh policy đó ⇒ 500 lúc chạy (bẫy Pe_* trong spec S155 §B3)")`.
  ⇒ code theo `spec:173` sẽ làm test này **đỏ**, và nếu lọt thì runtime ném lỗi policy-không-đăng-ký (**500**),
  không phải 403 như W2#5 kỳ vọng.
- **Đề xuất:** chốt **một** tên canonical trước khi phóng W1 (khuyến nghị giữ key ngắn `Csp` ⇒ policy `Csp.Read`
  …, hoặc đổi key thành `ContractSigningPlans*` cho khớp tên dài) và ghi vào bảng hợp-đồng-liên-lane (M4).
  Đây là **đúng lớp `hợp-đồng-đứt-giữa-2-bờ`** của S155: hai bờ đều "hợp lý" khi đọc riêng, chỉ chết lúc nối.

### H3 — W1 acceptance #4 đo một thứ **không tồn tại trong DB**; đó lại là dòng duy nhất canh phần authz của W1

- **Chứng:** `spec:234` — *"`sqlcmd` đếm policy: `|MenuKeys.All| × 4` tăng đúng **+16**; grep
  `ContractSigningPlans.Read` có trong list policy"*. Policy được đăng ký **in-memory** trong
  `Program.cs` (`AddAuthorization`, chính spec khai ở `spec:175`); DB chỉ có bảng `MenuItems` và `Permissions`.
  **Không có bảng policy nào để `sqlcmd` đếm**, cũng không có artifact "list policy" để grep.
- **Mức độ:** W1 chỉ có 6 dòng acceptance, phần authz gói trọn trong dòng này ⇒ nếu 4 key vào nhầm chỗ
  (đúng bẫy `Pe_*` mà `spec:182-183` vừa cảnh báo), **không phép đo nào của W1 phát hiện được**.
- **Con số thì đúng:** `docs/STATUS.md:453` Menu keys = **54** ⇒ 54×4 = 216 → 58×4 = 232 = **+16**. Lỗi nằm ở
  **cách đo**, không ở số.
- **Đề xuất:** thay bằng 3 phép chạy được — (i) `dotnet test --filter AuthorizePolicyRegression` PASS;
  (ii) `grep -c "MenuKeys.Csp" src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs` trong khối `All` = 4;
  (iii) 2 row canonical `docs/STATUS.md` bump `54→58` và `216→232` cùng commit.

### H4 — Phép "OR-of-N" của W3 **không chứng minh OR-of-N** (3 lỗ, trong đó 1 lỗ khiến service *không guard gì* cũng PASS)

- **Chứng:** `spec:268-269` — *"workflow 1 Bước có 1 Cấp 2 người → **người thứ hai** duyệt được và phiếu **tiến
  sang Bước kế** (không đòi cả 2 ký)"*.
- **Lỗ 1 (tự mâu thuẫn):** workflow **1 Bước** thì **không có "Bước kế"** — duyệt xong phải sang `DaDuyet`.
  Người chạy acceptance sẽ hoặc bỏ vế sau, hoặc kết luận FAIL trên bản đúng.
- **Lỗ 2 (trúng may):** dưới đúng bug Proposal mà `spec:100-101` mô tả (`SelectMany` flatten +
  `ElementAtOrDefault(order-1)` lấy **1 row**), trong 1 cấp 2 người thì **đúng một người** duyệt được. Nếu người
  đó tình cờ là "người thứ hai" theo thứ tự flatten, **phép thử PASS trên implement hỏng**. Nhãn "người thứ hai"
  cũng không định nghĩa (thứ tự tạo? thứ tự UI? thứ tự `Order` trong DB?).
- **Lỗ 3 (thiếu ca âm):** một service **bỏ hẳn guard approver** (ai cũng duyệt được) cũng PASS dòng này ⇒ phép đo
  cho **0 bit** thông tin về authz. Đây đúng "phép 2-thế-giới": còn-guard và bỏ-guard cho cùng kết quả.
- **Đề xuất (thay nguyên dòng #1):** hai phiếu song sinh cùng workflow (1 Bước, 1 Cấp, người A + người B):
  (i) phiếu-1 **chỉ A ký** → `Phase=DaDuyet`; (ii) phiếu-2 **chỉ B ký** → `Phase=DaDuyet`;
  (iii) mỗi phiếu `SELECT COUNT(*) FROM ContractSigningPlanLevelOpinions WHERE PlanId=@id` = **1** (không đòi 2 chữ ký);
  (iv) **ca âm bắt buộc:** người C ngoài Cấp `POST {id}/transitions` → **403**, và `Phase` không đổi.
  Thêm 1 ca 2-Bước để vế "tiến sang Bước kế" có đối tượng thật.

### H5 — Lưới test rỗng ở đúng hai wave cần răng nhất; W2 **không có dòng `dotnet test` nào**

- **W1 (`spec:233`):** *"562 PASS **giữ nguyên**, 0 fail (chưa có test mới)"* — W1 không thêm test, không chạm
  bảng mới ⇒ tiêu chí này chỉ là regression-guard, **0 bit** về việc W1 làm đúng. Tệ hơn: nó còn là **kỳ vọng
  sai** nếu code theo `spec:173` (xem H2) vì `AuthorizePolicyRegressionTests` sẽ đỏ ⇒ người chạy sẽ tưởng mình
  làm hỏng thứ khác.
- **W3 (`spec:280`):** *"562 + **N mới**, 0 fail"* — `N` thả tự do ⇒ **N = 0 vẫn PASS**. Trong khi rủi ro tự khai
  của chính W3 (`spec:281`) là *"copy nhầm Proposal ⇒ mất OR-of-N **âm thầm** (build sạch, test cũ vẫn xanh)"*.
  Tiêu chí duy nhất có thể bắt lại đúng là tiêu chí không có sàn.
- **W2 (`spec:247-256`):** 7 dòng acceptance, **không dòng nào chạy `dotnet test`** — dù W2 chính là wave đẻ
  controller + policy, tức đúng thứ mà `AuthorizePolicyRegressionTests` canh.
- **Đề xuất:** W1 = *"562 PASS **và** `AuthorizePolicyRegression` xanh"*; W2 = thêm `dotnet test` với **≥3 test
  mới có tên** (Create-happy · 409-trùng-PE · 400-PE-chưa-DaDuyet); W3 = pin **N ≥ 6** và **liệt tên** (OR-of-N ×2
  chiều · ca âm 403 · con-trỏ đôi · finalize · inbox 2 vế).
- **Ghi công bằng:** con số **562 là ĐÚNG canonical** — `docs/STATUS.md:642` ghi *"test 532→562"* cho đợt PE
  nút-xóa. Lỗi ở **dạng tiêu chí**, không ở con số.

### H6 — "Dark-launch, vô hình với user" **sai trên fe-admin**, và phép đo #5 sẽ trượt trên bản làm đúng

- **Chứng cơ chế (đo trên đĩa):**
  `src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs:86-102` —
  `IsVisible` chỉ được **bơm vào DTO** (`:88`), còn **bộ lọc cuối chỉ theo `CanRead`** (`:96-100`,
  `static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess)`). Không có mệnh đề nào lọc
  `IsVisible`.
  `src/Backend/SolutionErp.Application/Permissions/Dtos/MenuDtos.cs:13` ghi thẳng:
  *"`IsVisible = false` → ẩn khỏi response `/menus/me` FE-user, **fe-admin vẫn thấy**"* (cùng nội dung ở
  `src/Backend/SolutionErp.Domain/Identity/MenuItem.cs:12`).
- **Hệ quả 1 (phép đo sai):** `spec:235` — *"`GET /api/menus` không chứa `Csp`"* — **API vẫn trả node `Csp`**
  (kèm `isVisible=false`) vì việc ẩn nằm ở phía FE. Bản implement đúng sẽ **trượt** dòng acceptance này.
- **Hệ quả 2 (cờ deploy sai):** `spec:215` khai W1 *"Deploy riêng được ✅ (vô hình với user)"* — nhưng
  **fe-admin cố ý bỏ qua cờ**, mà admin chính là người UAT. Sau W1, admin thấy 4 mục menu tiếng Việt trỏ vào
  route chưa tồn tại (routes tới **W2**, `spec:246`) ⇒ **4 mục menu chết** trên prod.
- **Đề xuất (chọn 1):** (a) **dời 4 dòng seed menu sang W2**, W1 chỉ giữ `MenuKeys` + policy + schema ⇒ dark thật,
  không phụ thuộc cơ chế `IsVisible`; hoặc (b) giữ nguyên nhưng sửa acceptance thành *"`GET /api/menus/me` trả
  node `Csp` với `isVisible=false`; fe-user KHÔNG render; **fe-admin CÓ render** — chấp nhận admin thấy mục chết
  trong 1 nhịp deploy"* và sửa cột "Deploy riêng được" của W1 thành ✅*(trừ fe-admin)*.

---

## Điểm MED

### M1 — W1 khai "❌ không chặn" nhưng thực chất **đóng băng 2 quyết định của owner** vào migration
`spec:126` — format mã *"`KHKK/{YYYY}/{Seq:D3}` — **owner chốt format**"*; `spec:128-129` — *"biến thể tối thiểu
5 bảng … nếu owner muốn cắt, cắt #7 trước"*. Cả hai đều nằm **trong** phạm vi Mig 69 của W1 (`spec:227-229`),
trong khi bảng `spec:215` ghi *"Chặn bởi owner? ❌ không"*. Đổi ý sau khi Mig 69 lên prod = tốn Mig 70
(DropTable / đổi format mã). Ở W1 bảng còn rỗng nên chi phí chưa cao, nhưng sau W2 (đã có dữ liệu) thì đắt.
**Đề xuất:** cột chặn của W1 = ⚠️ (2 câu: 7-hay-5 bảng · format mã), hỏi owner **trước** khi phóng W1.

### M2 — W3 khai *"chạy được với mặc định"* nhưng acceptance **ép làm cả 3 đường**
`spec:217`: *"⚠️ Q3 (chọn cơ chế rẽ b.12) — nhưng chạy được với mặc định"* ⟂ `spec:273-274`: *"Test cả **3 đường**
tới `DaDuyet` (thường · `AllowApproverFinalize` · `CeoApprovalThreshold`)"*. Nếu thật sự chạy mặc định thì 2/3
dòng acceptance **không có đối tượng để đo**; nếu phải làm cả 3 thì Q3 **đang chặn** chứ không phải cảnh báo.
**Đề xuất:** cắt acceptance về đường thường (W3a) + đẩy 2 nhánh kết-thúc-sớm sang W3b, **hoặc** đổi cờ W3 thành ✅ Q3.

### M3 — Spec port đúng 2 cơ chế mà owner **vừa phán "PHÁ VỠ"** ở tiền lệ, và không nhắc lại phán quyết đó
`run.md:114-115` (bảng *OWNER CHỐT — LƯỢT 3*): *"Cờ F5 `AllowApproverFinalize` → **PHÁ VỠ**"*,
*"`CeoApprovalThreshold` → **PHÁ VỠ**"* — đối lại `run.md:116` *"`Step.DepartmentId` → **AN TOÀN**"*.
Spec `spec:159-160` (§2.3 mục 2) lại **port cả hai** vào module mới, còn `spec:273` **ép test cả hai**.
**Tôi không suy được câu hỏi gốc từ một ô hai chữ** — có thể "phá vỡ" chỉ áp cho bất-biến của việc xóa-khi-duyệt
ở S155. **Đề xuất:** trích nguyên văn câu hỏi S155 vào spec trước khi code; nếu "phá vỡ" nói về bất-biến duyệt thì
đây là **chép nợ có chủ đích** và phải khai trong wave.

### M4 — Thiếu **bảng hợp-đồng-liên-lane**, đúng thứ tiền lệ dựng ra sau khi bị cắn
`run.md:135-137`: *"HỢP-ĐỒNG LIÊN-LANE — chốt TRƯỚC khi phóng (bài học đợt 1: **2 nửa build sạch mà không nối
được**) … Lệch một ký tự = silent drop, cả hai build vẫn xanh"*, kèm bảng canonical route API / menu key / route FE.
Spec §③ giao W2 cho `implementer-backend` + `implementer-frontend` + `test-specialist` (`spec:243-244`) mà
**không có bảng đó** — và H2 chính là **ca lệch tên đã xảy ra ngay trong spec**.
**Đề xuất:** thêm bảng canonical: route `/api/contract-signing-plans` (kebab, khuôn `/api/purchase-evaluations`
`run.md:142`) · 4 menu key · 4 policy · 3 route FE · tên field DTO mà FE đọc (`peReferenceAmount`,
`proposedAmount`, `approvedAmount`).

### M5 — Rủi ro được kê nhưng **không dòng acceptance nào bắt được**
- W2 `spec:257-258` kê 2 rủi ro: quên `HasQueryFilter(!IsDeleted)` ⇒ phiếu xóa mềm vẫn hiện; UNIQUE filtered thiếu
  `WHERE IsDeleted=0` ⇒ chặn oan (gotcha #57). 7 dòng acceptance `spec:248-256` **không dòng nào chạm** 2 rủi ro này.
- W1 `spec:237` kê rủi ro *"seed-permission có >1 site (S155)"* — dòng đo gần nhất là #4, mà #4 đã hỏng (H3).
  Tiền lệ ghi rất rõ điều này: `run.md:163` — *"thêm key vào **CẢ HAI** danh sách permission seed `:2092-2096`
  **và** `:2492-2496`"*.
**Đề xuất:** W2 thêm 2 dòng — (i) xóa mềm 1 phiếu → `GET /list` **không còn** nó **và** `sqlcmd` thấy row
`IsDeleted=1`; (ii) xóa rồi tạo lại đúng cặp `(PlanId, SupplierId)` → **201**, không 409. W1 thêm: `grep -n`
đếm site seed-permission = **2** (hoặc số thật) và cả hai đều có 4 key mới.

### M6 — `DELETE` **không thuộc wave nào**; cộng với guard 409 thì W2-một-mình **khoá cứng** PE tạo nhầm
`spec:177` khai endpoint `DELETE {id}` (allow-list `DangSoanThao|TuChoi`), nhưng file-list W2 (`spec:245`) chỉ có
*"Create/Update/Get/List + DossierItem CRUD + attachment"*, và W3 (`spec:265`) không nhắc. Trong khi `spec:143`
quy định chỉ cho tạo lại *"sau `TuChoi`"* — mà `TuChoi` chỉ đến được qua endpoint `transitions` của **W3**.
⇒ Trên prod chỉ-W2: một phiếu tạo nhầm là **không xóa được, không từ chối được**, và PE đó **vĩnh viễn không có
phiếu đúng**. Đây là "nửa tính năng chết" theo đúng định nghĩa mà `spec:210` đặt ra.
**Đề xuất:** đưa `DELETE {id}` vào **W2** + acceptance *"tạo → xóa → tạo lại cùng PE = 201"*.

### M7 — Ba phép đo của W3 sai phạm vi hoặc mơ hồ tới mức bản đúng cũng trượt
- `spec:270-271` *"sau mỗi lượt duyệt, `CurrentWorkflowStepIndex` **và** `CurrentApprovalLevelOrder` **đều đổi
  đúng**"* — sai theo nghĩa đen: tiến từ Cấp 1 → Cấp 2 **trong cùng một Bước** thì `StepIndex` **không đổi**
  (đó mới là hành vi đúng của con-trỏ đôi). **Đề xuất:** bảng kỳ vọng từng lượt, ví dụ 3 Bước × 2 Cấp:
  `(0,1) → (0,2) → (1,1) → (1,2) → (2,1) → (2,2) → DaDuyet`.
- `spec:272-273` *"`sqlcmd COUNT(*) WHERE ApprovedAmount IS NULL` = 0"* — **thiếu `AND PlanId=@id`**: mọi phiếu
  nháp hợp lệ đều có `ApprovedAmount = NULL` (`spec:121` ghi *"chỉ ghi tại finalize"*) ⇒ chỉ cần tồn tại 1 nháp là
  phép đo **luôn khác 0** ⇒ FAIL giả vĩnh viễn.
- `spec:277` *"duyệt 2 lần cùng cấp → DB 1 row"* — không nêu **ai** duyệt lần 2; theo OR-of-N, sau lượt đầu thì cấp
  đã qua nên người thứ hai bị chặn ⇒ **kịch bản có thể không dựng được**. **Đề xuất:** nêu rõ actor (Admin duyệt-thay
  / cùng một người bấm lại) + trạng thái phiếu tại thời điểm bấm.

### M8 — Acceptance W5 **mâu thuẫn với chính thiết kế §2.5**, và sai theo hướng **phóng đại tiền**
`spec:199` (thiết kế): *"`giaTri = line.ApprovedAmount` **khớp `w.SupplierId`** thay `SUM` `:88-90`"* — tức
**per-winner**. `spec:298` (acceptance): *"`GiaTri == SUM(ApprovedAmount)`"* — tức **cộng mọi dòng**. Với PE
liên-danh nhiều winner (Mig 58 multi-winner), code theo acceptance sẽ nhét **tổng giá của tất cả NCC** vào **từng**
hợp đồng. Direction-of-error là phóng đại giá trị HĐ ⇒ không phải lỗi làm tròn.
**Đề xuất:** sửa acceptance thành *"HĐ của NCC X → `GiaTri == ApprovedAmount` của đúng Line `SupplierId = X`"* +
1 ca 2-winner để phân biệt hai công thức.

---

## Điểm LOW

### L1 — Hai lệnh của W1 không chạy đúng như viết
`spec:231` *"`dotnet ef migrations list` → `69_AddContractSigningPlans` ở cuối"* — EF in ra
`20260728xxxxxx_AddContractSigningPlans`, **không có tiền tố `69_`**; số 69 chỉ là cách đánh số trong tài liệu.
`spec:236` *"`dotnet ef migrations script 68 69`"* — tham số là **tên** migration, không phải số thứ tự.
`spec:236` *"chạy sạch"* cũng thiếu kỳ vọng (0 lỗi + số bảng quay lại mốc cũ).
*(Số **69 là đúng**: file mới nhất trên đĩa `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260727033522_AddPeAllowApproverDelete.cs` = Mig 68 theo `docs/STATUS.md:642`.)*
**Đề xuất:** kỳ vọng theo tên + `sqlcmd -Q "SELECT TOP 1 MigrationId FROM __EFMigrationsHistory ORDER BY MigrationId DESC"`.

### L2 — Phép đếm bảng lệch vị-ngữ so với canonical và không nêu đo trên DB nào
`spec:232` dùng `SELECT COUNT(*) FROM sys.tables`; canonical `docs/STATUS.md:448` dùng
`sys.tables` **`is_ms_shipped=0`** (cicd Run #482 LIVE = **89**). Hai vị ngữ có thể lệch nhau, và acceptance không
nói đo trên **prod** hay **LocalDB Dev** (Dev hay lệch do migration chưa apply).
*(Con số 89 **đúng và chưa trôi** — đo: 6 migration mới nhất `20260706 → 20260727` đều **0 `CreateTable`, 0 `DropTable`**.)*
**Đề xuất:** chép nguyên vị ngữ canonical + ghi rõ DB đích + giữ **delta +7** làm bất-biến chính.

### L3 — "SHA256 3 file mirror IDENTICAL" không nêu **3 file nào**, và mâu thuẫn quy ước 2 app
`spec:255` ⟂ quy ước *"duplicate giữa 2 app là CÓ CHỦ ĐÍCH, mỗi app UX riêng"* và `spec:189` (page của 2 app khác
nhau về nội dung). Tiền lệ chỉ áp mirror byte cho **đúng 1 component**: `run.md:166` — *"`PeWorkflowPanel.tsx`
phải giữ md5 giống nhau ×2 app"*.
**Đề xuất:** nêu đích danh file phải giống byte (`lib/menuKeys.ts` + `types/contractSigningPlan.ts`), bỏ page.

### L4 — W2#5 không nêu tài khoản nên "user thiếu quyền" là biến tự do
`spec:253`. **Đề xuất:** ghi rõ 2 tài khoản (ví dụ `nv.test@solutions.com.vn` = thiếu quyền → **403**;
`admin@solutions.com.vn` = có quyền → **201**), vì kết quả 401-vs-403 phụ thuộc token hợp lệ.

### L5 — Đánh số W4/W5 ngụ ý thứ tự không có thật
Cả hai chỉ phụ thuộc W3, **không phụ thuộc nhau**; và W5 mới là khúc nghiệp vụ b.12→13 mà `spec:1` đặt làm đề bài,
còn W4 là siết chất lượng. **Đề xuất:** ghi *"W4 ⟂ W5 — chạy theo thứ tự anh trả lời câu hỏi"*, tránh việc
implementer coi W4 là điều kiện tiên quyết của W5.

---

## Thứ tự wave — trả lời trực tiếp câu (c)

- **C1. W1 hiện KHÔNG phải "rẻ nhất mà đã có giá trị" — nó rẻ và có giá trị bằng 0** *(MED, đã tính trong M1/H1)*.
  `spec:225` tự khai mục tiêu *"không đổi gì người dùng thấy"*. So với tiêu chuẩn thật của tiền lệ —
  `run.md:125` *"đợt 1 xong là **UAT được ngay**, biết chắc nền đúng trước khi xây tiếp"* — W1 **không UAT được**.
  Nó chỉ thoả tiêu chí yếu *"không để nửa tính năng chết"* bằng cách không giao gì cả.
  **Đề xuất gộp 2 việc:** kéo wiring Designer type-10 (H1) vào W1 ⇒ W1 thành *"nền + admin dựng được quy trình
  type 10"*, UAT được ngay bằng 1 phép thật (tạo quy trình, thấy nó trong dropdown), và **mở khoá W2**.
- **C2. Nên tách W3 làm hai** *(MED, cùng gốc M2)*: **W3a** = advance + OR-of-N + inbox precompute + panel FE +
  `TraLai` toàn phần (chính `spec:162` đã chừa đường *"tối thiểu `TraLai` toàn phần nếu muốn gọn wave đầu"*) —
  đủ để phiếu chạy trọn vòng, deploy được. **W3b** = 2 nhánh kết-thúc-sớm + 4 return-mode + notify approver.
  Lý do: W3 hiện gánh 8 dòng acceptance và 3 cơ chế rủi ro cao trong một lần deploy; nếu vỡ thì không truy được
  vỡ ở nhánh nào.
- **C3. Không nên gộp W1 với W2** *(giữ nguyên)*: migration cần `database-agent` review trước khi apply
  (`spec:226`), và blast radius của Mig 69 là 7 bảng mới — tách ra vẫn đúng.

---

## Cờ CHẶN — trả lời trực tiếp câu (d)

| Wave | Spec khai | Đánh giá | Chứng |
|---|---|---|---|
| W1 | ❌ không chặn | 🔴 **SAI — đang chặn** | `spec:126` format mã *"owner chốt"* + `spec:128-129` 7-hay-5 bảng; cả hai đóng băng vào Mig 69 (M1) |
| W1 | ✅ deploy riêng, *"vô hình với user"* | 🔴 **SAI một nửa** | fe-admin cố ý bỏ qua `IsVisible` (`MenuDtos.cs:13`), query không lọc (`GetMyMenuTreeQuery.cs:96-100`) (H6) |
| W2 | ✅ deploy riêng *"bật menu, tạo được phiếu"* | 🔴 **SAI — tạo được 0 phiếu** | thiếu wiring type-10 ở mọi wave (H1) + `DELETE` vô chủ (M6) |
| W3 | ⚠️ Q3 *"chạy được với mặc định"* | 🟠 **Nhẹ hơn thực tế** | acceptance ép cả 3 đường (M2) |
| W4 | ✅ chặn O-Q1, O-Q2 | 🟢 **ĐÚNG** | `spec:287-289` — và acceptance cũng khai rõ *"khi có câu trả lời"* |
| W5 | ✅ chặn Q6, Q11 + xin phép chạm HĐ | 🟢 **ĐÚNG** | `spec:295` neo đúng lệnh owner *"chỗ hợp đồng cứ từ từ"* |

Ghi chú W4: nếu O-Q2 chốt *"số hoá thành checklist"* thì W4 **đẻ thêm schema** (bảng/cột checklist) — mà §2.2 chỉ
thiết kế 7 bảng, không có chỗ cho nó ⇒ chi phí W4 hiện **chưa bị chặn trên**. Nên ghi thẳng: *"nếu O-Q2 = số hoá
⇒ W4 kèm Mig 70"*.

---

## Điểm CỦNG CỐ

1. **Số 562 đúng canonical** — `docs/STATUS.md:642` *"test 532→562"*. Không phải số bịa; phê bình của tôi ở H5
   nhắm vào **dạng tiêu chí**, không nhắm con số.
2. **Số 89 bảng đúng và chưa trôi** — `docs/STATUS.md:448` (re-ground S97, `sys.tables is_ms_shipped=0`, Run #482
   LIVE = 89); tôi kiểm lại 6 migration mới nhất (`20260706 → 20260727`) đều **0 `CreateTable` / 0 `DropTable`**
   ⇒ mốc 89 vẫn sống ở Mig 68. Delta **+7** khớp đúng "7 CreateTable" của §2.2.
3. **Mig 69 là số đúng** — file mới nhất trên đĩa là `20260727033522_AddPeAllowApproverDelete` = Mig 68.
4. **Phép tính +16 policy đúng số học** — 54 key (`docs/STATUS.md:453`) × 4 → 58 × 4 = 232.
5. **`spec:182-183` bắt đúng khác biệt sống-còn** giữa `Off_DeXuat` (**trong** `MenuKeys.All`) và `Pe_*` (factory,
   **ngoài** `All` ⇒ không có policy). Đây chính là bẫy mà `AuthorizePolicyRegressionTests.cs:139` đặt tên — spec
   nhận diện đúng nguồn, chỉ tự vấp ở khâu đặt tên (H2).
6. **§2.1 chặn bẫy Proposal có dẫn chứng dòng thật + lead tự đọc xác minh** (`spec:99-111`), và W3#1 tuy hỏng cách
   đo nhưng **đặt lưới đúng chỗ** — đây là rủi ro số một của cả run.
7. **W2#5 (403 authz 2 tầng)** — phép chặn tái phát #82/S118, kèm câu *"bắt buộc chạy, không được bỏ"*
   (`spec:253-254`). Hiếm thấy trong spec; giữ nguyên, chỉ thêm tên tài khoản (L4).
8. **W3#4 (inbox) có ĐỦ hai vế** — *"approver Cấp đang chờ **thấy**; approver cấp khác **không thấy**"*
   (`spec:275-276`). Đúng kỷ luật hai-thế-giới; nên nhân bản chính kỷ luật này sang #1 (H4).
9. **Acceptance #6 của W1 đòi chạy `Down` thật trên DB copy** (`spec:236`, *"reversible thật, không chỉ nhìn code"*)
   — phản chứng thật chứ không phải đọc code đoán; cùng hạng với `spec:135-136` (cấm thêm cột `SigningPlanId` chết)
   và `spec:132`/`spec:238` (verify enum slot 10 **và** buộc re-verify ngay trước khi viết).
10. **Provenance trung thực** — `spec:193-194` khai thẳng claim của invest **không verify được** (fe-user matrix
    grep 0 hit) và buộc grep lại; `spec:325-326` tách bạch **"đo thật"** (Proposal 4 bảng · 849 LOC BE · 1.956 LOC
    FE) với **"ngoại suy"** cho 3 nhánh. Tôi **không đo lại** 3 số LOC trong lượt này — nhưng đường phản chứng có
    tồn tại (`wc -l` trên file twin), nên nhãn "đo thật" là hợp lệ.

<!-- END · TOTAL=19 DIEM (6H/8M/5L + 10 cung-co) · VERDICT=SUA-TRUOC-W1 -->
