16 KiB
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.cstests/.../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:1từng con số.
- Domain:
§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 trongSubmitAsync.ReturnOrRejectAsync(:359 plan.Phase = targetPhase) +ApplyApprovedValuesOnFinalize+:295 = DaDuyet0 dòng đổi ⇒ ranh B3-B4 GIỮ theo NỘI DUNG. - (b) K1
ContractCatalog*:git status --porcelaintrên 3 path K1 = 0 dòng (đã commit50e6d8c) ⇒ không bị đụng. - (c) route/menu:
App.tsx·lib/menuKeys.ts·components/Layout.tsxKHÔNG có trong diff ⇒ 0 route/menu mới (K4a turf giữ). (MenuKeys.csBE cũng không trong diff của wave K2 — chỉ.claude/skills/permission-matrix/SKILL.mdlà doc.) - (d) 3 test lead vá:
ContractSigningPlanSchemaTests.cs(+56/-…) +ContractSigningPlanApprovalTests.cs(+19) vẫn còn trong diff, file mớiContractSigningPlanGroupAndCatalogTests.cstá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:4nguyê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: "LineCatalogEntryId == 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-182theo đú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):
- select nhóm default 1 —
KhkkCreatePage.tsx:62useState<number>(KHKK_APPROVAL_GROUP_DEFAULT), const= 1(types/khkk.ts:121). ✅ - POST tường minh —
:99approvalGroup,trong body (không dựa default ngầm BE). ✅ - 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). ✅ - hết 8 nhóm = chữ —
:236free === 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ùngdisabled+ hậu tố "(đã có kế hoạch)":283-287; nút Lưudisabled … || 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 có HasQueryFilter(x => !x.IsDeleted) và CatalogEntryId là 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 và 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 có 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.