17 KiB
c3-l2
LANE 2/3 review cụm-3 — LENS ĐỦ-vs-SPEC + OG (owner-gate).
Target: sub-invest-fable-b2-cum3.md (K7 bridge 8v/8test + K8 dry-run 17 bước B0-B16).
Spec đối chiếu: spec-4gd-khkk-tong-quat-31-07-2026.md §②③④ + spec-cum1-chi-tiet-31-07-2026.md.
Mọi neo dưới đây ĐO LẠI TRÊN ĐĨA hôm nay (không mượn số của lane khác).
Findings
HIGH
H1 — Bridge đặt trên controller class-Policy ⇒ tập khóa hiệu lực là 2, spec khai 1; FE gate lỏng hơn server; 0 test authz.
- Claim của cum3
:54: "🔴 đúng-key-policy-endpoint-đích (gotcha #85) … keyContracts.Create, KHÔNG phảiKeHoachKyKet.*". - Đĩa:
src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs:26=[Authorize(Policy = "KeHoachKyKet.Read")]class-level; comment:12-19khai luật nhà: "tầng 1 classKeHoachKyKet.Read(mọi endpoint tối thiểu Read) + tầng 2 per-actionKeHoachKyKet.{Create|Update|Delete}— GHI ĐỦ TRÊN MỌI action". - ASP.NET Core: action-
[Authorize(Policy=X)]KHÔNG thay class-level, hai cái AND. ⇒ tập khóa THẬT của bridge ={KeHoachKyKet.Read ∧ Contracts.Create}— chưa ai khai. Đây là action ĐẦU TIÊN trên controller mang tiền tố khóa khác (khuôn nhà cùng hình dạng:EmployeesControllerclassHrm_HoSo.Read+ per-action cùng tiền tố —tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs:208-256). - Hệ quả đo được: (a) FE gate
can('Contracts','Create')(cum3:67) LỎNG HƠN server 1 khóa — cùng lớp bệnh gotcha #85/#82 tôi đã ghi @S162 (gate query phải = ĐÚNG tập khóa endpoint); (b) nếu câu III trả lời "người phòng Mua hàng bấm" mà vai đó KHÔNG cóKeHoachKyKet.Readthì nút chết 403 im lặng (#44), và A1 chạy bằng admin (bypass) sẽ KHÔNG bắt được; (c) T1-T8 (:73-82) 0 case authz. - Sev: HIGH. Đề nghị: spec khai TƯỜNG MINH tập khóa hiệu lực 2 vế + 2 test âm (thiếu
KeHoachKyKet.Read→ 403; thiếuContracts.Create→ 403) + FE gate khớp ĐÚNG tập đó; nếu muốn 1-khóa thì phải nói rõ endpoint chuyển sangContractsController(quyết của lead, không phải của tôi).
H2 — QĐ9 "khóa budget" có bước ĐO nhưng phép đo KHÔNG THỂ TRƯỢT (0-bit), và nhánh trigger thứ 2 (CEO) không được đo.
- Nghĩa vụ:
spec-4gd:19QĐ9 — freeze khi finalize tại CCM HOẶC CEO duyệt final; "phiếu DaDuyet đọc SNAPSHOT, không đọc live". - cum3 có B6 (
:119, kỳ vọng "ApprovedAmount ghi tại choke-point + FREEZE", sqlcmd 2 cột) và B8 (:121, "mở lại detail: số khóa 🔒; sửa line bị phase-guard chặn; sqlcmd số không đổi"). - Lỗ: không bước nào làm NHIỄU nguồn live rồi đo số đã-freeze có đứng yên không. B8 chặn sửa bằng phase-guard ⇒ số không đổi là chuyện đương nhiên kể cả khi display vẫn đọc LIVE. Hỏi theo sàn-sự-thật: "phép kiểm nào khiến kết luận đã freeze TRƯỢT nếu freeze hỏng?" — hiện KHÔNG có.
- Lỗ 2: nhánh CEO (
:120B7) nằm trong nhánh B "nếu còn giờ" (:105) và kỳ vọng chỉ là "Phase=DaDuyet đường chuẩn" — 0 assert freeze. ⇒ 1 trong 2 trigger của QĐ9 không có mặt trong kịch bản chính. - Sev: HIGH. Đề nghị: thêm bước "sau DaDuyet, đổi nguồn live (giá PE / NS tham chiếu / PeReferenceAmount) → mở lại phiếu KHKK: số HIỂN THỊ phải KHÔNG đổi, sqlcmd snapshot ≠ live" + ép nhánh CEO vào phần bắt buộc (hoặc khai thẳng "QĐ9 chỉ được chứng ở nhánh CCM, nhánh CEO nợ").
H3 — DoD "mọi bản ghi mới mang tiền tố ZZTEST" KHÔNG đạt được cho phiếu KHKK; và đúng bản ghi sống-vĩnh-viễn lại là bản ghi không mang dấu.
- Nghĩa vụ
spec-4gd:35. cum3 thực hiện bằng: B1:114"GhiChu tiền tố ZZTEST" (KHKK) + B10:123"TenHopDong tiền tố ZZTEST" (HĐ). - Đĩa:
ContractSigningPlanFeatures.cs:34-57ContractSigningPlanListItemDto— 22 field, KHÔNG cóGhiChu(entity cóContractSigningPlan.cs:45 GhiChu, nhưng list không trả). Phiếu KHKK cũng không có field tên tự do nào khác; detail header đọck.peTenGoiThau= tên gói thầu THẬT (fe-user/src/pages/khkk/KhkkDetailPage.tsx:288). - ⇒ Trên màn Danh sách/Đang duyệt/Đã duyệt KHKK, phiếu test hiện:
KHKK/2026/xxx+ tên gói thầu thật + dự án thật + NCC thật — không phân biệt được với phiếu thật. Mà theo chính cum3:131(HỐ-1-KHKK) đây là bản ghi không xoá được kể cả Admin ⇒ nó nằm lại lâu nhất và mang dấu ít nhất. - Sev: HIGH. Đề nghị: hoặc (a) khai thẳng "ZZTEST chỉ hiện ở GhiChu, KHÔNG hiện trên list — chấp nhận", hoặc (b) đây là lập luận MẠNH NHẤT cho phương án "bộ ZZTEST riêng" ở K8.2 (Project ZZTEST ⇒
ProjectNamehiện ZZTEST trên list) — hiện K8.2 không nêu lập luận này.
H4 — Câu giờ-G (K8.2) thiếu 2 hệ quả nặng nhất trên bản ghi THẬT, trong khi default là "im lặng = chạy trên data thật".
- K8.2
:140liệt cái giá: phiếu/HĐ ZZTEST nằm vĩnh viễn + cháy 1 seq. Thiếu:- Chiếm slot: phiếu KHKK DaDuyet của A/049 khoá luôn việc lập phiếu nhóm-4 THẬT cho PE đó tới khi sqlcmd (cum3 tự khai ở
:131nhưng KHÔNG đưa vào câu hỏi cho anh). Đĩa xác nhận rào:ContractSigningPlanFeatures.cs:341-347AnyAsync(p.PurchaseEvaluationId == pe.Id && p.Phase != TuChoi)→ 409. - Chiếm
pe.ContractId: cum3:52quyếtpe.ContractId ??= contract.Id. Đĩa:CreateContractFromEvaluationFeatures.cs:59-60if (pe.ContractId is not null) throw new ConflictException("Phiếu này đã tạo HĐ rồi.")⇒ bridge PE→HĐ CŨ 409 vĩnh viễn cho PE thật A/049 — và vì bridge cũ tạo N HĐ cho N winner trong 1 call (loopforeach (var w in winners):81+), 1 lần bridge KHKK khoá luôn đường tạo HĐ cho mọi winner còn lại của phiếu đó. cum3 gọi đây là "đúng ý, khóa đường cũ tự nhiên" — nhưng đó là rút một đường đang dùng được trên bản ghi thật, thuộc quyền anh, không phải hệ quả kỹ thuật trung tính.
- Chiếm slot: phiếu KHKK DaDuyet của A/049 khoá luôn việc lập phiếu nhóm-4 THẬT cho PE đó tới khi sqlcmd (cum3 tự khai ở
- Sev: HIGH. Đề nghị: viết lại K8.2 thành 2 phương án A/B, mỗi phương án 1 dòng "cái mất" gồm đủ 4 mục (2 mục trên + phiếu nằm lại + seq cháy); bỏ "im lặng = A" hoặc đổi default sang B.
H5 — 12/14 bước không có người thật, 0/14 có tài khoản; và KHÔNG có cửa nào bắt điền trước giờ-G.
- Bảng
:112-127: cột là {Ai · Hành động · Kỳ vọng · Bằng chứng} — không có cột email/tài khoản. Chỉ B6 (TP.CCM Phan Văn Chương) và B7 (CEO Trường) là người thật; B1/B2/B3 "BCH drafter [K3-bảng]", B4 "Trạm-1 [K3-bảng]", B5 "Trạm-2 [K3-bảng]", B9 "Người có Contracts.Create [⏳ câu III]", B11 "Approver HĐ [đội hình type-3]", B12 "HRA/CCM", B13 "Bất kỳ + Admin", B14 "Lead". - cum3
:104giải thích hợp lý vì sao chưa điền (Dev không cóQT-DN-V2-001) — nhưng B0 (:110) có 5 mục và KHÔNG mục nào là "điền đủ 14 dòng cột Ai từ bảng K3 + verify từng tài khoản login 200 + xác nhận người có mặt khung giờ X". Checklist:166cũng chỉ đòi "B0 đủ 5 mục + câu giờ-G". - Hệ quả: tới giờ-G mới phát hiện thiếu người ⇒ hoặc hoãn, hoặc chạy bằng Admin bypass (V2 Admin skip mọi check) — mà chạy bằng admin thì dry-run mất đúng thứ nó định chứng (gating trạm-by-trạm;
SignedByUserIdsẽ ra "Admin duyệt thay"). Spec không nói mode nào được phép. - Sev: HIGH. Đề nghị: B0 += mục (vi) "bảng 14 dòng đã điền tên+tài khoản, mỗi tài khoản đã thử login 200 trong 24h trước" + 1 dòng chính sách "được/không được dùng Admin thay trạm".
MED
M6 — B0(ii) tự-tham-chiếu: không bắt được lỗi mà OG-2 sinh ra để bắt.
:110 B0(ii) verify "…KHKK-N4 active + roster khớp bảng K3". Bảng K3 và seed KHKK-N4 cùng một nguồn (K3 sinh ra cả hai) ⇒ nếu K3 seed nhầm từ bản QT-DN-V2-001 archived, B0 vẫn PASS. OG-2 (spec-4gd:44) yêu cầu đúng cái này: "verify bản LIVE (không archived)". Tên QT-DN-V2-001 không xuất hiện trong B0. Đề nghị: B0 đo trực tiếp nguồn (ApprovalWorkflows Code=QT-DN-V2-001 + cờ active/version) rồi mới so xuống KHKK-N4.
M7 — QUYẾT-1 (PVC → nhóm B1 → KHKK-N4) dựng trên bảng CHƯA được anh soát, và 3/3 trích dẫn vào bảng đó đều lệch.
- OG-6 còn ⏳ (
spec-4gd:47: "anh soát bảng seed trước land"). QUYẾT-1 (cum3:102) chốt nhóm dựa trên bảng đó mà không khai phụ thuộc này ⇒ anh sửa bảng thì nhóm/mã workflow của cả buổi dry-run đổi theo. - Trích dẫn (đo bằng
sed -n 'Np'trêndanh-muc-sp002-transcribe.md):cum3 nêu nội dung THẬT tại dòng đó neo ĐÚNG :52= "B1 · Vật tư / Materials (23) → N4"| A3-07 | Điện tạm | Temporary power supply |:65:75= "B1-23 Vật tư chính khác"| B1-10 | Nylon | Nylon sheets |:88:20= "A1 = thiết bị/vật tư PHỤ → N1"| # | Nhãn menu đề xuất |(header bảng):33(hoặc:22dòng nhãn N1) - Sev: MED (kết luận nghiệp vụ có thể vẫn đúng, nhưng neo không dẫn tới bằng chứng — người soát bấm vào sẽ thấy thứ khác và mất niềm tin đúng chỗ cần nhất).
M8 — Neo ContractFeatures.cs:632-633 (HỐ-1 guard numeric) SAI, và sai theo kiểu "chép từ sổ S160" — trái chính luật K7.0 tự đặt.
cum3:130 viết: "DeleteContract guard numeric Phase >= DangInKy (ContractFeatures.cs:632-633, S160)". Đĩa: guard thật ở :780 if (entity.Phase >= ContractPhase.DangInKy) trong DeleteContractCommandHandler (:773, record :771). :632 nằm trong khối authz của GetDetail (isHardCopyActor/ForbiddenException("Bạn không có quyền xem HĐ này.")). Nội dung claim ĐÚNG, neo lệch ~148 dòng. K7.0 mở đầu bằng "đo lại hôm nay, không tin sổ" (:8) — nguyên tắc áp cho K7 nhưng không áp cho K8.
M9 — "4 file test SigningPlan" là 3; và module KHKK hiện KHÔNG có regression authz nào — K7 cũng không thêm.
cum3:14 liệt "(Approval/Crud/Schema/AuthorizePolicy)". Đĩa: tests/…/Application/ContractSigningPlanApprovalTests.cs, …/ContractSigningPlanCrudTests.cs, …/Common/ContractSigningPlanSchemaTests.cs = 3; tests/…/Api/AuthorizePolicyRegressionTests.cs là file CHUNG (grep -n "SigningPlan" → 0 hit), phủ ApprovalWorkflowsV2/HrmConfigs/Employees. ⇒ ContractSigningPlansController chưa từng bị test authz khoá, và T1-T8 không thêm (nối H1).
M10 — Câu III (ai bấm) đánh dấu ⏳ nhưng KHÔNG có cửa chặn, trong khi acceptance đã nướng sẵn đáp án.
cum3:56 "⏳ PENDING-OWNER"; B9 :122 ghi "[⏳ câu III]". Nhưng: không có dòng OG mới trong spec-4gd §④; không nằm trong B0 5 mục (:110); checklist :166 chỉ đòi câu giờ-G. Ngược lại checklist :163 chốt cứng "nút chỉ DaDuyet && can('Contracts','Create')". ⇒ câu treo mà đáp án đã thành tiêu chí nghiệm thu; anh trả lời khác thì acceptance sai. Đề nghị: hoặc nâng thành OG-10 chặn K7-FE, hoặc bỏ ⏳ và khai "đã tự chốt theo l1-F15, anh phủ quyết sau được".
M11 — QUYẾT-2 chọn HopDongNhaCungCap bằng lý do KỸ THUẬT (né hố mã "MB"), không phải nghiệp vụ.
cum3:103: "abbr 'NCC' CÓ trong RG-001 v02 — né hố 'MB' không gốc quy định, S160". Hệ quả: (a) dry-run không đi qua loại HĐ mà nghiệp vụ mua vật tư có thể dùng thật (HopDongMuaBan); (b) hố "MB" tiếp tục không lộ. Đề nghị: khai 1 dòng cho anh — "chọn NCC vì mã MB chưa có trong quy định; nếu nghiệp vụ là MuaBan thì cần chốt token mã trước" — và để anh quyết, đừng để lựa chọn tài liệu pháp lý bị lái bởi codegen.
M12 — DoD "HĐ đúng số lượng OG-1" mất phép đo; 2 định nghĩa "gộp" đang chỏi nhau.
spec-4gd:11 QĐ1: gộp theo cặp (NCC × dòng-danh-mục). cum3:41 guard-3: gộp chỉ theo SupplierId (không ràng buộc dòng-danh-mục). Hai định nghĩa cho ra số HĐ khác nhau khi 1 NCC trúng nhiều hạng mục. A1 (:86) đo "đúng N dòng chọn" = đo lại chính input của caller ⇒ không phép nào chứng được quy tắc cardinality. (UI 1-1 làm rủi ro thực tế thấp, nhưng DoD đang tự khai một thứ nó không đo.) Đề nghị: khai rõ "khoá gộp = SupplierId trong phạm vi 1 phiếu" và sửa DoD thành phát biểu đo được.
LOW
- L13 Guard-list 6 mục (
cum3:38-44) thiếu load + NotFound choProject/Supplier, trong khi codegen cầnproject.Code/supplier.Code(ContractWorkflowService.cs:390) và khuôn gốc CÓ 2 throw (CreateContractFromEvaluationFeatures.cs?? throw NotFoundException("Project"…)+TryGetValue → NotFoundException("Supplier"…)). "Copy trọn khuôn" mà rơi 2 throw. - L14 Checklist
:167đòiss-k8-01..13nhưngss-k8-07chỉ sinh ra ở nhánh B "nếu còn giờ" (:105,:120) ⇒ nhánh A (nhánh CHÍNH) luôn thiếu 1 chứng ⇒ checklist không bao giờ tick sạch. - L15 Cùng 1 HĐ mang 2 con số khác thời-điểm:
GiaTri = Σ ApprovedAmount(chốt lúc finalize) vsBudgetManualAmount = Σ PeReferenceAmount(snapshot lúc lập KHKK) —cum3:46, không acceptance nào phân biệt. Cùng lớp bệnh mà cụm-1 vá-8 đang lo (Σ×3,spec-cum1:15). - L16 Hệ quy chiếu test lệch giữa 2 cụm: cum3 dùng "≥ baseline+8" (
:92,:164), spec-4gd/cum1 dùng "≥590+k" (spec-4gd:24-26,spec-cum1:25). Không quy đổi được nếu wave khác land xen giữa. - L17
:110B0(iv) khẳng định "KHÔNG có cơ chế tắt notification per-record" — khẳng-định-VẮNG-MẶT không kèm lệnh đo/0-hit + control dương. Rẻ: dán 1 grep. - L18 QĐ9 dặn né #81-EXT (grep MỌI Phase-assignment-site kể cả admin-override/seeder —
spec-4gd:19). K8 là cửa cuối nhưng không có bước thử đường bypass (ví dụ admin đổi Phase trực tiếp → hook freeze có chạy?). Thuộc K2/K3 về mặt code, nhưng K8 nên có 1 dòng "đã xác nhận sweep, evidence: …".
Claim ĐÃ THỬ BÁC MÀ ĐỨNG (giữ nguyên, đừng sửa)
- P1
cum3:48"terminal V2 có guardif (MaHopDong is null)(ContractWorkflowService.cs:384)" — ĐÚNG NGUYÊN DÒNG::384 if (string.IsNullOrEmpty(contract.MaHopDong))(gen ở:390). Thiết kế "gen mã ngay lúc bridge" an toàn khỏi double-gen. - P2
cum3:131"global filter làm rào:342-344giải phóng slot khi soft-delete" — ĐÚNG: rào dùngdb.ContractSigningPlans.AnyAsync(...)KHÔNGIgnoreQueryFilters(cả file chỉ 1 hit:767, thuộc query khác). Lối thoát sqlcmd đứng. - P3 HỐ-1-KHKK ĐÚNG:
:1142-1145nhánh owner/admin CHẠY TRƯỚC,:1148allow-list{DangSoanThao, TuChoi}áp cho tất cả ⇒ phiếu DaDuyet không xoá được kể cả Admin. Phát hiện mới, có giá trị. - P4 BÁC tiền đề K7(b) (
:16-21) — đúng phương pháp (re-đo thay vì thi hành đề bài cũ), và việc CẤM đụngGetEligiblePhases(:162) là bảo vệ đúng chỗ. - P5 Mirror 2 app:
fe-uservàfe-adminpages/khkk/KhkkDetailPage.tsxcùng 26927 B / 687 dòng; neo:291-318đúng là khốiactionscủa PageHeader (:288-300xác nhận) ⇒ K7.3 chỉ đúng chỗ.
Verdict
FAIL (chưa cho build) — 18 finding: 5 HIGH · 7 MED · 6 LOW; 5 claim chịu được phản-biện.
Chia 2 nửa rất khác nhau:
- K7 (bridge): nền CHẮC — re-đo 3 nấc thật, bác được tiền đề lỗi thời, khuôn/atomicity/codegen đúng, 5/5 claim tôi thử bác đều đứng. Chặn duy nhất là H1 (tập khóa authz 2 vế chưa khai + FE gate lỏng + 0 test authz) và M10/M12 (câu III treo mà acceptance đã chốt; DoD cardinality không đo được). Vá 3 chỗ này là K7 chạy được.
- K8 (dry-run): chưa chạy được như viết. 12/14 bước không có người thật và không cửa nào bắt điền (H5); phép đo QĐ9 không thể trượt (H2); dấu ZZTEST không hiện trên list KHKK nên DoD tự khai là không đạt (H3); và câu hỏi trình anh thiếu đúng 2 hệ quả nặng nhất trên bản ghi thật (H4).
Ưu tiên vá: H4 → H2 → H1 → H5 → H3 → M6/M10.
END sub-reviewer-c3-l2 — TOTAL: 18 finding (5 HIGH · 7 MED · 6 LOW) + 5 positive.