Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md
2026-07-31 13:04:48 +07:00

18 KiB
Raw Blame History

c1-l3 đo-được + thứ-tự

LANE 3/3 review cụm-1 (spec B2-r). Lens: ĐO-ĐƯỢC (test có assert? checklist có lệnh+số?) + THỨ-TỰ (commit/mig/restart/deploy/seed/phụ-thuộc-chéo). Target: sub-invest-fable-b2-cum1.md (33.048B) · spec-4gd-khkk-tong-quat-31-07-2026.md §③/§④. ⚠️ Path lệch trong spec giao việc: TASK yêu cầu sub-reviewer-c1-l3.md, footer RUN-TRACE ghi sub-reviewer-2.md → theo TASK (fix-mới thắng footer-cũ, S139), khai ở đây. Đo bằng máy trong lượt này: sqlcmd (localdb)\MSSQLLocalDB SolutionErp_Dev (plans=0 · lines=0 · sys.tables=96) · sha256sum 3 cặp FE · 9 grep code-tree · 2 đếm trên transcribe.

Findings

Tổng: 15 (5 HIGH · 6 MED · 4 LOW) + 5 điểm chịu được soi.

HIGH

H1 · [Q3 thứ-tự] Mig 71 "item 6" (EndedByLevelFinalize) KHÔNG có trong mục schema K2.a, và K2 không có phép kiểm nào phát hiện được nó vắng mặt.

  • Evidence: sub-invest-fable-b2-cum1.md:83-88 (K2.a liệt 5 item cho Mig 71, không có item 6) ⟂ :181 (K3 khai "gắn vào Mig 71 item 6 (K2.a)") ⟂ :242 (thứ-tự commit khai "Mig 71 gồm cả item 6"). K2.h :167-172 = 0 dòng về cột này; K2 test k=7 :157-163 = 0 test.
  • Hệ quả đo được: implementer dựng Mig 71 theo đúng K2.a → thiếu cột → PASS 6/6 dòng K2.h + 7/7 test K2 → K3 (tự khai "0-mig") kẹt tại commit-time.
  • Cột mồ côi (câu hỏi Q3): :242 chỉ nói "nếu lead tách thì K3 tự mang Mig 72", KHÔNG nói gì nếu K3 đổi/bỏ thiết kế duyệt-sớm ⇒ còn lại 1 cột NOT NULL DEFAULT 0 0 người đọc = đúng lớp ghost-wire "defined-but-unread".
  • Đề nghị: chuyển item 6 vào chính K2.a (đánh số 6, có Down), thêm 1 dòng K2.h sqlcmd … SELECT COUNT(*) FROM sys.columns WHERE object_id=OBJECT_ID('ContractSigningPlans') AND name='EndedByLevelFinalize'1, và 1 câu rollback: "K3 bỏ duyệt-sớm ⇒ Mig 72 DropColumn cùng đợt".

H2 · [Q3 seed/deploy] Khuôn seeder mà K3.a chỉ định là khuôn ĐANG BỊ CỔNG DEMO — bê nguyên thì prod 0 workflow, mà mọi dòng acceptance vẫn xanh trên Dev.

  • Evidence: sub-invest-fable-b2-cum1.md:183 chọn khuôn DbInitializer.cs:171-217 = SeedSampleApprovalWorkflowsV2Async; hàm đó được gọi tại DbInitializer.cs:139 bên trong if (!demoSeedDisabled) (:135-140). Prod chạy DemoSeed:Disabled=true (comment :111-118, bài S91).
  • Spec cân nhắc gate chỉ cho K1 ("Gate: UNGATED", :35) và im lặng cho K3; ngược lại :244 khẳng định "prod IIS restart → … seed tự chạy" — sai với seeder demo-gated.
  • Acceptance không bắt được: :230 sqlcmd … → đúng 8 không nêu DB nào (K1.f :70 nêu rõ localdb Dev ⇒ mặc định đọc là Dev, nơi demoSeedDisabled=false). Test K3-1 gọi seeder trực tiếp ⇒ cũng không đi qua cổng.
  • Đề nghị: K3.a ghi thẳng "UNGATED, gọi NGOÀI khối if (!demoSeedDisabled) (cạnh SeedRealMasterDataAsync)" + 1 dòng acceptance đo trên prod (curl /api/approval-workflows-v2?applicableType=10 đếm 8) chứ không chỉ localdb.

