15 KiB
sub-review-q2-3 — reviewer ADVERSARIAL soi CÁCH ĐẶT VẤN ĐỀ Q2 (S156 BCH post-CEO)
Khung rỗng ghi @lượt-1 TRƯỚC KHI ĐỌC (chống #53), sau đó append. Owner đã chốt "chưa chốt Q2, cần bàn thêm" ⇒ reviewer soi CÁCH ĐẶT VẤN ĐỀ, KHÔNG chọn hộ phương án. ⚠️ LỆCH TÊN FILE trong spec: dòng đầu task yêu cầu
sub-review-q2-3.md; footer RUN-TRACE boilerplate ghisub-reviewer-2.md. Ghi theo lệnh 🔴 cụ thể (file này). Chỉ 1 file — không tạo file thứ 2. VERDICT:LENS-Q2: THIEU-PHUONG-AN — 14 điểm(4 HIGH · 6 MAJOR · 4 MINOR), kèm thiên vị có hướng: nghiêng (a)/(c), ép (b).
0. Nguồn đã chạm (3 Read + grep verify)
- Read
.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-invest-bch-1.md(223 dòng; Q2 = dòng 151-159, phần loại-trừ ẩn = dòng 44-49). - Read
src/Backend/SolutionErp.Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs(173 dòng; enumApplicableType:53-67, max =TravelRequest=9⇒ slot 10 trống — ĐÚNG). - Read
src/Backend/SolutionErp.Domain/Office/Proposal.cs(38 dòng — tiền lệ WorkflowApps đối chứng khuôn 12-bis). - Grep/đo (không Read): Migrations dir,
ApprovalWorkflowV2AdminFeatures.cs,ContractWorkflowService.cs,CreateContractFromEvaluationFeatures.cs,Contract.cs, LOC FE/BE Proposal.
1. Hỏi độc ác #1 — 3 phương án có VÉT CẠN không? → KHÔNG (2 HIGH + 2 MAJOR)
H1 (HIGH) — 3 phương án KHÔNG cùng PHẠM VI ⇒ so sánh chi phí vô nghĩa. Q2 tự đặt phạm vi "khúc 8-12" (:151). Nhưng:
- (a) phủ 8-12 (PostAward giữ hồ sơ 8-9 + chuỗi duyệt 10-12);
- (b) chỉ nói "trình 2 VÒNG (vòng giá 10-12, vòng ký 17-19)" — im lặng hoàn toàn về 8-9 (mẫu vật liệu + shopdrawing per-item);
- (c) chỉ phủ 8-9 ("attachment + checklist trên PE") rồi bỏ 10-12 ra ngoài hệ thống. Không gian quyết định thật là 2 trục (nơi chứa hồ sơ 8-9 × nơi chạy chuỗi duyệt 10-12), tối thiểu 3×3 ô; Q2 trình 3 điểm rời rạc như thể vét cạn. Ô hiển nhiên nhất bị bỏ: {8-9 nhẹ trên PE} × {10-12 CÓ workflow} — tức phần rẻ của (c) ghép phần có-vết của (a).
H2 (HIGH) — thiếu phương án "TÁI DÙNG module Proposal (ApplicableType=4)".
Proposal.cs:9-38 là aggregate generic: Title + Description + AmountEstimate decimal? + Attachments + LevelOpinions + ApprovalWorkflowId pin V2 + Status + smart-reject. Đó đúng hình dạng của b.9-12 ("hồ sơ so sánh giá + ĐỀ XUẤT giá trị ký HĐ" → PMH → CCM → CEO/CCM). Chi phí ước lượng: 1 cột loose-Guid PurchaseEvaluationId (tiền lệ PE.WorkItemId loose-Guid không FK, Mig 49) + 1 workflow config trong Designer — rẻ hơn (a) 1-2 bậc độ lớn, và giữ được vết duyệt (thứ mà (c) mất). Không liệt, không bác.
M1 (MAJOR) — phương án "dùng lại chính PE + workflow thứ 2" bị loại ở §1 nhưng KHÔNG hiện trong Q2.
sub-invest-bch-1.md:46-48 bác nó ("PE.Phase đã terminal DaDuyet… 2 con trỏ workflow trên 1 record (mâu thuẫn ApprovalWorkflowId 1-cột)"). Hai lỗi đặt vấn đề: (i) owner đọc §6 Q2 không thấy phương án này tồn tại — loại-trừ nằm ở chương khác; (ii) "1 cột" là chi phí +2-3 AddColumn không bảng mới, đúng lớp việc dự án làm thường xuyên (Mig 53 "3 AddColumn no new table", Mig 67 snapshot 11 cột), lại được dựng thành bất-khả kiến trúc. Chính tài liệu này ở chỗ khác coi AddColumn là rẻ ⇒ tiêu chuẩn kép.
M2 (MAJOR) — phương án "Contract-sớm nhưng tách entity con" không được liệt.
Lý do bác công bằng có sẵn và đo được: grep ApprovalWorkflowId trong src/Backend/SolutionErp.Domain (trừ thư mục V2) ⇒ 7 entity pin workflow — Contract.cs:23, PurchaseEvaluation.cs:24, Office/Proposal.cs:26, LeaveRequest.cs:20, OtRequest.cs:20, TravelRequest.cs:19, VehicleBooking.cs:21 — 100% là aggregate root, 0 tiền lệ entity-con pin workflow. Liệt + bác bằng câu đó tốn 1 dòng; bỏ hẳn thì owner không biết mình đang bỏ gì.
2. Hỏi độc ác #2 — Mô tả CÔNG BẰNG hay THIÊN VỊ? → THIÊN VỊ CÓ HƯỚNG (1 HIGH + 3 MAJOR)
H3 (HIGH) — "(b) phá state machine 5-trạng-thái" SAI như câu chữ đang viết.
Đo tại src/Backend/SolutionErp.Infrastructure/Services/ContractWorkflowService.cs:
:15-21(sơ đồ trong comment) +:70-80:(fromPhase == DangSoanThao || fromPhase == TraLai) → ChoDuyet. ⇒ChoDuyetVỐN ĐÃ tái-nhập được: HĐ bị Trả lại rồi trình lại đi vàoChoDuyetlần 2 ngay hôm nay, máy trạng thái không vỡ. Vậy "1 HĐ 2 lần ChoDuyet = phá state machine" không đúng với chính máy trạng thái đó.- 3 chặn THẬT (đều kiểm được, đều mạnh hơn lý do đang nêu):
:368-383(+:165-177nhánh V1) — hết Step là terminal vô điều kiệnDaPhatHanh+ gen mã ⇒ kết thúc vòng-giá sẽ phát hành HĐ;DaPhatHanhkhông có cung ra.Contract.cs:23pin 1 cộtApprovalWorkflowId+ 1 con trỏ; và không có đường re-pin —UpdateContractDraftkhông nhận field này (sub-invest-bch-1.md:98-100).CreateContractFromEvaluationFeatures.cs:95,108,113— HĐ tạo từ phiếu làDangSoanThao, pin V1WorkflowDefinitionId, và genMaHopDongNGAY ⇒ (b) đốt mã HĐ trước khi giá được chốt (b.12) và trước khi BCH có thể đổi khối lượng/chia gói. KẾT: kết luận "tránh (b)" vẫn đứng, nhưng lý do đang đưa cho owner là nhãn không kiểm được, trong khi 3 lý do kiểm được thì không đưa. Đây đúng lớp lỗi "measured-label": không có phép thử nào làm câu "phá state machine" TRƯỢT nếu nó sai.
M3 (MAJOR) — hiệu ứng hào quang: dữ kiện "rẻ" duy nhất được gắn vào phương án ĐẮT nhất.
Trong Q2 chỉ có đúng 1 dữ kiện kỹ thuật cụ thể: "ApplicableType=10 append-only theo tiền lệ Mig 37" (:154) — và nó nằm trong mô tả (a). Nhưng slot enum là lát mỏng nhất của chi phí (a): chi phí thật là 4 bảng + ~600 LOC BE + ~1.956 LOC FE (đo ở §4). Đặt sự-thật-rẻ cạnh phương án-đắt ⇒ owner đọc lướt thấy (a) rẻ.
M4 (MAJOR) — bất đối xứng ngôn từ. (a) mở bằng lợi ích ("tách bạch, mirror khuôn 12-bis"), (c) mở bằng lợi ích ("gần như 0 công BE"), chỉ (b) bị gắn tính từ rủi ro ("phá…", "rủi ro cao"). Không phương án nào có rủi ro định lượng. Thêm: câu chốt "Em nghiêng (a) hoặc (c) — (b) khuyên tránh; anh chốt?" (:159) đặt khuyến nghị trước lời mời chốt, thu hẹp còn 2 lựa chọn ngay trong câu hỏi mà owner nói là "cần bàn thêm".
M5 (MAJOR) — (c) "gần như 0 công BE" giấu chi phí QUYẾT ĐỊNH. (c) không chỉ mất vết duyệt: (i) Q6 ("giá CHỐT b.12 hay SUM báo giá PE") mất chỗ lưu ⇒ mặc định PE thắng, sửa tay draft; (ii) Q11 gate "duyệt mẫu/shopdrawing mới ký HĐ" mất cửa chặn cứng — chính invest viết "(cứng) … cần Q2=a để có trạng thái machine-checkable" (:206-207). Vậy (c) quyết định hộ 2 câu 🔴 CHẶN khác mà mô tả không nói.
3. Hỏi độc ác #3 — ApplicableType=10 append-only KHÔNG cần migration: tiền lệ Mig 37 CÓ THẬT? → CÓ (1 MAJOR đi kèm)
| Kiểm | Lệnh | Kết quả |
|---|---|---|
| Ordinal 37 | ls Migrations/*.cs | grep -v '\.Designer\.cs$' | grep -v '^ApplicationDbContextModelSnapshot\.cs$' | nl |
dòng 37 = 20260528082332_ExtendApplicableTypeForWorkflowApps.cs; tổng 68 migration |
Bẫy grep -v Snapshot (S155) |
ls | grep -i snapshot |
chỉ AddPeApprovedBudgetSnapshot (#67, SAU 37) ⇒ không lệch ordinal |
| Thân migration | cat 20260528082332_*.cs |
Up() RỖNG, Down() RỖNG ⇒ 0 DDL |
| Cơ chế (không chỉ tiền lệ) | ApprovalWorkflowV2AdminFeatures.cs:196-198 Enum.GetValues<ApprovalWorkflowApplicableType>() · :267-268 validator Enum.IsDefined · :170 label GetValueOrDefault(..., ToString()) |
type mới tự hiện trong Designer + validator nhận ⇒ claim đúng về bản chất, không chỉ vì có tiền lệ |
M6 (MAJOR) — tiền lệ bị kể MỘT NỬA (và trích dẫn tự phản chính nó).
(i) Vật được trích tên là migration và tồn tại đủ 3-file (#17): .cs 22 dòng + .Designer.cs 5.170 dòng. Câu đúng phải là "0 DDL, vẫn sinh 1 migration rỗng", không phải "KHÔNG cần migration".
(ii) Nợ đi kèm mà tiền lệ để lại: ApprovalWorkflowV2AdminFeatures.cs:90-95 AwLabels.Type chỉ có 3/9 entry (DuyetNcc / DuyetNccPhuongAn / Contract). 6 type do CHÍNH đợt Mig 37 thêm rơi vào fallback ToString() ⇒ UI hiện ProposalGeneral, LeaveRequest… (tên enum tiếng Anh) trong hệ thống quy ước 100% tiếng Việt. Tiền lệ vừa chứng minh "rẻ" vừa chứng minh "rẻ nên bị bỏ quên phần đuôi" — invest chỉ trích nửa đầu.
Phản-chứng đã chạy (nếu FE có bảng nhãn riêng thì kết luận này SAI): grep applicableType trên fe-admin/src + fe-user/src → 12 hit, 0 bảng nhãn, tất cả đều find(t => t.applicableType === N) rồi đọc applicableTypeLabel từ server; BE cũng không có dictionary thứ 2 cho enum này (chỉ chuỗi hardcode rời ApprovalWorkflowV2AdminFeatures.cs:975,982 cho RetainedOpinionRef — lại là 1 chỗ nữa module mới phải chèn tay).
4. Hỏi độc ác #4 — Chi phí ĐO ĐƯỢC hay CẢM TÍNH? → CẢM TÍNH, dù bản-sao-đo-được nằm sẵn trong repo (HIGH)
H4 (HIGH). 4/4 mệnh đề chi phí trong Q2 là tính từ: "+N bảng +menu +2 FE" (N không bao giờ giải), "phá… rủi ro cao", "gần như 0 công BE", "ít bảng mới". Trong khi twin gần nhất của (a) đã tồn tại (chính invest gọi tên "khuôn 12-bis") và đo bằng 3 lệnh:
| Trục | Đo trên tiền lệ Proposal (Mig 38, ApplicableType=4) |
Lệnh |
|---|---|---|
| Bảng mới | 4 CreateTable |
grep -c CreateTable 20260528082726_AddProposals.cs |
| BE | ProposalFeatures.cs 556 LOC + Proposal.cs 38 LOC (chưa kể controller/DbSet/Config) |
wc -l |
| FE (2 app) | 1.956 LOC = mỗi app 978 (Create 245 + Detail 386 + List 252 + types 95) | find … -iname "*Proposal*" -exec wc -l |
| Fan-out chéo module | +3 nhánh hardcode/module trong ApprovalWorkflowV2AdminFeatures.cs (7 module × 3 = 21 ref *LevelOpinions: Merge / rows.AddRange / n +=) |
grep -no 'db\.[A-Za-z]*LevelOpinions' | sort | uniq -c |
| Nhãn | +1 entry AwLabels.Type (6/9 tiền lệ đã quên — §3) |
sed -n '88,96p' |
Cận trên thô cho (b) cũng đo được 1 lệnh: DaPhatHanh = 38 ref BE + 3 ref FE; ApprovalWorkflowId trong Application/Contracts + ContractWorkflowService.cs = 15 ref. Không phải tất cả đều phải sửa, nhưng bậc độ lớn thì có ngay — hơn hẳn chữ "lớn".
5. Bảng điểm FLAG (14)
| # | Mức | Điểm |
|---|---|---|
| H1 | HIGH | 3 phương án lệch phạm vi ⇒ so sánh apples-to-oranges; option-space thật 2 trục |
| H2 | HIGH | Thiếu P4 tái-dùng Proposal (ApplicableType=4) — rẻ hơn (a), giữ vết duyệt (khác c) |
| H3 | HIGH | "(b) phá state machine" sai như viết (ChoDuyet tái-nhập được qua TraLai, :70); 3 chặn thật bị bỏ |
| H4 | HIGH | Chi phí 4/4 cảm tính dù twin đo được (4 bảng · 594 LOC BE · 1.956 LOC FE · +3 nhánh) |
| M1 | MAJOR | P "PE + pin thứ 2" loại ở §1:46-48, vô hình ở Q2; "1 cột" dựng thành bất-khả (thật = +2-3 AddColumn) |
| M2 | MAJOR | P "Contract-sớm + entity con" không liệt; lý do bác công bằng có sẵn: 7/7 entity pin V2 đều aggregate root |
| M3 | MAJOR | Halo: dữ kiện rẻ duy nhất (ApplicableType=10) gắn vào phương án đắt nhất, chạm lát mỏng nhất |
| M4 | MAJOR | Ngôn từ bất đối xứng + khuyến nghị đặt trước lời mời chốt (:159) |
| M5 | MAJOR | (c) giấu chi phí quyết định: khoá Q6 (chỗ lưu giá chốt) + Q11-cứng (invest tự ghi cần Q2=a, :206) |
| M6 | MAJOR | Tiền lệ Mig 37 kể nửa: đúng "0 DDL" nhưng vẫn là 1 migration rỗng 3-file; nợ nhãn 6/9 type |
| m1 | MINOR | "12-bis đã lặp 12×" nhưng liệt kê 7 mục; ground truth mold này = 7 entity / 9 type (số 12 ← migration-todos.md:766, phạm vi rộng hơn) |
| m2 | MINOR | Tiêu đề Q2 nhị phân ("module MỚI hay Contract-sớm") nhưng thân 3 phương án; (c) không thuộc cả hai |
| m3 | MINOR | (a) chưa tính +1 guard trên cầu create-contract (nay chỉ chặn Phase==DaDuyet+winner+ContractId==null) — thiếu thì "duyệt xong mới cho tạo HĐ" không có răng |
| m4 | MINOR | Q2↔Q6 phụ thuộc 2 chiều, chỉ nối 1 chiều (Q6:180-183 nhắc Q2; Q2 không nhắc Q6) — cả hai đều 🔴 CHẶN và Q2 đứng trước |
6. Điểm KHÁNG được soi (không hạ chuẩn để chiều)
ApplicableTypeslot 10 trống — ĐÚNG (ApprovalWorkflow.cs:66maxTravelRequest=9).- "Mig 37" — ĐÚNG cả ordinal lẫn tên lẫn bản chất
Up()rỗng. - "(b) phá" ở mức kết luận — vẫn đứng, chỉ lý do là sai; reviewer KHÔNG đề nghị chọn (b).
- Loại (b) vì "bước 8-12 xảy ra TRƯỚC khi có HĐ draft" (§1:47-49) — mạch lạc và khớp
:95,113.
7. Đề xuất SỬA CÁCH ĐẶT VẤN ĐỀ (acceptance, không phải code)
- Vẽ lại Q2 thành ma trận 2 trục (8-9 ∈ {PE-attachment, module mới, Proposal} × 10-12 ∈ {ngoài hệ thống, Proposal, module mới, Contract-2-vòng}), đánh dấu ô đã bác + lý do 1 dòng/ô.
- Mỗi phương án kèm 3 số đo cùng đơn vị: số bảng mới · LOC BE · LOC FE — lấy từ twin trong repo (Proposal/Mig 38) chứ không ước.
- Thay "phá state machine" bằng 3 chặn kiểm được (
ContractWorkflowService.cs:368-383·Contract.cs:23+ thiếu re-pin ·CreateContractFromEvaluationFeatures.cs:113). - Ghi rõ ràng buộc chéo: Q2=(c) ⇒ Q6 mặc định "PE thắng" ∧ Q11 chỉ còn cảnh báo mềm.
- Sửa "KHÔNG cần migration" → "0 DDL (Mig 37
Up()rỗng), vẫn sinh 1 migration rỗng + 1 entryAwLabels.Type".
END · TOTAL=14 DIEM