GATE-K5: IN-PROGRESS # Gate K5 — adversarial review cây pipeline GĐ2 mở 8 sub-folder nhóm (S166, 2026-08-01) Vai: reviewer (READ-only, verdict-only). Ghi TỪNG MỤC, Edit sau mỗi mục (chống #53). ## §0 Mốc đo (neo trước khi soi) - Lệnh: `git status --porcelain` + `git rev-parse HEAD` lúc 1st-measure. - 🔴 **LỆCH TIỀN-ĐỀ #1:** spec lead nói "diff chưa commit trên HEAD `88e7ecf`". Đĩa đo được **HEAD = `ff4d4dc9d0b745867f83b5349f3c239d6b2ee798`**. Phải truy `git log` xem `88e7ecf` nằm đâu trước khi tin base. - 🔴 **RÀO GIT #1:** `git status` có `?? menus.json` ở ROOT repo (untracked, ngoài spec 4 file). Cần đo: file gì, có bị commit ké không. - Cây làm việc lúc mốc-0 (8 file M + 3 ??): 4 file code đúng spec + 4 file `.claude/**` + 3 untracked. ## §1 Phân lập diff + rào git (đo, không tin lời khai) - `git log`: `88e7ecf` LÀ tổ tiên của HEAD ⇒ tiền-đề lead đúng về **nội dung**, sai về **tên mốc**: giữa `88e7ecf` và HEAD có **3 commit `wal: flush`**. `git diff --name-only 88e7ecf..HEAD` = **6 file, TOÀN BỘ `.claude/**`**, 0 file code ⇒ base code vẫn là `88e7ecf`. Verdict K5 đo trên `git diff` (working-tree vs HEAD). - 🔴 Cây PHÌNH GIỮA LƯỢT (lần thứ 3 trong 3 gate liên tiếp): HEAD `ff4d4dc` lúc đo-0 → `13f7f82` sau ~2 tool-call. Phân lập: `git diff --stat -- fe-admin fe-user src tests` = **đúng 4 file, +466/−108** khớp spec TỪNG SỐ. - `git status --porcelain -- src tests fe-admin fe-user` = **0 dòng `??`** ⇒ không có file code chưa track (bài "HIGH nằm ở git" KHÔNG tái phát trong vùng code). - 🔴 **RÀO GIT — `menus.json` 44.656 B nằm ở ROOT repo, untracked** (mtime 10:25, nội dung = payload `/api/menus/me`, sản phẩm phụ của lane cicd verify prod). Không phải file của K5, nhưng nếu lead commit bằng `git add -A`/`git add .` thì nó lên repo. Đây là finding **về THAO TÁC COMMIT sắp tới**, không phải về diff K5. - Cùng loại: `.claude/agent-memory/implementer-frontend/archive/2026-H2.md` untracked (hợp lệ, nên add có chủ đích). ## §2 Build gate — TÔI TỰ CHẠY, không nhận lời khai | lệnh | nơi chạy | exit | |---|---|---| | `npx tsc -b` | `fe-user` | **0** | | `npx tsc -b` | `fe-admin` | **0** | Lời khai lane (`tsc -b` ×2 exit 0 + `npm run build` ×2) khớp phần tôi đo lại. Bundle hash lane khai (`index-CZUcTUUg.js` / `index-BZWvx0Y8.js`) tôi **KHÔNG** kiểm lại — chưa deploy nên không có ô canonical để đối chứng; ghi rõ là **lời khai chưa đo**, đừng đọc thành đã-verify (bài K4a ③). ## §3 SOI-1 — Node nhóm đúng nguồn + route khớp K4b ✅ - **1 nguồn, 0 map thứ hai.** `grep KHKK_APPROVAL_GROUPS|khkkGroupMenuLabel` toàn `fe-user/src`+`fe-admin/src`: consumer = hook K5 (`:217/:228/:230`) + `KhkkCreatePage` + `KhkkListPage`, **tất cả import từ `@/types/khkk`**. Không có mảng 8 phần tử nào khác, không có bảng nhãn thứ hai. - 8 node dựng bằng `KHKK_APPROVAL_GROUPS.map(...)` (hằng `[1..8] as const`), nhãn `khkkGroupMenuLabel(n)` = `"{n}. " + KHKK_APPROVAL_GROUP_LABELS[n]` — **derive, không chép**. - 🟢 **ĐỐI CHIẾU LIVE (tôi tự đo, không nhận lời khai):** so nhãn cây suy ra ⟂ nhãn thật trong payload `/api/menus/me` (`menus.json` prod) → **8/8 KHỚP TỪNG KÝ TỰ**, cả 8 node `Khkk_G1..G8` đều `displayLabel = null`. Tức hôm nay cây và sidebar gọi cùng một nhóm bằng cùng một tên. - **Route:** `onOpenAll → /khkk/list?group=${n}`. Đối chiếu K4b: `Layout.tsx:217` `khkkLeafPath(group,'List')` = `/khkk/list?group=${group}`, và leaf tĩnh `Khkk_List: '/khkk/list?group=1'` (`:63`) ⇒ **chuỗi TRÙNG KHÍT**. - **Nối end-to-end (3 chặng, đo cả 3):** `KhkkListPage.tsx:79-80` đọc `searchParams.get('group')` + rào `^[1-8]$` → `:124` gửi `group` lên server → BE `ContractSigningPlanFeatures.cs:757-758` `q.Where(p => p.ApprovalGroup == request.ApprovalGroup)`. Không phải tham-số-trang-trí (khác hẳn bài K4a ①). - **Sáng đúng mục sidebar:** `Layout.tsx:388-390` `isActive = pathname khớp && queryMatches(...)` (so khớp TẬP query) ⇒ `?group=n` từ cây ăn khớp leaf nhóm n. Lời khai lane "trang tự sáng đúng mục" **đúng, đã kiểm**. ## §4 SOI-2 — Phân phiếu, fallback, bất-biến Σ ✅ (1 MINOR) - **KHÔNG có ca phiếu rơi khỏi cả 9 ngăn.** Vòng `:215-224` là if/else **toàn phần**: khớp `typeof g === 'number' && GROUPS.includes(g)` → ngăn n; **mọi thứ còn lại** (null · undefined · thiếu field · 0 · 9 · NaN · chuỗi) → `khkkUngrouped`. Không nhánh nào `continue`/bỏ qua. - **Σ enforce ở đâu — trả lời thẳng: ở CẤU TRÚC, không phải ở kỷ luật.** `khkkLeaves` và các ngăn cùng sinh từ **một mảng `khkkItems`** bằng một phép phân hoạch; `stage.leaves` giữ vai danh sách phẳng canonical và `stageCount()` (`PipelineStageFolders.tsx:96`) vẫn đếm trên nó ⇒ chip số của folder cha **không thể** lệch tổng các ngăn. `console.warn` `:252` chỉ là chuông, và là **DEV-only**. - Thứ tự đúng: ngăn "(chưa phân nhóm)" `push` ở `:238-245` **TRƯỚC** khi `reduce` tính Σ ở `:251` ⇒ chuông không tự báo động giả. - **Ngăn lạc gần như không thể xảy ra** (đo BE): `ApprovalGroup` là `int` non-null, validator `InclusiveBetween(1,8)` (`:353-356`), default 1 (`:399`), backfill Mig 71 = 1, và **0 đường update** nào ghi lại field này. ⇒ nhánh fallback là phòng thủ, không phải đường sống. - 🟡 **MINOR-1 — nhãn ngăn lạc hứa một đằng, đi một nẻo:** `onOpenAll: () => navigate('/khkk/list')` (`:243`). Không kèm `?group=` ⇒ BE **không lọc gì cả**, trang mở ra là **toàn bộ phiếu của cả 8 nhóm**, trong khi nhãn và tooltip nói "Mở danh sách: (chưa phân nhóm)". Sidebar cũng không sáng mục nào (query rỗng không khớp leaf nào). BE hiện **không có** bộ lọc "không thuộc nhóm nào" để trỏ tới, nên đây là hạn chế thật chứ không phải quên gõ. Đề nghị: bỏ `onOpenAll` cho riêng ngăn này (component đã có nhánh `` khi thiếu `onOpenAll`, `:151-155`) — mất một cú bấm gây hiểu nhầm, không mất gì khác. ## §5 SOI-3 — Ngăn rỗng render THẬT ✅ Đo đường đi, không đọc lời khai — **3 chốt, không chốt nào lọc theo `length`**: 1. Dựng: `KHKK_APPROVAL_GROUPS.map(...)` — `.map` trên hằng 8 phần tử, **0 `.filter`** trong cả khối. 2. Vẽ: `PipelineStageFolders.tsx:287-295` `stage.groups !== undefined ? stage.groups.map(g => )` — vẽ **thẳng mảng nhận được**, không điều kiện phụ. 3. Trong ngăn: `GroupFolder` `count === 0` → vẫn render `
` đủ (chevron + tên bấm được + `SoonChip`), thân in *"Chưa có phiếu ở nhóm này."* - Nhánh *"Chưa có dữ liệu ở giai đoạn này."* (`:296`) nay **bị nhánh `groups` chặn trước** ⇒ GĐ2 rỗng hiện 8 ngăn thay vì 1 dòng. Đúng spec "HIỆN ĐỦ 8 kể cả rỗng". - Không đụng `hideEmpty`: vẫn lọc ở tầng **stage** bằng `stageCount()` = `leaves.length` ⇒ đang tìm-kiếm mà gói không có KHKK thì cả folder GĐ2 ẩn, **không** đổ ra 8 ngăn rỗng. (đang tiếp tục §6)