diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-reviewer-2.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-reviewer-2.md new file mode 100644 index 0000000..40d08ef --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-reviewer-2.md @@ -0,0 +1,27 @@ +# SUB-REVIEWER — W1 KHKK adversarial diff review (S161, 2026-07-29) + +> Ghi-đĩa-TRONG-lúc-làm (chống #53 — 3 vai trước chết trong wave này). File = thứ sống sót. +> Scope: toàn bộ M/?? trong `git status` (trừ `.claude/`). 12 file M (+990/−19) + 20 file ?? (~8.2K dòng, trong đó Designer.cs 6.960 = sinh máy). +> KHÔNG phủ lại: FK/index/maxLength/parity 2 DB (database-agent 8/8) · fault-inject test (test-specialist 4 test) · suite 566/0 + build 0W/0E (lead). + +## TIẾN TRÌNH +- [x] Trục 1 — seeder `SeedKeHoachKyKetAccessAsync` (5 sub-check a→e) +- [x] Trục 5 — MenuKeys.All (vị trí + tác dụng phụ) +- [ ] Trục 2 — CodeGenerator twin-diff + DI +- [ ] Trục 3 — 3 site AWV2AdminFeatures + grep-cùng-lớp +- [ ] Trục 4 — FE containment 2 lớp +- [ ] Trục 6 — boundary + +--- + +## TRỤC 1 — Seeder (DbInitializer.cs) + +**(a) Call-site thứ tự — PASS.** `RevokeTemporarilyHiddenModulesAsync` `:2096` → `SeedProcurementMasterAccessAsync` `:2102` → **`SeedKeHoachKyKetAccessAsync` `:2109`**. Đứng SAU revoke ⇒ thắng revoke. Cũng SAU `SeedAllRolesReviewReadPermissionsAsync` `:2089` (nhánh read-only skip-existing) ⇒ không bị nhánh nào hạ cờ về sau. Không có seeder nào chạy sau `:2109` (2 call S65/S69 đã comment-out `:2119`/`:2128`). + +**(e) Revoker KHÔNG phủ Khkk_/KeHoachKyKet — PASS (rủi ro 2-chiều gotcha #84 KHÔNG hiện thực).** Đọc thân `RevokeTemporarilyHiddenModulesAsync:2296-2310`: predicate CHỈ 3 nhánh `MenuKey.StartsWith("Hrm") || StartsWith("Off") || == MenuKeys.Personal`. Nhánh S92 (Contracts/Master/Catalog) đã GỠ @S159. Không prefix nào ăn `Khkk_*` hay `KeHoachKyKet` ⇒ grant không bị lật ở restart kế. (Ghi chú: thứ tự `:2109` sau `:2096` vẫn giữ đúng phòng khi revoker sau này mở rộng prefix.) + +**(b) UPGRADE-if-exists — PASS.** `:2414-2470`: `existing.TryGetValue((role.Id,key), out row)` → nâng từng cờ `if(!row.CanX)` → `continue`; nhánh else `db.Permissions.Add(...)` chỉ chạy khi row CHƯA có (fresh DB). Đúng khuôn `SeedProcurementMasterAccessAsync:2375-2394`, KHÔNG phải insert-only NO-OP (F-S1 đóng). Khác khuôn gốc 2 điểm CÓ CHỦ ĐÍCH: (i) key dictionary là tuple `(RoleId, MenuKey)` vì quét 13 role thay vì 1 → đúng, vì `.Where(p => keys.Contains(p.MenuKey))` không lọc role nên `ToDictionary(p => p.MenuKey)` kiểu cũ sẽ **ném `ArgumentException` trùng khoá** — implementer tránh được bẫy này; (ii) thêm vế CanDelete. + +**(c) CanDelete chỉ Drafter+Admin, KHÔNG hạ cờ — PASS.** `allowDelete = role.Name == AppRoles.Admin || role.Name == AppRoles.Drafter`. Mọi phép gán đều dạng `if (!row.CanX) row.CanX = true` ⇒ monotonic tăng, 0 câu nào set `= false` trên nhánh update. Nhánh insert đặt `CanDelete = allowDelete` (role khác = false) — đúng. + +**(d) 7 key khớp bộ seed menu-row — PASS (so từng chuỗi).** Seeder `:2419-2424` = `MenuKeys.KeHoachKyKet` + `Khkk_WfView, Khkk_List, Khkk_Create, Khkk_Pending, Khkk_Approved, Khkk_Deleted`. Menu-row `:1779-1786` = `Khkk_G1` + đúng 6 chuỗi trên. `Khkk_G1` CỐ Ý loại (group điều hướng, CanRead-all đã có) — khai trong comment `:2417-2418`. 0 typo.