Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k1.md
2026-07-31 15:52:08 +07:00

125 lines
14 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.

REVIEWER-GATE-K1: PASS-WITH-FLAGS — 9 finding
# Reviewer gate K1 — ContractCatalogEntries (SP-002 danh mục loại HĐ)
Ngày: 2026-07-31 · vai: reviewer (adversarial, read-only prod-code)
Artifact ghi LIÊN TỤC chống #53 — mỗi finding đổ ngay khi có.
## Trạng thái các bước
- [x] B1 Đọc spec cụm-1 + transcribe + 3 lane return
- [x] B2 dotnet build tươi — **Build succeeded · 0 Warning(s) · 0 Error(s) · Time Elapsed 00:00:44.23** (6 project)
- [x] B3 dotnet test — **45 Domain + 552 Infra = 597 PASS / 0 FAIL / 0 SKIP** (đúng kỳ vọng, 0 flaky, không phải chạy isolation)
- [x] B4 Đối chiếu code-vs-spec (a→h)
- [x] B5 Ranh CẤM
- [x] B6 Xác nhận 2 khoảng-hở UX
## Log
### B2 build (nguyên văn)
```
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:44.23
```
6 project: Domain · Domain.Tests · Application · Infrastructure · Api · Infrastructure.Tests.
### B4-a Entity 10 cột — ĐẠT
`ContractCatalogEntry.cs:15-24` = Code · TenVi · TenEn? · GroupCode · ApprovalGroup · SignerRole · InitialsChain? · NduqNote? · IsActive · SortOrder = **10 cột** + 7 audit từ `AuditableEntity`. Khớp bảng spec `sub-invest-fable-b2-cum1.md:13-26`.
### B4-b Config + Mig — ĐẠT (1 nit tên index)
`ContractCatalogEntryConfiguration.cs:27` unique filtered `[IsDeleted] = 0` (#57) · `:28` index ApprovalGroup · `:29` HasQueryFilter.
Mig `20260731072441_AddContractCatalogEntries.cs`: 1 CreateTable + 2 CreateIndex, Down = DropTable. **0 RenameColumn**.
NIT: spec `:28` đặt tên `UX_ContractCatalogEntries_Code`, code để EF-default `IX_ContractCatalogEntries_Code` (`Mig:48`). Không ảnh hưởng chức năng.
### B3 test (nguyên văn)
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, Duration: 342 ms - SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 552, Skipped: 0, Total: 552, Duration: 1 m 7 s - SolutionErp.Infrastructure.Tests.dll
```
Tổng **597 PASS / 0 FAIL** = baseline 590 + 7 test ContractCatalog. Không có fail lẻ ⇒ KHÔNG cần chạy isolation.
### B4-c Seed 86 — ĐO MÁY, KHÔNG tin lời khai (ĐẠT tuyệt đối)
Script parse trực tiếp `DbInitializer.cs` vùng `SeedContractCatalogAsync` (regex tuple 6-thành-phần):
- total rows = **86** · unique codes = **86** · dup codes = **[]**
- by GroupCode = A1:5 A2:1 A3:9 A4:5 B1:23 B2:14 B3:15 C1:6 C2:3 C3:4 CEO:1
- by ApprovalGroup = **1:5 · 2:10 · 3:5 · 4:23 · 5:14 · 6:15 · 7:13 · 8:1** → khớp CHÍNH XÁC spec `sub-invest-fable-b2-cum1.md:41` (5/10/5/23/14/15/13/1)
- empty TenEn = 0 (86/86 có tên EN) · dup SortOrder trong cùng GroupCode = none
- max len: Code 6≤20 · TenVi 153≤500 · TenEn 174≤500 · GroupCode 3≤10 → 0 nguy cơ truncation
Đối chiếu TỪNG DÒNG với `danh-muc-sp002-transcribe.md` (86 row parse từ markdown):
- code-set diff 2 chiều = **rỗng cả 2 chiều**
- text mismatch = **8 dòng, TẤT CẢ là B2-07..B2-14 bỏ chú-thích `(ảnh: …)`** — đúng lệch mà lane BE đã khai (`sub-implbe-k1.md:16`, mục 3). Không có mismatch nào ngoài tập đã khai.
- ApprovalGroup vs heading transcribe: **0 mismatch**
- signByGroup 8 nhóm khớp bảng transcribe `:9-16`, ô "N/A" bỏ đúng (N3 3 ô · N4 2 ô · N7 3 ô · N8 2 ô).
### B4-d Gate seeder — ĐẠT
`DbInitializer.cs:129-131` call-site NẰM NGOÀI mọi `if (!demoSeedDisabled)` ⇒ UNGATED (đúng spec K1.b, gotcha #51). `IgnoreQueryFilters()` có mặt khi dò existing (né #75/#76 resurrect).
### B4-e MenuKeys + drift DELTA — ĐẠT (1 finding kéo theo ở B4-f)
Đo máy trên `MenuKeys.cs` (parse vùng mảng `All`, strip comment, tách dấu phẩy):
- `All count = 56` · `has ContractCatalog = True` · `Actions = [Read, Create, Update, Delete]` = 4 → **policies derived = 224**.
- Diff `MenuKeys.cs:28-31` const + `:173` VÀO `All` ⇒ đúng vá-1 spec-cum1 `:8` (ngoài All = 0 permission row = sidebar chết).
- Grant Admin: loop `SeedAdminPermissionsAsync` `DbInitializer.cs:2064-2066` nguồn = `MenuKeys.All` ⇒ tự động, 0 code thêm. Menu row `DbInitializer.cs:1773-1776` leaf order 5 dưới `Catalogs`, đi nhánh INSERT (key mới) nên không dính bẫy gotcha #11.
- 🔴 **Drift DELTA CHƯA vào doc:** `docs/STATUS.md:6` hiện vẫn `Mig 69 · 96 bảng · 590 test PASS · Policies 220 · Menu keys 55`. Acceptance spec-cum1 ③ đòi "drift DELTA khai cùng commit" ⇒ **F-2**.
### B4-f CQRS + Controller authz — ĐẠT-theo-khuôn, nhưng khoảng-hở phải khai
Soi TỪNG action (không nhận `[Authorize]` trần làm bằng chứng):
| Action | Attribute thật | File:line |
|---|---|---|
| class | `[Authorize]` trần | `ContractCatalogController.cs:22` |
| GET list | (thừa hưởng class) | `:25` |
| GET id | (thừa hưởng class) | `:34` |
| POST | `[Authorize(Roles = "Admin")]` | `:39` |
| PUT | `[Authorize(Roles = "Admin")]` | `:47` |
| DELETE | `[Authorize(Roles = "Admin")]` | `:56` |
Khuôn đối chứng `CatalogsController.cs`: `:13` route · `:14` `[Authorize]` trần · 12 action ghi đều `[Authorize(Roles="Admin")]` (`:23,31,40,53,61,70,83,91,100,113,121,130`) ⇒ **mirror khuôn CHÍNH XÁC**, đúng QUYẾT spec K1.c `:47`. **KHÔNG có OR key con** (gotcha #85 N-A: controller này không dùng policy nào).
🔴 Đo tiếp: `grep -rn "ContractCatalog\." src/Backend --include=*.cs | grep -i policy`**0 endpoint** dùng 4 policy vừa sinh. Hệ quả đo được (lớp gotcha #82): ô CRUD `ContractCatalog` trên ma trận phân quyền chỉ điều khiển **hiển thị menu**, KHÔNG điều khiển API — tick `CanCreate` cho CatalogManager vẫn 403 (`Roles="Admin"`), bỏ tick `CanRead` vẫn GET được (class `[Authorize]` trần). ⇒ **F-3**.
Validator (`ContractCatalogFeatures.cs:110-124` / `:168-183`): MaxLength lấy ĐÚNG từ EF config (20/500/500/10/100/300/500) — đối chiếu `ContractCatalogEntryConfiguration.cs:18-24` khớp 7/7. `ApprovalGroup` `InclusiveBetween(1,8)` cả Create lẫn Update. Dup-Code chạy trên active-set (`:131`, `:193`) ⇒ khớp index filtered.
🔸 `ListContractCatalogQuery` (`:42-46`) mặc định **KHÔNG lọc IsActive** ⇒ trả CẢ dòng đã tắt; spec K1.c `:50` ghi tham số `IncludeInactive?` (ngữ nghĩa mặc-định-loại). ⇒ **F-4**.
### B4-g FE 2 app — ĐẠT (đối xứng đo bằng hash)
- `CatalogsPage.tsx` SHA-256 **d71a9e3bb17b835b85b4f3467905af2ab414f52e3ec0c58e79d55ef57673985a** ×2 app IDENTICAL → **khớp claim lane FE `sub-implfe-k1.md:49`**.
- `menuKeys.ts` + `Layout.tsx` DIVERGE cả-file (đúng như lane khai) ⇒ đo hash **phần THÊM VÀO**: `menuKeys.ts` admin=`ab97ed56b743f262…` user=`ab97ed56b743f262…` **MATCH** · `Layout.tsx` admin=`4ed4ea6b6f059c0e…` user=`4ed4ea6b6f059c0e…` **MATCH**.
- Pattern 16-bis 4 chỗ: (1) page OK · (2) route `/master/catalogs/:kind` CÓ SẴN `fe-user/src/App.tsx:63` + `fe-admin/src/App.tsx:69` ⇒ đúng "không cần route mới" · (3) `menuKeys.ts` ×2 · (4) `Layout.tsx` staticMap ×2 (thiếu chỗ 4 = sidebar drop im lặng, gotcha #50).
- KIND_CONFIG kind-5: 10 field khớp ĐÚNG 10 field `CreateContractCatalogEntryCommand` · PUT gửi `{ id: form.id, ...body }` (`CatalogsPage.tsx:164`) ⇒ qua được chốt `id != body.Id → 400` (`Controller:50`) · `buildBody` ép `numeric` → 0 (BE `int` non-nullable) · `openCreate` mồi select N1 (né `InclusiveBetween`).
- 🔸 Page **0 hit** `PermissionGuard`/`usePermission` ⇒ nút Thêm/Sửa/Xóa hiện cho mọi user vào được trang (khuôn 4 kind cũ y vậy) ⇒ **F-5**.
- 🔸 `CatalogsPage.tsx:218` ×2 app: comment `{/* Sub-tabs cho 4 kind */}` còn "4" trong khi `KINDS` đã 5 ⇒ **F-7** (nit).
### B4-h Test — ĐẠT (7 [Fact], có T1b thật)
Đếm máy `[Fact]` = **7**: T1 `Seed_RunTwice_Keeps86Rows_NoDuplicateCode` · **T1b `Seed_AfterSoftDeleteOfOneCode_DoesNotResurrectIt`** · T2 phân-bố 8 nhóm + section + denorm-đồng-nhất · T3 unique filtered 2 CHIỀU · T4 validator biên 0/9/-1/99 ⟂ 1/4/8 · T5 vòng đời handler thật (soft-delete + tái dùng mã) · T6 menu-key ∈ All + MenuItem row + grant Admin.
T1b có RĂNG thật (không phải nhãn): fixture RIÊNG wire `AuditingInterceptor` (`:83`) ⇒ `Remove()` là xoá MỀM thật; có sanity chống-vacuous `raw.IsDeleted.Should().BeTrue()` (`:179`); assert 85 active **VÀ** 86 tổng `IgnoreQueryFilters` (`:185-190`) ⇒ gỡ `IgnoreQueryFilters()` khỏi seeder thì tổng thành 87 và test ĐỎ. Đúng phép đo cho #75/#76.
T6 còn cài invariant TOÀN BỘ `All`: `MenuKeys.All.Except(menuKeysInDb).Should().BeEmpty()` (`:414`) — bắt lỗi key ∈ All mà thiếu MenuItem row (FK nổ lúc boot).
### B5 Ranh CẤM — SẠCH
- `git status --porcelain | grep -cE "ContractSigningPlan"`**0**.
- `git diff --stat` cho `Infrastructure/Services/ContractSigningPlanWorkflowService.cs` + `Application/Contracts/ContractSigningPlanFeatures.cs`**rỗng** (0 thay đổi) ⇒ `WorkflowService.cs:342` KHÔNG bị đụng · vùng `Features.cs:342-347` (việc của K2+) KHÔNG bị đụng — đúng vá-2 spec-cum1 `:9`.
- Scope toàn diff: **17 file** (11 M + 6 untracked) = 6 FE + 10 BE + 1 test. 0 file ngoài phạm vi K1.
- 3-file rule Mig (gotcha #17): `20260731072441_AddContractCatalogEntries.cs` + `.Designer.cs` (cả 2 untracked, có trên đĩa) + `ApplicationDbContextModelSnapshot.cs` (modified, +83 dòng, có `ContractCatalogEntry`) = **đủ 3, cùng commit-set**; `grep -c RenameColumn` = **0**.
### B6 Hai khoảng-hở UX lane FE khai — XÁC NHẬN ĐÚNG THỰC TRẠNG (không quyết thay lead/owner)
1. **3 filter BE chưa có UI** — ĐÚNG. BE nhận 4 tham số (`groupCode`/`approvalGroup`/`isActive`/`q``Controller:27-30`), FE chỉ gửi `q` (`CatalogsPage.tsx:158`) ⇒ 3/4 filter chưa có đường bấm từ UI.
2. **Tab hiện cả fe-user** — ĐÚNG. `KINDS` render vô điều kiện 5 sub-tab, route `:kind` mở ở CẢ 2 app (`fe-user/src/App.tsx:63`), GET là `[Authorize]` trần ⇒ user thường vào `/master/catalogs/units` sẽ thấy tab "Danh mục Hợp đồng" và đọc được 86 dòng, dù leaf menu chỉ Admin được grant. Cùng lớp với F-3.
## FINDING (9 — 0 blocker kỹ-thuật; F-1 chờ lead/owner disposition trước khi commit seed)
1. **[HIGH · gate quy-trình]** `DbInitializer.cs:3147-3324` — 86 dòng seed đang nằm CÙNG change-set với code, trong khi spec đặt PRE-gate `[OG-6]` (owner soát transcribe) **TRƯỚC khi land seed** và kế hoạch tách 2 commit (`sub-invest-fable-b2-cum1.md:7` + `:240-241`: "Commit K1-seed (SAU [OG-6] anh gật)"). Nặng thêm vì seeder per-Code idempotent **không có nhánh update**: owner sửa ô sau khi seed đã chạy thì row cũ KHÔNG tự cập nhật (lane tự khai `sub-implbe-k1.md:51`). Tiêu chí đóng: hoặc owner gật OG-6 trước commit, hoặc tách seed sang commit riêng giữ chờ gật.
2. **[MEDIUM]** `docs/STATUS.md:6` — drift DELTA chưa vào doc: Mig 69→**70** · 96→**97** bảng · 590→**597** test · Policies 220→**224** · Menu keys 55→**56**. Acceptance spec-cum1 ③ đòi khai **cùng commit**.
3. **[MEDIUM · lớp #82]** `ContractCatalogController.cs:22,39,47,56` — 4 policy `ContractCatalog.{Read,Create,Update,Delete}` sinh ra do key vào `All` nhưng **0 endpoint tiêu thụ**; ô CRUD trên ma trận phân quyền chỉ điều khiển hiển-thị-menu, không điều khiển API (tick CanCreate vẫn 403 · bỏ tick CanRead vẫn GET được). KHÔNG phải lệch spec (K1.c QUYẾT mirror khuôn `CatalogsController.cs:14`) nhưng phải khai thành giới-hạn, đừng để lượt sau đọc ma trận thành "đã khoá API".
4. **[LOW]** `ContractCatalogFeatures.cs:42-46,59-60` — List mặc định trả CẢ dòng `IsActive=false`; entity khai mục đích cột này là "ẩn khỏi picker" (`ContractCatalogEntry.cs:23`) và spec K1.c `:50` đặt tham số `IncludeInactive?` (mặc-định-loại). Ngữ nghĩa hiện tại ngược ⇒ consumer K2 (picker) phải nhớ truyền `isActive=true`, quên là dòng đã tắt hiện lại.
5. **[LOW]** `fe-{admin,user}/src/pages/master/CatalogsPage.tsx` — 0 hit `PermissionGuard`/`usePermission`: nút Thêm/Sửa/Xóa render cho mọi user vào được trang, bấm mới nhận 403. Pre-existing của khuôn 4 kind cũ, không phải hồi quy do K1.
6. **[NIT]** `20260731072441_AddContractCatalogEntries.cs:48` — tên index `IX_ContractCatalogEntries_Code` (EF default) trong khi spec `sub-invest-fable-b2-cum1.md:28` đặt `UX_ContractCatalogEntries_Code`. Chức năng không đổi (unique + filter đúng).
7. **[NIT]** `fe-{admin,user}/src/pages/master/CatalogsPage.tsx:218` — comment `{/* Sub-tabs cho 4 kind */}` còn "4" trong khi `KINDS` đã 5 kind.
8. **[INFO · lệch đã khai]** `DbInitializer.cs` B2-07..B2-14 — 8 dòng bỏ hậu tố `(ảnh: …)` so với transcribe; đo máy xác nhận **đây là 8 mismatch DUY NHẤT** trên 86 dòng, đúng tập lane đã khai (`sub-implbe-k1.md:16`). Bản chất là ghi-chú người chép, nhưng vẫn thuộc phạm vi OG-6 owner soát.
9. **[LOW]** Acceptance K1.f `:70-71` (runtime) chưa đo: Mig 70 chưa áp Dev-DB (lane khai `sub-implbe-k1.md:48-49`), chưa có log "Seeded 86…" + `sqlcmd COUNT(*)=86` + `GROUP BY ApprovalGroup`. T1/T1b thay được phần logic-idempotent trên SQLite nhưng KHÔNG thay được "SQL Server nhận đúng filtered index + boot thật" ⇒ đừng tick 2 ô đó cho tới khi chạy API.
## Bằng chứng số (nguyên văn)
- build: `Build succeeded. · 0 Warning(s) · 0 Error(s) · Time Elapsed 00:00:44.23`
- test: `Failed: 0, Passed: 45` (Domain) + `Failed: 0, Passed: 552` (Infrastructure) = **597 PASS / 0 FAIL / 0 SKIP**
- seed: 86 row · 86 mã duy nhất · 5/10/5/23/14/15/13/1 · 0 mismatch ngoài 8 dòng đã khai
- MenuKeys.All = 56 · policies derived = 224 · [Fact] = 7 · RenameColumn = 0 · file KHKK bị chạm = 0
END sub-reviewer-gate-k1 — 9 finding · build+test XANH tươi · ranh CẤM sạch