# sub-reviewer-3 — chấm ②.4 bản đồ pre-fill + 2 lỗ nghiêm trọng + wave W3 (YC-032)

- **run-id:** `2026-08-13-S194-yc032-hd-thauphu-2form` · **vai:** `reviewer` (adversarial, READ-only)
- **vật chấm:** `spec-hd-thauphu-2form-13-08-2026.md` §②.4 + §③ W3 · nguồn thô `sub-investigator-codebase-3.md`
- **VERDICT:** `R3-PREFILL: PASS-WITH-FINDINGS — 9 finding (4 nặng)`

## 0. Khung

Mọi số dưới đây từ lệnh chạy trong lượt này. Công cụ: `grep` (repo), `sed -n` (đọc neo), `sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev` (đo dữ liệu THẬT). 0-hit đều kèm control-dương.

3 lớp tôi tách ra khi chấm (spec trộn cả 3 vào một câu ⇒ đây là gốc của 3/4 finding nặng):
1. **Lớp MÃ** — code có gán field đó không.
2. **Lớp DỮ LIỆU** — cột đó trên Dev có giá trị không.
3. **Lớp NGƯỜI ĐỌC** — có ai render nó ra không.

Spec §②.4 chấm rất tốt lớp 1, **không chạm lớp 2 và 3** — mà YC-032 là việc IN RA GIẤY nên lớp 2+3 mới là thứ owner nhìn thấy.

---

## 1. Verify 24 / 17 / 3

### 1a. Con số **3** — HELD (tự đo, không chép)

Đọc trọn `CreateContractFromSigningPlanFeatures.cs`. Initializer `new Contract` (`:215-238`) + codegen (`:243`) gán **15 cột**:
`Type:217 · Phase:218 · SupplierId:219 · ProjectId:220 · DepartmentId:221 · DrafterUserId:222 · GiaTri:223 · TenHopDong:224 · NoiDung:225 · BudgetManualName:228 · BudgetManualAmount:229 · WorkflowDefinitionId:230 · ApprovalWorkflowId:233 · SlaDeadline:236 · MaHopDong:243`.

Giao với 24 ô của bảng §C lane-3 ⇒ đúng **3 ô**: `TenHopDong`→C2 (`:224`) · `GiaTri`→D1 (`:223`) · `MaHopDong`→F1 (`:243`). **Claim ĐỨNG.**

### 1b. 🔴 Nhưng "3" chỉ đúng dưới MỘT định nghĩa — spec không khai định nghĩa đó [FINDING-1, MAJOR]

"Đã chảy" ở đây = *có một cột vô hướng trên hàng `Contracts` giữ đúng chuỗi in ra giấy*. Dưới định nghĩa **"lấy được lúc render mà không cần nối đường dữ liệu mới"** thì con số khác hẳn: `SupplierId:219` · `ProjectId:220` đã nằm sẵn trên hàng ⇒ **8 ô (B1-B6 + C3 + C4) chỉ cách 1 phép JOIN**, không phải "đường chưa nối".

Hệ quả không nhỏ: W3 hiện được mô tả như việc **nối ống dữ liệu**, trong khi phần lớn là việc **chọn kiểu đọc + viết DTO/render**. Và chỗ đó ẩn một quyết định kiến trúc mà spec chưa hề nêu: **ảnh chụp (snapshot vào `DraftData`) hay đọc sống (join runtime)?** HĐ đã ký mà địa chỉ NCC tự đổi theo master về sau là sai về pháp lý; ngược lại ảnh chụp thì phép so "byte-exact với nguồn" của W3 chỉ đúng tại thời điểm t0. **Spec phải chốt trục này trước khi W3 chạy.**

### 1c. Cận trên của "24" — 24 là số LOẠI ô, không phải số ô trên giấy [FINDING-2, MINOR]

Spec §④.6 tự khai "24 là cận dưới" — đúng nhưng chưa nói cận trên lớn cỡ nào. Ước lượng có cấu trúc:

