# lens3-seq — thứ tự + khuôn + đo được

Lens 3/3 ensemble B1b. Target: `spec-4gd-khkk-tong-quat-31-07-2026.md` (+ `draft-yeu-cau-owner.md` = nguồn PIN).
3 câu: Q3 thứ tự/phụ thuộc wave · Q4 chế-thêm trái luật khuôn · Q5 acceptance đo được.

## Findings

### F-L3-1 [HIGH] — Whitelist "chừa System subtree" GIỮ LẠI đúng 2 mục owner ra lệnh ẨN
- Claim: spec §② quyết-định 7 = "fe-admin `filterForAdmin` đảo blacklist→whitelist (chừa `System` subtree + AwV2)". Nhưng 2 mục owner đòi ẩn — "Quy trình Duyệt NCC" và "Quy trình HĐ" — đều là CON của `System`, nên whitelist đó giữ chúng lại.
- Evidence:
  - Yêu cầu ẩn: `draft-yeu-cau-owner.md:52` — "+ 2 mục quy trình cũ (Quy trình Duyệt NCC · Quy trình HĐ)" nằm trong danh sách ẨN; chừa = "Người dùng · Vai trò · Phân quyền · Menu eOffice" + "Quy trình duyệt (Mới)".
  - `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:1795` — `(MenuKeys.Workflows, "Quy trình HĐ", MenuKeys.System, 95, ...)`
  - `DbInitializer.cs:1798` — `(MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, ...)`
  - `DbInitializer.cs:1801` — `(MenuKeys.ApprovalWorkflowsV2, "Quy trình duyệt (Mới)", MenuKeys.System, 96, ...)` ⇒ "+ AwV2" trong spec là THỪA, và chính chỗ thừa đó tố cáo tác-giả tưởng AwV2 nằm NGOÀI System.
- Sev: HIGH (sai chức-năng so với lời owner, phát hiện được ngay bằng đọc seeder).
- Đề nghị: spec phải phát biểu rule theo TẬP KEY tường minh (4 leaf System + AwV2 subtree), KHÔNG theo "subtree System"; kèm acceptance liệt-kê-key.

### F-L3-2 [HIGH] — Acceptance K6 MÙ với chính lỗi F-L3-1
- Claim: `spec:41` — "sidebar admin chỉ còn HỆ THỐNG + Quy trình duyệt (Mới)". Nếu implement theo quyết-định 7, 2 mục quy trình cũ vẫn hiện — nhưng chúng hiện BÊN TRONG nhóm "HỆ THỐNG", nên người chấm đọc câu acceptance vẫn thấy "chỉ còn HỆ THỐNG + …" ⇒ PASS.
- Evidence: `spec-4gd-khkk-tong-quat-31-07-2026.md:41` vs `DbInitializer.cs:1795/1798` (2 mục là con System).
- Sev: HIGH (acceptance không có răng đúng ở trục đang sai).
- Đề nghị: acceptance = liệt-kê ĐÚNG N mục lá còn thấy trong sidebar admin (tên + số), FAIL nếu dư/thiếu 1 mục.

### F-L3-3 [HIGH] — K4 "leaf lọc đúng ApprovalGroup" cần tham-số lọc mà KHÔNG wave nào nhận nuôi
- Claim: 48 leaf lọc theo nhóm cần `ApprovalGroup` trong list-query BE; query hiện chỉ có 4 tham số, và K2/K4 không khai việc thêm.
- Evidence:
  - `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs:622-625` — `ListContractSigningPlansQuery(Phase, ProjectId, PurchaseEvaluationId, PendingMe)` — không có group.
  - `spec:23` K4 = "sidebar 48 leaf + grant seeder"; `spec:22` K2 = "Mig 71 + validator + FE lines editor" ⇒ không ai khai list-filter.
  - `spec:39` acceptance K4 đòi "leaf lọc đúng `ApprovalGroup`".
- Sev: HIGH (acceptance nằm ngoài scope wave đang chấm ⇒ hoặc trượt, hoặc lấn scope im lặng).
- Đề nghị: gán rõ chủ wave cho `ApprovalGroup` (cột + query-param + FE param), 1 dòng DoD.

