wal: flush 20260727T1700

This commit is contained in:
pqhuy1987
2026-07-27 17:00:32 +07:00
parent 9f931dbc1e
commit 1512a5e663
6 changed files with 583 additions and 16 deletions

View File

@ -28,6 +28,10 @@ chain:
[x] A2: đọc PDF (fitz — `pdftoppm` KHÔNG có, render bằng `fitz` rồi Read ảnh) + trích 21 bước vào `runs/2026-07-27-S156-bch-post-ceo-flow/run.md` + grounding 4 phép: `PE.DaDuyet=7` khớp "xong bước 6" · cầu PE→HĐ CÓ SẴN `CreateContractFromEvaluationFeatures.cs` · **`shopdrawing|TVGS|duyệt mẫu` = 0 hit toàn repo** (bước 8-9 đất trắng) · `ContractPhase` **6/9 `[LEGACY]`**
[x] A2 `wf:` `/fable-real investigator-codebase` (Fable) — 2 lượt (lượt 1 **#53 mất return**, cứu từ đĩa; lượt 2 resume cắt-ngắn, CLEAN). `sub-invest-bch-1.md` **22.659 B** verify khớp return. **VERDICT `LAI`**: b.13→21 **TÁI DÙNG** Contract V2 · b.7→12 **DỰNG MỚI**. Chứng quyết định: `ContractPhase.cs:3-13` — 7 phase vật-lý (góp ý/đàm phán/in ký/CCM/trình ký/đóng dấu) **bị CỐ Ý gỡ** post-Mig 21+S17, mà đó CHÍNH LÀ trạm của b.13→21 ⇒ hồi sinh = đi ngược kiến trúc
[x] A2 `wf:` `/fable-clone reviewer` **`wf_1618ae3c-a17` — 4/4 lane SẠCH 0 chết** (đóng gói C2 ăn: ≤3 file/lane + ép khung rỗng lượt 1-2 + trần 25; lane tốn nhất 7/25). **35 điểm**: verdict `LUNG-LAY 11` · fidelity `CO-VAN-DE 10` · q2 `THIEU-PHUONG-AN 14 + thiên-vị-có-hướng` · gaps `4/4 THẬT 0 dương-giả`. **Lõi `LAI` SỐNG**, đổ ở NHÃN chi phí + CÁCH ĐẶT VẤN ĐỀ. Synthesis: `review-synthesis.md`
[!] 🔴🔴 **LỖ HỔNG AN NINH PROD — gotcha #82 TÁI PHÁT, KHÔNG liên quan tính năng mới**: `ContractWorkflowService.cs:48-66` nhánh Reject chạy TRƯỚC MỌI GUARD (0 role/0 approver/0 fromPhase) + `ContractsController.cs:13` **`[Authorize]` trần duy nhất phủ 22 endpoint ghi** ⇒ **mọi user đăng nhập Từ-chối/Trả-lại được BẤT KỲ HĐ nào kể cả `DaPhatHanh` terminal**, kèm xoá `SlaDeadline`. **Lead tự verify 2 tầng độc lập** (đối chứng PE controller có 3 per-action policy ⇒ loại trừ "dự án không làm kiểu đó"; grep có răng: bắt 22 endpoint ghi). → chờ anh quyết tách task riêng
[ ] 🔴 invest có **1 claim SAI SỰ THẬT** phải sửa trước khi trình: "hardcoded policy fallback" → thật là `ConflictException :115-116` ⇒ HĐ từ phiếu **KẸT CỨNG ChoDuyet không ai duyệt được** (nặng hơn, không phải degrade êm). Gốc: **invest tin skill-doc `contract-workflow` hơn ĐĨA** (doc tả fallback `WorkflowPolicyRegistry`, đĩa 0 hit ở đường transition, chỉ còn ở đường HIỂN THỊ ⇒ lệch DISPLAY⟂GUARD mà doc stale che luôn)
[ ] A2 6 việc sửa bản invest trước khi trình anh 14 câu (xem `review-synthesis.md` §G) — gồm thêm Q vai-người-TRÌNH (`:70-79` PMH role `Procurement` bị 403 ở b.13/b.17 = lớp "cơ-chế đúng dữ-liệu không có" **lần 5**) + Q5 thêm phương án (d) `skipToFinal` ĐÃ WIRE mà invest bỏ sót + viết lại Q2 theo 2-trục + 3 số đo từ twin repo
[!] A2 **CHẶN: Q1-Q6 chờ anh** (14 câu tổng). §4 wire + §5 checklist chưa làm được vì "trả lời khác ⇒ wire khác". Nặng nhất **Q2 kiến trúc khúc 8-12** (a: module PostAward mới `ApplicableType=10` · b: HĐ trình 2 vòng — invest KHUYÊN TRÁNH · c: tối giản attachment+checklist trên PE). Rồi mới `/fable-clone review`
[ ] 🔴 4 lỗ "cột CÓ mà 0 ai đọc" invest đào ra — cầu PE→HĐ pin V1 không nhận V2 · `CeoApprovalThreshold` **0-consumer phía HĐ** (b.18 "CEO ký >5 tỷ" KHÔNG có cơ chế, admin set bị ngó lơ IM LẶNG) · SLA V2 hardcode `AddDays(7)` (sơ đồ ghi 3 ngày) · `AttachmentPurpose` không phân biệt ký-nháy/ký-chính. Cùng lớp lỗi đã cắn 4 lần đợt PE ⇒ **đặc tính kiến trúc** (schema chung ⟂ service riêng per-type), không phải xui
[ ] 🔴 vết lỗi lead @S156: viết `run.md` đã **điền SẴN kết-quả S2 cho lượt chạy CHƯA xảy ra** (byte-count + `TOTAL=14 GAP` + verdict = BỊA). Tự bắt ngay sau Write, sửa về `[ ]`, ghi vết trong `run.md`. Lớp `SHA-bịa` — xảy ra ngay trong lượt vừa khoe "đo-trước-tin" ⇒ ứng viên memory datum

View File

@ -0,0 +1,136 @@
# review-synthesis — S156 wave 2 · `/fable-clone reviewer` 4 lăng kính
> Lead-written (P3/P4 refute + synthesize). Run `wf_1618ae3c-a17` · **4/4 lane sạch, 0 chết, 0 rỗng**
> (đóng gói C2 có tác dụng: ≤3 file/lane + ép khung rỗng lượt 1-2 + trần 25; lane tốn nhiều nhất 7/25 lượt).
> Nguồn: `sub-review-verdict-1.md` · `sub-review-fidelity-2.md` · `sub-review-q2-3.md` · `sub-review-gaps-4.md`
## Bảng verdict
| Lăng kính | Verdict | Điểm |
|---|---|---|
| lens-verdict | `LUNG-LAY` | 11 (3H · 7M · 1L) |
| lens-fidelity | `CO-VAN-DE` | 10 (2H · 3M · 5L) |
| lens-q2 | `THIEU-PHUONG-AN` + thiên-vị-có-hướng | 14 (4H · 6M · 4L) |
| lens-gaps | 4/4 claim THẬT, **0 dương-giả** | 2 đáng làm · 1 đổi khung · 1 sai tầng |
**Tổng 35 điểm.** Lõi verdict `LAI` **SỐNG** (cả 4 lane không lane nào bác thế chia đôi 7→12 dựng mới ⟂
13→21 tái dùng). Cái đổ là **NHÃN CHI PHÍ****CÁCH ĐẶT VẤN ĐỀ**, không phải kết luận.
---
## A. 🔴 2 phát hiện MỚI — reviewer đào ra, invest KHÔNG có
### A1. LỖ HỔNG AN NINH THẬT trên production (lead đã tự verify độc lập)
`ContractWorkflowService.cs:48-66` — nhánh `Reject` chạy **TRƯỚC MỌI GUARD**:
```
if (decision == ApprovalDecision.Reject) {
... contract.Phase = TuChoi | TraLai
contract.SlaDeadline = null
SaveChanges(); return; ← thoát luôn, không qua guard nào phía dưới
}
```
**0 kiểm `actorRoles` · 0 kiểm actor có phải người duyệt lượt này · 0 kiểm `fromPhase`.**
Tầng 2 (authz API) **cũng hở**: `ContractsController.cs:13` = **đúng 1 `[Authorize]` trần**, 0 per-action
policy, phủ **22 endpoint ghi**. Đối chứng loại trừ "dự án không làm kiểu đó":
`PurchaseEvaluationsController`**3** `[Authorize(Policy...)]`.
⇒ **MỌI user đã đăng nhập có thể Từ-chối / Trả-lại BẤT KỲ hợp đồng nào — kể cả HĐ đã `DaPhatHanh`
(terminal, đã phát hành).** Và `SlaDeadline` bị xoá kèm.
🔴 **Đây là gotcha #82 TÁI PHÁT** (`feedback_permission_grant_two_layers`: display-layer ⟂ API-authz-layer
là 2 tầng độc lập; `[Authorize]` trần = lỗ hổng). Lần trước bắt ở `ReportsController` @S118 — cùng hình dạng.
**KHÔNG liên quan tính năng mới**; nó đang sống trên prod.
### A2. PMH không trình được HĐ — 403 ngay bước 13/17
`ContractWorkflowService.cs:70-79`: gate trình `DangSoanThao|TraLai → ChoDuyet` đòi role
`Drafter` **hoặc** `DeptManager`. PMH (Phòng cung ứng) mang role `Procurement``ForbiddenException`.
Mà sơ đồ giao **PMH** trình ở cả b.13 lẫn b.17. Đúng lớp *"cơ-chế đúng, thứ đi qua nó không có"* — lớp đã
cắn 4 lần đợt PE, nay lần 5.
---
## B. 1 claim của invest SAI SỰ THẬT (phải sửa trước khi trình owner)
Invest §1:51-55 viết cầu PE→HĐ *"rơi nhánh V1 legacy (hardcoded policy fallback nếu V1 không có active def)"*.
**Sai.** Đo: `grep WorkflowPolicyRegistry|WorkflowTypeAssignment` trên `ContractWorkflowService.cs` = **0 hit**.
Đường thật: `:98``:108-113`**`:115-116` throw `ConflictException`**.
⇒ Nếu `activeWfId` null thì HĐ sinh từ phiếu **trình được nhưng KHÔNG AI DUYỆT ĐƯỢC — kẹt cứng ở `ChoDuyet`**.
**Hỏng CỨNG, không "degrade êm"** như invest mô tả. Nặng hơn chứ không nhẹ hơn.
🔑 **Vì sao invest sai:** nó tin **skill-doc `contract-workflow`** (mô tả `LoadPolicyAsync` có fallback
`WorkflowPolicyRegistry`) **hơn đĩa**. Trên đĩa fallback đã chết ở đường *transition*, chỉ còn sống ở đường
*hiển thị* (`ContractFeatures.cs:448-455`) ⇒ **lệch DISPLAY ⟂ GUARD**, và doc stale che luôn cái lệch đó.
---
## C. Q2 — cách đặt vấn đề THIÊN VỊ CÓ HƯỚNG (owner đã nói "cần bàn thêm", nên phần này quan trọng nhất)
| # | Vấn đề | Chứng |
|---|---|---|
| H1 | **3 phương án KHÔNG cùng phạm vi** ⇒ so sánh chi phí vô nghĩa (apples-to-oranges) | option-space thật là **2 trục**, không phải 3 điểm |
| H2 | **Thiếu phương án tái dùng module `Proposal`** (`ApplicableType=4`) | có sẵn trong repo |
| M1 | Phương án "PE + workflow thứ 2" bị bác ở §1 nhưng **không hiện trong Q2** ⇒ owner đọc §6 không biết nó tồn tại. Và lý do bác dựng "1 cột" thành **bất-khả kiến trúc**, trong khi chính tài liệu này coi `AddColumn` là rẻ ở chỗ khác (Mig 53 "3 AddColumn", Mig 67 "11 cột") ⇒ **tiêu chuẩn kép** | |
| M2 | Thiếu phương án "Contract-sớm nhưng tách entity con" | |
| M3 | **Hiệu ứng hào quang**: dữ kiện "rẻ" DUY NHẤT (`ApplicableType=10` append-only) lại gắn vào phương án **ĐẮT NHẤT** (a) — mà slot enum là lát mỏng nhất của chi phí (a) (thật: 4 bảng + ~600 LOC BE + ~1.956 LOC FE) | |
| M4 | **Bất đối xứng ngôn từ**: (a) và (c) mở bằng lợi ích, **chỉ (b)** bị gắn tính từ rủi ro; 0 phương án nào có rủi ro **định lượng**. Câu chốt "Em nghiêng (a) hoặc (c) — (b) khuyên tránh" đặ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 | |
| HIGH | **Chi phí là CẢM TÍNH****bản sao đo được nằm sẵn trong repo** (Proposal / Mig 38) | |
**Cách sửa (reviewer đề, lead đồng ý):** mỗi phương án kèm **3 số cùng đơn vị — số bảng mới · LOC BE ·
LOC FE — lấy từ twin thật trong repo, không ước.**
---
## D. 4 lỗ invest tuyên bố: **0 DƯƠNG-GIẢ**, nhưng 2 lỗ đóng khung sai
| Lỗ | Verdict reviewer |
|---|---|
| **L1** cầu PE→HĐ bỏ pin V2 | ✅ THẬT, đáng làm (và hậu quả nặng hơn invest nói — xem §B) |
| **L2** `CeoApprovalThreshold` ghost-wire phía HĐ | ✅ THẬT, đáng làm — vòng lặp "đặt được → lưu → hiện lại → **không ai đọc**" khép kín, chứng từng mắt xích. `AllowApproverFinalize` cũng vắng |
| **L3** SLA hardcode `AddDays(7)` | ✅ THẬT nhưng **đổi khung**: là **thụt lùi so với V1** và là lỗ **toàn-V2 (cả PE)**, không riêng HĐ |
| **L4** `AttachmentPurpose` thiếu ký-nháy | ⚠️ Đúng chữ, **sai tầng**: `Purpose` hiện là **nhãn KHÔNG AI THI HÀNH** — validator chỉ `IsInEnum()`, 0 nhánh `Purpose ==` nào trong Backend. **Thêm enum là vô nghĩa nếu không dựng cổng đọc nó** |
> 🔑 Reviewer chốt: *"Invest không bịa lỗ nào; điểm yếu là **ĐÓNG KHUNG**, không phải bịa dữ kiện."*
---
## E. Điểm reviewer CỦNG CỐ cho invest (adversarial ≠ luôn hạ điểm)
Lập luận Q1 của invest (*"phase bị gỡ chính là trạm của b.13→21"*) **ĐỨNG**, và reviewer tìm được **chứng
MẠNH HƠN thứ invest dùng**: bảng vai còn sống `ContractFeatures.cs:369-371` map
`DangKiemTraCCM→CostControl` · `DangTrinhKy→Director/AuthorizedSigner` · `DangDongDau→HrAdmin`
= **trùng khít chuỗi CCM→CEO→HR** của b.17→18→19. Trùng 3 vai liên tiếp đúng thứ tự ≈ loại trừ "trùng
hợp tên gọi" — trong khi comment enum chỉ chứng được *"đã deprecated"*, không chứng được *"vì sao"*.
**Đính chính biên của invest:** chỉ **b.14→19** rơi vào vùng phase-đã-gỡ. b.13 dùng `DangSoanThao=2`
b.20-21 dùng `DaPhatHanh=9`**cả hai CÒN SỐNG**. Invest viết "13→21" là nống biên.
---
## F. Nhãn đúng cho verdict (thay "TÁI DÙNG")
> **"Tái dùng KHUNG V2 + WIRE MỚI 4 đường sống (visibility `ChoDuyet` · inbox · notify approver kế ·
> guard reject) + 1 rẽ nhánh tiền (ngưỡng CEO)"**
`ApproveV2Async` gánh được **KHUNG** (steps `:234` · level-group `:246` · OR-of-N `:259-260` · advance
`:365-392` · terminal `:371-385` không gen mã đúp) nhưng **KHÔNG gánh được ĐƯỜNG SỐNG**:
`grep ChoDuyet ContractFeatures.cs` = **0 hit** ⇒ view-guard `:343-349` + inbox `:365-371` legacy-only
**người duyệt không mở được phiếu đang chờ chính mình**. Notify `:407` chỉ báo Drafter.
---
## G. Việc phải làm trước khi trình owner 14 câu hỏi
1. Sửa claim SAI ở invest §1:51-55 (`hardcoded fallback``ConflictException`, nâng lên mức **CHẶN**)
2. Siết biên §0/§1: `b.14→19` chứ không `b.13→21`
3. Thêm Q mới: **vai người TRÌNH** (`:70-79` PMH 403)
4. Thêm cảnh báo **A1 lỗ hổng reject** — nhưng đây là **việc RIÊNG, không thuộc spec BCH**
5. Q5 thêm phương án **(d) `skipToFinal` + `AllowApproverSkipToFinal :322`** — đường bỏ-qua-CEO **ĐÃ WIRE**
ở Contract V2, 0 code BE. Invest bỏ sót hẳn.
6. Viết lại Q2 theo 2 trục + 3 số đo cùng đơn vị lấy từ twin repo
<!-- END · TOTAL=35 DIEM · 4/4 LANE SACH · VERDICT=LOI-SONG-NHAN-SAI -->

