diff --git a/.claude/WAL.md b/.claude/WAL.md index 80b0672..cc2abca 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -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 diff --git a/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/review-synthesis.md b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/review-synthesis.md new file mode 100644 index 0000000..01a45f5 --- /dev/null +++ b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/review-synthesis.md @@ -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Í** và **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` có **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** dù **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` và +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 + + diff --git a/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-fidelity-2.md b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-fidelity-2.md new file mode 100644 index 0000000..ff31487 --- /dev/null +++ b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-fidelity-2.md @@ -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. diff --git a/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-gaps-4.md b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-gaps-4.md new file mode 100644 index 0000000..9e84fa5 --- /dev/null +++ b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-gaps-4.md @@ -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 diff --git a/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-q2-3.md b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-q2-3.md new file mode 100644 index 0000000..9c53c22 --- /dev/null +++ b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-q2-3.md @@ -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()` · `: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 diff --git a/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-verdict-1.md b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-verdict-1.md index f2987e8..b468bf2 100644 --- a/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-verdict-1.md +++ b/.claude/workflows/runs/2026-07-27-S156-bch-post-ceo-flow/sub-review-verdict-1.md @@ -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` và `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)` và `: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