### F-L3-4 [HIGH] — 8 nhóm chỉ tách được ở tầng HIỂN THỊ; API là MỘT khoá chung ⇒ "0 rác 403" không đo được cái cần đo
- Claim: 48 leaf fan-out nhưng mọi endpoint KHKK dùng chung bộ policy `KeHoachKyKet.{Read,Create,Update,Delete}`; không có authz theo nhóm. Acceptance "13/13 role 0 rác 403" chỉ bắt được lỗi grant-lệch (leaf hiện mà thiếu quyền root), MÙ hoàn toàn với rò-rỉ chéo-nhóm.
- Evidence:
  - `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs:28` class-level `[Authorize(Policy = "KeHoachKyKet.Read")]`; :36/:53/:59/:71/:77 Read · :84 Create · :93 Update · :103 Delete — 1 bộ khoá cho TẤT CẢ.
  - `src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:42` + `:180` — chỉ `KeHoachKyKet` vào `All` ⇒ 4 policy, không có policy per-nhóm.
  - Rào duy nhất hiện có là IDOR-scope theo người, không theo nhóm: `ContractSigningPlanFeatures.cs:636-640` (comment "phiếu MÌNH soạn ∪ phiếu đã rời nháp thuộc quy trình mình có chân duyệt").
- Sev: HIGH (spec bán "8 nhóm" như đơn-vị phân-quyền; thực tế là nhãn hiển thị).
- Đề nghị: spec khai thẳng "phân-tách nhóm = HIỂN THỊ, không phải authz" (và nếu owner muốn authz thì đó là wave riêng); acceptance K4 đổi thành so-khớp TẬP: với mỗi role, {leaf render} vs {Permission row `KeHoachKyKet`} + {phiếu list trả về} — đo bằng truy vấn, không bằng click tay.

### F-L3-5 [HIGH] — `ApprovalGroup` cho phiếu CŨ không được định nghĩa ⇒ phiếu cũ có thể biến mất khỏi cả sidebar lẫn cây
- Claim: K2 acceptance chỉ lo Lines (`CatalogEntryId=NULL`), không nói gì về giá trị `ApprovalGroup` của phiếu đã tồn tại; mà K4 (leaf lọc theo nhóm) + K5 (cây group-by `approvalGroup`) đều lọc theo nó. Nhóm NULL/0 không có folder ⇒ vắng mặt IM LẶNG (đúng lớp "lọc = vanish" đã gặp).
- Evidence: `spec:37` (K2 acceptance — chỉ `CatalogEntryId=NULL`) · `spec:23` (K5 group-by `approvalGroup`) · `spec:39` (K4 leaf lọc `ApprovalGroup`) · entity hiện KHÔNG có cột nhóm: `src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlan.cs:22-45`.
- Sev: HIGH.
- Đề nghị: chốt backfill tường minh (mặc định nhóm 1 vì quyết-định 6 giữ `Khkk_G1` đổi label thành nhóm 1) + acceptance đếm: `COUNT(phiếu) trước = Σ COUNT(phiếu) hiện trong 8 folder`.

### F-L3-6 [MED] — Chủ sở-hữu cột `ApprovalGroup` bị chẻ đôi giữa quyết-định 3 và wave K2
- Claim: quyết-định 3 (workflow) gói luôn "plan +cột `ApprovalGroup int` + validator line-cùng-nhóm" ⇒ thuộc K3; nhưng K2 mô tả "phiếu mang hạng-mục+nhóm (Mig 71 …)" ⇒ thuộc K2. Cụm 2 lại ghi "chờ `approvalGroup` từ cụm 1" mà không nói chờ K2 hay K3.
- Evidence: `spec:14` (quyết-định 3) vs `spec:22` (K2) vs `spec:23` (mở đầu cụm 2).
- Sev: MED (2 wave cùng đụng 1 cột ⇒ hoặc trùng migration, hoặc rơi kẽ).
- Đề nghị: 1 dòng "cột X do wave Y đẻ", các wave khác chỉ đọc.