| nguồn phình | công thức | ghi chú |
|---|---|---|
| D4 bảng giá | `7 × N − 1` | 1 dòng bảng = 7 ô; §C đếm cả bảng là **1** ô |
| D5 tổng | `+2` | TỔNG CỘNG · VAT · TỔNG SAU THUẾ đếm là 1 |
| ô lặp (spec W2 tự liệt) | `+6` | Số ×2 · Hạng mục ×2 · Địa điểm ×2 · tên Bên B ×3 · Dự án ×2 |

⇒ **ô vật lý ≈ 31 + 7N**. Với N=10 hạng mục là **~101 ô**; N=20 là **~171 ô**. Chênh 4-7 lần so với "24" — đủ để đổi ước lượng công của W2/W3. Đề nghị spec ghi thẳng "24 = số LOẠI ô; số ô trên giấy ≈ 31 + 7N".

### 1d. "6/7 cột 1-1" đếm hụt 1 [FINDING-3, MINOR — sai theo chiều AN TOÀN]

Lane-3 (và spec §②.4 chép lại) nói `ThauPhuDetail` khớp **6/7 cột**. Đo thật: `ThauPhuDetail.cs` có `HangMuc·DonViTinh·KhoiLuong·DonGia·ThoiGianHoanThanh`, còn `ThanhTien` + `GhiChu` + **`Order`** nằm ở lớp cha `ContractDetailBase.cs:14-16`. Cột `STT` của .doc ↔ `Order` ⇒ **7/7, không phải 6/7**.

---

## 2. Regression cầu KHKK — claim ĐỨNG ở lớp MÃ, **SAI Ở MỨC ĐỘ** [FINDING-4, MAJOR]

### 2a. Mã: HELD, mở cả hai file so trực tiếp

| field | cầu PE cũ | cầu KHKK mới |
|---|---|---|
| `DraftData = pe.PaymentTerms` | **CÓ** — `CreateContractFromEvaluationFeatures.cs:104` (`// carry forward payment terms`) | **KHÔNG** — vắng trong `:215-238` |
| `BypassProcurementAndCCM` | **CÓ** — `:103` (từ `request`, mặc định `false` `:27`) | **KHÔNG** — vắng |

### 2b. 🔴 "Bug ĐANG SỐNG trên prod?" — **KHÔNG.** Đo 3 tầng, cả 3 đều bác

**(i) Dữ liệu:** `SELECT COUNT(*) ... FROM Contracts` → **9 HĐ, `DraftData IS NOT NULL` = 0, `BypassProcurementAndCCM=1` = 0.**
⇒ Câu của spec *"HĐ đi đường KHKK mất sạch điều khoản thanh toán mà HĐ đi đường PE vẫn có"* **không đúng với dữ liệu**: đường PE cũng **0/9**. Không có HĐ nào đang "vẫn có" cả.

**(ii) Người ghi đè:** `draftData: null` hard-code **4/4 site** — `fe-admin/src/pages/contracts/ContractCreatePage.tsx:366,:582` + `fe-user/...:366,:582` (spec §②.1 bẫy 3 — ĐỨNG, tôi đo lại đủ 4). Nghĩa là kể cả HĐ đi đường PE có mang `PaymentTerms`, **lần bấm Lưu đầu tiên là xoá trắng**. Chênh lệch giữa 2 cầu bị bịt ngay ở nhịp sau.

**(iii) Người đọc:** grep toàn repo `DraftData|draftData` = 87 hit, bỏ Migrations còn **10 hit sống**: 3 write BE (`ContractFeatures.cs:105` create · `:207` update · `CreateContractFromEvaluationFeatures.cs:104`), 2 khai DTO (`ContractFeatures.cs:851` · `ContractDtos.cs:46`), 1 entity, 1 EF config, **4 FE = 2 khai type + 2×2 gán `null`**. **0 chỗ render.**

⇒ **Phát biểu lại cho đúng:** đây không phải "regression làm mất điều khoản thanh toán". Đây là **`DraftData` = cột CHỈ-GHI: 3 người ghi, 0 người đọc, 4 site FE chủ động xoá.** Và đó mới là điều đáng lo cho YC-032, vì **W2 định đem toàn bộ thân hợp đồng nhét vào đúng cột này** — xây nhà trên cột mà hành vi sống duy nhất của nó hôm nay là *bị xoá*.

