# sub-reviewer-dot1 — review ADVERSARIAL diff đợt-1 YC-023 (S187, 2026-08-10)

> Vai: reviewer (READ-only, verdict-only). Diff = working-tree `git diff` TRƯỚC commit.
> Spec: `spec-duyet-hd-10-08-2026.md` · nền: `sub-plan-review-fable.md §2/§6`.
> Nguyên tắc phiên này: **claim nào cũng phải có lệnh đo kèm**; 0-hit nào cũng phải có **control-dương**.

---

## 0. Phạm vi đo được (không suy)

```
git diff --stat  →  9 file trong scope:
  3 BE : ApprovalWorkflowV2AdminFeatures.cs (+80/-13) · Contracts/ContractFeatures.cs (+89/-1)
         · Contracts/Dtos/ContractDtos.cs (+76/-1)
  1 test: tests/.../Application/UpdateAwDefinitionTests.cs (+295/-0)
  5 FE : fe-admin{types/contracts.ts +61, pages/contracts/ContractsListPage.tsx +44/-28}
         fe-user{types/contracts.ts +61, pages/InboxPage.tsx +12, pages/contracts/MyContractsPage.tsx +25/-6}
Ngoài scope nhưng đang M (bookkeeping của lead, KHÔNG review): agent-memory/implementer-backend/**,
  .session-counter.json, docs/HANDOFF.md, docs/governance/so-yeu-cau-chu-du-an.md
Migrations/ : 0 file (git status) ⇒ đúng "wave 0-mig".
```

**Phép đo tôi tự chạy (không mượn của trọng tài):**

| # | Lệnh | Kết quả |
|---|---|---|
| Đ1 | `dotnet test tests/SolutionErp.Infrastructure.Tests --filter "W1a_|W1b_"` | **Failed 0 · Passed 4 · Total 4** (5 s) — 4 test-before nay XANH thật |
| Đ2 | (hệ quả Đ1) build project `Application`+`Domain`+`Infrastructure`+`Tests` | 0 err — **BE diff biên dịch được, tôi tự chứng**. ⚠️ chưa tự chạy: project `Api`, `npm run build` ×2 app (nhận theo claim trọng tài) |
| Đ3 | sha256 khối `+` của 2 file `types/contracts.ts` | admin 61 dòng / user 61 dòng, **sha `5a56f59dd8eeb479…` IDENTICAL** ⇒ SHA-pair mirror ĐẠT |
| Đ4 | grep `TODO|FIXME|HACK|password|secret|Bearer …` trên toàn diff 9 file | **0 hit** — control-dương cùng lưới (`S187|typeFilter`) = **32 hit** ⇒ lưới sống, 0-hit là sạch THẬT |
| Đ5 | grep `console.log|alert(|// Mock` trên diff FE | 0 hit (cùng control Đ4) |
| Đ6 | grep chuỗi hiển thị mới trong diff FE | 0 chuỗi UI mới (mọi dòng khớp đều là **comment**) ⇒ mục "UI tiếng Việt" N-A theo hướng an toàn; chuỗi BE mới duy nhất `"(chưa gán hạng mục)"` = tiếng Việt |
| Đ7 | `grep -rn "activeOthers" fe-admin/src fe-user/src` | **0 hit** — control-dương `levelOpinions` = 10+ file ⇒ 0-hit THẬT (xem F-3) |
| Đ8 | `grep -rn "new ContractDetailDto(" src tests` | **1 site** (`ContractFeatures.cs:803`) ⇒ append-CUỐI an toàn (đo, không suy) |
| Đ9 | `grep -rn AwTypeSummaryDto` toàn kho | dựng: 1 site (`:239`); FE `fe-user/src/types/approvalWorkflowV2.ts:54` là type **structural** ⇒ chèn giữa record không vỡ biên dịch chỗ nào |

---

## 1. SOI-1 — W1 (per-Code type-3 + overview family)

### 1a. Tập-type TƯỜNG MINH, không regex (R-6) — ĐẠT

`ApprovalWorkflowV2AdminFeatures.cs:372-377`:
```csharp
private static readonly ApprovalWorkflowApplicableType[] PerCodeDeactivationTypes =
[ ApprovalWorkflowApplicableType.ContractSigningPlan, ApprovalWorkflowApplicableType.Contract, ];
```
`:422` `PerCodeDeactivationTypes.Contains(typeEnum)` — 0 regex, 0 pattern trên `Code`. Hai nhánh truy vấn giữ nguyên từng vế (per-Code 3 vế `Code == code && ApplicableType == typeEnum && IsActive` · per-TYPE 2 vế). **R-6 tôn trọng.** Test `W1a_...WithOutOfFamilyCode...` phủ đúng cả 2 chân (mã ngoài họ không nuke 7 loại · vẫn ratchet chính mã đó).

