# c2-l2

LANE 2/3 — lens ĐỦ-vs-SPEC cho cụm-2 (K4 sidebar 49-row · K5 cây groups · K6 admin 12-key blacklist).
Nguồn chấm: `sub-invest-fable-b2-cum2.md` + `spec-4gd-khkk-tong-quat-31-07-2026.md` (§③ cụm-2, §② QĐ6/7, OG-8) + `spec-cum1-chi-tiet-31-07-2026.md` (12 vá).
Tất cả evidence dưới đây tao tự đo lại trên ĐĨA/DB (không tin lời khai của lane).

## Findings

### HIGH

**H1 — `queryKey` thiếu `group` ⇒ 8 nhóm dùng CHUNG 1 ô cache (silent-wrong, build xanh).**
- Claim của lane: K4b mục 7 (`sub-invest-fable-b2-cum2.md:31`) liệt đúng 2 chỗ — `searchParams.get('group')` cạnh `:60-61` và truyền vào `params` `:75-82` (cả nhánh `/deleted` `:74`) — **KHÔNG có `queryKey`**.
- Evidence: `fe-user/src/pages/khkk/KhkkListPage.tsx:70` → `queryKey: ['khkk-list', { phase, search, page, deletedView }]`. File này SHA-identical với fe-admin (`sha256` 16 ký tự đầu = `40a650bf6964ab4b` cả 2 app) ⇒ lỗi nhân đôi.
- Hệ quả: đổi `?group=` KHÔNG đổi khoá cache ⇒ leaf nhóm 2 render danh sách nhóm 1 (rồi bị ghi đè chéo). `tsc` + `npm run build` + test đều XANH. Cùng họ với S159-③ (2 khoá cho 1 resource) nhưng CHIỀU NGƯỢC: 1 khoá cho 8 resource.
- Sev: HIGH. Đề nghị: mục 7 thêm gạch "`group` VÀO `queryKey` `:70`"; A2 thêm bước "bấm G1→G2→G1 KHÔNG F5, số phải đổi".

