14 KiB
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
- B1 Đọc spec cụm-1 + transcribe + 3 lane return
- B2 dotnet build tươi — Build succeeded · 0 Warning(s) · 0 Error(s) · Time Elapsed 00:00:44.23 (6 project)
- 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)
- B4 Đối chiếu code-vs-spec (a→h)
- B5 Ranh CẤM
- 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-31const +:173VÀOAll⇒ đúng vá-1 spec-cum1:8(ngoài All = 0 permission row = sidebar chết). - Grant Admin: loop
SeedAdminPermissionsAsyncDbInitializer.cs:2064-2066nguồn =MenuKeys.All⇒ tự động, 0 code thêm. Menu rowDbInitializer.cs:1773-1776leaf order 5 dướiCatalogs, đ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:6hiện vẫnMig 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.tsxSHA-256 d71a9e3bb17b835b85b4f3467905af2ab414f52e3ec0c58e79d55ef57673985a ×2 app IDENTICAL → khớp claim lane FEsub-implfe-k1.md:49.menuKeys.ts+Layout.tsxDIVERGE cả-file (đúng như lane khai) ⇒ đo hash phần THÊM VÀO:menuKeys.tsadmin=ab97ed56b743f262…user=ab97ed56b743f262…MATCH ·Layout.tsxadmin=4ed4ea6b6f059c0e…user=4ed4ea6b6f059c0e…MATCH.- Pattern 16-bis 4 chỗ: (1) page OK · (2) route
/master/catalogs/:kindCÓ SẴNfe-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.tsxstaticMap ×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ốtid != body.Id → 400(Controller:50) ·buildBodyépnumeric→ 0 (BEintnon-nullable) ·openCreatemồ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 khiKINDSđã 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 --statchoInfrastructure/Services/ContractSigningPlanWorkflowService.cs+Application/Contracts/ContractSigningPlanFeatures.cs→ rỗng (0 thay đổi) ⇒WorkflowService.cs:342KHÔNG bị đụng · vùngFeatures.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)
- 3 filter BE chưa có UI — ĐÚNG. BE nhận 4 tham số (
groupCode/approvalGroup/isActive/q—Controller:27-30), FE chỉ gửiq(CatalogsPage.tsx:158) ⇒ 3/4 filter chưa có đường bấm từ UI. - Tab hiện cả fe-user — ĐÚNG.
KINDSrender vô điều kiện 5 sub-tab, route:kindmở ở CẢ 2 app (fe-user/src/App.tsx:63), GET là[Authorize]trần ⇒ user thường vào/master/catalogs/unitssẽ 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)
- [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ự khaisub-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. - [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. - [MEDIUM · lớp #82]
ContractCatalogController.cs:22,39,47,56— 4 policyContractCatalog.{Read,Create,Update,Delete}sinh ra do key vàoAllnhư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ônCatalogsController.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". - [LOW]
ContractCatalogFeatures.cs:42-46,59-60— List mặc định trả CẢ dòngIsActive=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ềnisActive=true, quên là dòng đã tắt hiện lại. - [LOW]
fe-{admin,user}/src/pages/master/CatalogsPage.tsx— 0 hitPermissionGuard/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. - [NIT]
20260731072441_AddContractCatalogEntries.cs:48— tên indexIX_ContractCatalogEntries_Code(EF default) trong khi specsub-invest-fable-b2-cum1.md:28đặtUX_ContractCatalogEntries_Code. Chức năng không đổi (unique + filter đúng). - [NIT]
fe-{admin,user}/src/pages/master/CatalogsPage.tsx:218— comment{/* Sub-tabs cho 4 kind */}còn "4" trong khiKINDSđã 5 kind. - [INFO · lệch đã khai]
DbInitializer.csB2-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. - [LOW] Acceptance K1.f
:70-71(runtime) chưa đo: Mig 70 chưa áp Dev-DB (lane khaisub-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