# c2-l1

LANE 1/3 — LENS QUYẾT-vs-CODE. Đối tượng: `sub-invest-fable-b2-cum2.md` (K4 sidebar 49-row · K5 cây groups · K6 admin 12-key blacklist; 0 mig).

**Dòng-1:** `C2L1: 4/7 ĐÚNG · 0 SAI · 3 LỆCH` — 7 finding (2 MAJOR · 3 MEDIUM · 2 LOW), 0 blocker về QUYẾT.

## Findings

### F-1 [MEDIUM · LỆCH lý-do, giữ kết-luận] "group-key CẦN CanRead row để render" — bác bằng code

- Spec `:24` biện hộ site-2 extend +7 group vì "group-key CẦN CanRead row để render".
- `GetMyMenuTreeQuery.cs:96` — `static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess);` (filter `:97-101`) ⇒ node **không có CanRead vẫn được giữ** nếu bất kỳ con nào readable.
- ⇒ CanRead cho `Khkk_G2..G8` **không phải điều kiện cần**. Quyết định (extend site-2) vẫn hợp lý vì tính nhất-quán với G1, nhưng LÝ DO sai ⇒ đẻ bất-biến giả cho lượt sau.
- Đề nghị: đổi câu lý do thành "đồng bộ với G1 (`KhkkKeys():2185-2189` đang cấp CanRead cho `Khkk_G1`), KHÔNG phải điều kiện render".

### F-2 [🔴 MAJOR · mâu thuẫn NỘI TẠI: acceptance A1/A2 không thể PASS ở nấc spec tự xếp]

