REVIEWER-GATE-K2: IN-PROGRESS — chưa chốt
# sub-reviewer-gate-k2 — GATE K2 (adversarial, trước commit-push) — S164/S165
> Luật ghi-đĩa-trong-lúc-làm (#53): mỗi mục đo xong ghi NGAY. Dòng-1 = verdict, cập nhật cuối cùng.
> Vai: reviewer READ-only. KHÔNG sửa prod-code. Được chạy dotnet/npm build.
## §0 Tiền đề đo được (git, trước mọi phán)
- Branch `main`. `git status --porcelain`: 31 file M + 5 file untracked (2 mig K2 + 1 test file K2 + 2 auto-memory).
- Diffstat vùng code: 31 files changed, +1005/-126 (gồm cả .claude/docs).
- File K2 untracked: `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime.cs` + `.Designer.cs`
+ `tests/.../Application/ContractSigningPlanGroupAndCatalogTests.cs`.
## §1 Trọng tài BUILD/TEST — reviewer tự chạy tươi (nguyên văn)
- `dotnet build SolutionErp.slnx` → `Build succeeded. 0 Warning(s) 0 Error(s)` · `Time Elapsed 00:00:18.24` ⇒ **KHỚP claim 0W/0E**.
- `dotnet test SolutionErp.slnx`:
- Domain: `Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, Duration: 69 ms`
- Infrastructure: `Passed! - Failed: 0, Passed: 562, Skipped: 0, Total: 562, Duration: 1 m 11 s`
- Tổng **607/0 = 45D + 562I** ⇒ **KHỚP claim `sub-test-k2.md:1` từng con số.**
## §2 Ranh giới (a)-(d) — đo trên diff
- (a) `ContractSigningPlanWorkflowService.cs`: diff CHỈ 1 hunk `@@ -164,6 +164,23 @@` (+17/-0) nằm trọn trong `SubmitAsync`.
`ReturnOrRejectAsync` (`:359 plan.Phase = targetPhase`) + `ApplyApprovedValuesOnFinalize` + `:295 = DaDuyet` **0 dòng đổi** ⇒ ranh B3-B4 GIỮ theo NỘI DUNG.
- (b) K1 `ContractCatalog*`: `git status --porcelain` trên 3 path K1 = **0 dòng** (đã commit `50e6d8c`) ⇒ không bị đụng.
- (c) route/menu: `App.tsx` · `lib/menuKeys.ts` · `components/Layout.tsx` **KHÔNG có trong diff** ⇒ 0 route/menu mới (K4a turf giữ).
(`MenuKeys.cs` BE cũng không trong diff của wave K2 — chỉ `.claude/skills/permission-matrix/SKILL.md` là doc.)
- (d) 3 test lead vá: `ContractSigningPlanSchemaTests.cs` (+56/-…) + `ContractSigningPlanApprovalTests.cs` (+19) vẫn còn trong diff, file mới `ContractSigningPlanGroupAndCatalogTests.cs` tách riêng ⇒ không bị lane đè.
## §3 `npm run build` ×2 — reviewer tự chạy tươi
| App | Kết quả | Bundle (nguyên văn) | So claim lane |
|---|---|---|---|
| fe-admin | ✅ PASS, `✓ 1961 modules transformed` / `✓ built in 647ms` | `dist/assets/index-DORfkFnc.js 1,722.60 kB │ gzip: 428.08 kB` | KHỚP TỪNG KÝ TỰ |
| fe-user | ✅ PASS, `✓ built in 575ms` | `dist/assets/index-qYI2yszp.js 1,638.80 kB │ gzip: 413.92 kB` | KHỚP TỪNG KÝ TỰ |
Warning cả 2 app = pre-existing (chunk > 500 kB · `INEFFECTIVE_DYNAMIC_IMPORT` `realtime.ts`), 0 TS error.
`noUnusedLocals: true` ⇒ build XANH = phép thử TS6133 có răng (import `KHKK_APPROVAL_GROUP_LABELS` ở ListPage đã có người dùng thật).
## §4 Code-vs-spec K2
**Mig 71 — 6 item ĐỦ, 3-file ĐỦ.** `20260731085624_…FinalizeRuntime.cs` + `.Designer.cs` (2 file `??` mới) + `ApplicationDbContextModelSnapshot.cs` (M) — snapshot có đủ `ApprovalGroup` · `EndedByLevelFinalize` · `CatalogEntryId` · `TenHangMuc(500)` · `HasIndex("ApprovalGroup","Phase")` · `HasIndex("CatalogEntryId")`. Backfill `:61` `UPDATE [ContractSigningPlans] SET [ApprovalGroup] = 1;` **vô điều kiện** đúng chữ vá. Unique `:93-98` 3 cột + `filter: "[IsDeleted] = 0"` đúng #57.
**Đo DB (sqlcmd, `SolutionErp_Dev`) — reviewer tự chạy:** `tables=97` · top mig `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` · 2 cột header + 2 cột line **có mặt** · `sys.indexes is_unique=1` chỉ còn `PK_…` và `IX_…_ContractSigningPlanId_SupplierId_CatalogEntryId` filter `([IsDeleted]=(0))` ⇒ **unique cũ 2-cột ĐÃ CHẾT THẬT** (index swap, không phải thêm chồng). `plans=0 · lines_cnt=0` ⇒ Σ tiền local là **control RỖNG** — lane khai đúng độ mạnh, KHÔNG đòi thêm phép đo (xem §6).
**Submit-guard 2 vế — PHÁN vụ "vá-5 THẮNG nền `:126`": lead ĐỌC ĐÚNG ưu tiên.** Chứng bằng chữ:
- `spec-cum1-chi-tiet:4` nguyên văn: *"**12 VÁ dưới đây THẮNG khi xung đột**"* ⇒ luật ưu tiên do CHÍNH spec đặt, không phải lead tự phong.
- `spec-cum1-chi-tiet:12` (vá-5): *"auto-gen line lúc tạo phiếu được mồi null NHƯNG **submit-guard CHẶN null**"*.
- Nền `sub-invest-fable-b2-cum1.md:126`: *"Line `CatalogEntryId == null` (legacy nhóm-1 backfill) → CHO QUA"*.
⇒ Xung đột THẬT, vá thắng, code chọn CHẶN (`WorkflowService.cs:174-176`). Đúng. Kèm điểm cộng: cái GIÁ của lựa chọn (phiếu cũ bị Trả-lại sẽ **kẹt trình** tới khi K4b land) được khai THẲNG trong comment tại chỗ, không giấu.
Vế (ii) khác-nhóm 409 `:178-182` theo đúng code mẫu nền K2.d.
**List `?group=` end-to-end — 5/5 mắt xích có thật:** controller `ContractSigningPlansController.cs:42` `[FromQuery(Name="group")] int? group` → truyền vị trí thứ 5 `:49` → record `ListContractSigningPlansQuery:705 int? ApprovalGroup` → filter `:735-736` (đặt SAU vế IDOR, chỉ thu hẹp) → DTO `ListItemDto:63 ApprovalGroup` → FE `types/khkk.ts:158 approvalGroup: number`. FE **cố ý chưa đẻ ô lọc** (K4b/K5) — khai rõ ở `sub-implfe-k2.md §2` + comment `types/khkk.ts:28-32`. *(bẫy tên tham số → F-6.)*
**Σ×3 + display-gate:** `Features.cs:793-804` — PeReference · Proposed · **+Approved** (`.Sum(l => l.ApprovedAmount) ?? 0m`). Detail `:689` `BudgetFrozen = plan.Phase == DaDuyet`. FE: `KhkkListPage` +cột "Tổng duyệt" (8 `
` == 8 ` | `, `colSpan` 7→**8** ở CẢ 2 chỗ ×2 app — đếm tay khớp) · `KhkkDetailPage:439-446` badge 🔒 qua prop `actions` (`SectionCard` có sẵn prop này, không bịa).
**Phase-guard + T-f:** `EnsureDraftEditable` (`Features.cs:229-237`) có **3 call-site thật** `:518 :928 :982`; K2 KHÔNG dựng guard mồ côi. T-f (`…GroupAndCatalogTests.cs:570-641`) **nối đường ghi THẬT**: dựng `UpdateContractSigningPlanDraftCommandHandler` thật, `ThrowAsync`, rồi đọc lại DB khẳng định `GhiChu == null` **và** `Changelogs == 0` ⇒ không phải reflection suông, có cả vế "ném rồi thì không ghi gì".
**FE vá-4 + OG-7 — mở file thật, đối chiếu 4 điểm (≥3 yêu cầu):**
1. select nhóm default 1 — `KhkkCreatePage.tsx:62` `useState(KHKK_APPROVAL_GROUP_DEFAULT)`, const `= 1` (`types/khkk.ts:121`). ✅
2. POST tường minh — `:99` `approvalGroup,` trong body (không dựa default ngầm BE). ✅
3. nút "Tạo thêm cho nhóm khác" — `:240-243`, đặt NGOÀI ` |