Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4a.md
2026-08-01 09:37:26 +07:00

239 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

GATE-K4A: PASS-WITH-FLAGS 4
# Gate K4a — adversarial review wave K4a (49 row menu KHKK 8-nhóm)
- Spawn: S166 2026-08-01, HEAD `7a903cf`, diff CHƯA commit.
- Artifact ghi TỪNG MỤC (anti-#53). Dòng-1 = verdict.
## Mốc đo (neo — sẽ re-đo trước verdict, bài S166 cây-phình)
- HEAD THẬT lúc review = `ce15c64` ("wal: flush 20260801T0914"), KHÔNG phải `7a903cf` như đề bài.
Đã kiểm `git show --stat ce15c64`: chỉ chạm `.claude/**` (WAL + agent-memory + run-folder) ⇒
KHÔNG có code src/fe lọt vào commit đó. Đề bài lệch 1 commit governance = vô hại, ghi để neo.
- `git diff --stat -- src fe-user fe-admin` @T0 = **6 file, +213 / 5**.
- `git status --porcelain -- src fe-user fe-admin tests` @T0 = 6 M + 1 `??`
(`tests/SolutionErp.Infrastructure.Tests/Application/KhkkGroupMenuSeedTests.cs`) — khớp kỳ vọng 7 file.
- Ngoài scope code: 5 file `.claude/**` M (memory/run-folder/session-counter) — KHÔNG tính cây-phình
code, nhưng lead PHẢI `git add` có chọn lọc (đừng `git add -A` kéo `.session-counter.json` vào commit code).
## Mục 7 — thứ tự static initializer (ĐO)
`MenuKeys.cs`: `KhkkGroupNumbers` :72 → `KhkkLeafActions` :73 → `KhkkGroup/KhkkLeaf` :76-77 →
`KhkkGroupMenuKeys` :81-84 → spread `.. KhkkGroupMenuKeys` trong `All` :244. Thứ tự văn bản ĐÚNG.
Nếu ai dời khai báo xuống dưới `All`: spread mảng `null` → NRE trong static ctor →
`TypeInitializationException` ở BẤT KỲ truy cập `MenuKeys.*` ⇒ T1+T2 (và ~mọi test chạm MenuKeys) ĐỎ.
⇒ có răng, tuy răng là "nổ" chứ không phải assert-đẹp. PASS.
## Mục 6 — vá-10/11/13 (comment) — ĐO
- Vá-13 `1893`: `grep -n 1893 DbInitializer.cs` = **0 hit**; `grep -rn 1893 src/` chỉ ra file .dll nhị
phân (nhiễu, không phải mã nguồn). 2 comment đã trỏ `:2162-2179` + mỏ neo `existingItems.TryGetValue`.
Kiểm neo mới: `existingItems.TryGetValue` THẬT ở `:2164`, vòng `foreach` bắt đầu `:2162`, `added++`
kết `:2178`, đóng ngoặc `:2179` ⇒ khoảng trỏ ĐÚNG (chứa trọn nhánh "key đã tồn tại → re-set Order
→ continue" `:2164-2176`). PASS.
- Vá-11 revoker: comment `:2335-2340` khai "filter chỉ bắt prefix Hrm*/Off* + key Personal ⇒ 0 match
Khkk_*" — sẽ verify bằng đọc `RevokeTemporarilyHiddenModulesAsync` (mục dưới).
- Vá-10 lý-do site-2: comment khai `GetMyMenuTreeQuery.cs:96 HasAccess` — sẽ verify.
## Mục 4 — nhãn 8 nhóm vs transcribe (ĐO BẰNG SCRIPT, không mắt)
So khớp CHUỖI THÔ (`==`, không chỉ NFC) giữa `khkkGroupLabels[2..8]` + `("Khkk_G1", …)` trong
`DbInitializer.cs` và bảng `danh-muc-sp002-transcribe.md:22-29`: **8/8 rawEq=True**, gồm cả nhóm dài
nhất N2 `"2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)"` (CÓ vế "phá dỡ") và N8 có em-dash
`—` `"8. HĐ chưa ủy quyền — CEO (5.2)"`. Ngoài ra `tree.Add("Khkk_G1", …)` == `labelBackfill["Khkk_G1"]`
đúng từng ký tự ⇒ DB-trống và DB-đang-chạy hội tụ CÙNG một chuỗi. PASS.
## Mục 1 — routes/params fe-user (ĐO TRÊN ĐĨA)
Khớp:
- `/khkk/workflow-matrix` CÓ route fe-user (`App.tsx:83`), KHÔNG có fe-admin (`:107-110` chỉ
list/create/:id) ⇒ lời khai "fe-admin không có WorkflowMatrixViewPage" ĐÚNG.
- `filter=ChoDuyet` / `filter=DaDuyet`: `FILTER_TO_PHASE` (`KhkkListPage.tsx:40-46`) có ĐÚNG 2 khoá
`ChoDuyet`/`DaDuyet` — khớp hoa/thường từng ký tự (`Record<string,…>` ⇒ case-SENSITIVE, sai chữ là
`?? null` = list không lọc, câm).
- `view=deleted`: `:62 searchParams.get('view') === 'deleted'` — khớp chính xác, đổi endpoint sang
`/contract-signing-plans/deleted`.
- Không leaf nào rơi `if (!path) return null` Ở FE-USER: regex phủ đúng 6 action, mọi nhánh có return.
- Thứ tự query param: `queryMatches` dựng `URLSearchParams` 2 phía rồi so theo KHOÁ ⇒ thứ tự không
ảnh hưởng. OK.
🔴 **FLAG A (MAJOR) — `?group=n` KHÔNG AI ĐỌC.** Phép đo:
`grep -rn "get('group')" fe-user/src fe-admin/src` = **0 hit**; `grep -n group WorkflowMatrixViewPage.tsx`
chỉ ra `<colgroup>` (:195/:200). Hệ quả đo được, không phải suy diễn:
- `/khkk/list?group=5&filter=ChoDuyet` trả **cùng dữ liệu** với `group=2` (query key
`['khkk-list', {phase, search, page, deletedView}]` — không có `group`; `params` gửi BE cũng không có).
- `/khkk/create?group=5` KHÔNG chọn sẵn nhóm 5.
- `/khkk/workflow-matrix?type=10&group=5` bỏ qua `group` hoàn toàn (`:53` chỉ đọc `type`) ⇒ 7 leaf
"Luồng duyệt" của 7 nhóm hiện **y hệt nhau**: toàn bộ workflow type-10.
⇒ Sau K4a, sidebar có 8 nhóm nhưng **nội dung 8 nhóm giống hệt nhau** (trừ nhóm 1 vốn cũng vậy).
Comment `Layout.tsx:196-199` CÓ khai đúng bản chất ("`?group=` là DANH TÍNH điều hướng, không phải bộ
lọc") ⇒ đây là **khai-báo-thật, không phải claim sai**; nhưng nó là trạng-thái người dùng NHÌN THẤY
và chưa thấy dòng nào trong artifact 3 lane nói "K4a giao 8 nhóm rỗng-lọc". Cần lead/owner dispose
tường minh (giống FLAG-3) TRƯỚC khi commit, kèm chỉ đích wave nào nối (K4b? K5?).
🔴 **FLAG B (MAJOR) — 2 mỏ-neo dòng MỚI trong `fe-user/Layout.tsx` SAI, lệch đúng bằng chính khối vừa
chèn.** Cùng lớp lỗi mà diff này vừa VÁ ở BE (vá-13 `1893`), tái phát ở FE trong CÙNG một lượt:
| Comment mới nói | Dòng THẬT (đo `grep -n`) | Lệch |
|---|---|---|
| `TRANSIENT_QUERY_KEYS` (`:311`) — `Layout.tsx:198` | `:345` | +34 |
| `MenuLeaf :342 if (!path) return null``Layout.tsx:189` | `:376` (và `:431`) | +34 |
Khối K4a chèn vào fe-user `Layout.tsx` đúng **+34 dòng** ⇒ tác giả tính mỏ neo trên bản TRƯỚC khi chèn
chính khối của mình. Đây là mỏ-neo **tự-vô-hiệu ngay lúc land**, không phải trôi theo thời gian.
Đối chứng: 6 mỏ neo còn lại trong cùng đợt vá ĐỀU ĐÚNG (`MenuKeys.cs:77` ✔ · `DbInitializer.cs:1960` ✔ ·
`khkkGroupLabels :2127-2136` ✔ · `labelBackfill:2235` ✔ · `GetMyMenuTreeQuery.cs:96` ✔ ·
`ContractSigningPlanFeatures.cs:420` regex `^KHKK-N([1-8])$` ✔ · `KhkkListPage :61/:62` ✔) ⇒ lệch chỉ ở
đúng 2 mỏ neo TRỎ VÀO CHÍNH FILE ĐANG SỬA. Không chặn chạy, nhưng chính vá-13 vừa chứng loại lỗi này
tốn 1 lượt điều tra.
## Mục 6 (tiếp) — vá-8/10/11/15 nội dung có tự-mâu-thuẫn không
- Vá-11 (revoker) **ĐÚNG, đã đo**: `RevokeTemporarilyHiddenModulesAsync` `:2554-2567` lọc
`StartsWith("Hrm") || StartsWith("Off") || MenuKey == Personal``Khkk_G*` **0 match**. Comment khớp.
- Vá-10 (lý-do site-2) **ĐÚNG**: `GetMyMenuTreeQuery.cs:96`
`static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess);` ⇒ group KHÔNG cần
row của chính nó; đính chính lý-do sai cũ là chính xác.
- Vá-8 (fe-admin `isAdminHidden`) **ĐÚNG**: `:207-209` thân hàm đúng là `return key.startsWith('Ct_')`
⇒ lời khai "hàm này CHỈ ẩn `Ct_*`" khớp đĩa; `KeHoachKyKet` KHÔNG bị ẩn. Và `MenuGroup`
(`fe-admin/Layout.tsx:223-262`) render header + container **vô điều kiện** ⇒ 7 nhóm rỗng THẬT SỰ hiện
trên sidebar admin (khớp FLAG-3 lead đã dispose, không phải suy đoán).
- Vá-15 (2 khuôn key trong `MenuKeys.cs`) **ĐÚNG**: `Ct_*`/`Pe_*` thật sự KHÔNG có trong `All`
(chỉ factory), còn `AwV2_*`/`Khkk_G*` có ⇒ mô tả 2 khuôn không tự mâu thuẫn.
## Mục 2 — DELTA `Menu keys` 113 · `Policies` 452 (ĐO ĐỘC LẬP, không tin test cũng không tin lane)
Phép đo tự dựng (parse `MenuKeys.cs`, KHÔNG gọi lại bộ sinh — tránh tautology): tách phần tử của
`All`, phân biệt token thường vs token spread, resolve từng `const string` sang giá trị chuỗi, rồi
tự bung 7×(1+6):
- token thường = **64** · resolve được **64/64** (0 token không tra ra hằng)
- spread duy nhất = `.. KhkkGroupMenuKeys` → bung **49**
- **|All| = 113** · **trùng lặp = 0** (`duplicates: []`)
- **Policies = 113 × 4 = 452**, khớp đúng vòng sinh `Program.cs:82-89`
(`foreach menu in MenuKeys.All / foreach action in MenuKeys.Actions / AddPolicy($"{menu}.{action}")`),
`Actions` = 4 phần tử (`MenuKeys.cs:252`). Đây là chỗ DUY NHẤT sinh policy theo menu-key
(`grep AddPolicy Program.cs` = 2 hit, hit còn lại `:113` là rate-limit `auth-login`, không tính).
**452 = 113×4 ĐÚNG LOGIC SINH**, không phải số chép tay. PASS.
STATUS.md (lead vá SAU khi spawn — coi là phần của commit K4a):
- `:6` Menu keys **64→113** ✔ khớp máy · Policies **256→452** ✔ khớp máy.
- bảng canonical `:470-471` **452** + **113** ✔ khớp máy, kèm câu "test T2 đỏ nếu drift" — câu này
ĐÚNG (T2 assert `HaveCount(113)`), tức con số có phép-kiểm-làm-nó-trượt. Đạt cổng nhãn-đo-lường.
- `:6` test **614→620** (45D + 575I): file test mới có đúng **6 `[Fact]`** (`grep -c` = 6) ⇒ số học
khớp. Đang chạy `dotnet test SolutionErp.slnx` để đo tươi (kết quả ghi ở mục 8).
- bảng canonical `:465` Mig **71**: đếm đĩa `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`
(bỏ `.Designer.cs` + `ModelSnapshot`) = **71** ✔ khớp.
🔴 **FLAG C (MAJOR) — ô canonical bundle `STATUS.md:479` SAI NGAY LÚC VIẾT, đo bằng 2 lệnh curl.**
Ô mới ghi admin **`mySTlx42`** (css `Cf3iCOOv`) · user/eoffice **`CZAYiWWa`** (css `DX1FRk5n`).
Đo LIVE 2026-08-01 trong lượt review:
| Site | STATUS vừa ghi | Đo live (`curl \| grep assets/index-`) | Khớp? |
|---|---|---|---|
| admin.solutions.com.vn (js) | `mySTlx42` | **`1yiNV4VH`** | ✗ |
| admin (css) | `Cf3iCOOv` | `Cf3iCOOv` | ✓ |
| eoffice.solutions.com.vn (js) | `CZAYiWWa` | **`DoULfmdT`** | ✗ |
| eoffice (css) | `DX1FRk5n` | `DX1FRk5n` | ✓ |
2 css khớp ⇒ phép đo của tôi đang trỏ ĐÚNG site (không phải đo nhầm chỗ). Chỉ 2 hash JS lệch — đúng
như chính ô đó tiên đoán ("F-1 `7a903cf` + K4a push sau đó SẼ rotate tiếp"). Vấn đề: ô này là **hàng
canonical**, và nó đang ghi giá-trị đã chết trong khi giá-trị sống lấy được bằng 1 lệnh curl 25 giây.
Sửa rẻ: hoặc thay bằng 2 hash vừa đo, hoặc ghi thẳng "chưa đo lại sau `7a903cf`" thay vì để hash cũ
đứng ở ô canonical. (Đây là ca *claim mạnh hơn việc đã làm* — nhãn "FROZEN-hợp-lệ K3" đọc như đã-xác-
nhận, nhưng thứ được xác nhận là mốc K3, không phải trạng thái prod lúc commit.)
## Mục 3 — vá-4/12: diff có CHẶN phép đo prod không?
**Không chặn.** Đo từng vế:
- A4 (`SELECT DISTINCT MenuKey FROM Permissions WHERE MenuKey LIKE 'Khkk%'``MenuItems.Key`):
mọi key được grant đều có menu-row — site-1 cấp theo `MenuKeys.All` (113 key, invariant T6 K1 phủ),
site-2 cấp `KhkkKeys()` = 7 key G1 (có row cứng ở tree initializer) + 49 key mới (có row từ vòng
seed mới) ⇒ tập grant ⊆ tập row **by-construction**. Không có đường sinh Permission cho key
không-row.
- A5 (restart ×2 → `Khkk%` = 56 row): 7 + 49 = 56; không key nào khác bắt đầu bằng `Khkk`
(`AwV2_KhkkN*` bắt đầu bằng `AwV2`) ⇒ phép đếm `LIKE 'Khkk%'` sạch, không nhiễu.
- ⚠️ **Bẫy CHO LANE CICD, không phải lỗi của diff:** `MenuItems` có 2 cột override
(`MenuItem.cs`: `IsVisible` mặc định `true`, `DisplayLabel` nullable — Mig 27). Seeder **không bao
giờ ghi 2 cột này** (`grep DisplayLabel DbInitializer.cs` = 0 hit) ⇒ 49 row mới sinh ra `IsVisible=true`,
`DisplayLabel=null` (đúng, hiện được). NHƯNG với `Khkk_G1` trên prod: nếu admin từng bấm đổi tên qua
`PATCH /api/menus/{key}` thì `DisplayLabel` khác null và **fe-user render `DisplayLabel`, không phải
`Label`** (`GetMyMenuTreeQuery.cs:86-88`) ⇒ acceptance đo NHÃN TRÊN UI có thể thấy chữ cũ dù
`labelBackfill` đã đổi `Label` thành công. ⇒ khi nghiệm thu A5 phải đo `Label` **bằng SQL**, và nếu
đo trên UI thì kiểm `DisplayLabel IS NULL` trước. Ghi ra đây để cicd không kết luận nhầm "backfill hỏng".
## Mục 8 — idempotent + revoker-proof: test đủ răng chưa?
**Đo tươi suite (chính tôi chạy, không tin lane):** `dotnet test SolutionErp.slnx` exit-0 —
Domain **45/45**, Infrastructure **575/575**, **tổng 620 PASS / 0 FAIL**. Khớp đúng số STATUS `:6`
ghi (620 = 45D+575I). Con số này có phép-kiểm-làm-nó-trượt và tôi đã chạy phép kiểm đó.
- **Site-2 `.Concat` chạy 2 lần — KHÔNG double-grant.** 3 lớp chặn, đọc trên đĩa:
`reviewKeys` kết thúc bằng `.Distinct()` (`:2480`) ⇒ trong 1 lượt không có key lặp;
② nhánh key đã tồn tại: `existingRows.TryGetValue((role.Id, key), …)` → non-Pe `continue`
(`:2497-2508`) ⇒ lượt 2 là no-op;
③ backstop DB: `PermissionConfiguration.cs:26` `HasIndex(x => new { x.RoleId, x.MenuKey }).IsUnique()`
⇒ nếu 2 lớp trên hỏng thì NỔ chứ không âm thầm nhân đôi. PASS.
- **T3/T4 có răng cho phần chúng đo**: T3 seed ×2 rồi so `khkkAfter2.Equal(khkkAfter1)` + `totalAfter2
== totalAfter1` (đếm TOÀN bảng, không riêng họ Khkk) ⇒ bắt cả nhân-row chỗ khác. T4 đo DB↔DB lấy
nhóm 1 làm mốc + chốt trước "6 nhãn nhóm 1 phân biệt" (chống vacuous). T5 dùng ma-trận
cùng-ruột-khác-1-biến (G1 có entry backfill tự lành / G2 không có entry giữ chữ hỏng) — đúng cách
chứng CƠ CHẾ chứ không chứng kết quả. Đây là bộ test viết tốt hơn mức trung bình của repo.
🔴 **FLAG D (MAJOR) — bất-biến DUY NHẤT từng vỡ trên prod lại là bất-biến KHÔNG có test.**
`RevokeTemporarilyHiddenModulesAsync` không đụng `Khkk_*` — tôi đã verify bằng mắt trên LINQ
(`:2554-2567`, chỉ `Hrm`/`Off`/`Personal`) và comment vá-11 khai đúng. Nhưng:
- Không test nào khoá điều đó. Thêm 1 chữ `|| p.MenuKey.StartsWith("Khkk")` vào bộ lọc ⇒ **620/620
vẫn XANH**, còn thực tế 49 row CanRead vừa cấp bị lật sạch ở lần restart kế.
- Đúng kịch bản đã xảy ra THẬT và được chính comment `:2558-2565` dẫn lại: Run #423, "447/494 row rơi,
11/13 role về 0".
- Khuôn có sẵn, rẻ: `AdminOnlyModulesRevokeTests.cs:188` `Revoke_DoesNotTouch_PeModule` — mirror thành
`Revoke_DoesNotTouch_KhkkGroups` là xong.
- T3/T4/T5/T6 đều gọi **từng seeder lẻ qua reflection**, không chạy chuỗi `SeedAsync` đầy đủ ⇒ **thứ
tự** grant → revoke → site-3 hoàn toàn ngoài vùng test. Vì vậy acceptance A5 (restart API ×2 THẬT)
**chưa thể thay bằng test** — giữ nguyên là việc bắt buộc của lane cicd, đừng tick sớm.
## Mục 9 — đụng key cũ? cây phình?
- **Va key: 0.** Đo bằng chính phép parse `All`: `duplicates: []`; 7 key nhóm 1 (`Khkk_G1`,
`Khkk_WfView`…`Khkk_Deleted`) **không** nằm trong `All` (đúng chủ ý, T2 khoá bằng `NotContain`);
giao của 49 key sinh với 7 key nhóm 1 = **∅**; họ `AwV2_KhkkN1..N8` (K3) tách namespace hoàn toàn
(`AwV2_` ≠ `Khkk_`), 8/8 vẫn nguyên trong `All`.
- **Kế thừa quyền: không lây.** `GetMyMenuTreeQuery` chỉ inherit từ 4 root (`Contracts`, `Workflows`,
`PurchaseEvaluations`, `PeWorkflows`); `KeHoachKyKet` không có case ⇒ 49 key mới **không** thừa cờ
từ root, đúng thiết kế "grant từng dòng" (khớp gotcha #85 — endpoint vẫn gate `KeHoachKyKet.*`).
- **Cây phình (mốc CUỐI, re-đo sau khi lead vá STATUS):**
- HEAD trôi 2 lần trong lượt review: `ce15c64` → `21908b4` → **`0cae243`**; cả 2 commit mới là
`wal: flush`, đo `git diff --stat ce15c64..HEAD -- src fe-user fe-admin tests docs` = **RỖNG**
⇒ không có mã nào lọt vào commit governance. OK.
- `git diff --stat -- src fe-user fe-admin docs` = **7 file, +219 / 11** (6 code + `docs/STATUS.md`).
- `git status --porcelain` = 10 M + 1 `??`. Trong đó **8 file thuộc commit K4a** (6 code + STATUS +
file test untracked); **3 file KHÔNG thuộc**: `.claude/auto-memory/MEMORY.md`,
`.claude/auto-memory/feedback_return_terminates_agent.md`, `.claude/governance/.session-counter.json`.
- ⚠️ **Note E:** `git add -A` sẽ kéo 3 file governance vào commit mã. Add đích danh 8 file.
- So mốc đầu (T0: 6 file code) → mốc cuối (7 file + test): phần thêm đúng bằng `docs/STATUS.md` mà
coordinator đã báo trước ⇒ **không có phình lén**.
## VERDICT — PASS-WITH-FLAGS 4 (0 blocker)
Phần lõi K4a **đúng và đo được**: 49 key by-construction 3 phía (`All` · seed menu-row · grant site-2),
`|All| = 113` / `Policies = 452` khớp logic sinh, 8/8 nhãn khớp TỪNG KÝ TỰ với bảng owner đã soát,
0 va key, idempotent 3 lớp, revoker không đụng, suite **620/620 PASS đo tươi**. Không có gì trong diff
chặn phép đo prod A4/A5. Không kiến nghị sửa mã — dưới đây là vấn đề + tiêu chí nghiệm thu.
| # | Mức | Vấn đề | Tiêu chí đóng |
|---|---|---|---|
| A | MAJOR | `?group=n` **không code nào đọc** (`get('group')` 0 hit ×2 app) ⇒ 7 nhóm mới × 6 leaf mở ra **cùng một nội dung**; `WfView` hiện TOÀN BỘ workflow type-10 chứ không riêng `KHKK-N{n}`. Chính spec đã tiên đoán (`sub-invest-fable-b2-cum2.md:9` "silent-wrong, không lỗi build") | Lead/owner dispose tường minh như FLAG-3, ghi rõ wave nào nối (K4b đọc `group` → params API; K4c lọc WfView) và **không deploy ra người dùng trước khi nối**, hoặc chấp nhận có văn bản |
| B | MAJOR | 2 mỏ neo MỚI trong `fe-user/Layout.tsx` sai đúng +34 dòng = kích thước khối vừa chèn: `TRANSIENT_QUERY_KEYS (:311)` → thật `:345`; `MenuLeaf :342` → thật `:376`. Cùng lớp lỗi mà vá-13 vừa dọn ở BE | Sửa 2 số, hoặc bỏ số dùng mỏ neo grep (như BE đã làm: `grep existingItems.TryGetValue`) |
| C | MAJOR | `docs/STATUS.md:479` ghi bundle admin `mySTlx42` / user `CZAYiWWa`, **đo live cùng lượt = `1yiNV4VH` / `DoULfmdT`** (2 css khớp ⇒ đo đúng site). Ô canonical sai ngay lúc commit | Thay bằng 2 hash vừa đo, hoặc ghi thẳng "chưa đo lại sau `7a903cf`" thay vì để hash chết ở ô canonical |
| D | MAJOR | Bất-biến "revoker không đụng `Khkk_*`" — thứ **đã vỡ thật trên prod** (Run #423, 447/494 row rơi, chính comment `:2558-2565` dẫn lại) — **không có test**. Thêm 1 chữ vào bộ lọc revoke thì 620/620 vẫn xanh | Thêm `Revoke_DoesNotTouch_KhkkGroups` mirror khuôn sẵn có `AdminOnlyModulesRevokeTests.cs:188` |
| E | NOTE | 3 file `.claude/**` (auto-memory ×2 + `.session-counter.json`) đang dirty cùng cây | Add đích danh 8 file (6 code + `docs/STATUS.md` + file test), KHÔNG `git add -A` |
Ghi chú độc lập: A5 (restart API ×2 THẬT) **không** được coi là đã phủ bởi T3/T4 — 4 test đều gọi
từng seeder lẻ qua reflection, chuỗi `SeedAsync` (grant → revoke → site-3) chưa từng chạy trong test.
END sub-reviewer-gate-k4a — VERDICT=PASS-WITH-FLAGS 4 (A `?group=` chết-lâm-sàng · B 2 mỏ-neo tự-vô-hiệu ·
C ô bundle canonical sai đo-được · D bất-biến-từng-vỡ không test) · 0 blocker · suite 620/620 đo tươi ·
All=113/Policies=452 đo độc lập · nhãn 8/8 khớp từng ký tự