View File

@ -0,0 +1,163 @@
# sub-review-fidelity-2 — LENS FIDELITY (reviewer adversarial)
> Vai: reviewer ADVERSARIAL. Nhiệm vụ: chấm ĐỘ TRUNG THỰC bảng §3 của `sub-invest-bch-1.md`
> so với bảng 21 bước ở `run.md` (= BẢN TRÍCH SƠ ĐỒ GỐC, nguồn sự thật duy nhất; PDF gốc là ảnh).
> KHUNG RỖNG ghi @lượt 1 TRƯỚC KHI ĐỌC (chống #53 return-garble) — nội dung append dần bên dưới.
## 0. Trạng thái file (append-log)
- [x] Lượt 1: khung rỗng đã ghi đĩa.
- [x] Lượt 2-5: đọc 2 file nguồn + 4 phép đo máy (lane-diff, SLA-col, disclaimer-count, grep drop).
- [x] Chốt: verdict `CO-VAN-DE — 10 điểm` (2 HIGH · 3 MAJOR · 5 MINOR). Khung xương SẠCH.
## 1. Nguồn đo (2 file, KHÔNG đọc gì khác)
| Ký hiệu | Đường dẫn | Vai trò |
|---|---|---|
| SRC | `.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/run.md` | bảng 21 bước `:18-40` + SLA `:42-43` + ghi-chú `:45-56` = NGUỒN SỰ THẬT |
| TGT | `.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-invest-bch-1.md` | §3 `:106-137`, bảng `:112-128` (15 hàng b.7→21) = BỊ CHẤM |
**Chênh lệch cấu trúc (gốc của mọi vấn đề bên dưới):** SRC bảng có **3 cột** (`| # | Lane | Nội dung |`,
`run.md:18`). TGT bảng có **9 cột** (`| # | Lane | Actor | Trigger | Input | Output | Điều kiện chuyển |
Nhánh lỗi | SLA |`, `sub-invest-bch-1.md:112`). ⇒ **5 cột (Actor/Trigger/Input/Điều-kiện-chuyển/Nhánh-lỗi)
được DỰNG HOÀN TOÀN bởi invest**, `Output` bán-dẫn-xuất từ cột `Nội dung`. Legend `:108-110` chỉ khai
xuất-xứ cho **1 cột duy nhất là SLA** ("Cột SLA = đúng những gì sơ đồ ghi (3 mốc), '—' = sơ đồ không ghi").
## 2. Bốn phép đo máy (chạy trên đĩa, không đọc bằng mắt)
| # | Phép | Lệnh | Kết quả |
|---|---|---|---|
| P1 | Tập bước | so `awk` cột 1 TGT `:114-128` vs SRC `:26-40` | **15 vs 15, khớp 7..21, 0 gap 0 thừa** |
| P2 | Lane | so cột 2 từng hàng | **15/15 KHỚP TUYỆT ĐỐI** (kể cả `Procurement + CCM` b.17) |
| P3 | Disclaimer | `awk NR 114-128 /sơ đồ (KHÔNG\|không) (vẽ\|ghi)/` | **5/15 hàng** có khai (b.8,10,11,12,15); **10/15 im lặng** |
| P4 | Rơi ghi-chú | `grep -c` 5 cụm ghi-chú BCH-lane của SRC trong TGT | `năng lực`0 · `khảo sát`0 · `loại bớt`0 · `THAM VẤN`0 · `danh sách mời thầu`0 — **0-hit toàn file** |
## 3. Bảng đối chiếu từng hàng (15/15)
| # | Lane SRC→TGT | Nội dung | Điều kiện chuyển | Nhánh lỗi | Phán |
|---|---|---|---|---|---|
| 7 | Proc→Proc ✅ | khớp `run.md:26` | "Gửi xong → 8" (suy từ số) | sự-thật-hệ-thống (SMTP TODO) ✅ | OK |
| 8 | PROJECT ✅ | khớp `:27` | suy từ số | **KHAI** "sơ đồ KHÔNG vẽ vòng lặp" ✅ | OK · Input bịa xuất-xứ → m7 |
| 9 | PROJECT ✅ | khớp `:28` | suy từ số | ⚠ neo ghi-chú SRC `:51-52` vào b.9 → **M3** | LỖI |
| 10 | Proc ✅ | khớp `:29` | suy từ số | **KHAI** "(sơ đồ không vẽ nhánh trả)" ✅ | OK |
| 11 | CCM ✅ | khớp `:30` | suy từ số | **KHAI** "sơ đồ không vẽ" ✅ | OK |
| 12 | CEO ✅ | khớp `:31`, giữ "CEO **hoặc** CCM" | suy từ số | **KHAI** "sơ đồ KHÔNG ghi ❓Q3" ✅ | OK |
| 13 | Proc ✅ | khớp `:32` | suy từ số | sự-thật-hệ-thống ✅ | OK · SLA chủ-thể → M5 |
| 14 | PROJECT ✅ | khớp `:33` | suy từ số | ⚠ "lặp 13↔14 (sơ đồ ghi PHỐI HỢP)" → **M4** | LỖI |
| 15 | Proc ✅ | `:34` MƠ HỒ | ⚠ "TP đồng ý + **ký nháy**" → **H2** | KHAI "sơ đồ không vẽ ❓Q13" ✅ | LỖI |
| 16 | PROJECT ✅ | `:35` | — | ⚠ Trigger/Input chốt "TP đã ký nháy" → **H2** | LỖI |
| 17 | Proc+CCM ✅ | khớp `:36`, trích nguyên ngoặc | suy từ số | sự-thật-hệ-thống ✅ | OK |
| 18 | CEO ✅ | khớp `:37` ">5 tỷ" | "Ký (hoặc ≤5 tỷ: skip ❓Q4)" ✅ | ⚠ "CEO từ chối→TuChoi/TraLai" **0 disclaimer** → m6 | ~ |
| 19 | Proc ✅ | khớp `:38` | suy từ số | "—" ✅ | OK |
| 20 | CCM ✅ | khớp `:39` | suy từ số | "—" ✅ | OK |
| 21 | Proc ✅ | khớp `:40` | suy từ số | "vận đơn ngoài hệ thống ❓Q14" (thêm, có ❓) | OK |
## 4. Năm câu hỏi bắt buộc — trả lời
### Q1 — BỊA (có ở §3, KHÔNG có ở nguồn)? → **KHÔNG có bước bịa.**
P1: tập bước TGT = {7..21} = tập SRC. 0 bước phát minh. Ở cấp **nội dung ô** thì có 1 ca nhỏ (m7).
### Q2 — RƠI (có ở nguồn, mất khỏi §3)? → **0 bước rơi**; nhưng **5 cụm ghi-chú BCH-lane rơi im lặng** (m8).
`run.md:46-49` khối *"Phối hợp BCH dự án"* (cập nhật danh sách thầu phụ · gom tóm tắt tiến độ/scope/bản vẽ FC ·
kiểm tra bổ sung trước khi mời thầu · **bố trí khảo sát dự án** · **kiểm tra năng lực thầu phụ / loại bớt đơn vị
làm không đạt**) + `:50` *THAM VẤN* → P4 cho **0-hit trên TOÀN file TGT**. Đây là việc của ĐÚNG lane được giao
(PROJECT/BCH), rơi trước b.7 nên ngoài dải §3 — nhưng file **không hề khai** "các ghi-chú BCH này nằm trước b.7,
ngoài phạm vi". Người đọc không biết chúng tồn tại.
### Q3 — Lane/actor gán SAI? → **Lane: 15/15 ĐÚNG (P2). Actor: 1 ca sai/mơ-hồ (H2, b.15-16).**
Không hàng nào đổi lane. `Procurement + CCM` (b.17) và `CEO hoặc CCM` (b.12) đều giữ nguyên hình dạng lai của
nguồn thay vì ép về 1 lane — đây là điểm **làm tốt**, chống được cám dỗ đơn-giản-hoá.
### Q4 — SUY DIỄN không khai (ĐIỂM NGUY NHẤT) → **CÓ, cả cấp CỘT lẫn cấp Ô.**
- **Cấp CỘT (H1):** SRC không có bất kỳ điều-kiện-chuyển nào (3 cột, `run.md:18`) ⇒ **15/15 ô 'Điều kiện chuyển'
là suy diễn từ THỨ-TỰ-SỐ**, không ô nào khai. Cột 'Nhánh lỗi' chỉ 5/15 khai (P3). Legend `:108-110` khai
xuất-xứ cho **mỗi cột SLA** ⇒ đọc ngược lại: im-lặng ở 2 cột kia không phân biệt được "sơ đồ có" vs "em suy".
- **Cấp Ô:** M3 (neo sai chỗ), M4 (trích-dẫn nguỵ-trang suy diễn), m6 (bất đối xứng khai báo), m7 (bịa xuất-xứ).
### Q5 — SLA 3 mốc → **Khoảng gán ĐÚNG 3/3; nhưng CHỦ-THỂ mốc "3 ngày" chốt 1 chiều không khai (M5).**
| Mốc SRC `run.md:42-43` | Neo SRC | TGT gán | Phán |
|---|---|---|---|
| `10-14 ngày` | b.1→4 | không xuất hiện (ngoài dải 7-21) | ✅ đúng — không kéo bừa vào |
| `3 ngày` | b.13-14 (`:53-54`) | `:120` b.13 + `:121` b.14 | ✅ khoảng đúng · ⚠ chủ-thể → M5 |
| `7-10 ngày` | b.13→18 | `:120` b.13 … `:125` b.18 "kết khúc" | ✅ khoảng đúng · ô 15/16/17 "trong khúc 7-10d" = nội suy (m9) |
## 5. Danh sách điểm (10)
**H1 [HIGH] — Không có khai xuất-xứ ở CẤP CỘT cho 2 cột suy-diễn-nặng-nhất.**
`sub-invest-bch-1.md:112` bảng 9 cột vs `run.md:18` nguồn 3 cột ⇒ 5 cột dựng mới. Legend `:108-110` chỉ khai
cho SLA. Đo: 'Điều kiện chuyển' **15/15 ô** không có neo nguồn (nguồn 0 điều-kiện); 'Nhánh lỗi' **10/15 ô**
không có disclaimer (P3). Hệ quả: người đọc §3 để viết spec sẽ tưởng sơ đồ CÓ vẽ các nhánh này.
*Tiêu chí nhận:* thêm 1 dòng legend đúng kiểu dòng SLA đã có, cho cả 2 cột; hoặc gắn ký-hiệu `[SĐ]`/`[SUY]`
từng ô. (Khuôn đã tồn tại ngay trong chính file — chỉ chưa áp cho 2 cột kia.)
**H2 [HIGH] — b.15/16: gán "THẦU PHỤ ký nháy" trong khi nguồn mơ hồ, có thể là BCH ký nháy.**
Nguồn `run.md:34`: *"thầu phụ chuyển về BCH dự án ký nháy gửi về công ty"* — đọc B (chuỗi động từ đổi chủ ngữ
sau "về BCH dự án": **BCH** ký nháy rồi gửi về công ty) ít nhất ngang bằng đọc A của invest, và ăn khớp hơn với
`run.md:36` b.17 *"CCM - Kiểm tra, ký nháy"* (chuỗi ký nháy NỘI BỘ: BCH → CCM → CEO ký chính). `run.md:35` b.16
*"BCH nhận hợp đồng ký nháy"* cũng đọc được cả 2 chiều. TGT chốt đọc A **3 lần, 0 lần khai mơ hồ**:
`:122` "TP đồng ý + ký nháy → 16" · `:123` "Nhận bản **TP đã ký nháy**" · `:212` Q12 "b.16 (TP ký nháy)".
*Vì sao chặn:* nếu đọc B đúng thì hệ thống cần **vai ký-nháy nội bộ của BCH** (user + trạng thái + chứng từ),
khác hẳn với "chứng từ do bên ngoài ký" — Q12 đang hỏi sai đối tượng.
*Tiêu chí nhận:* biến thành câu hỏi owner (Q15) hoặc khai "2 cách đọc, em chọn A vì …".
**M3 [MAJOR] — b.9: neo ghi-chú nhánh-lỗi vào bước 9, trình bày như thể sơ đồ đặt nó ở đó.**
`:116` viết `Ghi chú sơ đồ: "hồ sơ sai sót thiếu khối lượng, sai spec, thiếu phạm vi → gửi mail làm rõ xác nhận"`.
Nội dung CÓ trong nguồn (`run.md:51-52`) ⇒ trích đúng, **nhưng nguồn để nó KHÔNG NEO BƯỚC** (nằm trong khối
"Ghi chú phối hợp (chữ nhỏ trên sơ đồ)"). Chỉ dấu ngược: nội dung trùng gần hết với **b.3** `run.md:22`
*"Nhận và kiểm tra hồ sơ báo giá (có thể **làm rõ** - update giá nếu chưa đủ **phạm vi**)"* ⇒ nhiều khả năng
ghi-chú thuộc khúc 1-4 (nhận báo giá), không phải b.9. Prefix "Ghi chú sơ đồ:" biến 1 phỏng-đoán-vị-trí thành
1 trích-dẫn-có-thẩm-quyền. *Tiêu chí nhận:* "sơ đồ có ghi-chú nhánh lỗi **không neo bước**; em gán vào 9 vì …".
**M4 [MAJOR] — b.14: trích-dẫn nguỵ-trang suy diễn (citation-laundering).**
`:121` "Thiếu info → **lặp 13↔14** (sơ đồ ghi PHỐI HỢP)". Nguồn `run.md:53-54` chỉ nói *BCH gửi full thông tin
bản vẽ/khối lượng/tính toán để PMH soạn HĐ gửi BCH trong vòng 3 ngày* — **không có vòng lặp**, không có nhánh
"thiếu info". Dấu ngoặc đơn dẫn nguồn ở ngay sau mệnh đề suy diễn ⇒ đọc lướt thành "sơ đồ có vẽ vòng lặp".
Đây đúng lớp M3 nhưng nguy hơn vì phần được dẫn nguồn (PHỐI HỢP) là THẬT, phần suy diễn (vòng lặp) là mới.
**M5 [MAJOR] — SLA "3 ngày": chủ-thể đồng-hồ chốt 1 chiều, không khai mơ hồ.**
Nguồn `run.md:53-54`: *"BCH gửi full thông tin … **để PMH soạn HĐ gửi BCH trong vòng 3 ngày**"* — trạng ngữ
"trong vòng 3 ngày" đứng NGAY SAU "PMH soạn HĐ gửi BCH" ⇒ đọc tự nhiên nhất là **deadline của PMH**. TGT chốt
ngược: `:120` "**BCH gửi info** trong 3 ngày", `:121` "3 ngày", và `:200` Q10 cũng chỉ ghi 'SLA "3 ngày" (13-14)'
⇒ owner không được hỏi lại. *Vì sao chặn:* SLA gắn sai vai = đếm ngược sai người, thông báo trễ hạn sai người,
và Q10 (có nên ENFORCE) đang hỏi trên tiền-đề chưa chắc.
**m6 [MINOR] — b.18 thiếu disclaimer trong khi hàng cùng loại thì có.**
`:125` "CEO từ chối → TuChoi/TraLai" — sơ đồ `run.md:37` chỉ vẽ "CEO ký HĐ nếu >5 tỷ", không vẽ nhánh từ chối.
b.10/b.11 (`:117`,`:118`) cùng tình huống thì CÓ ghi "sơ đồ không vẽ". Kỷ luật khai báo không đều ⇒ củng cố H1.
**m7 [MINOR] — b.8 Input bịa xuất-xứ.** `:115` "Mẫu **vật liệu** + shopdrawing **từ TP/NCC**" — `run.md:27` chỉ
có "Duyệt mẫu + shopdrawing với TVGS". "vật liệu" và "từ TP/NCC" là bổ sung của invest (hợp lý về nghiệp vụ,
nhưng vẫn là thêm dữ kiện vào ô trình bày như mô tả sơ đồ).
**m8 [MINOR] — 5 cụm ghi-chú BCH-lane rơi im lặng (P4 = 0-hit).** Xem Q2 ở trên. Không vi phạm dải §3 (7→21)
đã khai, nhưng file không có 1 dòng nào nói chúng tồn tại và nằm ngoài dải — với deliverable mang tên
"lane PROJECT (BCH)" thì đây là khoảng trống người đọc không thấy được.
**m9 [MINOR] — legend "3 mốc" quá lời so với chính bảng.** `:110` "Cột SLA = đúng những gì sơ đồ ghi (3 mốc),
'—' = sơ đồ không ghi" — bảng chỉ chứa **2 mốc** (mốc `10-14 ngày` thuộc b.1→4 ngoài dải, đúng khi bỏ), và 3 ô
b.15/16/17 ghi "trong khúc 7-10d" = **nội suy thành-viên-khoảng**, không phải chữ trên sơ đồ, cũng không phải "—".
Suy diễn ở đây minh bạch ("trong khúc") nên rủi ro thấp, nhưng legend tự nó bị vi phạm.
**m10 [MINOR/INFO] — lane ⑥ KÝ KẾT HỢP ĐỒNG (và lane ① TENDER) không có bước số nào neo vào.**
`run.md:14` khai 6 lane; bảng 21 bước `run.md:20-40` chỉ dùng 4 (Procurement/CCM/CEO/PROJECT). Lane ⑥ — đích
của cả đề bài ("→ Ký kết HĐ") — chỉ sống trong ghi-chú `run.md:55-56`. TGT kế thừa đúng (không bịa lane) nhưng
KHÔNG nêu mâu thuẫn nội tại này của nguồn. Nửa "Thầu phụ ký song song HĐ chủ đầu tư" chỉ xuất hiện lạc trong
Q11 `:208-209`, nửa "NCC phải duyệt mẫu mới ký" ở prose `:137` — cả hai **không có ô nào trong bảng**, nên ràng
buộc THỜI ĐIỂM ký (song song / ký trước) không vào được §3.
## 6. Điều làm ĐÚNG (ghi để không hạ chuẩn nhầm chỗ — Smart-Friend guard 2 chiều)
- Khung xương **SẠCH 15/15**: 0 bước bịa, 0 bước rơi, 0 lane sai (P1+P2). Đây là trục dễ sai nhất và nó đúng.
- Giữ nguyên **hình dạng lai** của nguồn thay vì ép chuẩn hoá: b.12 "CEO **hoặc** CCM", b.17 "Procurement + CCM".
- **Không kéo** mốc `10-14 ngày` (ngoài dải) vào bảng — đúng kỷ luật phạm vi.
- 5 hàng khai thẳng "sơ đồ không vẽ/không ghi" + 14 câu ❓Q neo vào ô ⇒ khuôn khai-báo **đã có sẵn**, vấn đề
chỉ là **áp không đều** (H1/m6). Chi phí vá thấp.
- Trích nguyên văn có ngoặc kép ở b.17 ("ký nháy hoặc ký chính") — đúng cách phân biệt trích vs diễn giải.
## 7. Verdict
**`LENS-FIDELITY: CO-VAN-DE — 10 điểm`** (2 HIGH · 3 MAJOR · 5 MINOR).
Khung xương trung thực; **sai lệch tập trung TRỌN ở trục KHAI-BÁO XUẤT-XỨ**, đúng chỗ đề bài cảnh báo là
nguy nhất. Không có điểm nào đòi sửa nội dung nghiệp vụ — 8/10 điểm vá bằng cách **thêm nhãn/câu hỏi**, 2 điểm
(H2, M5) phải **hỏi owner** vì nguồn thật sự mơ hồ và invest đã chốt hộ.
**Chặn:** H2 + M5 nên thành câu hỏi owner TRƯỚC khi lead ghi spec — cả hai đang là tiền-đề im lặng cho Q10/Q12.

View File

@ -0,0 +1,63 @@
# sub-review-gaps-4 — reviewer ADVERSARIAL verify 4 lỗ (S156 BCH post-CEO flow)
> KHUNG RỖNG ghi @lượt-1 TRƯỚC KHI ĐỌC (chống #53 return-garble). Append dần theo lượt.
> ⚠️ PATH-CONFLICT khai báo: task-body chỉ định file này (`sub-review-gaps-4.md`); RUN-TRACE footer chỉ định `sub-reviewer-3.md`. Áp luật "fix-mới thắng footer-cũ" → ghi vào file này, khai trong return.
## 0. Meta
- Vai: reviewer adversarial (READ-only verdict, KHÔNG sửa code-tree)
- Nguồn claim: `sub-invest-bch-1.md` (investigator)
- Ngân sách: 25 lượt (1-2 khung · 3-18 verify · 19-25 chốt)
- Thang chấm mỗi lỗ: **THẬT** / **DƯƠNG-GIẢ** / **ĐÚNG-NHƯNG-KHÔNG-QUAN-TRỌNG**
- Kỷ luật phép đo: MỌI grep 0-hit phải kèm **chứng-có-răng** (grep chuỗi BIẾT CHẮC có, cùng file/cùng lệnh) trước khi kết luận.
## 1. Trạng thái 4 claim (điền dần)
| # | Claim (tóm) | Verdict | Bằng chứng file:line | Chứng-có-răng |
|---|---|---|---|---|
| L1 | cầu PE→HĐ pin V1 `WorkflowDefinitionId`, không nhận `ApprovalWorkflowId` V2 | **THẬT** (cao) | `CreateContractFromEvaluationFeatures.cs:68-71,108` vs `ContractFeatures.cs:105,109`; hệ quả `ContractWorkflowService.cs:98` | cùng-file có hit `ApprovalWorkflowId``:195,199` ⇒ pattern đúng chính tả, vắng-mặt trong initializer là THẬT |
| L2 | `CeoApprovalThreshold` 0 consumer phía HĐ | **THẬT** (cao, ghost-wire) | field `ApprovalWorkflow.cs:43`; `ApplicableType.Contract=3` `ApprovalWorkflow.cs:57`; `ContractWorkflowService.cs:228-232` nạp `aw` mà không đọc; quyết-định terminal `:366-385` thuần vị-trí; đối-chứng PE `PurchaseEvaluationWorkflowService.cs:894` | grep repo-wide `CeoApprovalThreshold` = 40+ hit; grep cùng-file `ApproveV2Async` = 7 hit ⇒ file greppable, 0-hit là THẬT |
| L3 | SLA V2 hardcode `AddDays(7)`, Step không có SlaDays | **THẬT** (vừa) | `ApprovalWorkflow.cs:70-79` (Step 4 prop, 0 SLA); hardcode `ContractWorkflowService.cs:341,358,390` (trong `ApproveV2Async`) + `:85,182,194` | `SlaDays` CÓ ở `WorkflowDefinition.cs:37` (V1) + `PurchaseEvaluationWorkflowDefinition.cs:32` ⇒ token đúng chính tả |
| L4 | `AttachmentPurpose` không phân biệt ký-nháy/ký-chính | **ĐÚNG-NHƯNG-KHÔNG-QUAN-TRỌNG** (như phát biểu) | `ContractAttachment.cs:5-11` đúng verbatim; nhưng 0 nhánh logic đọc `Purpose`: `ContractAttachmentFeatures.cs:51,80,89` + `ContractsController.cs:78` | grep `Purpose` toàn Backend = 25+ hit (Office/PE/VehicleBooking) ⇒ pattern có răng; riêng nhánh `Purpose ==` = 0 |
## 2. Nhật ký đo (append theo lượt)
### Lượt 1-2 — khung rỗng
- Đã ghi file này trước khi đọc bất kỳ file nguồn nào. Chưa đo gì.
### Lượt 3-10 — đo
**L1.** Cầu PE→HĐ chỉ truy V1: `CreateContractFromEvaluationFeatures.cs:68-71` query `db.WorkflowDefinitions.Where(w => w.ContractType == request.ContractType && w.IsActive)`, rồi `:108` gán `WorkflowDefinitionId = activeWfId`. KHÔNG có dòng nào gán `ApprovalWorkflowId`. Nhánh tạo-HĐ-thường thì gán CẢ HAI: `ContractFeatures.cs:105` (`WorkflowDefinitionId = activeWfId`) + `:109` (`ApprovalWorkflowId = request.ApprovalWorkflowId`, comment ghi rõ "[Plan B S29 2026-05-22 Chunk E1] Pin V2 workflow nếu Drafter pick"). Cổng rẽ nhánh: `ContractWorkflowService.cs:98` `if (contract.ApprovalWorkflowId is Guid awId)``ApproveV2Async`, else rơi V1 legacy `:108-149`.
Hệ quả ĐO ĐƯỢC của việc rơi legacy (không phải suy đoán): (a) `skipToFinal` ném cứng `ConflictException``ContractWorkflowService.cs:104-106`; (b) mất Section 5 ý-kiến-cấp-duyệt V2 — `ContractDtos.cs:51-53` ghi "nếu null → V1 legacy KHÔNG Section 5 V2"; (c) mọi cờ per-slot V2 (F2/F5/F6) vô hiệu vì chỉ đọc trong `ApproveV2Async`.
Hạ-nhiệt trung thực (chống thổi phồng): HĐ sinh từ phiếu vẫn DUYỆT ĐƯỢC vì V1 `WorkflowDefinition` có seed thật (`DbInitializer.cs:721,754`) ⇒ `activeWfId` thường non-null. Đây là "rơi nhánh cũ", KHÔNG phải "hỏng". Rủi ro có điều kiện: nếu 1 ContractType không còn V1 def IsActive thì `activeWfId=null` → tới lượt duyệt mới ném `ConflictException` "HĐ chưa pin workflow definition" (`ContractWorkflowService.cs:115-116`) — tức lỗi hiện MUỘN ở bước duyệt chứ không chặn lúc tạo.
**L2.** Vòng lặp "đặt được → lưu → hiện lại → không ai đọc" khép kín, chứng từng mắt xích:
- Đặt được: input "Ngưỡng giá trị gói CEO (đ)" render VÔ ĐIỀU KIỆN `ApprovalWorkflowsV2Page.tsx:907-912` — không có bọc `applicableType === 1|2` (mọi hit `applicableType` trong file là routing/label/payload: `:262,268,417,733,761`).
- Nhận: validator chỉ chặn `< 0``ApprovalWorkflowV2AdminFeatures.cs:277-278`; KHÔNG gate theo `ApplicableType`.
- Lưu: `ApprovalWorkflowV2AdminFeatures.cs:352` passthrough.
- Hiện lại: badge "Ngưỡng CEO: … đ" `ApprovalWorkflowsV2Page.tsx:474-479` ⇒ admin nhìn thấy xác nhận một luật KHÔNG tồn tại.
- Không đọc: `ContractWorkflowService.ApproveV2Async:228-232` nạp NGUYÊN entity `aw` (Include Steps→Levels) nhưng không hề chạm `CeoApprovalThreshold`; quyết định kết-thúc `:366-385` thuần vị-trí (`nextIdx >= steps.Count`).
**Leo thang ngoài phát biểu của invest:** cờ F5 `AllowApproverFinalize` ("Duyệt là kết thúc, không trình CEO") CŨNG vắng trong `ContractWorkflowService.cs` — grep 0 hit, trong khi `AllowApproverSkipToFinal` có 3 hit cùng file (`:210,319,322`) ⇒ chứng grep có răng. Vậy phía HĐ thiếu CẢ HAI cơ chế quyết-định-theo-giá-trị mà PE đã có. Lỗ rộng hơn 1 cột.
**L3.** Schema: `ApprovalWorkflowStep` (`ApprovalWorkflow.cs:70-79`) đúng 4 prop `ApprovalWorkflowId / Order / Name / DepartmentId` — không SLA; `ApprovalWorkflowLevel:95-171` cũng không. Hardcode trong `ApproveV2Async`: `:341` (skipToFinal), `:358` (sang Cấp kế), `:390` (sang Bước kế) — cả 3 đều `AddDays(7)`.
Hai đính chính so với phát biểu của invest:
1. **Đây là THỤT LÙI so với V1, không phải "chưa làm"**: V1 đọc SLA theo cấu hình (`WorkflowPolicy.cs:199` `sla[s.Phase] = s.SlaDays is int d ? …`, nguồn `WorkflowDefinition.cs:37`), và chính lúc tạo HĐ vẫn xài `workflow.GetPhaseSla(...)` (`ContractFeatures.cs:110`).
2. **Không phải lỗ riêng của HĐ — là lỗ toàn schema V2**: PE V2 hardcode y hệt (`PurchaseEvaluationWorkflowService.cs:268,534,838,935,960,1163`).
SLA có răng thật (không phải số trang trí): `SlaExpiryJob.cs:77` lọc `SlaDeadline < now` bắn thông báo quá hạn, `:140-153` cảnh báo sắp hết hạn. Nên hệ quả = đồng hồ quá-hạn chạy sai nhịp cho MỌI bước V2, chứ không chặn luồng.
**L4.** Enum đúng verbatim: `DraftExport=1 / ScannedSigned=2 / SealedCopy=3 / Other=99` (`ContractAttachment.cs:5-11`). Nhưng phát biểu bị đóng khung SAI TẦNG: `Purpose` hiện là **nhãn không ai thi hành** — validator chỉ `IsInEnum()` (`ContractAttachmentFeatures.cs:51`), lưu (`:80`), trả ra DTO (`:89`), controller mặc định `Other` (`ContractsController.cs:78`); không có nhánh `Purpose ==` nào trong Backend, cũng không có ràng buộc "ScannedSigned chỉ ở phase DangInKy". PE còn chép nguyên nhận định này thành comment: `PurchaseEvaluationWorkflowService.cs:190` "FE KHÔNG check Purpose enum → BE cũng không".
⇒ Thêm 2 giá trị enum ký-nháy/ký-chính sẽ KHÔNG tạo ra năng lực nào, vì không có mã nào đọc. Lỗ thật nằm chỗ khác và lớn hơn: đính-kèm không có cổng thi-hành theo mục-đích. Đồng thời có lối thoát rẻ sẵn có (`Note``ContractAttachment.cs:21`, và `Other=99`).
## 3. Kết luận
- **2 lỗ đáng làm:** L1 (cầu PE→HĐ bỏ pin V2) và L2 (ngưỡng CEO là ghost-wire phía HĐ, kèm F5 cũng vắng).
- **1 lỗ thật nhưng phải đổi khung:** L3 — là thụt-lùi-so-V1 và là lỗ toàn-V2 (cả PE), không riêng HĐ; hệ quả ở tầng thông-báo.
- **1 lỗ đúng-chữ nhưng sai-tầng:** L4 — sửa enum là vô nghĩa nếu không dựng cổng đọc `Purpose`.
- **0 DƯƠNG-GIẢ.** Invest không bịa lỗ nào; điểm yếu của invest là ĐÓNG KHUNG (L3 hẹp thành lỗi HĐ, L4 quy về schema thay vì thi-hành), không phải bịa dữ kiện.
END · TOTAL=4 CLAIM

View File

@ -0,0 +1,110 @@
# 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

View File

@ -1,35 +1,126 @@
# sub-review-verdict-1 — reviewer ADVERSARIAL tấn công verdict `LAI`
> Ghi TRƯỚC khi đọc (khung rỗng lượt 1). Append dần theo từng phép đo.
> Mục tiêu: công verdict §1 bản invest — "b.13→21 TÁI DÙNG Contract V2 · b.7→12 DỰNG MỚI".
> Ranh: read-only. Không sửa code-tree, không commit.
> Khung ghi trước khi đọc (lượt 1), nội dung append theo từng phép đo.
> Đối tượng công: `sub-invest-bch-1.md` §1*"b.13→21 TÁI DÙNG Contract V2 (không dựng mới, không hồi sinh LEGACY enum) · b.7→12 DỰNG MỚI"*.
> Ranh: read-only. Không sửa code-tree, không commit. Mọi số liệu đo lại từ đĩa.
## 0. Meta
## 0. Meta + phạm vi đo
- Run: 2026-07-27-S156-bch-post-ceo-flow
- Target đọc (tối đa 3): ContractPhase.cs · ContractWorkflowService.cs · CreateContractFromEvaluationFeatures.cs
- LENS-VERDICT: _(chốt ở §6)_
- Run: `2026-07-27-S156-bch-post-ceo-flow`
- **3 file Read đầy đủ** (đúng trần): `ContractPhase.cs` (29 dòng) · `ContractWorkflowService.cs` (443 dòng, đường thật `src/Backend/SolutionErp.Infrastructure/Services/` — KHÔNG phải `Application/Contracts/` như spec ghi) · `CreateContractFromEvaluationFeatures.cs` (203 dòng).
- **4 lệnh grep xác minh** (không phải Read — dùng để chấm claim của invest trỏ ra ngoài 3 file, đúng mandate "đo, đừng tin"): fallback-policy · `WorkflowPolicyRegistry` consumers · `ChoDuyet` trong `ContractFeatures.cs` · `Authorize` trong `ContractsController.cs`.
- **LENS-VERDICT: LUNG-LAY — 11 điểm** (chi tiết §5-§6). Lõi verdict SỐNG, nhãn "TÁI DÙNG" SAI CHI PHÍ, 1 sub-claim SAI SỰ THẬT trên đĩa.
## 1. Q1 — "phase bị gỡ = trạm của b.13→21" có đứng không?
---
_(chờ đo)_
## 1. Q1 — "phase bị gỡ = trạm của b.13→21": ĐỨNG, và đứng vững hơn cả cách invest chứng
## 2. Q2 — `ApproveV2Async` gánh được chuỗi CCM→CEO→HR?
**Kết luận: KHÔNG phải trùng hợp tên gọi. Đây là tương ứng CẤU TRÚC — chứng bằng ACTOR, không bằng tên.**
_(chờ đo)_
Invest chỉ trích `ContractPhase.cs:3-13` (comment). Chứng mạnh hơn nằm ở bảng vai còn sống trong `ContractFeatures.cs`, invest KHÔNG dùng:
## 3. Q3 — bước 15-16 (đàm phán + ký nháy ngoài hệ thống) map vào attachment — mất vết gì?
| Legacy phase (đã gỡ) | Vai được map trên đĩa | Trạm sơ đồ | Khớp? |
|---|---|---|---|
| `DangGopY=3` | `ContractFeatures.cs:366` ProjectManager+Procurement+CostControl+Finance+Accounting+Equipment | b.14 BCH bổ sung/góp ý | ✅ |
| `DangDamPhan=4` | `:367` Drafter, DeptManager, ProjectManager | b.15 đàm phán TP | ✅ |
| `DangInKy=5` | `:368` Drafter, DeptManager, ProjectManager | b.16 ký nháy / in ký | ✅ |
| `DangKiemTraCCM=6` | `:369` **CostControl** | b.17 CCM kiểm tra | ✅ |
| `DangTrinhKy=7` | `:370` **Director, AuthorizedSigner** | b.18 CEO ký | ✅ |
| `DangDongDau=8` | `:371` **HrAdmin** | b.19 HR đóng dấu | ✅ |
| `DangChon=1` | (không map trong `PhaseActorRoles`) | TIỀN-13 (chọn NCC = PE) | ❌ ngoài dải |
_(chờ đo)_
Chuỗi vai `CostControl → Director/AuthorizedSigner → HrAdmin` (`:369-371`) **trùng khít** chuỗi CCM→CEO→HR của b.17→18→19. Xác suất trùng ngẫu nhiên 3 vai liên tiếp đúng thứ tự ≈ 0 ⇒ hệ thống ĐÃ TỪNG mô hình hoá đúng dải này rồi gỡ. Lập luận invest **ĐỨNG**.
**3 khiếm khuyết về độ chính xác (không lật kết luận):**
1. **Trích dẫn nống (điểm 2).** Invest §0:18-21 viết *"`ContractPhase.cs:3-13` ghi thẳng"* rồi gán cho dải đó cả `CurrentApprovalLevelOrder` + *"workflow V2 admin-config (`ApprovalWorkflows` Steps=Phòng > Levels=Cấp OR-of-N)"*. Đọc thật `ContractPhase.cs:3-13`: chỉ có state-machine 5 trạng thái + câu *"LEGACY values (...) deprecated post-Mig 21 — giữ enum cho data cũ"*. **Không một chữ** nào về V2 / ApprovalWorkflows / Cấp / OR-of-N; `CurrentWorkflowStepIndex` nằm ở `:25` (ngoài dải 3-13), `CurrentApprovalLevelOrder` nằm ở file KHÁC (`ContractWorkflowService.cs:84`), mô tả Steps/Levels ở `:204-216`. File chỉ nói **ĐÃ GỠ**, không nói **GỠ ĐỂ THAY BẰNG GÌ** — phần "vì sao" là suy luận của invest, không phải trích dẫn.
2. **Lệch biên dải (điểm 3).** Mệnh đề *"các trạm của bước 13→21 chính là những phase đã bị gỡ"* chỉ đúng cho **b.14→19**. b.13 chạy trên `DangSoanThao=2` (CÒN SỐNG, `ContractPhase.cs:17`) và b.20-21 chạy trên `DaPhatHanh=9` (CÒN SỐNG, `:24`). Chính §3 của invest map đúng (khúc 2 và khúc 4) — tức §0 phát biểu rộng hơn §3 tự đo. Mâu thuẫn nội bộ nhẹ.
3. **"Đi ngược kiến trúc" = quy ước, KHÔNG phải rào chắn (điểm bổ sung tại §4-5).** Không có gì trong mã CẤM gán lại legacy: `ContractWorkflowService.cs:191-199` nhánh admin override set `contract.Phase = targetPhase` **bất kỳ**, không lọc legacy. Hồi sinh enum là *rẻ mà dở*, không phải *bất khả*. Nếu owner hỏi "sao không dùng lại?", câu trả lời phải là chi phí vận hành (2 mô hình song song), không phải "hệ thống chặn".
---
## 2. Q2 — `ApproveV2Async` gánh được CCM→CEO→HR không?
**Trả lời: gánh được KHUNG (thứ tự trạm), KHÔNG gánh được ĐƯỜNG SỐNG (nhìn thấy / được gọi / rẽ nhánh theo tiền).**
### 2.1 Cái CÓ (invest đúng)
- 3 trạm = 3 Step: `:234` `steps = aw.Steps.OrderBy(Order)`, `:246` group Levels theo Order = Cấp, `:259-260` OR-of-N match `ApproverUserId`.
- Tiến trạm: `:355-361` (cấp kế trong Bước) → `:365-392` (Bước kế, Cấp 1) → `:366-385` terminal `DaPhatHanh` + gen mã.
- Gen mã không đúp cho HĐ sinh từ phiếu: `:371` `if (string.IsNullOrEmpty(contract.MaHopDong))` — HĐ từ phiếu đã có mã tại `CreateContractFromEvaluationFeatures.cs:113` ⇒ giữ nguyên. ✅
- UPSERT ý kiến cấp duyệt `:292-316` + `SignedByUserId` `:306`/`:314`. ✅
### 2.2 Cái THIẾU — 5 cơ chế, trong đó 3 cái invest KHÔNG nêu
| # | Thiếu gì | Bằng chứng | Hệ quả cho b.17→19 | Invest có nêu? |
|---|---|---|---|---|
| a | Rẽ nhánh CEO theo GiaTri | `grep CeoApprovalThreshold\|AllowApproverFinalize` trên `ContractWorkflowService.cs` = **0 hit** | b.18 ">5 tỷ mới CEO ký" không tự động được | ✅ có (§2 + Q5) |
| b | Tín hiệu bàn giao trạm kế | `:407` `if (contract.DrafterUserId is Guid drafterId && drafterId != actorUserId)` — notify **chỉ Drafter** | CCM duyệt xong, CEO **không được báo** | ✅ nêu §2-bis, ❌ không hạ cấp verdict |
| c | Người duyệt XEM được phiếu | `grep ChoDuyet src/.../ContractFeatures.cs` = **0 hit**; view-guard `:343-349` map 100% legacy | CCM/CEO/HR mở HĐ đang chờ chính mình → chặn | ✅ nêu §2-bis, ❌ không hạ cấp verdict |
| d | **PMH trình được HĐ** | `:70-79` — trình `DangSoanThao/TraLai → ChoDuyet` đòi role `Drafter` HOẶC `DeptManager`; role `Procurement` **không nằm trong đó**`ForbiddenException :77` | b.13/b.17 "PMH trình ký" **403** nếu PMH chỉ mang role Procurement | ❌ **BỎ SÓT** |
| e | **Guard cho Trả lại / Từ chối** | `:49-66` nhánh Reject nằm **TRƯỚC MỌI** kiểm tra: không check role, không check approver-hiện-tại, không check `fromPhase`. Handler chỉ chặn chưa-đăng-nhập (`ContractFeatures.cs:224` `UnauthorizedException`), controller chỉ `[Authorize]` trần (`ContractsController.cs:13`, 0 per-action policy) | **Mọi user đã đăng nhập** POST reject → set HĐ **bất kỳ**, **phase bất kỳ kể cả `DaPhatHanh` terminal**`TuChoi`/`TraLai` + `SlaDeadline=null` | ⚠️ invest chỉ ghi "không guard theo cấp" — **nhẹ hơn sự thật nhiều** |
⇒ Trả lời độc ác cho Q2: chuỗi CCM→CEO→HR **chạy được trên bàn thí nghiệm** (admin bấm, hoặc user được pin đúng Level và tự mò vào URL), **không chạy được trong vận hành** — người duyệt không thấy phiếu (c), không được báo (b), người trình có thể bị chặn (d), và bất kỳ ai cũng có thể giết phiếu (e). Nhãn "TÁI DÙNG — không dựng mới" do đó **sai về chi phí**: tối thiểu 4 hạng mục phải wire mới (visibility, inbox, notify-approver-kế, guard reject) trước khi 17→19 dùng được, chưa kể (a).
---
## 3. Q3 — b.15-16 map vào attachment: mất 3 vết, invest chỉ bắt 1
Invest bắt: `AttachmentPurpose` không phân biệt ký-nháy vs ký-chính (Q12). **Đúng nhưng là cái nhẹ nhất.** Hai cái nặng hơn bị bỏ:
1. **Mất TRẠNG THÁI máy-đọc-được.** Upload attachment KHÔNG đổi `Phase`; suốt b.13→16 HĐ đứng nguyên `DangSoanThao`. Hệ thống không phân biệt được "chưa soạn" / "đang đàm phán" / "chờ TP ký nháy" / "đã ký nháy, chờ trình". Hệ quả dây chuyền: **Q11 (gate lane ⑥ "duyệt mẫu xong mới ký HĐ") mất luôn đối tượng để chấm** — muốn chặn cứng thì phải có cờ trạng thái, mà mô hình attachment không sinh ra cờ nào. Enum cũ (`DangDamPhan`/`DangInKy`) từng cho đúng thứ đó.
2. **Mất ĐỒNG HỒ SLA đúng chỗ sơ đồ đặt.** `ContractWorkflowService.cs:31-32` `GetPhaseSla` trả 7 ngày **CHỈ** cho `ChoDuyet`, `null` cho mọi phase khác. Khúc b.13→16 nằm ngoài `ChoDuyet` ⇒ chỉ có 1 deadline tĩnh cấp lúc tạo (`CreateContractFromEvaluationFeatures.cs:109-110`, fallback 7 ngày), và **`:62` Trả lại set `SlaDeadline = null`** ⇒ sau 1 lần trả lại thì HĐ hết đồng hồ vĩnh viễn. Mà chính khúc này sơ đồ ghi "BCH 3 ngày" + "13→18 hết 7-10 ngày". Invest Q10 chỉ hỏi SLA per-step **trong V2** — bỏ đúng khúc mà sơ đồ ghi số.
3. **Mất định danh chữ ký** (invest đã bắt — Q12): ai ký nháy, ngày nào, bản nào — chỉ còn tên file + comment tự do.
⇒ Mapping "trạm offline → attachment" **hợp triết lý Mig 21** (đúng như invest nói) nhưng **không miễn phí**: đổi 3 thứ đo được lấy 1 thứ linh hoạt. Verdict nên khai giá này, không nên trình bày như tương đương.
---
## 4. Q4 — nhánh invest BỎ QUA
_(chờ đo)_
**4.1 🔴 Sub-claim SAI SỰ THẬT (nặng nhất).** Invest §1:51-55 (Điểm LAI thứ 3): *"HĐ sinh từ phiếu sẽ rơi nhánh V1 legacy (**hardcoded policy fallback nếu V1 không có active def**)"*.
Đo trên đĩa: `grep "WorkflowPolicyRegistry|WorkflowTypeAssignment"` trên `ContractWorkflowService.cs` = **0 hit**. Đường duyệt thật: `:98` không có `ApprovalWorkflowId` → V1 → `:108-113` nạp `WorkflowDefinitions`**`:115-116` `if (def == null || def.Steps.Count == 0) throw new ConflictException("HĐ chưa pin workflow definition hoặc workflow không có step.")`**.
**KHÔNG có fallback nào.** Nếu `activeWfId` null tại `CreateContractFromEvaluationFeatures.cs:68-71` (không có V1 def active cho `ContractType` đó) thì HĐ sinh ra **trình được nhưng KHÔNG AI DUYỆT ĐƯỢC** — 409 ngay lần approve đầu, HĐ kẹt cứng ở `ChoDuyet`. Đây là hỏng **cứng**, không phải "degrade êm" như invest mô tả. (`WorkflowPolicyRegistry` chỉ còn sống ở `ContractFeatures.cs:448-455``DbInitializer.cs:720` — xem 4.4.)
**4.2 Bỏ sót đường bỏ-qua-CEO ĐÃ WIRE.** Q5 của invest liệt 3 phương án cho ngưỡng b.18 và khẳng định *"cơ chế ngưỡng phía HĐ CHƯA TỒN TẠI"*. Đúng cho **tự động theo tiền**, nhưng thiếu phương án (d) **0 dòng code BE**: `ContractWorkflowService.cs:318-352` `skipToFinal` + `matchingLevel.AllowApproverSkipToFinal` (`:322`) cho phép CCM ở Bước 1 **nhảy thẳng tới Bước cuối/Cấp cuối** (`:339-340`) — tức bỏ qua Step CEO, HR vẫn duyệt thật để về `DaPhatHanh`. Cơ chế này Contract V2 ĐÃ CÓ (khác `AllowApproverFinalize` vốn PE-only). Nhược: thủ công (người bấm), không tự chấm theo 5 tỷ. Phải đưa vào bảng phương án để owner cân.
**4.3 Bỏ sót gate vai người TRÌNH** — xem §2.2 mục (d), `:70-79`.
**4.4 Bỏ sót lệch DISPLAY ⟂ GUARD.** `ContractFeatures.cs:448-449` vẫn dựng policy hiển thị bằng `WorkflowPolicyRegistry.FromDefinition(def)` / `.ForContract(c)``:455` `.ForContractWithOverrides(...)` — tức phần trả về FE (`workflow.nextPhases`) sinh từ **mô hình legacy 9-phase**, trong khi `TransitionAsync` chỉ chấp nhận mô hình phẳng V1-step/V2-level. Đây là lớp lỗi "FE bày nút mà BE không nhận" (hoặc ngược lại) — cần đo trước khi hứa "tái dùng UI sẵn có" cho b.17→19.
**4.5 Bỏ sót hệ quả multi-winner ở khúc duyệt.** `CreateContractFromEvaluationFeatures.cs:145` `pe.ContractId = contracts[0].Id` + guard `:59-60` `if (pe.ContractId is not null) throw` ⇒ (i) N HĐ liên-danh phải chạy **N chuỗi duyệt độc lập** (CEO ký N lần cho 1 gói thầu) — nhân đôi tải cho b.17→19 mà §3 bảng không ghi; (ii) cầu **không tái chạy được**: thêm winner sau khi đã tạo HĐ là bất khả qua API này. Q4 của invest bắt đúng phần ngưỡng per-HĐ vs per-gói, nhưng bỏ phần tải vận hành + khoá idempotent.
**4.6 (ủng hộ invest — đo lại thấy ĐÚNG).** 2 claim nặng nhất của invest đứng vững: `CeoApprovalThreshold` 0-hit phía HĐ ✅; `ChoDuyet` 0-hit trong `ContractFeatures.cs` ✅ (⇒ view-guard `:343-349` + inbox `:365-371` legacy-only là THẬT). Cầu pin V1 `:108` `WorkflowDefinitionId = activeWfId`, 0 tham số V2 ✅.
---
## 5. Bảng điểm công
_(chờ đo)_
| # | Điểm | Mức | Ảnh hưởng verdict |
|---|---|---|---|
| 1 | "hardcoded policy fallback" SAI — thật là `ConflictException` `:115-116`, HĐ kẹt cứng | 🔴 HIGH | nâng nặng "Điểm LAI thứ 3" từ *nên sửa**chặn* |
| 2 | Trích dẫn nống `ContractPhase.cs:3-13` (V2/Cấp không nằm trong dải) | 🟡 MED | không lật, hạ độ tin cite |
| 3 | Lệch biên: chỉ b.14→19 mới là phase-đã-gỡ (13, 20-21 dùng phase sống) | 🟢 LOW | không lật |
| 4 | Q1 dưới-chứng: bỏ bảng vai `:347-349`/`:369-371` = chứng mạnh nhất | 🟡 MED | củng cố verdict (invest có lợi) |
| 5 | Notify chỉ Drafter `:407` — đo đúng nhưng không hạ cấp nhãn "tái dùng" | 🟡 MED | sai chi phí |
| 6 | **Bỏ sót**: gate role trình `:70-79` chặn PMH thuần Procurement | 🔴 HIGH | thêm việc bắt buộc cho b.13/17 |
| 7 | **Bỏ sót**: Reject `:49-66` 0-guard + `[Authorize]` trần ⇒ mọi user giết HĐ mọi phase (kể cả `DaPhatHanh`) | 🔴 HIGH | lỗ bảo mật, phải vá trước khi mở luồng |
| 8 | **Bỏ sót**: mất đồng hồ SLA khúc 15-16 (`:31-32` + `:62`) | 🟡 MED | Q10 hỏi thiếu chỗ |
| 9 | **Bỏ sót**: mất trạng thái máy-đọc-được ⇒ Q11 gate cứng vô đối tượng | 🟡 MED | ràng buộc thiết kế |
| 10 | **Bỏ sót**: `AllowApproverSkipToFinal` `:318-352` = phương án (d) 0-code cho b.18 | 🟡 MED | thiếu lựa chọn cho owner |
| 11 | **Bỏ sót**: display dựng từ `WorkflowPolicyRegistry` (`ContractFeatures.cs:448-455`) ⟂ guard phẳng | 🟡 MED | rủi ro FE khi tái dùng |
3 HIGH · 7 MED · 1 LOW = **11 điểm**.
---
## 6. LENS-VERDICT
_(chờ đo)_
**LUNG-LAY (không BÁC).**
- **Giữ:** thế chia đôi `b.7→12 dựng mới ⟂ b.13→21 tái dùng Contract` **sống**. Q1 đứng, và đứng chắc hơn cách invest chứng (bảng vai `ContractFeatures.cs:347-349`, `:369-371` khớp CCM→CEO→HR).
- **Lung lay:** cụm chữ **"TÁI DÙNG (không dựng mới)"** sai về chi phí. Đúng hơn: *"tái dùng KHUNG duyệt V2, nhưng phải WIRE MỚI 4 đường sống (visibility `ChoDuyet` · inbox · notify approver kế · guard reject) + 1 rẽ nhánh tiền (b.18) trước khi 17→19 vận hành được"*. Nếu owner đọc "tái dùng" thành "gần như không tốn công", ước lượng sẽ hụt.
- **Phải sửa trong bản invest trước khi trình owner:** (i) bỏ chữ "hardcoded policy fallback" — thay bằng `ConflictException :115-116` + nâng "Điểm LAI thứ 3" lên mức CHẶN; (ii) thêm Q về vai người TRÌNH (`:70-79`); (iii) thêm cảnh báo bảo mật Reject `:49-66`; (iv) bổ sung phương án (d) `AllowApproverSkipToFinal` vào Q5; (v) siết lại câu §0 cho khớp §3 (b.14→19, không phải 13→21).
- **Không đề nghị sửa mã** (ranh vai): trên đây là mô tả lỗi + tiêu chí nghiệm thu, thực thi thuộc em main / implementer.
END · TOTAL=11 DIEM