### F-L3-7 [MED] — Cổng owner (OG-1, OG-2) nằm NGOÀI đồ thị wave, không có chốt chặn trước wave phụ thuộc
- Claim: OG-1 (cardinality) quyết hình dạng UNIQUE của K2 và luật gộp của K7; OG-2 (roster vai→user) chặn chính việc seed của K3 ("cần anh gật trước khi seed"). Nhưng K1→K2→K3 được xếp như chuỗi chạy thẳng, không wave nào có DoD "gate đã chốt".
- Evidence: `spec:27-28` (OG-1, OG-2) · `spec:22` (cụm 1 chạy thẳng) · `spec:36-38` (checklist K1/K2/K3 không có dòng gate).
- Sev: MED-HIGH (fan-out B4 sẽ hoặc đứng, hoặc tự chế roster; OG-1 lật SAU K2 = phải sửa migration đã land).
- Đề nghị: mỗi wave phụ-thuộc-gate có dòng đầu "PRE: OG-n đã chốt (dẫn chứng: …)"; OG-1 phải chốt TRƯỚC K2, OG-2 TRƯỚC K3.

### F-L3-8 [MED] — "Cụm 2 chờ cụm 1" gộp 3 phụ-thuộc KHÁC nhau vào 1 mũi tên
- Claim: trong 6 leaf/nhóm của K4, phụ thuộc thực khác nhau: (a) MenuItem row + grant seeder + thứ tự sidebar = KHÔNG phụ thuộc gì (tầng Identity, chạy song song cụm 1 được); (b) 4 leaf danh-sách (Danh sách/Đang duyệt/Đã duyệt/Đã xóa) + Thao tác = chờ `ApprovalGroup` (K2 hoặc K3, xem F-L3-6) + chờ query-param (F-L3-3); (c) leaf "Luồng duyệt" (`WfView`) = chờ K3 (8 workflow tồn tại), KHÔNG chờ K2.
- Evidence: bộ 6 leaf khuôn ở `DbInitializer.cs:1780-1787` (KHKK) và factory PE `MenuKeys.cs:142-158` (`Group/List/Create/Pending/Approved/Deleted/WfView`) · `spec:23` chỉ ghi 1 mũi tên "chờ `approvalGroup` từ cụm 1".
- Sev: MED (vừa nối-thừa gây tuần-tự-hoá phí, vừa giấu 1 phụ-thuộc THẬT là K4→K3).
- Đề nghị: tách bảng phụ-thuộc theo LEAF-GROUP, ghi rõ K4a (không phụ thuộc) / K4b (K2) / K4c (K3).

### F-L3-9 [MED] — K6 "độc lập, kéo sớm được" đúng CÓ ĐIỀU KIỆN: 8 designer của K3 phải nằm dưới `ApprovalWorkflowsV2`
- Claim: sau khi đảo sang whitelist, mọi menu MỚI mặc định BỊ ẨN ở admin (fail-closed). K3 sinh "designer entry" cho 8 nhóm; nếu chúng không phải con của `ApprovalWorkflowsV2` thì K6-kéo-sớm làm 8 designer vô hình — mà không ai báo lỗi.
- Evidence: khuôn hiện tại có đúng 2 con dưới AwV2: `DbInitializer.cs:1802-1803` (`Duyệt NCC (Mới)` / `Duyệt NCC và Giải pháp (Mới)`) · `spec:23` K6 "(độc lập — kéo sớm được)" · `spec:22` K3 "designer entry".
- Sev: MED.
- Đề nghị: ghi ràng buộc "8 designer = con của `ApprovalWorkflowsV2`" vào DoD K3, và thêm dòng acceptance K6 "sau K3, đếm được 8 designer trong sidebar admin".

### F-L3-10 [MED] — Spec nén "8 designer" của owner thành "designer entry" (số ít, không số)
- Claim: draft đòi `"Quy trình duyệt (Mới)" += 8 designer nhóm-duyệt`; spec K3 chỉ còn "designer entry", checklist K3 (`spec:38`) không có dòng nào đếm designer.
- Evidence: `draft-yeu-cau-owner.md:52` vs `spec:22` + `spec:38`.
- Sev: MED (mất số ⇒ mất phép đo; đúng lớp "nhãn ✓ không có phép đo").
- Đề nghị: checklist K3 thêm "sidebar admin hiện đúng 8 designer nhóm; mở designer nhóm n load đúng workflow `KHKK-N{n}`".

