# sub-reviewer-1 — chấm quyết định ②.2(a) A1-vs-A2 + wave W1 (YC-032, S194)

> Vai: `reviewer` (adversarial, read-only). Vật chấm: `spec-hd-thauphu-2form-13-08-2026.md` §②.1 · §②.2(a) · W1.
> Kỷ luật: mọi số phải từ lệnh vừa chạy (kèm lệnh). 0-hit BẮT BUỘC kèm control-dương.

## 0. Khung

Chấm 5 điểm theo lệnh lead. Mọi số dưới đây từ lệnh chạy trong lượt này (git grep / sed / sqlcmd), KHÔNG lấy từ spec, KHÔNG lấy từ trí nhớ.

Kết luận sớm: quyết định **A1 (thêm 2 enum) là ĐÚNG** — lập luận (2) đứng vững, lập luận (3) đứng vững. Nhưng **con số "13 site" của spec là ƯỚC THẤP**, và **W1 bỏ sót một lớp site nguyên vẹn (tầng chi-tiết per-loại, BE + FE)** — chính là tầng mà câu "0 migration nếu 2 loại mới dùng chung `ThauPhuDetails`" phụ thuộc vào. Ngoài ra **acceptance "test hiện có vẫn 697 PASS" là SAI BY-CONSTRUCTION**.

## 1. Đếm lại site

### 1.1 Mẫu số thô

```
git grep -c "ContractType" -- "src/*.cs"        → 30 file NGOÀI Migrations (+78 file Designer/snapshot = nhiễu)
git grep -n "ContractType\." -- "*.cs" | wc -l  → 152 dòng (gồm cả tests)
git grep -n "case ContractType" -- "*.cs"       → 14 dòng, TẤT CẢ ở ContractDetailsFeatures.cs
   (control-dương: git grep -c "case " -- "*.cs" → ContractSigningPlanWorkflowGuards.cs:3, DbInitializer.cs:1 … ⇒ lưới `case` CÓ chạy)
```

### 1.2 Danh sách MUST-EDIT tự dựng (không chép spec)

W1 có kê (✅) / W1 KHÔNG kê (🔴):

| # | Site | W1? | Ghi chú đo được |
|---|---|---|---|
| 1 | `Domain/Contracts/ContractType.cs:5-11` (7 member) | ✅ | enum 1..7, đọc tận mắt |
| 2 | `Domain/Contracts/WorkflowPolicy.cs:155` `_ => "SkipCcm"` | ✅ | CÂM — xem §3 |
| 3 | `Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42` `Members` | ✅ | nở by-construction: `Codes:45`, `CodeFor:48`, `MenuKeys.ContractWorkflowMenuKeys:156` |
| 4 | `Domain/Identity/MenuKeys.cs:40` `ContractTypeCodes` | ✅ | 5 consumer loop: `DbInitializer.cs:2192,:2212,:2578,:2614` + `:2049` |
| 5 | `Infrastructure/Services/ContractCodeGenerator.cs:20-29` | ✅ | 🔴 nhưng `_ => "HĐ"` = CÂM THỨ HAI, spec không khai — xem §3 |
| 6 | `Infrastructure/Reports/ContractExcelExporter.cs:30-36` `TypeLabels` | ✅ | dict 7 dòng |
| 7 | `Application/Contracts/WorkflowAdminFeatures.cs:64-72` `TypeLabels` | ✅ | `:159` dùng `Enum.GetValues<ContractType>()` ⇒ tự nở; `:136` `GetValueOrDefault(…, ToString())` ⇒ thiếu nhãn thì rơi về tên enum, KHÔNG nổ |
| 8 | `DbInitializer` seed menu-row + nhãn (`:2054`, `:2375`) + seed quy trình `HD-*` | ✅ | |
| 9 | FE `TYPE_CODE_TO_INT` ×4 (`fe-{admin,user}/Layout.tsx`, `ApprovalWorkflowsV2Page.tsx:172`, `WorkflowsPage.tsx:77`) | ✅ | |
| **10** | **`Application/Contracts/ContractDetailsFeatures.cs:41-113`** — switch #1, **KHÔNG default** | **🔴** | rơi ra ⇒ trả bundle RỖNG, HTTP 200. CÂM. |
| **11** | **`Application/Contracts/ContractDetailsFeatures.cs:434-462`** — switch #2 (Delete) | **🔴** | rơi ra ⇒ `removed=false` ⇒ `NotFoundException("ContractDetail")` = **báo sai nguyên nhân** |
| **12** | **`fe-{user,admin}/src/components/contracts/ContractDetailsTab.tsx:33` `TYPE_TO_SLUG`** | **🔴** | thiếu key ⇒ URL `/details/undefined` |
| **13** | **cùng file `:77-83`** — 7 dòng `bundle.type === N &&` | **🔴** | type 8/9 ⇒ **không render gì**, im lặng |
| **14** | **cùng file `:517` `FIELDS_BY_TYPE: Record<number, FieldDef[]>`** | **🔴** | thiếu ⇒ form nhập rỗng |
| **15** | **cùng file `:583-597` `switch (contractType)` KHÔNG default** | **🔴** | rơi ra ⇒ `return common` = payload **thiếu sạch field nghiệp vụ** |
| **16** | **`fe-admin/src/lib/contractWorkflowFamily.ts:46-52`** — 7 cặp `{code, contractType}` chép tay | **🔴** | mirror FE của `Members` |
| **17** | **`fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:59-65`** — `Record<number,string>` 7 dòng | **🔴** | file tự khai `:56` *"chép TỪNG KÝ TỰ"* |