### 1b. Hành vi type khác GIỮ Active-đơn — ĐẠT

Chỉ 2 type vào danh sách; test `W1b_OverviewForSingleActiveType_...` chốt type-1 vẫn đúng-1-đương-nhiệm và không lẫn type khác. Đ1 xanh.

### 1c. Comment `:363` (neo cũ; sau sửa nằm `:404-425`) khớp code mới — ĐẠT

Câu tự-khai cũ ("các loại khác: per-TYPE như cũ — 1 active/type là bất biến của chúng", vốn sẽ thành SAI khi type-3 mọc 7 quy trình) đã bị thay bằng câu neo vào **TẬP TYPE** + giữ nguyên vết sai cũ có nhãn. Đạt AC W1(iii).

### 1d. Overview family: không leak type khác, không đôi bản ghi — ĐẠT

`:237` `var actives = versions.Where(v => v.IsActive).ToList();` với `versions` đã lọc `d.ApplicableType == type` ⇒ 0 leak. `Active = actives.FirstOrDefault()` + `ActiveOthers = actives.Skip(1)` ⇒ **hợp = trọn tập, giao = rỗng**, và `Active` giữ **đúng phần tử cũ** (`versions.FirstOrDefault(v => v.IsActive)`) ⇒ 0 rủi ro cho `ApprovalWorkflowsV2Page.tsx:381-389/455-479` đang đọc `t.active`.

### 1e. 🔴 GIẢ THUYẾT ĐỐI KHÁNG CỦA TÔI — BỊ BÁC BẰNG ĐO (ghi để đừng phá)

Nghi vấn: W1a **gỡ bỏ bảo đảm "1 active / type-3"** ⇒ nơi nào đang bốc "cái active của type-3" sẽ bốc ngẫu nhiên (bệnh cardinality-change, memory `feedback_cardinality_change_grep_consumers`). Đo 3 consumer:
- BE tạo HĐ: `ContractFeatures.cs:37/79/109` — `ApprovalWorkflowId` **đến từ request**, BE KHÔNG tự chọn bản active nào ⇒ không có đường bốc ngẫu nhiên.
- `fe-user/src/pages/contracts/ContractCreatePage.tsx:334-342` — đọc `typeBucket.history` rồi `.filter(isUserSelectable)`, user tự chọn.
- `KhkkDetailPage.tsx:216-228` (**cả 2 app**) — đọc `history` rồi `.filter(isUserSelectable && isActive)`.
⇒ **Không consumer nào đọc `bucket.active` cho type-3.** Nghi vấn ĐỔ. Đây là điểm kháng cự được soi.

---

## 2. SOI-2 — W2 BE (Source reverse-join + cây thô)