### 2c. Vế `BypassProcurementAndCCM` gần như VÔ HẠI với HĐ sinh từ KHKK [FINDING-5, MINOR]

Grep người đọc cờ này: chỉ `WorkflowPolicy.cs:164` (`ForContract`) + `:172` (`ForContractWithOverrides`), và 2 hàm đó chỉ được gọi ở `ContractFeatures.cs:690-697` (dựng policy hiển thị). Máy chuyển phase thì `ContractWorkflowService.cs:165` **rẽ V2 TRƯỚC**: `if (contract.ApprovalWorkflowId is Guid awId) → ApproveV2Async → return`. Mà cầu KHKK **luôn pin V2** (không pin được thì ném 409, `:171-177`) ⇒ với mọi HĐ sinh từ KHKK, cờ bypass **không đụng vào đường duyệt**. Dev: `bypass_true = 0/9`.
⇒ Xếp 2 vế này ngang nhau trong một gạch đầu dòng "REGRESSION" là **thổi phồng vế nhẹ**. W3 nên vá `DraftData` (có lý do thật, xem 2b) và **bỏ** vế bypass, hoặc khai rõ nó chỉ để đối xứng mã.

### 2d. Số đo mà W3 đòi nhưng chưa ai chạy — tôi chạy rồi

`SELECT COUNT(*) FROM PurchaseEvaluations` → **4 phiếu, `PaymentTerms IS NOT NULL` = 1** (25%).
⇒ Vá regression sẽ mang được dữ liệu cho **1/4 phiếu**. Không phải 0 (nên vá vẫn có nghĩa), nhưng cũng đủ nhỏ để đừng đặt nó làm trục demo cho owner.

---

## 3. Chuỗi tiền + VAT — **HƯỚNG cảnh báo đúng, CHUỖI NHÂN QUẢ SAI** [FINDING-6, CRITICAL]

Đây là claim nặng nhất của spec (*"sai thì owner ra quyết định sai về tiền"*) nên tôi truy từng mắt xích.

### 3a. Truy ngược đúng như spec vẽ

`Contract.GiaTri` = `lines.Sum(l => l.ApprovedAmount!.Value)` (`CreateContractFromSigningPlanFeatures.cs:202`,`:223`)
→ `ApprovedAmount`: grep toàn `src/` (bỏ Migrations) ⇒ **write-site DUY NHẤT** `ContractSigningPlanWorkflowService.cs:358` `line.ApprovedAmount ??= line.ProposedAmount;`
→ `ProposedAmount`: **SỬA TAY ĐƯỢC** — `ContractSigningPlanFeatures.cs:751-754` (`line.ProposedAmount = amt`) + `:862` (tạo dòng theo request); mồi ban đầu `= peReference` (`:573`)
→ `PeReferenceAmount` = Σ `Quote.ThanhTien` WHERE `IsSelected` (`:552-554`)
→ `PurchaseEvaluationQuote.ThanhTien` — entity chú thích `// = KL × ChuaVat (tính sẵn)`.

Mắt xích cuối chính là chỗ spec dựa vào để kết luận "chưa VAT". **Mắt xích đó đã mục.**

### 3b. 🔴 `ChuaVat` là cột CHẾT — UI không nhập nó nữa

`fe-user/src/components/pe/PeDetailTabs.tsx:2959-2962` (nguyên văn):

```
// Session 20 turn 3: user yêu cầu "tạm thời chỉ cần nhập số tiền, không
// cần 3 cột có VAT / không VAT / tổng". UI chỉ 1 input thanhTien; bgVat /
// chuaVat / note vẫn gửi BE giữ schema (default 0 / empty cho row mới,
// giữ giá trị cũ nếu existing).
```

Payload xác nhận: `bgVat: existing?.bgVat ?? 0, chuaVat: existing?.chuaVat ?? 0, thanhTien: form.thanhTien` (`:2975-2978`). BE nhận nguyên si, không tính lại: `PurchaseEvaluationDetailFeatures.cs:310` (`existing.ThanhTien = request.ThanhTien`) + `:341`.