⇒ **17 site must-edit** (chưa tính test). Spec nói **13**. Bốn cái #12-#15 nằm trong **1 file × 2 app** nên nếu spec đếm "file" thì lệch ít hơn, nhưng **cả 8 lượt sửa đó đều không xuất hiện ở W1 dưới bất kỳ hình thức nào** — grep `ContractDetails` trong spec = 0 hit.

🔴 **Điểm chí tử:** ②.2(a) tự đặt cược *"0 migration **nếu 2 loại mới dùng chung `ThauPhuDetails`**"*. "Dùng chung `ThauPhuDetails`" KHÔNG tự xảy ra — nó **chính là** #10 + #12..#15. Spec dùng giả thiết đó để hạ chi phí A1 xuống, rồi **không kê việc làm cho giả thiết đó thành thật**.

### 1.3 By-construction: 6/13 có đúng không?

Đếm lại thứ **thật sự tự nở**: `Family.Codes:45` · `Family.CodeFor:48` · `MenuKeys.ContractWorkflowMenuKeys:156` · 4 vòng `foreach MenuKeys.ContractTypeCodes` (`DbInitializer:2192,:2212,:2578,:2614`) · `WorkflowAdminFeatures.cs:159 Enum.GetValues` · `ContractWorkflowGuards` (đọc qua `Family`, `:159-166 ShortNameOf` cũng loop `Members`).
⇒ "~6 by-construction" **HỢP LÝ**, không bác. Vấn đề nằm ở **mẫu số**, không ở tử số.

## 2. Migration rỗng?

**KẾT LUẬN: spec ĐÚNG.** Ba phép đo độc lập:

```
git grep -n "HasConversion<string>" -- "src/*.cs"   → 0 hit (exit 1)
   control-dương: git grep -c "HasConversion<int>"  → ContractConfiguration.cs:7, WorkflowAppsConfiguration.cs:9, … ⇒ lưới CHẠY
```
Cả 4 cột mang `ContractType` đều int:
- `ContractConfiguration.cs:15` `b.Property(x => x.Type).HasConversion<int>()`
- `WorkflowDefinitionConfiguration.cs:15` `HasConversion<int>()`
- `WorkflowTypeAssignmentConfiguration.cs:13` `HasConversion<int>()`
- `ContractTemplateConfiguration.cs:16` `HasConversion<int?>()`

sqlcmd LocalDB `SolutionErp_Dev`:
```
CHECKS_ON_TYPE=0
CONTROL_TOTAL_CHECKS=0      ← ⚠️ xem dưới
ContractsTypeCol=Contracts/int
```

