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

110 lines
16 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-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_…``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 `<th>` == 8 `<td>`, `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<ConflictException>`, 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<number>(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 `<button>` PE (bọc `<div key>`, tránh button-lồng-button). ✅
4. hết 8 nhóm = **chữ**`:236` `free === null ? <span className="italic …">Đã dùng hết 8 nhóm duyệt</span> : <Button…>` ⇒ không mời bấm thứ chắc chắn 409. ✅
Kèm: option nhóm đã dùng `disabled` + hậu tố "(đã có kế hoạch)" `:283-287`; nút Lưu `disabled … || groupTaken` `:360`.
**SHA-pair 5/5 — reviewer tự tính lại (`sha256sum`, 12 ký tự đầu):**
`types/khkk.ts 72a71451b98f` · `KhkkCreatePage 677f4ab803b0` · `KhkkDetailPage 1863153b4f99` · `KhkkListPage e892310434f2` · `KhkkWorkflowPanel 2b64144ef6e8`**admin == user cả 5**, và **panel == baseline `2b64144ef6e8`** ⇒ control DƯƠNG đứng: phép đo có phân biệt được "đụng" và "không đụng". 5/5 khớp con số lane tự khai.
## §5 Nghi-vấn lead-solo (adversarial)
**(i) Có đường trình nào né submit-guard không? — KHÔNG.** `grep -rn "\.Phase = \|Phase = ContractSigningPlanPhase" src/Backend` cho đúng 4 site ghi: `Features.cs:428` (`= DangSoanThao`, create) · `WorkflowService.cs:191` (`= ChoDuyet`) · `:295` (`= DaDuyet`, finalize) · `:359` (`= targetPhase`, chỉ nhận TraLai/TuChoi từ switch `:127-136`). **`= ChoDuyet` duy nhất 1 site**, nằm TRONG `SubmitAsync` SAU guard; `SubmitAsync` private, caller duy nhất là switch `case "submit"`. Guard cũng KHÔNG có nhánh `isAdmin` bỏ qua. ⇒ bác nghi-vấn.
**(ii) Backfill=1 vô-điều-kiện có đè nhóm ≠1 không? — KHÔNG, an toàn theo CẤU TRÚC.** `AddColumn` `:47-52` chạy NGAY TRƯỚC `Sql UPDATE` `:61` trong cùng transaction ⇒ tại thời điểm UPDATE **mọi row đều = 0**, không tồn tại row mang nhóm ≠1 để đè. Kịch bản rollback+re-apply: `Down()` `:124-127` **DropColumn** trước ⇒ dữ liệu nhóm mất **do Down**, không do backfill; re-apply set 1 là hành vi đúng của cột vừa tái sinh. ⇒ bác nghi-vấn. *(Dư lượng thật, xem F-9: `defaultValue: 0` để lại DEFAULT constraint 0 trên cột.)*
**(iii) DTO append-CUỐI có vỡ FE cũ không? — KHÔNG, và lane khai ĐỦ.** 4 record đổi (`ListItemDto` +2 · `LineDto` +2 · `DetailDto` +2 · `ApprovedPeAwaitingPlanDto` +1), tất cả append cuối; JSON bind theo TÊN. Construction site positional duy nhất của mỗi record nằm cùng file và đã sửa — `dotnet build 0 Error` chính là phép chứng (thiếu 1 site sẽ là CS7036). FE khai đúng cả chỗ **lệch**: `KhkkDetailDto extends KhkkListItemDto` nhưng BE `DetailDto` KHÔNG có `TotalApprovedAmount` ⇒ FE để `number | null` + ghi lý do tại chỗ (`types/khkk.ts:158-164`).
## §6 Σ tiền — xác nhận độ mạnh khai báo
Lane khai: (a) tầng CẤU TRÚC `grep PeReferenceAmount|ProposedAmount|ApprovedAmount` trong file mig = **0 hit** — reviewer đọc lại file mig, xác nhận 0 hit; (b) tầng PHÉP ĐO local = **control RỖNG** (`plans=0 · lines_cnt=0`, reviewer đo lại bằng sqlcmd, khớp). Lane **tự dán nhãn "control rỗng, không phải bằng chứng có răng"** thay vì dán "PASS". ⇒ **Khai đúng độ mạnh; reviewer KHÔNG đòi phép đo không tồn tại.** Răng thật nằm ở phép đo prod post-deploy (cicd K2).
## §7 FINDING (10) — 0 blocker kỹ-thuật, 1 gate quy-trình
**F-1 [MEDIUM · data-integrity · sửa ở K4b]** `ContractSigningPlanWorkflowService.cs:178-182` — vế "khác nhóm" hỏi SAI câu: `AnyAsync(c => catalogIds.Contains(c.Id) && c.ApprovalGroup != plan.ApprovalGroup)` = "có tồn tại entry nhóm khác không", trong khi điều cần bảo đảm là "MỌI id đều resolve tới entry ĐÚNG nhóm". `ContractCatalogEntryConfiguration.cs:29``HasQueryFilter(x => !x.IsDeleted)``CatalogEntryId`**loose-Guid 0 FK** ⇒ dòng trỏ tới hạng mục **đã xoá mềm** (hoặc Guid rác) **biến mất khỏi tập truy vấn**`Any` = false → **trình QUA**. Vắng-mặt đọc thành sạch. Tiêu chí đóng: đếm `entries.Count(id ∈ ids && ApprovalGroup == plan.ApprovalGroup) == ids.Distinct().Count()`, lệch thì 409 nêu rõ id nào không resolve. *(Chưa khai thác được ở K2 vì chưa có đường gán hạng mục — nhưng K4b mở đường đó, phải đóng cùng lúc.)*
**F-2 [MEDIUM · claim mạnh hơn việc đã làm]** 2 site khai lớp bảo vệ (a)/(1) — *"validator đường gán hạng mục: `CatalogEntryId` BẮT BUỘC — dòng mới qua API không bao giờ mang NULL"* — ở **THÌ HIỆN TẠI**, nhưng lớp đó **CHƯA TỒN TẠI** (K2 không có command ghi Line; validator thuộc K4b). Site: `20260731085624_…FinalizeRuntime.cs:36-40``ContractSigningPlanLineConfiguration.cs:26-31`. Hiện thật **1 lớp** (submit-guard), không phải 2. Đối chứng cùng diff: comment trong `SubmitAsync:180` viết ĐÚNG thì tương lai (*"validator đường-gán K4b là fail-fast tầng trước"*) ⇒ 2 giọng lệch nhau trong cùng một wave. Tiêu chí đóng: đổi 2 site sang thì tương lai + đánh dấu nợ K4b.
**F-3 [MAJOR · gate quy-trình · đóng TRƯỚC/CÙNG commit]** `docs/STATUS.md:6` đang ghi `Mig **70** … **598 test PASS** (45D+553I)`; thực đo sau K2: **Mig 71** · **607 test (45D+562I)** · bảng vẫn **97** (Mig 71 = 0 CreateTable) · MenuKeys/Policies KHÔNG đổi. `docs/STATUS.md` **không nằm trong working-tree diff** ⇒ commit K2 nguyên trạng = drift tức thì. Cùng LỚP với finding đã nêu ở gate K1 (`STATUS.md:6` chưa khai DELTA) — **tái phát, chưa đóng**.
**F-4 [LOW-MEDIUM · doc sẽ đi kèm commit]** `.claude/skills/ef-core-migration/SKILL.md` (uncommitted) ghi `Total: **96 bảng**` và bảng migration **dừng ở Mig 69** — đã bị chính K1 (`50e6d8c`, Mig 70 `+ContractCatalogEntries`) làm sai TRƯỚC khi kịp land; đo thật `sys.tables = 97`. Thiếu row Mig 70 **và** Mig 71. Ai commit K2 sẽ mang con số sai này vào lịch sử.
**F-5 [LOW · neo-số trong chú thích]** `KhkkListPage.tsx:176-177` (×2 app) còn ghi *"bảng **7 cột** cuộn ngang"* trong khi CHÍNH diff này nâng lên **8 cột** (đã sửa đúng `colSpan` 7→8 hai chỗ). Sửa mã mà không sửa số trong chú thích cạnh nó.
**F-6 [LOW · acceptance tự lão hoá + không có test phủ]** Nền `sub-invest-fable-b2-cum1.md` K2.h ghi acceptance `curl list ?approvalGroup=2`, nhưng controller bind `[FromQuery(Name="group")]` ⇒ gõ `?approvalGroup=2` **không lọc gì cả**, trả **toàn bộ** nhóm với HTTP 200 — sai IM LẶNG chứ không 400. Thêm: 9 test mới đo ở tầng **query handler**, **0 test** chạm tên tham số ở controller ⇒ đổi/gõ nhầm tên sẽ không ai bắt. Tiêu chí đóng: sửa chữ acceptance thành `?group=2` (và/hoặc 1 test controller-binding).
**F-7 [MEDIUM · lý-do-thiết-kế nêu SAI]** Rào (PeId, ApprovalGroup) là **check-then-insert không khoá** (`Features.cs:388-395` `AnyAsync` rồi `Add`) ⇒ 2 request đồng thời cùng (PE, nhóm) đều qua rào → **2 phiếu sống cùng nhóm**, làm hỏng đúng bất-biến `COUNT == Σ 8 folder`. Lý do bỏ unique DB được nêu **2 lần** (`ContractSigningPlanConfiguration.cs:33-36` + `Features.cs:390-393`): *"unique index không diễn tả được vế `Phase != TuChoi`"***sai kỹ thuật**: SQL Server cho phép filtered index với so sánh hằng, `UNIQUE (PurchaseEvaluationId, ApprovalGroup) WHERE [Phase] <> 3 AND [IsDeleted] = 0` là hợp lệ và tự nhả chỗ khi phiếu chuyển TuChoi. Không đòi phải dựng index ngay — đòi **sửa lý do**, vì lý do sai sẽ được chép tiếp sang K4b/K5.
**F-8 [LOW · rollback]** `Down()` `:132-137` tạo lại UNIQUE (Plan, Supplier) SAU khi đã DropColumn `CatalogEntryId` ⇒ một khi K4b sinh **nhiều dòng/NCC khác hạng mục** (đúng năng lực K2 mở ra), rollback Mig 71 sẽ **FAIL giữa chừng**. Khai trước rẻ hơn phát hiện lúc cần rollback.
**F-9 [INFO · dư lượng của (ii)]** `AddColumn<int>(… defaultValue: 0)` `:47-52` để lại **DEFAULT constraint = 0** trên cột `ApprovalGroup` ở DB. Mọi đường EF an toàn (entity default `= 1`, handler luôn set tường minh), nhưng INSERT bằng **SQL thô** (seeder tương lai) sẽ ra nhóm **0** — ngoài dải 1..8, rơi NGOÀI cây 8 folder mà không có gì kêu.
**F-10 [INFO · positive validation]** Những chỗ CHỊU được soi: (a) `existingGroups` dùng chung `db.ContractSigningPlans` nên **thừa hưởng global filter `!IsDeleted`** (`Configuration:53`) ⇒ nghi-vấn mở của FE lane §5.3 ("phiếu xoá mềm có rơi khỏi tập không") = **CÓ RƠI**, picker nhất quán với rào Create; (b) consumer của endpoint picker chỉ có `KhkkCreatePage` ×2 app (grep toàn `src/Backend` + `fe-*/src`) ⇒ đổi hình dạng không làm phồng badge/KPI nào khác; (c) auto-gen line không sinh 2 dòng cùng NCC nên unique 3-cột toàn-NULL **không đẻ lỗi 2601 mới** so với unique 2-cột cũ (tập hành vi tương đương ở nhánh NULL).
## §8 Ghi chú phạm vi commit (không tính là finding)
`git status`**10 file governance/doc** (`CLAUDE.md`, `.claude/skills/*`, `.claude/commands/*`, `.claude/auto-memory/*`, `docs/governance/*`, `.session-counter.json`) **không thuộc K2** đang cùng nằm trong working tree. Người commit cần stage CÓ CHỦ ĐÍCH (hoặc khai chúng trong commit message), đừng để `git add -A` gộp im lặng.
END sub-reviewer-gate-k2 — VERDICT=PASS-WITH-FLAGS · 10 finding (F-3 = gate quy-trình phải đóng trước/cùng commit; F-1/F-2/F-7 nợ K4b) · build 0W/0E · test 607/0 (45D+562I) · npm ×2 PASS · SHA-pair 5/5 + control-dương đứng.