| Kiểm | Kết quả |
|---|---|
| reverse-join qua `ContractSigningPlanLines` | `ContractFeatures.cs:721-725` `.Where(l => l.ContractId == c.Id)` — `ContractSigningPlanLine.cs:46` xác nhận `ContractId` là con-trỏ; `Contract` không mang con-trỏ ngược ⇒ hướng join ĐÚNG |
| null-path HĐ 0 dòng nối | `:726` `if (planLines.Count > 0)` ⇒ `sourceDto` giữ null ⇒ JSON `source: null`. ĐẠT |
| `AsNoTracking` | có ở **cả 4** truy vấn mới (lines `:721` · plan `:734` · workflow `:758` · departments `:775`) + `userManager.Users.AsNoTracking()` `:766`. ĐẠT |
| N+1 | **KHÔNG**. Đếm query THÊM cho 1 GET detail: tối thiểu **1** (chỉ lines), tối đa **5** (lines · plan · workflow-Include · users · departments). Tên người + tên phòng gom **1 lượt** ngoài vòng lặp (`:766-778`) ⇒ 0 truy vấn trong loop |
| positional record append-CUỐI | 3 field mới đứng CUỐI + default `null` (`ContractDtos.cs:65-91`); Đ8 chứng chỉ **1 call-site** ⇒ wire cũ không vỡ |
| load chỉ khi `ApprovalWorkflowId != null` | `:756` `if (c.ApprovalWorkflowId is Guid awIdTree)`. ĐẠT (V1 legacy ⇒ `workflowSteps: null`) |
| cây mirror khuôn KHKK, KHÔNG precompute status | `ContractWorkflowLevelDto` (`ContractDtos.cs:129`) `(Id, Order, Name, ApproverUserId, ApproverFullName, AllowApproverFinalize)` vs KHKK `ContractSigningPlanWorkflowLevelDto` (`ContractSigningPlanFeatures.cs:131-140`) — **khớp từng field**; Step (`ContractDtos.cs:119`) `(Id, Order, Name, DepartmentId, Levels, DepartmentName = null)` vs KHKK `:142-152` — **khớp từng field**; **0 field `Status`** ở cả 2 bên. ĐẠT |
| Σ tiền | `linesOfPlan.Sum(l => l.ApprovedAmount ?? 0)` **in-memory** (lý do SQLite-decimal khai trong comment, đúng) |
| soft-delete | `HasQueryFilter(x => !x.IsDeleted)` xác minh TẠI ĐĨA: `ContractSigningPlanLineConfiguration.cs:52` + `ContractSigningPlanConfiguration.cs:55` ⇒ comment cite ĐÚNG, không cần `.Where` tay |
| an ninh | IDOR guard chạy ở `:614-638`, **TRƯỚC** khối W2 (`:712+`) ⇒ không rò mã KHKK/PE-id cho người không được xem HĐ. ĐẠT |
| cite trong comment | spot-check 3/3 đúng: `Line.cs:46` (ContractId) · bridge `:238` (`foreach … l.ContractId = contract.Id`) · `:121` (`"(chưa gán hạng mục)"` — **đúng nguyên văn**) |

---

## 3. SOI-3 — FE

### 3a. types ×2 app: khối THÊM identical — ĐẠT (Đ3)

### 3b. 🔴 SHAPE **KHÔNG** KHỚP JSON BE — 3 tên field + 1 nullability (F-1, F-2 dưới)

Đối chiếu **từng field**, C# → camelCase (chứng: `Program.cs:33-34` chỉ thêm `UtcDateTimeJsonConverter`, **KHÔNG** set `PropertyNamingPolicy`; `grep JsonPropertyName src/.../Contracts/` = 0 hit ⇒ camelCase mặc định):