⚠️ **Cảnh báo về THƯỚC (không đổi kết luận):** control-âm ở đây **thoái hoá** — cả DB có **0** check-constraint, nên "0 CHECK trên `Type`" đúng nhưng **không mang thông tin riêng** về cột `Type`; nó đúng với mọi cột. Phép đo mang thông tin thật là **kiểu cột = `int`** + **0 `HasConversion<string>`**. Nếu spec định dùng "0 CHECK" làm bằng chứng chính thì đó là bằng chứng yếu; kiểu cột mới là cái đỡ lưng.

**Không có bảng lookup loại HĐ** (không có `ContractTypes` table). Có 3 bảng *tham chiếu* enum bằng cột int + index (`WorkflowTypeAssignments` unique, `WorkflowDefinitions` composite, `ContractTemplates`) — đều là **DATA**, không phải schema ⇒ không sinh migration, nhưng **buộc seed row** (W1 đã kê phần quy trình `HD-*`).

⇒ Lập luận (2) **HELD**. Tiền lệ Mig 62 `SupplierType +CaHai=6` không cần kiểm thêm vì cơ chế đã chứng trực tiếp.

## 3. Nhánh nuốt im lặng

### 3.1 Hai claim của spec — cả hai HELD

**`WorkflowPolicy.cs:155` CÂM — ĐÚNG.** Đọc tận mắt `:154-158`:
```csharp
public static string DefaultPolicyNameFor(ContractType type) => type switch
{
    ContractType.HopDongThauPhu or ContractType.HopDongGiaoKhoan or ContractType.HopDongNhaCungCap => "Standard",
    _ => "SkipCcm",
};
```
Nặng hơn spec mô tả: loại 1 (Thầu phụ gốc) = `"Standard"`, mà **2 biến-thể Thầu phụ mới nếu quên thêm sẽ rơi vào `"SkipCcm"`** ⇒ **mất trạm CCM** đúng cho HĐ Thầu phụ — loại DUY NHẤT mà YC-032 đụng tới. Xác suất trúng đạn ở đây là cao nhất, không phải trung bình.

**`ContractWorkflowGuards.cs:147-152` TO TIẾNG — ĐÚNG.** `expected is null` ⇒ `throw new ConflictException("Loại HĐ '{contractType}' chưa có quy trình riêng trong họ 'HD-*'…")`. Fail-closed thật.

### 3.2 🔴 Nhánh CÂM spec KHÔNG khai (tìm thêm theo lệnh)

`git grep -n "case ContractType"` chỉ ra 14 dòng; `switch` **biểu thức** (`=>`) phải grep riêng — spec chỉ soi 1 trong 2 dạng.

| # | Vị trí | Nhánh rơi | Hỏng kiểu gì |
|---|---|---|---|
| **C-1** | `Infrastructure/Services/ContractCodeGenerator.cs:28` | `_ => "HĐ"` | **CÂM.** Mã HĐ sinh ra thành `{Project}/HĐ/SOL&{NCC}/{Seq}` thay vì `…/HĐTP/…` ⇒ **sai RG-001, không nổ, và mã đã cấp thì không rút lại được** (`ContractCodeSequences` đã tăng). W1 CÓ kê site này nhưng **kê như một việc chép nhãn**, không kê như bẫy câm. |
| **C-2** | `Application/Contracts/ContractDetailsFeatures.cs:41-113` | **không có `default`** | **CÂM.** `GetContractDetailsQuery` trả `bundle` với **cả 7 list rỗng**, HTTP 200. UI hiện "không có chi tiết" — không phân biệt được với "HĐ chưa nhập dòng nào". |
| **C-3** | `fe-{user,admin}/src/components/contracts/ContractDetailsTab.tsx:583-597` | `switch` không `default`, sau đó `return common` | **CÂM.** POST đi với payload chỉ có `{id, order, ghiChu}` — **rụng sạch** `hangMuc/donGia/khoiLuong…` |
| **C-4** | `fe-…/ContractDetailsTab.tsx:77-83` | 7 dòng `bundle.type === N &&` | **CÂM.** type 8/9 ⇒ không component nào render. |
| C-5 | `Application/Contracts/WorkflowAdminFeatures.cs:136` | `TypeLabels.GetValueOrDefault(d.ContractType, d.ContractType.ToString())` | **BÁN CÂM** — nhãn tụt về `"HopDongThauPhuNhanCongVatTu"` (tên enum, dính chữ) thay vì tiếng Việt. UI xấu, không nổ. |
| C-6 | `ContractDetailsFeatures.cs:434-462` | không `default` ⇒ `if (!removed) throw NotFoundException("ContractDetail")` | **TO TIẾNG NHƯNG SAI TỘI** — báo "không tìm thấy dòng chi tiết" trong khi lỗi thật là "loại HĐ chưa được khai". Người debug đi sai hướng. |
| C-7 | `DbInitializer.cs:1246-1247` | `type is DichVu or MuaBan or NguyenTacNCC or NguyenTacDichVu ? … : …` | Rơi về nhánh **có CCM** — **đúng ý** cho 2 loại Thầu phụ mới. Chỉ là seed demo. Không cần sửa, ghi để khỏi ai đó "sửa cho đủ". |

