# 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: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`. ⇒ **`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-pin** — `UpdateContractDraft` 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/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)
- `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