**Đo trên Dev:** `PurchaseEvaluationQuotes` = **34 dòng · `ChuaVat=0` = 12 · `BgVat=0` = 12**; trong 22 dòng còn lại, **22/22 thoả `ThanhTien = KhoiLuongNganSach × ChuaVat`** ⇒ đúng công thức seed `DbInitializer.cs:1565`.
⇒ Phân đôi sạch: **22 dòng seed giữ công thức · 12 dòng người nhập qua UI thì `ChuaVat=0`**.

**Kết luận đảo lại claim:** `Contract.GiaTri` **không phải "số chưa VAT"**. Nó là **số tiền người ta gõ tay, không có bất kỳ chỗ nào trong schema ghi số đó đã gồm VAT hay chưa**. Chú thích entity `// = KL × ChuaVat` là **chú thích thối** (đúng cho seed, sai cho mọi dòng UI từ Session 20).

🔴 **Vì sao đây là finding nặng nhất:** spec đang định trình owner một lựa chọn nhị phân *"số hiện tại chưa VAT — có nhân 1,1 lên không?"*. Owner trả lời "có" ⇒ **mọi báo giá vốn đã gõ gồm-VAT sẽ bị cộng VAT lần hai**. Câu hỏi đúng phải là: ***"số tiền NCC gõ vào phiếu Duyệt NCC là gồm VAT hay chưa — và hệ thống có cần ghi lại điều đó không?"*** — tức là câu hỏi về **quy ước nhập liệu + cột mới**, không phải câu hỏi về phép nhân.

### 3c. Phụ đề: "VAT 10%" của .doc cũng không có chỗ dựa

Cột `BgVat` ("Báo giá NCC gửi (đã VAT)") **đã là** đơn giá gồm VAT ⇒ nếu nó được nhập thì thuế suất suy ra được (`BgVat/ChuaVat − 1`) và **không nhất thiết là 10%**, có thể khác nhau từng dòng. Nhưng đo ra `BgVat=0` ở 12/34 ⇒ đường đó cũng không dùng được hôm nay. Dòng "VAT 10%" in cứng trên .doc vì thế là **giả định của tờ giấy**, chưa có dữ liệu nào đỡ.

### 3d. 🔴 LỖ TIỀN THỨ HAI — spec KHÔNG nêu, nặng hơn VAT vì không cần giả định gì [FINDING-7, CRITICAL]

W3 định lấy **bảng giá 7 cột từ PE** (Σ = `PeReferenceAmount`) trong khi **ô "Giá trị HĐ" lấy `Contract.GiaTri`** (= Σ `ApprovedAmount`). Mã tự khai 2 số này **cố ý khác nhau** (`CreateContractFromSigningPlanFeatures.cs:226-229`: *"Σ số PE ... KHÔNG phải số đã chốt"*).

Đo 2/2 dòng KHKK đã bridge trên Dev (không phải seed — grep `ContractSigningPlanLine` trong `DbInitializer.cs` = **0 hit**, đây là dữ liệu chạy thật):

| dòng | `PeReferenceAmount` | `ProposedAmount` | `ApprovedAmount` | `Contract.GiaTri` | `BudgetManualAmount` |
|---|---|---|---|---|---|
| 9B10… | 1.600.000.000 | 1.550.000.000 | 1.500.000.000 | **1.500.000.000** | 1.600.000.000 |
| 9B10… | 550.000.000 | 520.000.000 | 500.000.000 | **500.000.000** | 550.000.000 |

⇒ **2/2 dòng (100%) có `ApprovedAmount ≠ PeReferenceAmount`**, lệch **100 triệu (6,25%)** và **50 triệu (9,1%)**.