**H2 — Cỗ máy làm 42 leaf mới bấm-được (regex) nằm ở K4c (chờ K3), nhưng acceptance A1/A2 đo nó ở nấc K4a/K2 ⇒ A1 bất-khả-PASS.**
- `sub-invest:43` A1 = "48 leaf HIỆN sidebar" và `:119` khẳng định "A1/A3-A6 đo được NGAY sau K4a". Nhưng URL cho `Khkk_G{n}_*` chỉ sinh ở mục 10 (`:37`) = K4c, mà K4c `:114`/`:120` CHỜ K3.
- Evidence code: `fe-user/src/components/Layout.tsx:338` `if (!path) return null` trong `MenuLeaf` ⇒ key không có trong `staticMap`/regex = **DROP IM LẶNG** (chính gotcha #50 mà file này tự dán ở `:151-153`). Sau K4a-only: 7 group-row hiện, 42 leaf **KHÔNG render** ⇒ A1 đếm 6, không phải 48.
- Sev: HIGH (acceptance khẳng định đo được ở nấc mà nó chắc chắn trượt).
- Đề nghị: tách mục 10 → **10a** (List/Pending/Approved/Deleted/Create — 0 phụ thuộc, kéo về K4a) + **10b** (WfView filter theo Code — giữ ở K4c chờ K3). Hoặc sửa A1 thành "đo sau K4a+10a".

**H3 — 2 con số drift STATUS SAI trong MỌI kịch bản của chính thứ tự ship lane đề xuất.**
- Lane: `sub-invest:27`/`:128` "Menu keys 64→**113** · Policies 256→**452** (55→64 đã khai cụm-1)".
- Canonical đo tươi: `docs/STATUS.md:471` Menu keys = **55**; `:470` Policies = **220** (DERIVED `|MenuKeys.All| × 4`).
- Base 64 = 55 + 9, mà 9 key cụm-1 = 1 `ContractCatalog` (K1) + 8 `AwV2_KhkkN*` (**K3**) theo `spec-cum1-chi-tiet:8`. Thứ tự ship lane đề xuất (`:117`) là `K6 → K4a → [K2] → K4b+K5 → [K3] → K4c` ⇒ lúc commit K4a, K3 **chưa** land ⇒ STATUS đang ở 55 hoặc 56, **không bao giờ** 64. Ghi "64→113" = ghi sai canonical mà `governance-detectors.ps1` đọc.
- Sev: HIGH (số mang nhãn đo-lường; phép kiểm làm nó trượt = `grep 'Menu keys' docs/STATUS.md` ngay trước commit).
- Đề nghị: khai **DELTA** (+49 key / +196 policy) + luật "đọc row STATUS ngay trước commit rồi cộng delta" (B1).

### MED

**M1 — Lý do giữ site-2 SAI (kết luận vẫn ổn).** `sub-invest:24` khẳng định "group-key CẦN CanRead row để render". Đo: `src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs:96-97` — `static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess)` ⇒ **cha render khi CON có CanRead**. Extend site-2 cho 7 group-key = tuỳ chọn (thêm ~91 row Permission), không phải điều kiện render. Giữ hay bỏ đều được — nhưng phải sửa lý do, kẻo lượt sau suy sai sang cây khác.

**M2 — "3 site" bỏ sót site thứ 4 lịch sử (revoker #84); tao đo hộ: an toàn.** `RevokeTemporarilyHiddenModulesAsync` (`DbInitializer.cs:2286`) được gọi ở `:2097` — **GIỮA** site-2 (`:2090`) và site-3 (`:2110`). Prefix revoke đo được `:2297-2300` = `Hrm*` / `Off*` / `Personal` ⇒ `Khkk*` KHÔNG dính. Kết luận lane đúng nhưng chưa từng soi chỗ đã lật 447/494 row (#84). Đề nghị: thêm 1 dòng "site-4 revoker: đã soi, 0 match `Khkk`" — A5 (restart ×2) chính là phép thử nó.

**M3 — `phase` không re-sync theo URL ⇒ A2 "lọc đúng group+phase" sẽ trượt khi bấm leaf→leaf.** `fe-user/src/pages/khkk/KhkkListPage.tsx:63-65` khởi tạo `useState(initialFilter ? FILTER_TO_PHASE[...] : null)` — chỉ chạy lúc MOUNT; grep cả file: `setPhase(` duy nhất `:153` (chip lọc). Route `/khkk/list` không keyed (`fe-user/src/App.tsx:81` `element={<KhkkListPage />}`) ⇒ đổi query KHÔNG remount. `deletedView` thì đọc thẳng searchParams `:61` nên phản ứng đúng — lệch nhau trong cùng 1 file. Pre-existing với 4 leaf G1, nhưng ×8 nhóm và A2 đo trúng nó. Đề nghị: K4b thêm việc "derive `phase` từ searchParams" HOẶC hạ A2 xuống "mỗi leaf mở bằng F5".

**M4 — "Thao tác" (Create) bị dời từ K4b sang K4c ⇒ trễ 1 wave vô cớ + lệch spec owner đã đọc.** `spec-4gd-khkk-tong-quat:29` xếp "**K4b** 4 leaf list + **Thao tác** (chờ K2 param)"; `sub-invest:29` đổi tiêu đề K4b thành "4 leaf list" và đẩy Create preset xuống `:39` (mục 12, K4c "chờ K3"). Preset chỉ cần picker của vá-4 cụm-1 (K2) — 0 phụ thuộc K3. Đề nghị: trả mục 12 về K4b (đi cùng 10a ở H2).

**M5 — 8 nhãn menu tự chốt ngoài artifact OG-6, mà chi phí sửa-sau KHÔNG rẻ như lane nói.** `sub-invest:131` "owner chỉnh CHỮ tự do… không chặn wave nào" >< chính `:11` khai "sửa chữ trong `tree.Add` VÔ TÁC DỤNG DB đang chạy" (comment `DbInitializer.cs:1959-1961`) ⇒ mỗi lần owner đổi chữ sau khi seed = +1 dòng labelBackfill + restart. Bảng OG-6 owner đang soát (`danh-muc-sp002-transcribe.md:7-16`) chỉ có cột Nhóm/Ký-chính/Nháy — **không chứa 8 nhãn này**. Kèm 1 sai lệch nội dung: nhãn N2 "Kết cấu, hạ tầng & DV hỗ trợ thi công (A2+A3)" **mất vế "phá dỡ"** (`transcribe:29` A2 = "Kết cấu, hạ tầng, phá dỡ"; dòng `:30` A2-01 = "Thầu phụ hạ tầng & phá dỡ"). 6/8 nhãn còn lại khớp transcribe (N7 rút gọn thành tên KHỐI "BCH công trường" — chấp nhận được). Đề nghị: đính 8 nhãn vào chính file OG-6 để owner soát 1 lượt, trước K4a.

**M6 — Quyết "fe-admin KHÔNG nới regex" buộc chặt vào K6, mà K6 là display-only đảo được bất cứ lúc nào.** `sub-invest:40`/`:94` (dead-by-K6) >< `:93` (route admin còn nguyên, thu gọn = menu). Nếu owner bỏ ẩn `KeHoachKyKet` trên admin ⇒ 48 leaf DROP IM LẶNG (#50), không ai nhớ ràng buộc. Đề nghị: nới regex fe-admin luôn (rẻ, 2 file đang SHA-identical) HOẶC ghi ràng buộc ngược ngay tại `ADMIN_HIDDEN_KEYS`: "gỡ key này ⇒ PHẢI nới regex `Layout.tsx` trước".

**M7 — [Q6 tự-chốt] "Group RỖNG không render" đi ngược scope "cây mirror sidebar".** `sub-invest:55`. Spec ① `:7` gọi mảnh-3 là "cây mirror sidebar"; DoD K5 `spec-tổng:30` = "sub-folder khớp sidebar". Sidebar LUÔN đủ 8 nhóm (menu-row cố định) còn cây ẩn nhóm rỗng ⇒ 2 mặt hết mirror; user tìm "nhóm 5" trên cây tưởng mất. Lý do lane nêu (8 folder rỗng = noise) hợp lý nhưng đây là quyết-định hiển-thị của owner. Đề nghị: hỏi 1 câu / hoặc mặc định hiện đủ 8 + toggle "ẩn nhóm rỗng".

### LOW

**L1 — A5 đo nhãn G1 có thể PASS-ở-DB nhưng SAI-ở-UI.** `MenuItem.cs:13,16`: `DisplayLabel` override label cho fe-user (`IsVisible=false` ẩn khỏi `/menus/me`). labelBackfill sửa `Label`, không sửa `DisplayLabel`. Đo Dev: 10/10 row `Khkk*` + `KeHoachKyKet` có `DisplayLabel = NULL`, `IsVisible = 1` ⇒ hiện an toàn. Đề nghị A5 thêm vế `AND DisplayLabel IS NULL`.

**L2 — [PASS mạnh, có số] Tập key K6 ĐỦ trên Dev; C1 chốt N = 7.** Đo `SolutionErp_Dev`: `MenuItems` TOTAL = **142** (khớp con số STATUS ghi cho prod); root (ParentKey NULL) = **12** = {Dashboard 10, PurchaseEvaluations 25, KeHoachKyKet 26, Hrm 28, Off 29, Personal 30, Contracts 31, HopDongCung 32, Forms 40, Reports 50, Master 80, System 90} ⇒ 10/12 nằm trong `ADMIN_HIDDEN_KEYS`, 2 chừa đúng {Dashboard, System}; con của System = 7 {ApprovalWorkflowsV2, MenuVisibility, Permissions, PeWorkflows, Roles, Users, Workflows} ⇒ ẩn 2 (Workflows, PeWorkflows), chừa 5. `AwV2_*` = đúng **2 row**, **0 `AwV2_Contract`** ⇒ **N = 7 lá** (Dashboard + Users + Roles + Permissions + MenuVisibility + 2 AwV2). Còn lại chỉ cần xác nhận prod.

**L3 — "route-map ×2 app khai trong PR" trỏ artifact không tồn tại.** 0 hit `route-map` trong `docs/PROJECT-MAP.md|STATUS.md|HANDOFF.md`; và cụm-2 KHÔNG đẻ route React nào — 48 leaf tái dùng `/khkk/list|create|workflow-matrix` đã có (`fe-user/src/App.tsx:81-83`). Surface drift thật = `resolvePath` trong `Layout.tsx` (fe-user) + 2 row STATUS. Đề nghị đổi chữ thành "bảng key→URL dán PR body".

**L4 — Thứ tự sidebar 4 GĐ không liền mạch (đo Dev, xem L2):** GĐ1=25, GĐ2=26, **Hrm 28 / Off 29 / Personal 30**, GĐ3=31, GĐ4=32. Sau K4a, GĐ2 phình thành 8 nhóm ngồi giữa dải bị chen. Owner S159 vẽ "menu = 4 giai đoạn" (`DbInitializer.cs:1772-1773`) nên đây là thứ anh ấy sẽ thấy ngay. Không thuộc scope — đề nghị hỏi 1 câu (đổi Order 27/28), cơ chế upsert re-set Order đã có (`:1926-1943`).

**L5 — Lý luận bất-đối-xứng giữa K4c và K5 về bề mặt admin.** `PipelineTreePanel` render trong `fe-admin/src/pages/khkk/KhkkListPage.tsx:139` và `fe-admin/src/pages/contracts/HardCopiesPage.tsx:117` ⇒ sau K6 hai trang này mất lối vào từ menu admin (chỉ deep-link). Lane đã khai chuyện "dead-by-K6" cho regex nhưng KHÔNG khai cho K5. Giữ mirror (đúng §3.9) nhưng nên khai 1 dòng để lượt sau không tưởng thừa.

**L6 — B2 khả thi, 0 nợ drift ẩn (đo trước):** 6/6 cặp file ×2 app hiện SHA-identical — `usePipelineStages.ts` 7144eeb7… · `PipelineStageFolders.tsx` d449cc8d… · `PipelineTreePanel.tsx` d70ac315… · `KhkkListPage.tsx` 40a650bf… · `KhkkCreatePage.tsx` ea51e762… · `types/khkk.ts` 8b757f5b… (`types/khkk.ts` ĐÃ tồn tại sẵn ⇒ K5 mục 5 là mở rộng, không phải tạo mới).

### Điểm chịu được soi (khai để không hạ chuẩn 1 chiều)

- **P1 — mục 5 "sửa grounding 13 vai" ĐÚNG:** `DbInitializer.cs:2427` loop `roleManager.Roles` (mọi role), `allowDelete = Admin || Drafter` (`:2436-2437`); "7 vai" thật sự là seeder PE `:2669-2675`. Lane bác đúng đề bài, có chứng.
- **P2 — "extend `KhkkKeys()`" là BẮT BUỘC, không thừa:** `InReviewScope(key) => key.StartsWith("Pe_")` (`DbInitializer.cs:2158`) ⇒ đưa 49 key vào `MenuKeys.All` KHÔNG tự kéo chúng vào site-2.
- **P3 — trích dẫn dòng sạch 100%:** tao soi ~25 cite (seed `:1760/:1771/:1775/:1776/:1780-1787/:1788/:1789/:1791-1794/:1795/:1797/:1798/:1801-1803/:1809/:1827/:1853` · `KhkkKeys():2185-2189` · `SeedKeHoachKyKetAccessAsync:2415` + keys `:2420-2425` · `isAdminHidden:182-184` + `filterForAdmin:186-190` + comment S57 `:179-181` · fe-user `Layout.tsx:59-62/:79/:82-83/:341-347` · `KhkkListPage:60-61/:74/:75-82` · `PipelineStageFolders:58-72/:68-69/:74-76` · `usePipelineStages:189+/:222-224/:231/:240`) — **0 sai**.
- **Q5 trả lời:** việc "viết lại comment `:177-184`" CÓ trong danh sách (K6 mục 2) và mô tả đúng: `:179-181` = "[S57] BỎ ẩn Danh mục… (đảo S29 hide)" ⇒ QĐ7 là lần đảo THỨ 2. Không rơi.
- **Q1 trả lời gọn:** K4a-labelBackfill ✓ · K4b `?group` ✓ (thiếu queryKey — H1) · K4c WfView-lọc-Code ✓ (mục 11, KHÔNG rớt) · Create-preset ✓ nhưng sai wave (M4) · K5 GĐ3/4 group-by `c.type` ✓ (mục 4, có làm, không chỉ GĐ2) · K5 fallback "(chưa phân nhóm)"/"(ngoài chỉ mục)" ✓ · K6 `git diff --stat -- fe-user/src = 0 file` ✓ (C2). **Mục RƠI thật sự = 1: `queryKey`.**
- **Q4 trả lời:** precedent-split khai ĐỦ 2 khuôn (`sub-invest:9` + evidence `DbInitializer.cs:2193-2195`). Chỗ người build dễ nhầm = (a) tưởng "vào All ⇒ site-2 tự có" — bác bằng P2; (b) tưởng "không có row ⇒ không render" — bác bằng M1.

## Verdict

**PASS-WITH-FLAGS** — 16 finding (3 HIGH · 7 MED · 6 LOW), 0 blocker chặn ship nếu H1+H2+H3 được áp trước khi K4a/K4b vào việc.
Nền cụm-2 chắc: tập key K6 đo được là ĐỦ (L2), trích dẫn sạch 100% (P3), 2 claim chịu-lực đúng (P1/P2). Lỗ nằm ở 3 chỗ quen thuộc: **cache-key** (H1), **thứ-tự-vs-acceptance** (H2), **số-tuyệt-đối-trong-doc** (H3).
