18 KiB
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ầusub-reviewer-c1-l3.md, footer RUN-TRACE ghisub-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) ·sha256sum3 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):
:242chỉ 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ộtNOT NULL DEFAULT 00 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:183chọn khuônDbInitializer.cs:171-217=SeedSampleApprovalWorkflowsV2Async; hàm đó được gọi tạiDbInitializer.cs:139bên trongif (!demoSeedDisabled)(:135-140). Prod chạyDemoSeed: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:244khẳng định "prod IIS restart → … seed tự chạy" — sai với seeder demo-gated. - Acceptance không bắt được:
:230sqlcmd … → đúng 8không nêu DB nào (K1.f:70nêu rõ localdb Dev ⇒ mặc định đọc là Dev, nơidemoSeedDisabled=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ạnhSeedRealMasterDataAsync)" + 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:
:57vừa khai "MenuKeys.ContractCatalog(NGOÀIAll)" vừa khai "grant admin (SeedAdminPermissionsAsync loop)". Loop đó làDbInitializer.cs:2058foreach (var menuKey in MenuKeys.All)⇒ key ngoàiAllkhông bao giờ được cấp. - Không có đường cứu:
GetMyMenuTreeQuery.cs:96-99lọcCanReadtừ row Permission thật, không có Admin-bypass trong file;CatalogsKHÔNG phải inherit-root (:70-73chỉ Contracts/Workflows/PurchaseEvaluations/PeWorkflows — gotcha #35). - Tiền lệ bị bê nhầm:
Pe_*ngoàiAllvì là leaf sinh động qua factory (MenuKeys.cs:142-158, comment:150); còn 4 leaf anh em CÙNG groupCatalogsthì đều nằm trongAll(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
:214mắc y hệt (8AwV2_Khkk*ngoàiAll, trong khi 2 leaf anh emApprovalWorkflowDuyetNccV2/…PhuongAnV2nằm trongAll—MenuKeys.cs:182) nhưng K3.e:231có 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àiAllkè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_Dev→ContractSigningPlans= 0 row,ContractSigningPlanLines= 0 row (sys.tables=96). - (a)
:167SUM(PeReferenceAmount)trước == sau →NULL == NULL, đúng với mọi migration kể cả migration làm mất số. - (b)
:168COUNT == Σ 8 nhóm→0 == 0, PASS kể cả khi quêndefaultValue: 1(EF sinhdefaultValue: 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-30ghi 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(*) … > 0mớ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:
ReturnOrRejectAsynccó đúng 2 call-site, cả hai truyền hằng literal —ContractSigningPlanWorkflowService.cs:128(TraLai) và:133(TuChoi) — trongswitchđóng kín:117vớidefaultném lỗi:138-139⇒targetPhasekhô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 đườngapprove". Spec:105trích:39như 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ọiApplyApprovedAmountskhô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ớiDaDuyet. - Đề nghị: chốt (ii); assert = "gọi transition với mọi action hợp lệ, không action nào đưa Phase về
DaDuyetngoàiapprove"; 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) "GrepPhase\s*=[^=]toànContractSigningPlan*" ⟂:106(K2.b) khẳng định "seeder/DbInitializer hiện 0 site gán phase KHKK". FileDbInitializer.cskhông bao giờ khớp globContractSigningPlan*⇒ lệnh này mù với đúng lớp file được miễn trừ. - (Tôi đã đo hộ:
grep -rn "ContractSigningPlanPhase.DaDuyet" src/Backendngoài service = 0 hit;grep -rn "ContractSigningPlan" DbInitializer.cs= 0 hit ⇒ nộ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:623param default ·:173/:245/:341var fromPhase = plan.Phase·:493-494DTO 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 siteDaDuyet, 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:
:210chốt parse suffix Code; nhưngfe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:167-170DEFAULT_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 DoDspec §③-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/épKHKK-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:
:185if (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đoCOUNT = 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-75và K2.h:167-172= 0 dòngnpm run build, 0 dòng so mirror. Dòng chung:255cò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.tsxea51e7626eb1 ·KhkkDetailPage.tsxc8ab2eadd3fd ·types/khkk.ts8b757f5b1422) 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 +sha2563 cặp KHKK bằng nhau (khuôn K5spec…: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:
:87nê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"
:246liệ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:
:161Finalize_FreezesAllLines_NoNullApprovedAmount⟂ test đang chạytests/…/Application/ContractSigningPlanApprovalTests.cs:475Finalize_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??=(:513333→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+ 1CEO-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.tablesDev đo được 96; baseline test 590 khớpdocs/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-v2trảAwLevelDtogồmApproverUserId+ FullName +ApproverEmail(ApprovalWorkflowV2AdminFeatures.cs:25-31, projection:189-195) ⇒ đối chiếu 11 email là khả thi bằng curl. - P4
AllowApproverFinalizecó thật ở cấp Level (ApprovalWorkflow.cs:158+ApprovalWorkflowConfiguration.cs:100default 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 (
:62no-resurrect), K1-4 (:64biên 0/9 fail + 1/8 pass), K3-2 (:222shape từng cấp), K3-4 (:2243 chiều), K3-5 (:225multi-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ằngdefaultValuetrong 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.Alltrong 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 (mirrorCatalogsController[Authorize]lớp +[Authorize(Roles="Admin")]per-write — đo khớpCatalogsController.cs:13-14+ 12 action:23…:130; loose-Guid không FK; tái dùngApprovedAmountthay 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ữ.