H3 · [Q4 trái khuôn + Q2 checklist mù] Leaf menu đặt NGOÀI MenuKeys.All thì SeedAdminPermissionsAsync KHÔNG cấp được — leaf tàng hình với MỌI user kể cả Admin; K1 không có dòng nào bắt.

  • Evidence mâu thuẫn nội tại: :57 vừa khai "MenuKeys.ContractCatalog (NGOÀI All)" vừa khai "grant admin (SeedAdminPermissionsAsync loop)". Loop đó là DbInitializer.cs:2058 foreach (var menuKey in MenuKeys.All) ⇒ key ngoài All không bao giờ được cấp.
  • Không có đường cứu: GetMyMenuTreeQuery.cs:96-99 lọc CanRead từ row Permission thật, không có Admin-bypass trong file; Catalogs KHÔNG phải inherit-root (:70-73 chỉ Contracts/Workflows/PurchaseEvaluations/PeWorkflows — gotcha #35).
  • Tiền lệ bị bê nhầm: Pe_* ngoài All vì là leaf sinh động qua factory (MenuKeys.cs:142-158, comment :150); còn 4 leaf anh em CÙNG group Catalogs thì đều nằm trong All (MenuKeys.cs:168) ⇒ đây là rời khuôn chứ không phải theo khuôn.
  • Đối chứng cho thấy đây là lỗi CHECKLIST chứ không phải mơ hồ thiết kế: K3.c :214 mắc y hệt (8 AwV2_Khkk* ngoài All, trong khi 2 leaf anh em ApprovalWorkflowDuyetNccV2/…PhuongAnV2 nằm trong AllMenuKeys.cs:182) nhưng K3.e :231 có dòng "sidebar đếm được 8 menu-con" nên bắt được; K1.f không có dòng tương ứng nên lọt.
  • Đề nghị: hoặc đưa key vào All (đúng khuôn 4 leaf anh em, chấp nhận +4 policy ⇒ sync 2 row STATUS), hoặc giữ ngoài All kèm grant tường minh (site riêng như SeedProcurementMasterAccessAsync); và thêm dòng K1.f: "đăng nhập admin → sidebar Danh mục đếm được 5 leaf".

H4 · [Q2 acceptance mù] 3 lớp bảo vệ cho "backfill=1 + Σ tiền bất biến" cùng mù trên đúng cái DB mà checklist nêu tên.

  • Đo tươi (lượt này): SolutionErp_DevContractSigningPlans = 0 row, ContractSigningPlanLines = 0 row (sys.tables=96).
  • (a) :167 SUM(PeReferenceAmount) trước == sau → NULL == NULL, đúng với mọi migration kể cả migration làm mất số.
  • (b) :168 COUNT == Σ 8 nhóm0 == 0, PASS kể cả khi quên defaultValue: 1 (EF sinh defaultValue: 0 ⇒ phiếu thật rơi ra ngoài 8 folder).
  • (c) test Mig71_BackfillsApprovalGroup1_… (:157) không chạy được như tên gọi: harness là SqliteDbFixture + EnsureCreated() dựng từ model (tests/…/Common/SqliteDbFixture.cs:36-45), và grep -rl "Migrat" tests/ chỉ ra DLL, 0 file .cs ⇒ không có migration nào từng chạy trong test suite. Còn lại nó chỉ assert được default CLR = trùng ngôn (tautology).
  • Chuẩn chống-mù đã là chuẩn NHÀ: ProcurementMasterAccessSeedTests.cs:29-30 ghi rõ "Kèm CONTRAST assert chống vacuous-pass".
  • Đề nghị: mỗi so-sánh-bằng phải có control-dương đứng trước (SELECT COUNT(*) … > 0 mới có nghĩa), nêu rõ ĐO TRÊN DB NÀO (prod là nơi duy nhất có phiếu thật), và đổi tên/nội dung test (1) + (7) thành model-level hoặc dời sang bước tay có ghi số.

H5 · [Q1 test không assert + Q3 đo thiếu] Yêu cầu "🔴 PHẢI XỬ site :342" chưa chạy phép đo; theo số đo thì phương án (i) là code chết.

  • Đo: ReturnOrRejectAsyncđúng 2 call-site, cả hai truyền hằng literalContractSigningPlanWorkflowService.cs:128 (TraLai) và :133 (TuChoi) — trong switch đóng kín :117 với default ném lỗi :138-139targetPhase không thể mang giá trị DaDuyet.
  • Chính file đã khai sẵn: :36-42"service này có ĐÚNG MỘT nhánh set DaDuyet … Admin muốn kết thúc phiếu vẫn phải đi đúng đường approve". Spec :105 trích :39 như bằng chứng NGUY HIỂM nhưng bỏ ngỏ "nếu nhánh này reachable" — phép đo tốn đúng 1 lệnh grep.
  • Hệ quả: K2 test 6 (:162) khai assert "theo phương án (i)/(ii) đã chọn"1/17 test không có assert cố định (chọn lúc code = không phải tiêu chí nghiệm thu). Nếu implementer chọn (i) thì thêm 1 nhánh gọi ApplyApprovedAmounts không bao giờ chạy tới — vừa là code chết vừa tạo thêm một site trông như đường hợp lệ tới DaDuyet.
  • Đề nghị: chốt (ii); assert = "gọi transition với mọi action hợp lệ, không action nào đưa Phase về DaDuyet ngoài approve"; và thay lệnh sweep bằng đúng 2 lệnh mà code tự kê ở :292-293.

MED

M1 · [Q2 thước hỏng] Lệnh sweep grep 'Phase\s*=[^=]' bị giới hạn --include="ContractSigningPlan*" — không thể chứng minh chính cái miễn-trừ mà K2.b viện dẫn.

  • Evidence: :171 (K2.h) "Grep Phase\s*=[^=] toàn ContractSigningPlan*" ⟂ :106 (K2.b) khẳng định "seeder/DbInitializer hiện 0 site gán phase KHKK". File DbInitializer.cs không bao giờ khớp glob ContractSigningPlan* ⇒ lệnh này mù với đúng lớp file được miễn trừ.
  • (Tôi đã đo hộ: grep -rn "ContractSigningPlanPhase.DaDuyet" src/Backend ngoài service = 0 hit; grep -rn "ContractSigningPlan" DbInitializer.cs = 0 hitnội dung claim ĐÚNG, chỉ lệnh không chứng được.)
  • Thêm nhiễu: lệnh trả 12 dòng, trong đó 4 dòng không phải assignment (Features.cs:623 param default · :173/:245/:341 var fromPhase = plan.Phase · :493-494 DTO changelog); checklist không nêu số kỳ vọng ⇒ "liệt đủ site" nghiệm thu được bằng bất kỳ danh sách nào.
  • Đề nghị: dùng 2 lệnh repo-wide ở :292-293 + ghi số kỳ vọng (hiện tại: 1 site DaDuyet, 1 site helper).

M2 · [Q4/Q1] Convention ^KHKK-N([1-8])$ bị thoát bằng chính giá trị mặc định UI đang mời.

  • Evidence: :210 chốt parse suffix Code; nhưng fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:167-170 DEFAULT_CODE_BY_TYPE[TYPE_INT_CONTRACT_SIGNING_PLAN] = 'QT-KHKK-V2-001' ⇒ quy trình admin tự tạo mặc định không khớp pattern → validator "pass + LogWarning".
  • K3 test 4 (:224) assert chính lối thoát đó là CỐ Ý (đúng), nhưng DoD spec §③-K3 (spec…:26) hứa "phiếu nhóm 3 pin workflow nhóm 5 → 409" như một bất-biến ⇒ hứa rộng hơn thứ enforce được.
  • Đề nghị: viết vào DoD "chỉ enforce cho Code seeded KHKK-N*; quy trình admin tự đặt tên = ngoài rào (có warn)", hoặc để designer type-10 prefill/ép KHKK-N{n} khi đến từ deep-link nhóm.

M3 · [Q3 seed một-chiều] Seed idempotent kiểu "có Code thì bỏ qua" ⇒ hình dạng sai lần đầu bị ĐÓNG BĂNG, mà acceptance chỉ đo SỐ LƯỢNG.

  • Evidence: :185 if (Any(w => w.Code == code)) continue; (không có đường cập nhật) ⟂ ô ⚠️ chưa chốt "Trưởng/Trường/Trình" :203 — đúng cái field dễ sai nhất. Acceptance :230 đo COUNT = 8; test shape (:222) chỉ chạy trên DB test sạch, không soi prod.
  • Đề nghị: giữ PRE-gate verify-LIVE (đang có, tốt) + ghi đường sửa (script SQL 1 lần hoặc seeder-corrective có điều kiện) + 1 dòng acceptance đối chiếu email từng cấp trên prod sau seed, không chỉ đếm 8.

M4 · [Q2 thiếu trục] K1 và K2 đều ship FE nhưng 0 dòng acceptance FE; bất-biến mirror đang SỐNG bị bỏ trống.

  • Evidence: K1.d :56-57 (KIND_CONFIG ×2 app + menuKeys ×2) · K2.f :149-150 (6 file FE ×2 app) ⟂ K1.f :69-75 và K2.h :167-172 = 0 dòng npm run build, 0 dòng so mirror. Dòng chung :255 còn miễn byte-verify cho "wave BE-only" — K1/K2 không BE-only.
  • Đo tươi: 3 cặp file KHKK hiện SHA256 giống hệt giữa 2 app (KhkkCreatePage.tsx ea51e7626eb1 · KhkkDetailPage.tsx c8ab2eadd3fd · types/khkk.ts 8b757f5b1422) và file tự khai bất-biến này (fe-user/src/pages/khkk/KhkkCreatePage.tsx:3 "File MIRROR SHA256 identical với fe-admin") ⇒ K2 có thể phá âm thầm.
  • Đề nghị: mỗi commit chạm FE thêm 2 dòng: npm run build ×2 PASS + sha256 3 cặp KHKK bằng nhau (khuôn K5 spec…:30 đã có sẵn tiêu chí này).

M5 · [Q3 phụ-thuộc chéo NGƯỢC] Cái chặn va-chạm NULL của UNIQUE mới nằm ở cụm-2, và không tái hiện được trên harness.

  • Evidence: :87 nêu bẫy "NULL == NULL trong UNIQUE của SQL Server" và giao việc chặn cho "validator: line MỚI/SỬA bắt buộc có CatalogEntryId" — nhưng validator duy nhất K2 đặc tả (:118-126) chỉ kiểm nhóm; editor dòng là K4b (cụm-2) theo chính :113. 0 test + 0 dòng checklist K2 cho "line mới thiếu CatalogEntryId → 400".
  • Thêm: SQLite coi các NULL là phân biệt trong unique index ⇒ kể cả có test cũng xanh-trên-harness/đỏ-trên-prod.
  • Mục "Phụ thuộc chéo" :246 liệt chiều K2→K1, K3→K2 nhưng thiếu chiều cụm-2 nợ cụm-1 này.
  • Đề nghị: đưa rule "CatalogEntryId bắt buộc cho line mới/sửa" vào K2 (validator + 1 test), hoặc khai limitation kèm đo tay trên SQL Server.

M6 · [Q1 đếm không ra phủ] K2 test 5 trùng test đã có ⇒ "+17" đạt được mà hành vi mới vẫn không ai đo.

  • Evidence: :161 Finalize_FreezesAllLines_NoNullApprovedAmount ⟂ test đang chạy tests/…/Application/ContractSigningPlanApprovalTests.cs:475 Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan — bản cũ mạnh hơn: assert 0-null (:505-506), cách ly liên-phiếu (:518, :528), và giữ số sửa tay ??= (:513 333→300).
  • Đề nghị: bỏ test 5 khỏi k (k=6) hoặc đổi nội dung sang hành vi MỚI (freeze + EndedByLevelFinalize + line có CatalogEntryId), kẻo con số "+17" mua được bằng trùng lặp.

LOW

L1 :70 acceptance COUNT(*) WHERE IsDeleted=0 = 86 mâu thuẫn với kịch bản test 2 trên DB đã dùng: chỉ cần 1 lần xoá mềm hợp lệ là số đọc ra 85 và checklist báo FAIL giả. → nêu tiền đề "DB chưa xoá dòng nào", hoặc đo COUNT(*) gồm cả xoá mềm.

L2 :74 "tên VN+EN đủ 86/86" không kèm lệnh → thêm SELECT COUNT(*) … WHERE TenEn IS NULL OR LTRIM(RTRIM(TenEn))=''0.

L3 Cửa sổ "trang rỗng" giữa commit-1 (:240, ship FE kind + leaf menu) và commit-2 (:241, seed chờ OG-6): user prod thấy trang danh mục trống trong khoảng thời gian không xác định. Spec chỉ nói "restart local verify bảng rỗng OK". → hoặc ẩn leaf tới khi seed land, hoặc khai chấp nhận.

L4 3-file rule: :69 "git status show 3" yếu hơn bài S161-W1 (2/3 file mig untracked trong khi snapshot tracked) và :250 git show --stat chỉ soi được sau commit. → thêm cổng ngay trước commit: git status --porcelain -- src tests | grep '^??' phải RỖNG.

Chịu được soi (đo rồi, giữ nguyên)

  • P1 Phân bố 86 dòng 5/10/5/23/14/15/13/1 (:41, :70, :251) khớp transcribe: đếm được 85 mã [A-C][1-4]-\d\d + 1 CEO-01; heading khai A1=5 · A2=1 + A3=9 → N2=10 · A4=5 · B1=23 · B2=14 · B3=15 · C1=6+C2=3+C3=4 → N7=13 · CEO=1. Cộng = 86, khớp cả 2 chiều.
  • P2 96 → 97 bảng (:31, :250) đúng: sys.tables Dev đo được 96; baseline test 590 khớp docs/STATUS.md:6 ⇒ "+17 = 607" là con số có gốc.
  • P3 Bước verify-LIVE đội hình (:229) đo được thật: GET /api/approval-workflows-v2 trả AwLevelDto gồm ApproverUserId + FullName + ApproverEmail (ApprovalWorkflowV2AdminFeatures.cs:25-31, projection :189-195) ⇒ đối chiếu 11 email là khả thi bằng curl.
  • P4 AllowApproverFinalize có thật ở cấp Level (ApprovalWorkflow.cs:158 + ApprovalWorkflowConfiguration.cs:100 default false) ⇒ khẳng định "0 mig cho cờ này" (:180) đứng vững.
  • P5 5 test có assert 2 chiều/biên rõ ràng, xoá đi là mất phủ thật: K1-2 (:62 no-resurrect), K1-4 (:64 biên 0/9 fail + 1/8 pass), K3-2 (:222 shape từng cấp), K3-4 (:224 3 chiều), K3-5 (:225 multi-assert đường duyệt-sớm).

Trả lời gọn 4 câu hỏi

  • Q1 (17 test): 12/17 có assert cụ thể và sẽ ĐỎ nếu impl sai. Hỏng: K2-1 (không chạy được — H4c), K2-6 (không có assert cố định — H5), K2-7 nửa vế "AcrossMig" (H4c), K2-5 trùng test cũ (M6), K3-1 không đi qua cổng demo nên không bắt được H2. Thiếu hẳn test cho: cột EndedByLevelFinalize (H1), "line mới thiếu CatalogEntryId" (M5), "leaf menu render được" (H3).
  • Q2 (checklist): phần lớn dòng có {lệnh · số}, nhưng 3 dòng PASS được bằng impl SAI: K2.h Σ-tiền + COUNT-nhóm (H4a/b, đo 0-row), K1.f không có dòng nào bắt leaf tàng hình (H3), K2.h grep sai phạm vi (M1). Thiếu trục FE hoàn toàn ở K1/K2 (M4).
  • Q3 (thứ tự): khung K1→K2→K3 hợp lý và cửa sổ config-lie #78 KHÔNG mở ở mức commit (seed + nhánh (a) + gỡ khoá FE cùng commit 4 — đúng lệnh spec…§④ OG-3). Lệch nằm ở: item-6 lọt khỏi K2.a (H1), khuôn seeder demo-gated (H2), backfill=1 đi bằng defaultValue trong Mig (đúng khuôn, nhưng không có phép kiểm còn răng — H4), 3-file rule đo sau commit (L4), và phụ thuộc chéo thiếu chiều cụm-2→cụm-1 (M5) + thiếu điều khoản cột-mồ-côi (H1).
  • Q4 (chế thêm trái khuôn): 1 chỗ rời khuôn thật = leaf ngoài MenuKeys.All trong khi 4 leaf anh em cùng group đều ở trong (H3, lặp lại ở K3.c). 1 chỗ tạo quy ước mới = parse Code lấy nghĩa nghiệp vụ (M2) — chấp nhận được nếu khai giới hạn. Các quyết còn lại (mirror CatalogsController [Authorize] lớp + [Authorize(Roles="Admin")] per-write — đo khớp CatalogsController.cs:13-14 + 12 action :23…:130; loose-Guid không FK; tái dùng ApprovedAmount thay chép 11 cột PE Mig 67) đều bám khuôn, không chế thêm.

Verdict

PASS-WITH-BLOCKERS — nền spec chắc (khuôn đúng, 3 con số gốc kiểm được, 12/17 test có răng), nhưng KHÔNG land như hiện trạng: H1 + H2 + H3 mỗi cái đều để một impl SAI đi trọn checklist mà vẫn xanh, H4 làm 3 lớp bảo vệ tiền/backfill cùng mù, H5 mời implementer viết code chết. Chặn tối thiểu: vá H1, H2, H3 trước commit K1; vá H4, H5 trước commit K2/K3.

END sub-reviewer-c1-l3 — TOTAL: 15 finding (5 HIGH · 6 MED · 4 LOW) + 5 điểm giữ.