Nếu W3 làm đúng y như đang viết, hợp đồng in ra sẽ có **dòng TỔNG CỘNG của bảng = 1,6 tỷ** còn **ô Giá trị hợp đồng = 1,5 tỷ**, trên **cùng một tờ giấy**. Đây là hợp đồng tự mâu thuẫn về tiền, **không cần dính dáng gì tới VAT**, và đo được ở 100% dữ liệu hiện có. W3 buộc phải chốt: hoặc bảng scale về `ApprovedAmount`, hoặc thêm dòng "điều chỉnh sau thương thảo", hoặc bảng đọc số chốt chứ không đọc số PE.

### 3e. Bẫy liên đới: hai cột khối lượng [FINDING-8, MINOR — hôm nay còn ngủ]

`PurchaseEvaluationDetail` có **cả** `KhoiLuongNganSach` và `KhoiLuongThiCong`. Bản đồ lane-3 lấy `KhoiLuongThiCong` cho cột KHỐI LƯỢNG, còn tiền (`Quote.ThanhTien` seed) tính theo `KhoiLuongNganSach` ⇒ khi 2 số lệch thì **KL × ĐơnGiá ≠ ThànhTiền ngay trong một dòng bảng**. Đo Dev: `KhoiLuongThiCong <> KhoiLuongNganSach` = **0/12** ⇒ **đang ngủ, chưa cháy**. Vẫn phải chốt bằng chữ trong W3 chọn cột nào.

---

## 4. Đọc vượt tầng qua `plan.PurchaseEvaluationId` — HELD, không N+1, nhưng có 2 bẫy

### 4a. Đường CÓ mở thật

`CreateContractFromSigningPlanFeatures.cs:199-200`: `var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(e => e.Id == plan.PurchaseEvaluationId, ct);` — **TRACKED** (cố ý, để ghi `pe.ContractId ??=` ở `:277`). Claim W3 **ĐỨNG**.

`ContractSigningPlan.cs:24` `public Guid PurchaseEvaluationId` — **non-nullable**, loose-Guid không FK. Write-site duy nhất `ContractSigningPlanFeatures.cs:529` `= pe.Id`. Đo Dev: **1 phiếu KHKK, 0 phiếu trỏ tới PE không tồn tại, 0 PE soft-deleted** ⇒ đường vượt tầng hôm nay 1/1 nối được.

### 4b. Ranh tầng: KHÔNG phá — đã có tiền lệ trong CHÍNH file

File này vốn đã đọc PE (`pe.MoTa` `:225`, `pe.TenGoiThau` `:209`) và namespace `Application.ContractSigningPlans` đã `using SolutionErp.Domain.Contracts` (`:13`). Đọc thêm `pe.Details/.Quotes` **không mở ranh mới**, chỉ dùng sâu hơn đường đã mở. Cầu PE cũ cũng `Include(p => p.Details).ThenInclude(d => d.Quotes)` (`CreateContractFromEvaluationFeatures.cs:48`) ⇒ có khuôn sẵn.

### 4c. N+1: KHÔNG — nhưng chỉ khi viết bằng `Include`

Cầu chạy **1 lần cho 1 HĐ**, không lặp theo N. Rủi ro N+1 chỉ xuất hiện nếu implementer duyệt `pe.Details` rồi truy `d.Quotes` mà quên `Include`. Đo: `UseLazyLoadingProxies` = **0 hit** trong `src/` (control-dương `UseSqlServer` = 2 hit ⇒ lưới sống) ⇒ **không có lazy-loading**: quên `Include` sẽ ra **danh sách rỗng lặng lẽ**, không phải N+1 chậm. **Hỏng câm, không hỏng ồn** — nguy hơn.

### 4d. 🔴 BẪY KHOÁ NỐI — hai chữ "SupplierId" là hai thứ khác nhau [FINDING-9, MAJOR]

- `ContractSigningPlanLine.SupplierId` = **`Supplier.Id` master** (chứng: `:559 SupplierId = w.SupplierId`; và cầu dùng nó tra `db.Suppliers` `:185`).
- `PurchaseEvaluationQuote.PurchaseEvaluationSupplierId` = **`PurchaseEvaluationSuppliers.Id`** (hàng nối PE×NCC) — cầu PE cũ lọc đúng bằng `q.PurchaseEvaluationSupplierId == w.Id` với `w` là hàng PE-supplier (`CreateContractFromEvaluationFeatures.cs:89`).