| BE (C#) | JSON thật | FE khai | Verdict |
|---|---|---|---|
| `ContractDetailDto.Source` | `source` | `contractSource` (admin `:182` · user `:186`) | ❌ **LỆCH** |
| `ContractSourceDto.PlanId` | `planId` | `planId` | ✓ |
| `MaKeHoach` (`string?`) | `maKeHoach: string\|null` | `maKeHoach: string` (non-null) | ❌ **lệch nullability** |
| `ApprovalGroup` (`int?`) | `approvalGroup` | `approvalGroup?: number\|null` | ✓ |
| `PurchaseEvaluationId` (`Guid?`) | `purchaseEvaluationId` | `purchaseEvaluationId?: string\|null` | ✓ |
| `TenHangMucs` (`List<string>`) | `tenHangMucs` | `tenHangMuc` (thiếu **s**) | ❌ **LỆCH** |
| `ApprovedAmountTotal` (`decimal`) | `approvedAmountTotal` | `totalApprovedAmount` (**đảo thứ tự từ**) | ❌ **LỆCH** |
| `CurrentWorkflowStepIndex` | `currentWorkflowStepIndex` | `currentWorkflowStepIndex` | ✓ |
| `WorkflowSteps` | `workflowSteps` | `workflowSteps` | ✓ |
| `ContractWorkflowStepDto` 6 field | id/order/name/departmentId/levels/departmentName | khớp 6/6 | ✓ |
| `ContractWorkflowLevelDto` 6 field | id/order/name/approverUserId/approverFullName/allowApproverFinalize | khớp 6/6 | ✓ |

### 3c. ContractsListPage 3 fix — ĐẠT

- server-filter: `type: typeFilter ?? undefined` trong params; vế `items.filter(c => c.type === typeFilter)` **đã gỡ** (grep còn lại chỉ là comment).
- badge total (`:119`,`:128`) + phân trang (`:216`,`:219`,`:231`) đọc `list.data.total` — nay là tổng **đã lọc** vì BE lọc TRƯỚC `CountAsync` (`ContractFeatures.cs:322` `if (request.Type is not null) …` → `CountAsync` → `Skip/Take`, đọc tại đĩa) ⇒ 2 lỗi (b)(c) tự đóng theo (a). Claim "3 fix" ĐỨNG.
- rào `NaN`: `Number.isFinite(parsedType) && parsedType > 0` ⇒ `?type=abc`/`?type=0` không lên dây. Tốt.

### 3d. InboxPage CHỈ comment, 0 đổi hành vi — ĐÚNG NHƯ KHAI

`git diff` = +12 dòng, **toàn comment**; vế lọc client còn nguyên (`InboxPage.tsx:103`). Tiền đề của quyết định KIỂM TẠI ĐĨA và **ĐÚNG**: `ContractsController.cs:41-43` `Inbox(CancellationToken ct)` → `GetMyInboxQuery` **không tham số** (`ContractFeatures.cs:479`), và `:550` `q.Take(100)`. ⇒ xem F-4 (AC vs thực tế).

### 3e. MyContractsPage — ĐẠT

`type` gửi cho **cả 2 nhánh** (thường + deleted; param có thật ở `ContractsController.cs:25` và `:34`); `queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted]` đã mang `typeFilter` từ trước ⇒ đổi nguồn không đẻ ô cache sai; vế lọc client gỡ **cùng lúc** với `typeFilter` khỏi deps của `useMemo` (không để deps thừa/thiếu).

---

## 4. SOI-4 — cross-cut

0 secret (Đ4) · 0 file `Migrations/` · 0 file ngoài scope trong 9 file review · 0 TODO bỏ quên (Đ4) · 0 chuỗi UI mới, chuỗi BE mới là tiếng Việt (Đ6) · commit chưa tạo (review TRƯỚC commit, đúng cửa).

---

## 5. FINDING (nặng → nhẹ)

### F-1 🔴 MAJOR — hợp-đồng FE↔BE VỠ 3 TÊN FIELD; tsc/build XANH vì chưa ai đọc

- `fe-admin/src/types/contracts.ts:182` + `fe-user/src/types/contracts.ts:186` → `contractSource`, BE trả **`source`** (`ContractDtos.cs:87`).
- admin `:204` / user `:208` → `tenHangMuc`, BE trả **`tenHangMucs`** (`ContractDtos.cs:79`).
- admin `:206` / user `:210` → `totalApprovedAmount`, BE trả **`approvedAmountTotal`** (`ContractDtos.cs:82`).
- **Chính file FE tự treo cờ** (admin `:193-195` / user `:197-199`): *"nếu BE lane đặt property `Source` thì đây là 1 dòng rename"* — **điều kiện đã xảy ra**, cờ chưa đóng. Spec AC W2 (`spec:26`) viết `source.maKeHoach` ⇒ **BE theo spec, FE lệch spec**.
- Vì sao đây là MAJOR chứ không phải "sửa sau": 3 field hiện **0 consumer** ⇒ `tsc`/`npm run build`/674 test **KHÔNG bắt được**. Đến W5 (card "Nguồn gốc KHKK") thì `detail.contractSource` = `undefined` ⇒ card **không bao giờ render**, và theo đúng lời chú thích của chính file (*"vắng key = payload chưa có W2"*) sẽ bị đọc thành "HĐ tạo tay" — **sai câm, đúng cái bẫy mà comment cảnh báo**.
- Acceptance để đóng: FE đổi 3 tên (hoặc BE đổi 3 tên) sao cho **tên hai bên trùng nhau**, và AC W2 của spec được đối chiếu; kèm 1 phép chứng chạy được — ví dụ GET detail 1 HĐ có nguồn KHKK rồi so **tập khoá JSON** với tập field FE, hoặc dán payload thật vào PR-note. Ai sửa, sửa bên nào = quyền lead.

### F-2 🟠 MAJOR (cùng ổ F-1) — `maKeHoach` khai NON-NULL trong khi BE cho phép null

`ContractFeatures.cs:730-733`: `plan` lấy bằng `FirstOrDefaultAsync` **qua global filter `!IsDeleted`** ⇒ phiếu xoá mềm ⇒ `plan == null` ⇒ `MaKeHoach: null`. Chính comment DTO (`ContractDtos.cs:70-75`) khai rõ "3 field nullable = dữ liệu PHIẾU CHA, null khi phiếu không đọc được nữa". FE khai `maKeHoach: string` ⇒ nơi đọc sẽ không bị TS ép null-check ⇒ `source.maKeHoach.trim()`/`.slice()` nổ runtime ở ca hiếm. Acceptance: `maKeHoach: string | null`.

### F-3 🟠 MAJOR — W1b: `ActiveOthers` **defined-but-unread**; khiếm khuyết UI mà nó sinh ra để chữa VẪN CÒN

- Đ7: `activeOthers` **0 hit** trong `fe-admin/src` + `fe-user/src` (control-dương `levelOpinions` 10+ file).
- Màn hình vẫn đọc **đúng 2 ngả cũ**: thẻ "đang áp dụng" ← `t.active` (`ApprovalWorkflowsV2Page.tsx:381-389`, `:455-479`) · mục "phiên bản cũ" ← `type.history.filter(d => !d.isActive)` (`:485`, `:492`). ⇒ với type-3 nhiều active, **N-1 bản vẫn không xuất hiện ở đâu** — đúng nguyên văn triệu chứng mà comment BE `:229-235` mô tả.
- Test W1b XANH **không mâu thuẫn** với điều đó: `SurfacedAsCurrent` (`UpdateAwDefinitionTests.cs:1614-1629`) đo **hình dạng DTO bằng reflection**, không đo màn hình. Đây là acceptance đo-shape, hợp lệ cho BE, **không** chứng minh "bom-2 đã tháo".
- 🔴 Điểm đắt hơn: **FE đã có sẵn khuôn chữa mà KHÔNG cần BE đổi gì** — panel nhóm KHKK cắt lại từ `history` rồi tự suy `active` (`:330-341`), và `history` vốn chứa CẢ bản đang áp dụng. Nếu panel HĐ đi theo đúng khuôn đó (rất có khả năng, vì W6 chọn mirror KHKK) thì `ActiveOthers` là **field chết vĩnh viễn** — thêm 1 ngả dữ liệu thứ hai cho cùng một sự thật (`History` đã đủ), tức đẻ thêm chỗ để lệch.
- Acceptance để đóng — chọn **một**: (i) chỉ mặt wave sẽ đọc `activeOthers` và ghi vào `spec §2`/`sub-plan-review §7` (kèm file FE đích), hoặc (ii) bỏ `ActiveOthers`, chữa ở FE bằng lát-cắt-theo-Code sẵn có, giữ nguyên 4 test (test cố ý XANH với **cả hai** hình dạng vá — chính nó khai vậy ở `:1610-1612`). Điều KHÔNG được: tuyên "W1b đã chữa xong bom-2 giấu-6" khi màn hình chưa đổi một pixel.

### F-4 🟠 MAJOR — AC W7 không đạt TRỌN, và phần thiếu đang nằm trong **comment** chứ không trong sổ

- `spec:17` giao *"InboxPage.tsx:91 ×2 app gửi `?type=` lên BE (param sẵn `ContractsController.cs:23-27`)"* — **tiền đề của spec SAI**: endpoint inbox không có param (đo ở §3d). Implementer **không làm** và khai 12 dòng comment — **quyết định đúng** (gửi `?type=` lên endpoint không nhận = nuốt câm; gỡ filter client = giết câm bộ lọc 7 leaf `Ct_*_Pending` qua `Layout.tsx:150`).
- Nhưng AC `spec:27` (*"request inbox/list mang `?type=N`"*) vì thế **CHƯA ĐẠT**, và **nợ thật** `ContractFeatures.cs:550 q.Take(100)` (hộp thư >100 dòng ⇒ loại hiếm vẫn biến mất im lặng) hiện chỉ sống trong comment — **không có mã wave, không ai canh**.
- Ghi thêm cho khỏi đo nhầm về sau: `fe-admin` **không có** `pages/InboxPage.tsx` ⇒ "×2 app" cho inbox vốn không đo được; "×2 app" của W7 trên thực tế = `ContractsListPage` (admin) + `MyContractsPage` (user), **cả hai đã làm**.
- Acceptance: lead sửa AC W7 trong spec (ghi rõ inbox out-of-scope + lý do) **và** mở một mục nợ có mã ("thêm `type` + bỏ trần 100 cho `GET /contracts/inbox`") ở nơi có người canh, không để trong comment.

### F-5 🟡 minor — cùng-lớp còn sót: `ContractCreatePage.tsx` (CẢ 2 app) vẫn lọc-client-sau-khi-cắt-trang

`fe-admin/src/pages/contracts/ContractCreatePage.tsx:50-53` lấy `/contracts` `pageSize: 100` **không truyền `type`**, rồi `:80` `items = items.filter(c => c.type === typeFilter)`; `fe-user/.../ContractCreatePage.tsx:80` y hệt. Đây **đúng lớp lỗi** W7 vừa chữa (user >100 HĐ ⇒ loại hiếm biến mất im lặng). Ngoài scope spec W7 — nêu để không ai tưởng "class đã đóng" (bài học: vá 1 CLASS thì phải grep MỌI matcher cùng-lớp). Không chặn commit.

### F-6 🟡 minor — chèn field vào **GIỮA** positional record, ngược convention tự-khai của kho

`ActiveOthers` chèn giữa `Active` và `History` (`ApprovalWorkflowV2AdminFeatures.cs:97`). Đ9 chứng **không vỡ biên dịch** (1 site dựng, FE structural) nên không chặn. Nhưng file anh em tự khai convention ngược lại: `ContractSigningPlanFeatures.cs:148-150` — *"append-CUỐI (KHÔNG chèn cạnh `DepartmentId`… JSON bind theo TÊN)"* — và chính wave W2 trong cùng diff tuân thủ append-CUỐI. Hai chuẩn trong một commit. Không chặn, nhưng nếu F-3 đi hướng (ii) thì vấn đề tự biến mất.

### F-7 🟡 minor — 1 truy vấn VÔ ĐIỀU KIỆN thêm vào mọi `GET /contracts/{id}`

`ContractSigningPlanLines` reverse-join chạy cả với HĐ chắc chắn không có nguồn KHKK (HĐ cũ trước K7). Chi phí 1 index-seek trên `ContractId` — chấp nhận được, ghi để sau này ai đo p95 detail-page không phải đi tìm.

### F-8 🟡 minor — ca "2 phiếu trỏ cùng 1 HĐ" bị **thu hẹp im lặng**

`:723-726` chốt phiếu theo dòng ĐẦU rồi lọc lại ⇒ nếu dữ liệu lệch (INSERT thô), card ra số của đúng 1 phiếu — **không có tín hiệu nào** cho biết đã bỏ qua dòng của phiếu khác. Comment tự khai chủ đích và tôi đồng ý hướng chọn (thà đúng 1 phiếu còn hơn trộn tiền 2 phiếu). Ghi làm vết, không đòi sửa.

---

## 6. Điểm KHÁNG CỰ được soi (positive — đừng phá ở wave sau)

1. `Active` giữ **đúng phần tử cũ từng byte** ⇒ 0 rủi ro hồi quy cho 2 chỗ FE đang đọc `t.active`.
2. `new ContractDetailDto(` **1 site duy nhất** — append-CUỐI được chứng bằng đo, không bằng niềm tin.
3. Cây thô Contract mirror KHKK **khớp từng field, 0 `Status`** — đúng quyết T2 của spec (khuôn KHKK, không phải cặp precompute PE).
4. IDOR guard nằm **trước** code W2 ⇒ dữ liệu KHKK không rò qua cửa detail.
5. Rào `NaN` cho `?type=` (2 trang) — dùng lại khuôn `HardCopiesPage.tsx:66-68`, không đẻ khuôn thứ hai.
6. Test W1a-2 có **2 chân** (mã ngoài họ + ratchet chính mã đó) ⇒ giết được cả bản vá regex lẫn bản vá lười "type-3 đừng tắt gì".
7. Giả thuyết đối kháng về cardinality (§1e) bị **bác bằng đo 3 consumer**, không bằng suy luận.

---

## 7. VERDICT

**`PASS_WITH_FLAGS` — 8 issue (0 blocker tuyệt đối, 2 MAJOR phải đóng TRƯỚC khi W5 chạm tới field).**

- Commit đợt-1 được: BE W1+W2 đúng spec, 0-mig, test-before xanh (tôi tự chạy), 0 secret, mirror SHA-pair đạt.
- **Điều kiện kèm theo:** F-1 + F-2 (tên/nullability FE) nên đóng **NGAY trong commit này** — chỉ 4 dòng, và để lại thì W5 sẽ debug một card "không render" mà mọi máy đều báo xanh. F-3 + F-4 là **nợ phải có chủ**: hoặc ghi vào spec/sổ nợ có mã, hoặc đổi hướng vá; không được đóng bằng cách tuyên đã xong.
- Không tự sửa gì (vai verdict-only). Fix thuộc em-main/implementer.

== HẾT ==
