# 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ệ.