Dev có **11 hàng `PurchaseEvaluationSuppliers`** so với **24 `Suppliers`** ⇒ hai không gian Id khác hẳn. Viết `q.PurchaseEvaluationSupplierId == line.SupplierId` sẽ khớp **0 dòng** ⇒ **bảng giá in ra RỖNG mà không có lỗi nào nổ**. Cộng với 4c (không lazy-load) thì đây là **hai đường dẫn tới cùng một kiểu hỏng câm**. W3 phải ghi rõ: *trước hết tra `PurchaseEvaluationSuppliers` theo `(PurchaseEvaluationId, SupplierId)` để lấy id hàng nối, rồi mới lọc quote.*

---

## 5. Acceptance W3 — CHƯA ĐỦ

Acceptance đang viết: *"tạo HĐ từ KHKK thật trên Dev → đếm ô tự điền ≥14 · so từng ô với nguồn byte-exact · ĐO DỮ LIỆU TRƯỚC: COUNT(PE có PaymentTerms != null)"*.

### 5a. 🔴 Ngưỡng "≥14" không có mẫu số — đo HÌNH DẠNG chứ không đo HÀNH VI

Đếm lại bảng §C của lane-3 theo từng dòng: 24 dòng = 5 (✍ gõ tay) + 2 (❓) + **17 (có nguồn)**; trong 17 có 3 đã chảy **và có A1 "in cứng trong form — không cần nối"**. ⇒ **số ô còn phải nối thật sự = 17 − 3 − 1 = 13, không phải 14.** Spec đặt ngưỡng **≥14 trên một tập chỉ có 13 phần tử** ⇒ **cài đặt hoàn hảo vẫn trượt**, trừ khi đếm D4 thành 7 ô (lúc đó ngưỡng 14 lại quá dễ, qua ngay ở dòng bảng thứ hai).

Nặng hơn: "ô tự điền" **không định nghĩa ô rỗng tính hay không tính**. Xem 5b — phần lớn ô nối xong sẽ rỗng, mà máy đếm vẫn ghi "đã điền". **Acceptance xanh trong khi tờ hợp đồng trắng.**
⇒ Sửa: bỏ ngưỡng trần trụi, thay bằng **bảng điểm danh 13 dòng, mỗi dòng ghi `<tên ô> | <nguồn file:line> | <giá trị đọc được> | có/rỗng`** — điểm danh từng dòng thì không Goodhart được.

### 5b. 🔴 "ĐO DỮ LIỆU TRƯỚC" chỉ kê 1 phép — thiếu ít nhất 5 phép NẶNG HƠN. Tôi đo giúp, kết quả xấu:

| # | phép đo | kết quả trên Dev | ảnh hưởng |
|---|---|---|---|
| 1 | PE có `PaymentTerms` (spec CÓ kê) | **1/4 (25%)** | vá regression mang được data cho 1/4 phiếu |
| 2 | `Suppliers.BankAccount` rỗng | **20/24 (83%)** | ô B4 **in trắng** |
| 3 | `Suppliers.LegalRepresentative` rỗng | **20/24 (83%)** | ô B6 **in trắng** |
| 4 | `Suppliers.TaxCode` rỗng | **6/24 (25%)** | ô B5 trắng 1/4 |
| 5 | `Projects.Investor` rỗng | **64/69 (93%)** | ô C3 **in trắng** |
| 6 | `Projects.Location` rỗng | **64/69 (93%)** | ô C4 **in trắng** |
| — | (đối chứng) `Suppliers.Address` / `Phone` rỗng | 2/24 (8%) | B2·B3 ổn |
| — | (đối chứng) PE `DiaDiem`/`MoTa`/`TenGoiThau` rỗng | **0/4 (0%)** | PE điền rất đủ |

