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

111 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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