### F-L3-11 [HIGH] — K7 hỏi "W5/W6 đã land chưa" bằng câu NHỊ-PHÂN, trong khi trạng thái thật là NỬA-LAND
- Claim: cầu KHKK→HĐ hiện có ĐỦ phần schema (cột + index) nhưng KHÔNG có một chỗ ghi nào. Ai "RE-ĐO" bằng cách soi schema sẽ đọc ra "đã land" và bỏ qua đúng phần còn thiếu.
- Evidence:
  - `src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanLine.cs:25` — `public Guid? ContractId { get; set; } // [C6 review-schema] HĐ sinh ra từ dòng này (W5)`.
  - `src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractSigningPlanLineConfiguration.cs:26` — `b.HasIndex(x => x.ContractId);`
  - Grep toàn `src/Backend` (trừ Migrations) tìm phép GÁN `ContractId` cho dòng kế-hoạch: **0 kết quả** — chỉ ra 1 comment (`ContractSigningPlanFeatures.cs:1185`) và 1 khai-báo index. Không có write-site.
- Sev: HIGH.
- Đề nghị: đổi câu RE-ĐO từ nhị-phân sang 3 nấc ĐO ĐƯỢC — (a) cột/index có chưa, (b) write-site có chưa (grep phép gán), (c) test/E2E chứng đường quay ngược — và nêu xử lý cho nấc giữa.