🔴 **Đây là finding có sức nặng nhất với owner:** trong 8 ô master-data mà W3 định nối, **5 ô sẽ in ra trắng trên đa số hợp đồng** dù mã hoàn toàn đúng. Đúng lớp *"cơ-chế đúng, dữ-liệu không có"* đã lặp nhiều lần ở dự án này (S157: 57/62 dự án rỗng `Location`; nay **64/69** — dữ liệu **xấu đi**, không tốt lên).
⇒ W3 phải kèm **một nhánh dữ liệu**: hoặc chiến dịch điền master (Bên B: TK + người đại diện; Dự án: chủ đầu tư), hoặc cho người soạn **ghi đè tay** vào đúng ô đó và lưu ở `DraftData`. Nối ống mà không có nước thì owner bấm xong vẫn thấy tờ giấy trắng và kết luận "chưa làm được".
⇒ Và đổi ngay nguồn ô C4: **`pe.DiaDiem` (0/4 rỗng) tốt hơn hẳn `Project.Location` (64/69 rỗng)** — lane-3 có ghi `∨ pe.DiaDiem` như phương án phụ, **phải đảo thành phương án chính**.

### 5c. "So byte-exact" chưa đủ nghĩa để chạy

Thiếu 3 vế: (a) so với **nguồn nào** khi ô có 2 nguồn (C4: `Project.Location` hay `pe.DiaDiem`)? (b) so ở **thời điểm nào** — nếu snapshot vào `DraftData` thì byte-exact chỉ đúng tại t0 (xem 1b); (c) **ô rỗng có coi là khớp không** — theo 5b thì 5 ô sẽ "rỗng khớp rỗng" và qua bài.

### 5d. 3 ca acceptance còn thiếu hẳn

1. **Ca tiền tự mâu thuẫn (3d):** tạo HĐ từ dòng KHKK có `ApprovedAmount ≠ PeReferenceAmount` (Dev đang có sẵn 2/2 dòng như vậy) ⇒ **khẳng định TỔNG bảng và ô Giá trị HĐ nói cùng một số**.
2. **Ca hỏng câm (4c+4d):** cố tình lọc quote bằng `line.SupplierId` ⇒ phải thấy bảng rỗng; acceptance phải **cấm bảng rỗng đi qua** (0 dòng = FAIL, không phải "không có gì để điền").
3. **Ca `DraftData` bị xoá (2b):** nhập → Lưu → F5 → còn nguyên. Spec đã đặt ca này ở **W2**, nhưng W3 ghi vào cùng cột ấy ⇒ **W3 phụ thuộc CỨNG vào việc W2 gỡ xong 4 site `draftData: null`**. Quan hệ phụ thuộc này **không có trong spec** (W3 chỉ khai phụ thuộc W0). Nếu W3 land trước W2 thì mọi ô pre-fill lưu vào `DraftData` sẽ bị chính FE xoá ở lần Lưu đầu tiên.

---

## 6. VERDICT + finding

**`R3-PREFILL: PASS-WITH-FINDINGS — 9 finding (4 nặng)`**

Spec §②.4 **không có claim nào bịa**: 24/17/3 đúng, regression đúng ở lớp mã, đường vượt tầng đúng là đã mở. Nhưng **3 trong 5 kết luận bị lệch MỨC ĐỘ theo hai chiều ngược nhau** — một vế bị thổi (regression), một vế bị hụt (tiền) — và cả hai lệch đều do spec dừng ở lớp mã, không chạm lớp dữ liệu.