⇒ Spec khai **1** nhánh câm; đo được **4 nhánh câm + 1 bán câm + 1 sai-tội**. Câu ②.1 *"mỗi cái đã cháy thật"* đúng, nhưng danh sách **không đầy đủ**, và nó được trình bày như bản kiểm đã quét hết (`"Bốn cái BẪY phải né"`).

## 4. Acceptance W1

W1 có 5 mục acceptance. Chấm từng mục theo trục **đo HÌNH DẠNG ⟂ đo HÀNH VI**:

| # | Acceptance W1 | Đo gì | Chấm |
|---|---|---|---|
| A1 | `dotnet ef migrations add` sinh migration **RỖNG** | **HÌNH DẠNG** file sinh ra | ⚠️ YẾU + có tác dụng phụ |
| A2 | boot Dev **không nổ Permission FK** | hành vi, nhưng **sai chỗ** | ⚠️ YẾU |
| A3 | 2 leaf mới **hiện trên sidebar** | HÀNH VI thật | ✅ ĐẠT |
| A4 | pin chéo quy trình ⇒ **409** | hành vi, nhưng **PASS VACUOUS** | 🔴 HỎNG |
| A5 | test hiện có vẫn **697 PASS** | **SAI BY-CONSTRUCTION** | 🔴 HỎNG |

**A1 — yếu + đẻ rác.** Đo hình dạng của một artifact do chính mình sinh ra, rồi artifact đó phải xoá đi (`ef migrations remove`); quên xoá thì gotcha #17 (luật 3 file) kéo theo 3 file no-op vào commit. *Thay bằng:* `dotnet ef migrations has-pending-model-changes` — cùng câu hỏi, **không tạo file**, đọc bằng exit-code. Cộng thêm 1 phép đo HÀNH VI: ghi 1 HĐ `Type=8`, đọc lại, `Type == 8` (chứng round-trip int thật, không chỉ chứng "EF không thấy gì để đổi").

**A2 — đo lệch mục tiêu.** `MenuKeys.cs:69` khai rõ: *"`Ct_*` / `Pe_*` leaf CỐ Ý NGOÀI `All` — sinh động qua factory"*. Nghĩa là 12 key `Ct_*`/`Hdc_*` mới **không vào `All` ⇒ không sinh policy ⇒ không có FK để nổ**. Thứ duy nhất vào `All` là 2 key `AwV2_Hd*` (từ `Family.Members`). Vậy "boot không nổ FK" chỉ canh **2/14** key mới; 12 key kia thiếu row thì boot vẫn **xanh** còn sidebar thì **thiếu mục**. *Thay bằng:* dùng thẳng test đã có (xem §5) cho vế `All`, và tách riêng phép đếm menu-row `Ct_*`/`Hdc_*` = 7→9 bộ.