### F-L3-12 [HIGH] — Acceptance K6 "diff bundle fe-user = 0" đo bằng thứ gotcha #69 đã tuyên bố KHÔNG ổn định
- Claim: bundle hash không deterministic và CI rebuild FE vô-điều-kiện mỗi run ⇒ "diff bundle = 0" hoặc luôn FAIL, hoặc bị lách bằng cách không build lại.
- Evidence: `docs/gotchas.md:1207` (tiêu đề #69: "FE bundle hash KHÔNG deterministic + `deploy.yml` rebuild FE vô-điều-kiện mỗi run") · `:1211` cơ chế (Vite/rolldown emit content-hash không deterministic) · `:1213` guard chính thức: "Phát-hiện FE-ship THẬT = `git diff fe-admin/src fe-user/src <range>`, KHÔNG tin hash-delta".
- Sev: HIGH (phép đo sai bản chất, đã có luật ngược trong repo).
- Đề nghị: viết lại thành `git diff --stat <base>..<head> -- fe-user/src` = 0 file.

### F-L3-13 [HIGH] — "13/13 role 0 rác 403" PASS được bằng kết quả suy biến (không cấp quyền cho ai)
- Claim: nếu grant seeder không cấp leaf KHKK cho role nào thì 0 leaf render ⇒ 0 lệnh gọi ⇒ 0 rác 403 ⇒ acceptance PASS trong khi tính năng vô hình. Thiếu ma-trận KỲ VỌNG (role nào PHẢI thấy nhóm nào) thì phép đo chỉ có 1 chiều.
- Evidence: `spec:39` · anchor con số "13" có thật (`src/Backend/SolutionErp.Domain/Identity/AppRoles.cs` = 13 `public const string`) nhưng seeder quyền hiện chỉ phủ 7 vai (`DbInitializer.cs:2671-2675`: Drafter, DeptManager, Procurement, CostControl, ProjectManager, Director, AuthorizedSigner) ⇒ 6 vai còn lại không có kỳ-vọng nào được khai.
- Sev: HIGH.
- Đề nghị: acceptance 2 chiều — (i) mọi leaf render đều gọi được (0 rác 403); (ii) số leaf render của từng vai == số kỳ-vọng khai trước trong bảng 13 dòng.

### F-L3-14 [MED] — K1 "bảng danh mục N dòng == số extract file gốc (khai N)" là mệnh đề TỰ ĐÚNG
- Claim: N do chính bước extract sinh ra rồi lại lấy N so với N ⇒ 0 bit thông tin; không có đối-chứng độc lập.
- Evidence: `spec:36` · đối-chứng độc lập DUY NHẤT nằm ở draft và spec đã bỏ mất: `draft-yeu-cau-owner.md:42` — "Tổng danh mục ~85 dòng (đếm từ ảnh trang 5-9)".
- Sev: MED.
- Đề nghị: acceptance = "N khớp ~85 (đếm ảnh trang 5-9); lệch > 5 dòng phải giải trình từng dòng lệch".

### F-L3-15 [MED] — Mọi dòng "test PASS" không có SỐ ⇒ xoá test vẫn PASS
- Claim: `dotnet test` PASS (K1), "test validator ÂM PASS" (K2), "test bridge PASS" (K7) đều không nêu baseline hay số ca thêm.
- Evidence: `spec:36`, `:37`, `:42` · baseline có thật trong draft: `draft-yeu-cau-owner.md:67` — "Test: 590 PASS (45D+545I) baseline".
- Sev: MED.
- Đề nghị: mỗi wave ghi "≥ 590 + k, khai k và tên ca".

### F-L3-16 [MED] — K8 dry-run: 3 guard nêu ở phần rủi ro KHÔNG xuống tới dòng acceptance
- Claim: quy ước ZZTEST, HỐ-1 (HĐ `DaPhatHanh` không xoá được qua API) và "notification bắn user thật" đều nằm ở §② nhưng dòng checklist K8 chỉ đòi chạy hết trạm + screenshot.
- Evidence: `spec:24` (ZZTEST) + `spec:32` (HỐ-1, notification) vs `spec:43` (acceptance K8 — không có 3 thứ đó).
- Sev: MED (dry-run trên prod để lại rác không xoá được).
- Đề nghị: K8 thêm 3 dòng đo được — tiền-tố ZZTEST trên mọi bản ghi tạo mới; danh sách bản ghi không thể xoá + cách sống chung; tắt/chuyển hướng thông báo trước khi chạy.

### F-L3-17 [MED] — Dòng chung "mỗi wave: cicd PASS + bundle byte-verify (#77)" sinh nhiễu cho wave BE-only
- Claim: K1/K2/K3 gần như thuần BE, nhưng bundle vẫn rotate mỗi run (#69) ⇒ "byte-verify" luôn thấy đổi mà không có nghĩa; đồng thời dòng này ngụ ý mỗi wave đều deploy prod, trong khi không wave nào khai bước deploy.
- Evidence: `spec:44` · `docs/gotchas.md:1211` (rebuild vô-điều-kiện + hash rotate) · `:1301` (#77 là về cache-lag sau deploy).
- Sev: MED.
- Đề nghị: tách 2 loại wave — wave có ship FE mới cần #77; wave BE-only chỉ cần cicd PASS + smoke API.

### F-L3-18 [MED] — Guard "grep MỌI consumer Lines" không có dòng acceptance nào; bề mặt đếm được là 16
- Claim: đổi cardinality của `Lines` (1 dòng/NCC → N dòng/NCC) là lớp lỗi S87/S88, nhưng checklist K2 không đo việc rà consumer.
- Evidence: `spec:13` + `spec:32` (guard) vs `spec:37` (acceptance không có) · bề mặt: 10 chỗ chạm `.Lines` ở BE (`ContractSigningPlanFeatures.cs:389/412/417/500/524/568`, `ContractSigningPlanLineConfiguration.cs:29`, `ContractSigningPlanWorkflowService.cs:111/296/303`) + 6 file FE (`fe-{user,admin}/src/pages/khkk/KhkkCreatePage.tsx`, `KhkkDetailPage.tsx`, `types/khkk.ts`).
- Sev: MED.
- Đề nghị: acceptance "liệt 16 chỗ, mỗi chỗ ghi ĐÃ-XỬ-N hoặc KHÔNG-ĐỔI kèm lý do".

### F-L3-19 [LOW] — K5 "2 file pipeline SHA-identical" thiếu tên file (bar hiện đang ĐẠT)
- Evidence: `spec:40` · đo thực tế bây giờ: `fe-{admin,user}/src/hooks/usePipelineStages.ts` cùng `7144eeb7…`; `fe-{admin,user}/src/components/pipeline/PipelineTreePanel.tsx` cùng `d70ac315…` ⇒ đây là bar hợp lệ, chỉ cần nêu đích danh 2 đường dẫn.
- Sev: LOW. Đề nghị: chép 2 đường dẫn vào dòng acceptance.

### F-L3-20 [LOW] — Checklist không có dòng 3-file rule cho Mig 70/71
- Evidence: `spec:36`/`:37` (K1/K2 đẻ migration) không nhắc; luật nằm ở `draft-yeu-cau-owner.md:67` ("mig mới = 3-file rule").
- Sev: LOW. Đề nghị: thêm "3 file migration cùng commit" vào DoD K1 + K2.

## Đã nghi rồi BÁC BỎ (nêu kèm chứng — không để nghi ngờ trôi thành cáo buộc)

- **48 leaf KHÔNG phải chế thêm.** Khuôn Duyệt NCC đúng là 1 group + 6 leaf: `MenuKeys.cs:142-158` (`…Group/List/Create/Pending/Approved/Deleted/WfView`); KHKK hiện đã dựng đúng bộ đó: `DbInitializer.cs:1780-1787` với chú thích ":1781 — đủ 6 leaf 'y chang Duyệt NCC'". 8 × 6 = 48 là NHÂN KHUÔN.
- **Cột `ApprovalGroup` cũng KHÔNG phải khái-niệm mới** — nó là bản sao vai trò của `EvaluationType` bên PE (`MenuKeys.cs:55` — "Module Duyệt NCC … 2 EvaluationType" chính là thứ sinh ra fan-out `Pe_<typeCode>_*`). Hệ quả: đã mirror thì mirror trọn — `EvaluationType` không rỗng từ lúc tạo, nên `ApprovalGroup` cũng phải có backfill (xem F-L3-5).
- **Tầng sub-folder trong cây là owner ĐÒI, không phải tự chế:** `draft-yeu-cau-owner.md:14` — "Thêm các sub folder con ở đây".
- **Nghi "whitelist bỏ rơi Menu eOffice" — SAI:** `DbInitializer.cs:1794` cho thấy Menu eOffice là con của `System`, nằm trong vùng chừa.
- **Nghi "N dòng/NCC làm vỡ tra cứu NCC" — SAI:** `ContractSigningPlanFeatures.cs:524-526` đã có `.Distinct()` trước `ToDictionaryAsync`.
- **`ApprovedAmount` per-Line có thật** (`ContractSigningPlanLine.cs:23`) và spec trích `:25` cho `ContractId` cũng đúng ⇒ K7 không đọc field ma.

## Verdict

**FAIL — spec chưa nên đem đi fan-out ở dạng hiện tại.** 6 lỗi HIGH, 8 MED, 2 LOW, 6 điểm nghi đã bác bỏ.

Ba lỗi chặn đường (phải sửa trước B2):
1. `F-L3-1` + `F-L3-2` — luật ẩn admin giữ lại đúng 2 mục owner ra lệnh ẩn, và acceptance được viết theo cách không thể phát hiện chuyện đó.
2. `F-L3-3` + `F-L3-4` + `F-L3-5` — trục `ApprovalGroup`: thiếu chủ wave cho tham số lọc, thiếu backfill cho phiếu cũ, và "8 nhóm" chỉ tồn tại ở tầng hiển thị vì mọi endpoint dùng chung một bộ khoá.
3. `F-L3-11` — câu hỏi RE-ĐO nhị-phân sẽ trả lời "đã land" trên một trạng thái mới land nửa chừng.

Về khuôn (Q4): spec **không** vi phạm luật "giống nhau — chỉ khác form" ở chỗ dễ nghi nhất (48 leaf, cột nhóm, tầng sub-folder đều là nhân khuôn hoặc do owner đòi). Chỗ lệch khuôn thật sự là cơ chế ẩn admin: đảo blacklist thành whitelist đổi luôn kiểu hỏng mặc định của menu mới (mọi menu thêm sau sẽ tự ẩn ở admin), và đó là thay đổi hành vi vượt quá yêu cầu "ẩn các mục cũ".