| # | mức | nội dung | neo |
|---|---|---|---|
| 6 | 🔴 CRITICAL | Chuỗi "GiaTri ← ThanhTien = KL × ChuaVat ⇒ chưa VAT" **SAI**: `ChuaVat` là cột chết (UI gửi 0, 12/34 dòng Dev = 0), `ThanhTien` là số gõ tay **không khai VAT**. Hỏi owner theo khuôn cũ ⇒ rủi ro **cộng VAT hai lần**. | `PeDetailTabs.tsx:2959-2962` · `PurchaseEvaluationDetailFeatures.cs:310,341` · sqlcmd 12/34 |
| 7 | 🔴 CRITICAL | Bảng giá lấy từ PE (Σ=`PeReferenceAmount`) trong khi ô Giá trị HĐ = `GiaTri` (Σ=`ApprovedAmount`) ⇒ **hợp đồng tự mâu thuẫn về tiền**; Dev **2/2 dòng lệch 100tr & 50tr**. Spec **không nêu**. | `CreateContractFromSigningPlanFeatures.cs:202,223,226-229` · sqlcmd 2/2 |
| 5b | 🔴 MAJOR | 5/8 ô master-data sẽ **in trắng**: TK 83% · người đại diện 83% · chủ đầu tư 93% · địa điểm 93% · MST 25% rỗng. W3 thiếu nhánh dữ liệu. Đổi C4 sang `pe.DiaDiem` (0% rỗng). | sqlcmd Suppliers/Projects/PE |
| 5a | 🔴 MAJOR | Ngưỡng "≥14 ô" đặt trên tập chỉ có **13** phần tử + không định nghĩa ô-rỗng ⇒ đo hình dạng, không đo hành vi. Thay bằng bảng điểm danh 13 dòng. | đếm lại §C lane-3 |
| 4 | MAJOR | Regression bị **thổi phồng**: `DraftData` **0/9 HĐ** có giá trị (cả đường PE), **0 người đọc**, 4 site FE xoá. Vấn đề thật = **cột chỉ-ghi** mà W2 định dùng làm nhà cho cả thân HĐ. | `ContractCreatePage.tsx:366,582` ×2 app · grep 10 hit sống · sqlcmd 0/9 |
| 9 | MAJOR | Bẫy khoá nối: `Line.SupplierId` (master) ⟂ `Quote.PurchaseEvaluationSupplierId` (hàng PE×NCC, 11 hàng) ⇒ lọc nhầm = **bảng rỗng câm**; không lazy-load nên không nổ. | `…SigningPlanFeatures.cs:559` · `…FromEvaluationFeatures.cs:89` · 0-hit `UseLazyLoadingProxies` (control 2) |
| 1 | MAJOR | "3 ô đã chảy" đúng nhưng thiếu định nghĩa; 8 ô khác **cách 1 JOIN**. Trục **snapshot ⟂ đọc sống** chưa ai chốt — ảnh hưởng cả tính pháp lý lẫn phép so byte-exact. | `…FromSigningPlanFeatures.cs:219-220` |
| 2 | MINOR | "24 ô" là số **loại** ô; ô trên giấy ≈ **31 + 7N** (N=10 ⇒ ~101). | §④.6 + W2 danh sách ô lặp |
| 3 | MINOR | "6/7 cột 1-1" thật ra **7/7** — `STT ↔ ContractDetailBase.Order`. | `ContractDetailBase.cs:14-16` |
| 8 | MINOR | `KhoiLuongThiCong` ⟂ `KhoiLuongNganSach` (tiền tính theo cột sau) — Dev **0/12 lệch**, đang ngủ. | `PurchaseEvaluationDetail.cs` · `DbInitializer.cs:1565` |

**Việc phải làm trước khi W3 chạy (theo thứ tự):**
1. Sửa câu hỏi VAT gửi owner — hỏi **quy ước nhập liệu**, đừng hỏi phép nhân (F-6).
2. Chốt bảng giá đọc số nào để TỔNG khớp `GiaTri` (F-7).
3. Thêm nhánh dữ liệu master + đảo C4 sang `pe.DiaDiem` (F-5b).
4. Thay ngưỡng ≥14 bằng bảng điểm danh 13 dòng, cấm bảng-giá-rỗng qua bài (F-5a, F-9).
5. Khai phụ thuộc **W3 → W2** (gỡ `draftData: null`) vào spec (F-4, 5d-3).

---

### Ghi chú thủ tục (không phải finding về spec)

Bản giao việc tự mâu thuẫn về đường ghi: phần TASK yêu cầu ghi `sub-reviewer-3.md`, khối RUN-TRACE ở cuối lại ghi `sub-reviewer-2.md` (và khai `reviewer` **không** có Write tool, trong khi lượt này có). Tôi theo TASK (cụ thể, đích danh vai trò của lượt) ⇒ file này là `sub-reviewer-3.md`. `sub-reviewer-2.md` **không bị tôi đụng tới**.
