Files
solution-erp/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/sub-review-wave-1.md
pqhuy1987 d7eaeceaa7 [CLAUDE] Docs: spec "Ke hoach ky ket Hop dong" S157 (khuc con thieu PE -> HD) + review 4 lang kinh
Anh giao: dieu tra quy trinh CON THIEU giua Duyet NCC va HD NTP/NCC, ra spec day
du + chia wave. Chay /fable-real invest (Fable deep-pass) roi /fable-clone
reviewer 4 lang kinh.

PHAT HIEN NEN (lead do, invest + reviewer xac nhan tu nguon goc):
- 7 phase [LEGACY] cua ContractPhase CHINH LA quy trinh ISO 9 buoc ma-hoa cung
  (khop 1:1 dung thu tu). Khung V2 sinh ra de bieu dien chinh quy trinh do.
- Khoang trong nam DUNG giua ISO-1 va ISO-2, va ISO doc CUNG KHONG CO no
  => thieu o CA HAI tang (doc + code) => spec vua la spec phan mem vua la de
  xuat bo sung ISO.

OWNER CHOT trong cua: (O-1) cau truc trinh ky GIONG, chi khac NOI DUNG => tai
dung khung duyet V2, 0 bang approval moi => truc tranh luan Q2 hom qua DONG.
(O-2) SLA chi tham khao => khong SLA-engine, khong wave SLA.
(O-3) "cho hop dong cu tu tu" => lo hong an ninh HD NGOAI SCOPE.

REVIEW 4/4 lane sach 0 chet (dong goi C2 an), 112 diem. Claim nen DUNG VUNG.
Bat 7 loi NANG, da va het trong ban nay:
1. Ten menu key `Csp*` != tien to policy `ContractSigningPlans.*` => Program.cs
   khong sinh policy => HTTP 500 luc chay. Repo DA CO test bat
   (AuthorizePolicyRegressionTests.cs:138-139). Day la loi TU-VAP-BAY-MINH-VUA-
   NHAN-DIEN: §2.4 bat dung bay Pe_* roi §2.3 van vap bang co che khac.
2. W2 day rieng tao duoc 0 phieu (wiring Designer type-10 roi khoi moi wave)
   => keo vao W1, tai pham dung §F.0 S155.
3. Thieu bang Approvals ma service duoc lenh copy CO ghi vao.
4. Changelogs thieu 4 cot => copy service khong bien dich.
5. Thieu 3 site ApprovalWorkflowV2AdminFeatures => FK Restrict no 500.
6. Status enum bat dau tu 0 => bay falsy JS.
7. 4 anchor lech, nang nhat ApprovalWorkflowLevel.cs KHONG TON TAI.

Kem: bo tieu chi dark-launch sai co che (IsVisible chi pass-through, filter la
CanRead) - thay "sqlcmd dem policy" (khong do duoc, policy in-memory) bang test
regression - OR-of-N nang tu 1 ve len 4 ve co ca am - cat CodeSequences (tai
dung WorkflowAppCodeSequence dung chung 4 module Office).

Su co #53: invest luot 1 skeleton-ruot-rong (202K tok, dia 1.661B khung rong)
=> ghi-dia-trong-luc-lam la CAN KHONG DU, phai verify RUOT. Luot 2 resume cat
con 2 muc => CLEAN 23.085B. Luot 3 tien-trinh CLI thoat, §C/§D chua ghi => lead
tu viet theo H21 (engine propose-only, spec do lead ghi).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:30:03 +07:00

26 KiB
Raw Blame History

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-174spec: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-139MenuKeys.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 MenuItemsPermissions. 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_*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→58216→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}/transitions403, 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 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 ≥ 6liệ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 canonicaldocs/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-102IsVisible 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 AllowApproverFinalizePHÁ VỠ", "CeoApprovalThresholdPHÁ VỠ" — đối lại run.md:116 "Step.DepartmentIdAN 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 :2492-2496". Đề xuất: W2 thêm 2 dòng — (i) xóa mềm 1 phiếu → GET /list không còn 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 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 list69_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"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 canonicaldocs/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ôidocs/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 buộc re-verify ngay trước khi viết).
  10. Provenance trung thựcspec: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ệ.