Files
solution-erp/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-q2-3.md
2026-07-27 17:00:32 +07:00

15 KiB
Raw Blame History

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 ghi sub-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; enum ApplicableType :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:21100% 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. ⇒ ChoDuyet VỐN ĐÃ tái-nhập được: HĐ bị Trả lại rồi trình lại đi vào ChoDuyet lầ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):
    1. :368-383 (+ :165-177 nhánh V1) — hết Step là terminal vô điều kiện DaPhatHanh + gen mã ⇒ kết thúc vòng-giá sẽ phát hành HĐ; DaPhatHanh không có cung ra.
    2. Contract.cs:23 pin 1 cột ApprovalWorkflowId + 1 con trỏ; và không có đường re-pinUpdateContractDraft không nhận field này (sub-invest-bch-1.md:98-100).
    3. CreateContractFromEvaluationFeatures.cs:95,108,113 — HĐ tạo từ phiếu là DangSoanThao, pin V1 WorkflowDefinitionId, và gen MaHopDong NGAY ⇒ (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/src12 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)

  • ApplicableType slot 10 trống — ĐÚNG (ApprovalWorkflow.cs:66 max TravelRequest=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)

  1. 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/ô.
  2. 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.
  3. 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).
  4. 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.
  5. Sửa "KHÔNG cần migration" → "0 DDL (Mig 37 Up() rỗng), vẫn sinh 1 migration rỗng + 1 entry AwLabels.Type".

END · TOTAL=14 DIEM