- Spec `:119` — "Acceptance A1/A3-A6 đo được **NGAY sau K4a**"; `:104` — ship K4a một mình thì "leaf **RENDER được**, LỌC chưa đúng"; `:33` gọi đó là "silent-wrong".
- Code bác: `fe-user/src/components/Layout.tsx:336-338`
  ```
  const path = resolvePath(node.key)
  const location = useLocation()
  if (!path) return null
  ```
  ⇒ leaf không có route bị **DROP IM LẶNG** (đúng gotcha #50 mà chính spec trích ở mục 7/10; comment cùng file `:86` + `:98` nói y vậy).
- Route cho `Khkk_G{n}_{...}` chỉ sinh ở **K4c mục 10** (regex `resolvePath`), mà sơ đồ `:114` + thứ tự `:117` xếp K4c **CUỐI CÙNG** vì "chờ K3".
- ⇒ Trạng thái thật sau K4a-một-mình: 7 group `Khkk_G2..G8` hiện **rỗng**, **42 leaf KHÔNG hiện** ⇒ A1 ("48 leaf HIỆN sidebar") FAIL by-construction; A2 cũng không bấm được. Failure-mode **tệ hơn** mô tả của spec (vô hình ≠ lọc sai).
- Đề nghị (rẻ): **tách K4c mục 10 khỏi phụ-thuộc K3** và ship CÙNG K4a. Regex chỉ cần khuôn URL (`/khkk/list?group=n`, `&filter=…`, `?view=deleted`, `/khkk/create?group=n`, `/khkk/workflow-matrix?type=10&group=n`) — **không** cần Code `KHKK-N{n}`. Chỉ **mục 11** (WfView lọc `Code === 'KHKK-N'+n`) mới thật sự chờ K3. Sửa kèm: `:104`, `:119`, sơ đồ `:106-115`, checklist `:125`.

### F-3 [MEDIUM · chú-thích đặt nhầm app] BÁC-2 đúng, nhưng K6 là "điều kiện tiên quyết ẩn" của K4c-13

- Verify 2 dòng spec nêu: `fe-admin/src/components/Layout.tsx:186-190` `filterForAdmin` **đệ quy** (`.filter(n => !isAdminHidden(n.key)).map(n => ({...n, children: filterForAdmin(n.children)}))`), gọi tại `:315` ⇒ ẩn root = subtree không render ✅ ĐÚNG.
- fe-admin **không có** `WorkflowMatrixViewPage`: grep toàn repo — file chỉ ở `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx`; fe-admin chỉ có **comment** nhắc tên tại `:130`; route `/khkk/workflow-matrix` chỉ tồn tại `fe-user/src/App.tsx:83` ✅ ĐÚNG (comment lệch-có-chủ-đích `:129-133` khớp verbatim).
- 🔴 Nhưng spec mục 13 + K6-4 dặn ghi comment khai lệch **tại regex fe-user**. Rủi ro nằm ở **fe-admin**: ai gỡ `'KeHoachKyKet'` khỏi `ADMIN_HIDDEN_KEYS` lượt sau ⇒ 42 leaf render, `resolvePath` fe-admin trả null (`fe-admin/Layout.tsx:268-270`) ⇒ drop im lặng.
- Đề nghị: comment nằm NGAY tại `ADMIN_HIDDEN_KEYS` / `isAdminHidden` (`fe-admin/Layout.tsx:182-184`), nội dung "gỡ `KeHoachKyKet` khỏi set ⇒ phải nới regex `Khkk_G{n}_*` cùng lượt, nếu không 42 leaf drop im lặng (#50)".

### F-4 [MEDIUM · dụng cụ đo sai tầng] C1: SQL `MenuItems LIKE 'AwV2%'` KHÔNG chốt được N

- Đo thật Dev LocalDB (`SolutionErp_Dev`): `SELECT [Key] FROM MenuItems WHERE [Key] LIKE 'AwV2%' OR [Key] LIKE 'Khkk%'` ⇒ **AwV2 = 2 row** (`AwV2_DuyetNcc`, `AwV2_DuyetNccPhuongAn`), **Khkk = 7 row** (`Khkk_G1` + 6 leaf) ✅ khớp "7 key cũ". `AwV2_Contract` = **0 hit** toàn `src/ · scripts/ · fe-*/src` (chỉ có so chuỗi `code === 'Contract'` ở `fe-admin/Layout.tsx:169`).
- 🔴 Row MenuItems tồn tại **không** kéo theo hiện sidebar: `AwV2_Contract` ∉ `MenuKeys.All` (`MenuKeys.cs:180-183` chỉ 3 key AwV2) ⇒ site-1 `:2058` không phát row Admin, cũng không nằm trong `reviewKeys` (`:2210-2224`) ⇒ `resolved` thiếu key ⇒ `HasAccess=false` (`GetMyMenuTreeQuery.cs:96`) ⇒ **bị lọc khỏi cây dù row tồn tại**. N=8 chỉ xảy ra khi prod có CẢ row MenuItems LẪN row Permissions CanRead (grant tay).
- Đề nghị đổi phép đo C1 sang JOIN, chạy trên **env nghiệm thu** (prod), không phải Dev:
  `SELECT m.[Key] FROM MenuItems m JOIN Permissions p ON p.MenuKey=m.[Key] JOIN Roles r ON r.Id=p.RoleId WHERE r.Name='Admin' AND p.CanRead=1 AND m.[Key] LIKE 'AwV2%'`.

### F-5 [LOW-MED · A5 đo nhãn sai dụng cụ] `DisplayLabel` che `Label` ở fe-user

- `MenuItem.cs:12-13` khai rõ: `IsVisible=false` → ẩn khỏi `/menus/me` fe-user, fe-admin vẫn thấy; `DisplayLabel="X"` → **fe-user render X thay Label**, fe-admin giữ Label.
- Xác nhận code: `fe-user/Layout.tsx:204-206` `effectiveLabel = (displayLabel && trim) || label`; fe-admin **0 hit** `displayLabel`.
- ⇒ A5 "labelBackfill đổi đúng 1 nhãn G1" đo bằng UI ×2 app có thể ra **2 nhãn khác nhau** nếu prod từng đổi tên qua MenuVisibility (Mig 27) ⇒ chẩn đoán nhầm "backfill không ăn".
- Đề nghị: A5 đo CỘT — `SELECT [Key],Label,DisplayLabel FROM MenuItems WHERE [Key]='Khkk_G1'`, khai rõ nếu `DisplayLabel IS NOT NULL`.

### F-6 [🔴 MAJOR · nền drift treo vào thứ tự ship] "64→113 / 256→452" chỉ đúng NẾU cụm-1 land trước

- Số học tự nhất quán ✅: 49 = 7 + 42 · 64×4 = 256 · 113×4 = 452 · 64+49 = 113.
- Nền hiện tại đo thật: `MenuKeys.All` đếm tay = **55** (Dashboard 1 + Master-set 4 + Catalogs 5 + Contracts/Forms/Reports 3 + PE 1 + Hrm 2 + HrmConfig 5 + Vehicles/Drivers 2 + Off 3 + PhongHop 4 + DeXuat 4 + DonTu 4 + DatXe/ItTicket/ChamCong/AttReport/HrmDash 5 + Personal 1 + KeHoachKyKet 1 + System-set 7 + AwV2 3) — khớp canonical `docs/STATUS.md:470-471` (Policies **220** = 55×4 · Menu keys **55**). Cụm-1 khai 55→64 (+9) tại `spec-cum1-chi-tiet-31-07-2026.md:8`.
- 🔴 Nhưng cụm-2 tự xếp `:118` "K6 + K4a chạy **song song cụm-1** ngay hôm nay (0 chặn)" + sơ đồ `:109-110` vẽ K4a độc lập ⇒ K4a **có thể land TRƯỚC K1**. Khi đó dòng đúng là **55→104 · 220→416**; ghi "64→113 · 256→452" = ghi **số SAI vào canonical** (đúng loại lỗi mà B1/detector đang canh).
- Đề nghị: khai drift theo **DELTA** (`+49 key · +196 policy`) + lệnh đo lại `MenuKeys.All` từ mã tại thời điểm commit; hoặc ràng buộc cứng "K4a ship SAU K1".
- Phụ: `MenuItems LIKE 'Khkk%'` = 56 sau K4a ✅ (7 đo thật + 49 mới) — root `KeHoachKyKet` không khớp LIKE nên 56 đúng.

### F-7 [LOW · neo số trong chú thích] comment `:1959-1961` tự trỏ upsert STALE

- Spec mục 3 trích comment `DbInitializer.cs:1959-1961` ("sửa chữ trong `tree.Add` VÔ TÁC DỤNG") — comment đó tự ghi upsert ở "`:1893-1905`", nhưng upsert THẬT ở **`:1926-1943`** (đúng như spec mục 2 ghi).
- Diff K4a chạm ngay khối này ⇒ sửa luôn cite trong comment (bài S161: diff sửa mã phải sửa MỌI số/neo trong chú thích).

## Bảng verify claim chịu lực (grep/đo, KHÔNG suy)

| # | Claim (spec) | Kết | Dòng chứng |
|---|---|---|---|
| a1 | site-1 All-loop CHỈ Admin | ĐÚNG | `DbInitializer.cs:2047` method · `:2049` `FindByNameAsync(AppRoles.Admin)` · `:2058` `foreach (var menuKey in MenuKeys.All)` · `:2061` `RoleId = adminRole.Id` |
| a2 | `KhkkKeys():2185-2189` feed `reviewKeys` | ĐÚNG (EXACT) | `:2185-2189` (7 key: `Khkk_G1`+6 leaf) → `:2216 .Concat(KhkkKeys()).Concat(HdcKeys())` trong `reviewKeys :2210-2224`; method `SeedAllRolesReviewReadPermissionsAsync :2144`, gọi `:2090` |
| a3 | site-2 = CanRead mọi role, skip-existing | ĐÚNG | `:2225` `roleManager.Roles` · `:2251` `continue` (non-Pe skip) · `:2254-2260` insert `CanRead=true, CanCreate=isPe(false), CanUpdate=false, CanDelete=false` |
| a4 | site-3 độc quyền cờ GHI, comment `:2418-2419` | ĐÚNG (verbatim) | `:2415` method · `:2418-2419` "Khkk_G1 = group điều hướng, CanRead-all đã có từ nhánh read-only" · `:2440-2447` nâng R/C/U + D · site-2 không bao giờ set C/U/D cho key non-Pe |
| a5 | "KHÔNG retire site nào" | ĐÚNG | 3 site không giao nhau về cờ; thứ tự `:2090` site-2 → `:2097` revoke (chỉ `Hrm*`/`Off*`/`Personal`) → `:2110` site-3 ⇒ Khkk không bị revoke |
| a6 | đủ chỗ sửa cho 13-role THẤY leaf | **LỆCH** | thiếu ràng buộc thứ tự cho `resolvePath` fe-user ⇒ **F-2**. Không thiếu: `fe-*/lib/menuKeys.ts` (**0 hit `Khkk`** cả 2 file) · `IsVisible` mặc-định `true` (`MenuItem.cs:15` + `MenuItemConfiguration.cs:18`) nên `fe-user/Layout.tsx:200` không nuốt row mới |
| b | `:2427` 13/13 role · Delete=Drafter+Admin `:2437` · keys từ đâu | ĐÚNG | `:2427` `var roles = await roleManager.Roles.ToListAsync()` (KHÔNG lọc) · `AppRoles.cs:23-28` All = **13** · đo `SELECT COUNT(*) FROM Roles` = **13** · `:2437` `role.Name == Admin \|\| Drafter` · **biến `keys` = `:2420-2425`, 7 phần tử, KHÔNG có `Khkk_G1`** ⇒ convention "group không nhận cờ ghi" là THẬT ⇒ site-3 "+42 leaf (không group)" nhất quán |
| b2 | BÁC-1: "7 vai" là của PE, không áp KHKK | ĐÚNG | `:2669-2675` `roleNames` = Drafter/DeptManager/Procurement/CostControl/ProjectManager/Director/AuthorizedSigner = **7** |
| c | fe-admin khỏi regex (đệ quy + thiếu page) | ĐÚNG (+F-3) | `fe-admin/Layout.tsx:186-190` đệ quy, gọi `:315` · `WorkflowMatrixViewPage` chỉ ở fe-user (`fe-user/App.tsx:83` route `/khkk/workflow-matrix`) · comment `:129-133` |
| d1 | bẫy `content` bắt truyền count tay | ĐÚNG (EXACT) | `PipelineStageFolders.tsx:68-69` "Bắt buộc khi dùng `content`" · `:74-76 stageCount` · nhánh `content` render `:164-165` TRƯỚC leaves ⇒ mất empty-state `:166-167` + `<ul>` `:169-202` · type `:58-72` · `ml-3 border-l` `:156` · `STAGE_STYLE:39-44` · `hideEmpty` `:228` |
| d2 | 3 file mirror; `PipelineTreePanel` không cần đổi | ĐÚNG | `PipelineTreePanel.tsx:16-17` import `usePipelineStages`+`PipelineStageFolders` · `:93 buildStages` · `:299 <PipelineStageFolders` ⇒ **consumer**, groups chảy qua. Bonus: 4 file hiện **byte-identical ×2 app** (10.677 B · 15.819 B) ⇒ B2 đo được ngay |
| d3 | builder GĐ2/GĐ3/GĐ4 cite | ĐÚNG (±1) | GĐ2 `usePipelineStages.ts:188-206` (spec ghi 189-206) · `approvalGroup` **0 hit** ⇒ đúng là chờ K2 · "(ngoài chỉ mục 200)" `:224` (comment `:222-223`) · `c?.hasSealedCopy` `:231` · `c.type` `:240` |
| e | 12 key blacklist đủ root, không ẩn nhầm | ĐÚNG (12/12 cite EXACT) | Master `:1760` · Contracts `:1771` · KeHoachKyKet `:1775` · HopDongCung `:1776` · Forms `:1788` · Reports `:1789` · Workflows `:1795` · PurchaseEvaluations `:1797` · PeWorkflows `:1798` · Hrm `:1809` · Off `:1827` · Personal `:1853` (+HrmConfig parent Master `:1816`). Root trong seed = 12 (thêm Dashboard `:1756`, System `:1790`) ⇒ phủ 10 root + 2 con-System; CHỪA Dashboard + System-4-leaf `:1791-1794` + AwV2 `:1801-1803` ⇒ **0 root nghiệp vụ sót · 0 System-leaf ẩn nhầm · AwV2 sống** |
| f | SQL chốt N 7/8 | **LỆCH** | xem **F-4** (đo Dev = 2 AwV2; cần JOIN Permissions + đo trên prod) |
| g | drift 64→113 · 256→452 | **LỆCH nền** | số học đúng; nền 55/220 (`docs/STATUS.md:470-471`) ⇒ xem **F-6** |

## Verified-OK (không phải lỗi — ghi để lane khác khỏi false-flag)

- `AuthorizePolicyRegressionTests.cs:139` + `:402` chỉ assert `MenuKeys.All.Should().Contain(...)`, **không** assert `|All|` ⇒ thêm 49 key KHÔNG vỡ test hiện có; "1 test BE" ở checklist `:129` là bổ sung, không phải sửa test cũ.
- "Policy sinh thêm 49×4 registered-unused = vô hại" ✅ — `Program.cs:82` loop thuần `All × Actions`, không nơi nào đếm.
- `group` KHÔNG nằm trong `TRANSIENT_QUERY_KEYS` (`fe-user/Layout.tsx:307` = `id,q,editHeader,page,phase,awId`) ⇒ `group` **mang danh tính** trong `queryMatches`/`isActive` (`:326`, `:347`) ⇒ quyết định K4b mục 8 (6 URL G1 phải mang `group=1`) là BẮT BUỘC, spec ghi đúng.
- Root `KeHoachKyKet` có con ⇒ render `MenuGroup`, không đi `MenuLeaf` ⇒ không đụng active-check; staticMap `:79` trở thành đường chết vô hại.
- fe-admin vẫn có `pages/khkk/{KhkkListPage,KhkkCreatePage,KhkkDetailPage,KhkkWorkflowPanel}.tsx` ⇒ mục 13 "fe-admin VẪN sửa 2 page (deep-link)" có cơ sở; `PipelineTreePanel` còn nhúng ở `fe-admin/pages/khkk/KhkkListPage.tsx:139` + `pages/contracts/HardCopiesPage.tsx:117` ⇒ cây K5 sống trên admin qua deep-link dù K6 ẩn menu (không mâu thuẫn; B1 nên đo trên fe-user và khai rõ).

## Verdict

**PASS-WITH-FLAGS** — 0 blocker về QUYẾT (khuôn `groups` K5, blacklist-12 K6, 3-site-grant K4a đều đứng vững trước code). 7 flag: **2 MAJOR** phải xử trước khi ship (F-2 tách regex khỏi K3 ⇒ A1 mới đo được · F-6 khai drift theo DELTA hoặc ràng buộc thứ tự), **3 MEDIUM** (F-1 sửa lý-do · F-3 dời comment sang fe-admin · F-4 đổi SQL sang JOIN + đo prod), **2 LOW** (F-5 A5 đo cột · F-7 vá cite stale trong comment).
