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ự