**A3 — GIỮ, có răng thật.** `fe-user/src/components/Layout.tsx:114` + `:186` tự khai cơ chế: `resolvePath` trả `null` ⇒ **`MenuLeaf` DROP IM LẶNG** (gotcha #50). Nên "hiện trên sidebar" đúng là phép đo phân biệt được. Chỉ cần siết 1 nấc: **bấm vào leaf → landing đúng `?type=8` / `?type=9` và danh sách trả 200**, vì `:163 if (!typeInt) return null` chỉ bảo vệ vế hiển thị.

**A4 — 🔴 PASS VACUOUS, đây là lỗi nặng nhất của khối acceptance.**
`ContractWorkflowGuards.cs:147-152`: khi loại HĐ **chưa có** trong `Family.Members` thì `expected is null` ⇒ **cũng ném 409**. Nghĩa là "pin chéo quy trình ⇒ 409" **cho kết quả XANH ở cả hai thế giới**: thế giới làm đúng, và thế giới **quên hẳn bước thêm 2 tuple vào `Members`**. Nó không phân biệt được gì.
*Thay bằng — bắt buộc có cặp âm/dương:*
- (dương) pin **ĐÚNG** mã của loại mới (vd `HD-TP-NCVT` cho `Type=8`) ⇒ **thành công 200/201**, `Contract.ApprovalWorkflowId` pin đúng;
- (âm) pin mã của loại **KHÁC** (vd `HD-GK`) cho `Type=8` ⇒ **409** với câu *"là quy trình duyệt của loại Hợp đồng KHÁC"* (`:157-160`), **KHÔNG** phải câu *"chưa có quy trình riêng trong họ"* (`:150-152`).
Phân biệt **hai câu 409 khác nhau** chính là chỗ acceptance cũ mù.

**A5 — 🔴 SAI BY-CONSTRUCTION, và nguy hiểm vì nó đẩy người làm về phía sai.**
`697` là số canonical đúng hiện tại (`docs/STATUS.md:6`, `test 680→697`) — không bác số. Bác **mệnh đề**: thêm 2 tuple vào `Family.Members` **BẮT BUỘC** làm đỏ các assert sau, vì chúng neo cứng số 7/17:

| File:line | Assert | Sau W1 |
|---|---|---|
| `AwV2DesignerMenuKeyTests.cs:132` | `family.Should().HaveCount(7, …)` | 9 ⇒ ĐỎ |
| `AwV2DesignerMenuKeyTests.cs:182` | `family.Should().HaveCount(7, …)` | 9 ⇒ ĐỎ |
| `AwV2DesignerMenuKeyTests.cs:204` | `awV2RowKeys.Should().HaveCount(17, …)` | 19 ⇒ ĐỎ |
| `AwV2DesignerMenuKeyTests.cs:232` | `children.Should().HaveCount(17, …)` | 19 ⇒ ĐỎ |
| `AwV2DesignerMenuKeyTests.cs:70-71` | `ContractWorkflowCodes` = 7 mã chép tay | phải +2 |
| `AwV2DesignerMenuKeyTests.cs:81` | `Expected7` (tên biến cũng phải đổi) | phải +2 |
| `AwV2DesignerMenuKeyTests.cs:87` | `ExpectedCount = "7"` | phải đổi |

3 test method dính: `MenuKeys_All_HasSevenContractDesignerKeys_AndKeepsTheTenLegacyOnes` (`:120`) · `SeedMenuTree_ContractDesignerLeaves_HaveRowsUnderApprovalWorkflowsV2_AndMatchAllBothWays` (`:163`) · `SeedMenuTree_AllDesignerLeaves_HaveDistinctLabelsAndDistinctOrders` (`:224`).

🔴 **Vì sao nguy hiểm chứ không chỉ sai:** người thi công cầm acceptance *"697 PASS"* mà thấy 3 test đỏ sẽ bị kéo về "giải pháp" làm chúng xanh lại — cách rẻ nhất là **không thêm 2 loại vào `Family.Members`** (giữ 7). Mà `Members` chính là nguồn by-construction của menu-key + policy + guard + message. Bỏ nó = giữ test xanh, mất luôn 4 thứ. Đây đúng khuôn Goodhart *"rời tập-đo"*.
*Thay bằng:* **"test = 697 + N, N khai trước; 3 method neo-số ở `AwV2DesignerMenuKeyTests` được phép sửa 7→9 / 17→19 và PHẢI sửa trong CÙNG commit với code (spec-change, `docs/rules.md §7`); 0 test bị XOÁ; 0 test bị `Skip`."** Khai rõ "được sửa cái nào" thì mới chặn được đường sửa bậy cái khác.

**Thiếu hẳn khỏi acceptance W1 (không có mục nào phủ):**
- `WorkflowPolicy.DefaultPolicyNameFor(Type=8/9) == "Standard"` — chính là bẫy CÂM #1 mà ②.1 gọi tên. Đề xuất: thêm 2 dòng `[InlineData]` vào `WorkflowPolicyTests.cs:150-156` (đã có sẵn khuôn Theory 7 dòng) ⇒ rẻ, test-before được.
- `ContractCodeGenerator` sinh token đúng cho 8/9 (bẫy C-1) — đã có `ContractCodeGeneratorTests.cs`.
- Bundle chi-tiết cho `Type=8` trả về **đúng list `ThauPhu`** (bẫy C-2) — chính là giả thiết "dùng chung `ThauPhuDetails`" mà ②.2(a) dựa vào.

## 5. Test canh menu-row

**CÓ, và nó có RĂNG THẬT.** Spec nhắc đúng địa chỉ. Đọc tận mắt `tests/SolutionErp.Infrastructure.Tests/Application/ContractCatalogTests.cs`:

```
:434  await InvokePrivateSeedAsync(db, "SeedMenuTreeAsync");
:435  await InvokePrivateSeedAsync(db, "SeedAdminPermissionsAsync", rm);
:444  var menuKeysInDb = await db.MenuItems.AsNoTracking().Select(m => m.Key).ToListAsync();
:445  MenuKeys.All.Except(menuKeysInDb).Should().BeEmpty(
:446      "mọi key trong MenuKeys.All phải có row MenuItem — thiếu row là Permission FK nổ ngay lần boot đầu");
```

Đây **không** phải test trên danh sách dựng tay: nó **chạy seeder thật** rồi đối chiếu `MenuKeys.All` với `MenuItems` **đã ghi vào DB**. Vì `MenuKeys.ContractWorkflowMenuKeys` (`:156`) sinh **by-construction** từ `Family.Members`, thêm 2 tuple là 2 key tự vào `All` ⇒ nếu quên seed 2 menu-row thì test này **ĐỎ ngay**, trước cả khi ai boot Dev.

Có test thứ hai phủ cùng chỗ, chặt hơn (đối chiếu **hai chiều**): `AwV2DesignerMenuKeyTests.cs:191` (`key ∈ MenuKeys.All ⇒ BẮT BUỘC có menu-row … ParentKey=…`) và `:208` (`awV2RowKeys.Except(MenuKeys.All).Should().BeEmpty()`).

⇒ **Trả lời lệnh (5): W1 RẺ HƠN spec nói ở đúng vế này.** ②.1 bẫy #4 mô tả "Permission FK nổ ngay lần boot đầu" như rủi ro sống, và A2 dựng nguyên một bước acceptance thủ công (boot Dev) để canh nó — trong khi **CI đã canh sẵn, tự động, ở 2 test độc lập**. Nên bỏ A2 dạng thủ công, thay bằng "2 test trên PASS".

🔴 **Nhưng phải khai kèm — cùng cái làm W1 rẻ đi lại làm nó đắt lên ở chỗ khác:** cơ chế by-construction khiến 2 key mới **tự động** vào `All`, mà `All` chính là thứ 3 method ở `AwV2DesignerMenuKeyTests` neo cứng số. Vậy **không tồn tại đường "thêm 2 loại mà 697 giữ nguyên"**. Rủi ro spec đánh giá thấp **không phải** FK boot (đã có lưới), mà là **chi phí spec-change trên test** — thứ acceptance A5 đang phủ nhận sự tồn tại.

## 6. VERDICT + finding đánh số

**VERDICT: PASS-WITH-FINDINGS — quyết định ②.2(a) chọn A1 GIỮ NGUYÊN, W1 phải sửa trước khi giao.**

Hai trụ đỡ quyết định A1 đều đứng vững sau khi tự đo: enum là `int` trần (§2) và họ `HD-*` nở by-construction từ `Members` (§1.3). Không có căn cứ nào để lật sang A2 (cờ biến-thể). Tất cả finding dưới đây là **lỗi của W1 (bản kê việc + acceptance)**, không phải lỗi của lựa chọn kiến trúc.

| # | Mức | Finding | Bằng chứng |
|---|---|---|---|
| **F-1** | 🔴 NẶNG | **A4 pass vacuous.** "Pin chéo ⇒ 409" xanh cả khi QUÊN thêm `Members` — cùng hàm ném 409 cho ca "loại ngoài họ". Acceptance mất khả năng phân biệt. | `ContractWorkflowGuards.cs:147-152` ⟂ `:157-160` |
| **F-2** | 🔴 NẶNG | **A5 sai by-construction.** "697 PASS" bất khả thi; 4 assert neo 7/17 + 3 hằng test buộc đỏ. Tệ hơn: nó thưởng cho cách sửa SAI (không đụng `Members`). | `AwV2DesignerMenuKeyTests.cs:132,182,204,232` + `:70-71,:81,:87`; canonical 697 = `docs/STATUS.md:6` |
| **F-3** | 🔴 NẶNG | **W1 bỏ sót trọn tầng chi-tiết per-loại**, đúng tầng mà "0 migration nhờ dùng chung `ThauPhuDetails`" phụ thuộc: 2 switch BE + 4 map FE × 2 app. Spec 0 hit chữ `ContractDetails`. | `ContractDetailsFeatures.cs:41-113`, `:434-462`; `ContractDetailsTab.tsx:33,:77-83,:517,:583-597` |
| **F-4** | 🟠 VỪA | **Đếm site thấp: 13 → đo được 17.** Thiếu #10-#17. | bảng §1.2 |
| **F-5** | 🟠 VỪA | **3 nhánh CÂM nữa không được khai**, trong đó `ContractCodeGenerator.cs:28 _ => "HĐ"` đẻ **mã HĐ sai RG-001 không hồi tố được** (seq đã cháy). ②.1 trình bày như đã quét hết ("Bốn cái BẪY"). | `ContractCodeGenerator.cs:28`; `ContractDetailsFeatures.cs:41`; `ContractDetailsTab.tsx:583` |
| **F-6** | 🟠 VỪA | **2 mirror FE của họ `HD-*` chép tay không nằm trong W1.** Một cái tự khai là bản chép ("chép TỪNG KÝ TỰ"). | `fe-admin/src/lib/contractWorkflowFamily.ts:46-52`; `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:59-65` (chú `:55-56`) |
| **F-7** | 🟡 NHẸ | **A2 canh sai chỗ** — 12/14 key mới ngoài `All` nên không có FK để nổ; vế `All` thì CI đã canh sẵn ⇒ bước boot thủ công thừa. | `MenuKeys.cs:69`; `ContractCatalogTests.cs:445` |
| **F-8** | 🟡 NHẸ | **A1 đo hình dạng + đẻ rác** (phải `migrations remove`, dính luật 3 file). Có lệnh chuẩn thay thế. | đề xuất `has-pending-model-changes` §4 |
| **F-9** | 🟡 NHẸ | **Bẫy CÂM #1 của chính ②.1 không có acceptance nào phủ.** Khuôn Theory 7 dòng đã sẵn, thêm 2 dòng là xong. | `WorkflowPolicyTests.cs:150-156` |
| **F-10** | ⚪ GHI CHÚ | **Control-âm của phép đo CHECK-constraint bị thoái hoá**: cả DB 0 check ⇒ "0 CHECK trên `Type`" đúng nhưng vô thông tin. Kết luận không đổi (kiểu cột `int` + 0 `HasConversion<string>` mới là bằng chứng). | sqlcmd: `CHECKS_ON_TYPE=0`, `CONTROL_TOTAL_CHECKS=0` |

**Việc phải làm trước khi W1 rời bàn:** sửa A4 thành cặp âm/dương phân biệt 2 câu 409 (F-1) · sửa A5 thành "697+N, khai N, cho phép sửa đúng 3 method neo-số, cấm xoá/skip" (F-2) · thêm tầng chi-tiết vào bản kê việc W1 (F-3) · thêm acceptance cho `DefaultPolicyNameFor` và cho token codegen (F-5, F-9).
