# sub-reviewer — LANE C (anchor/claim vs ĐĨA) — S160 KHKK dry-run plan

> Lăng kính: verify ANCHOR/CLAIM vs ĐĨA. Đích: `spec-wave-w7-ban-cung-hdc-29-07-2026.md` + `spec-wave-w8-authz-29-07-2026.md` + `plan-cha-dry-run-toan-trinh-29-07-2026.md` §3-§5.
> Mọi số dưới đây tự đo lại từ đĩa/prod ngày 2026-07-29, KHÔNG chép từ spec.

## 0. VERDICT

**SUA-TRUOC-THI-CONG** — 3 HIGH (F-C1, F-C2, F-C3) đều nằm ở tiền đề dữ liệu/quyền của W7-W8: sửa 3 dòng spec là đủ, nhưng nếu thi công y nguyên thì W7 ra trang rỗng cho đúng những người phải bấm, và W8 khoá cửa đúng 2 vai đó.

- Anchor ĐO ĐƯỢC: **24/26 ĐÚNG** (2 sai: 1 claim param không tồn tại, 1 nhãn file sai).
- Baseline plan §3: **6/6 khớp** `docs/STATUS.md:6` + prod curl.

## 1. BẢNG ANCHOR ĐO ĐƯỢC

| # | Anchor spec | Phán | Bằng chứng đĩa |
|---|---|---|---|
| 1 | `ContractAttachment.cs:5-11` enum | ✅ ĐÚNG | enum `AttachmentPurpose` chiếm đúng :5-11 (DraftExport=1, ScannedSigned=2, SealedCopy=3, Other=99) ⇒ chèn `=4` an toàn |
| 2 | tiền lệ PE `ComparisonTable=4` | ✅ ĐÚNG | `PurchaseEvaluationAttachment.cs:10` |
| 3 | enum-extend là BẮT BUỘC (không phải tuỳ chọn) | ✅ ĐÚNG (spec chưa nói) | `ContractAttachmentFeatures.cs:51` `RuleFor(x => x.Purpose).IsInEnum()` ⇒ hôm nay `purpose=4` bị 400 |
| 4 | `DbInitializer.cs:1862-1867` loop 7-leaf | ✅ ĐÚNG | `:1862` `foreach (var code in MenuKeys.ContractTypeCodes)` … `:1867` `tree.Add(("Hdc_" + code, …))`; `MenuKeys.cs:34-35` = 7 mã |
| 5 | `ContractListItemDto` `:323-327` 12-field | ✅ ĐÚNG | projection `:323-327` liệt kê đúng **12** giá trị, KHÔNG có attachments |
| 6 | `MyContractsPage.tsx:29-46` | ✅ ĐÚNG (kết luận suy ra thì SAI — F-C1) | `:29` typeFilter · `:34` phaseFilter · `:35` showDeleted · `:45` gửi `type` **chỉ khi** `showDeleted` · `:60` lọc type CLIENT-side · `:44` `pageSize:100` |
| 7 | `fe-user/…/Layout.tsx:63-69` | ✅ ĐÚNG | đúng 7 dòng `Hdc_*` → `/coming-soon?stage=4&t=01..07` |
| 8 | mirror fe-admin | ✅ ĐÚNG + CẦN THẬT | `fe-admin/…/Layout.tsx:40-46` cũng 7 dòng; `:178-180` `isAdminHidden` chỉ chặn `key.startsWith('Ct_')` ⇒ `Hdc_*` KHÔNG bị ẩn |
| 9 | `ContractsController.cs:13` class-trần | ✅ ĐÚNG | `[Authorize]` tại `:13`; grep `Authorize` cả file = 3 hit (:13 class, :27 comment, :29 deleted) |
| 10 | `ContractsController.cs:16-24` "list params type/phase" | ❌ **SAI** | `:16-24` khai 7 param: page,pageSize,search,sortDesc,**phase**,supplierId,projectId — **KHÔNG có `type`** → F-C1 |
| 11 | `ContractsController.cs:28-29` tiền lệ policy | ✅ ĐÚNG | `:28` `[HttpGet("deleted")]` · `:29` `[Authorize(Policy = "Contracts.Read")]`; `type` chỉ có ở đây (`:31` + `ListDeletedContractsQuery` `ContractFeatures.cs:364`) |
| 12 | "22 endpoint ghi" | ✅ ĐÚNG (đếm được 22) | `[HttpPost|Put|Delete]` tại 46,53,61,68,75,85,110,123,130,137,144,151,158,165,172,179,186,193,200,207,214,221 = **22**, **0/22** có policy |
| 13 | `SuppliersController.cs:67-99` khuôn Master | ✅ ĐÚNG (khuôn là HỖN HỢP) | `:67`/`:78`/`:86`/`:99` `[Authorize(Policy="Suppliers.Update")]`; nhưng `:57` DELETE dùng `[Authorize(Roles="Admin,CatalogManager")]` ⇒ W8 chọn policy thuần cho DELETE là QUYẾT ĐỊNH, nên khai |
| 14 | `MenuKeys.cs:163-181` `Contracts` ∈ All | ✅ ĐÚNG | mảng `All` = `:163-181`, `Contracts` ở `:168`; `Actions` `:183` |
| 15 | `MenuKeys.cs:41` `KeHoachKyKet` const + "comment dặn tái dùng" + ngoài All | ✅ ĐÚNG | `:41` const; comment `:37-40` "CỐ Ý NGOÀI `All` … Wave-1 GĐ2 phải TÁI DÙNG/thay root này" |
| 16 | `ContractWorkflowService.cs:49-66` Reject-trước-guard | ✅ ĐÚNG | `:49` `if (decision == ApprovalDecision.Reject)` → set Phase → log → `SaveChanges` → `return` `:65`; trước đó `:44-46` chỉ gán biến, 0 guard |
| 17 | `ContractWorkflowService.cs:191-198` admin-override | ✅ ĐÚNG (điều kiện tới được thì SAI — F-C8) | `:191` `if (isAdmin) { contract.Phase = targetPhase; … return; }` |
| 18 | `ContractFeatures.cs:632-633` delete-guard numeric | ✅ ĐÚNG (phạm vi hụt — F-C8) | `:632` `if (entity.Phase >= ContractPhase.DangInKy)` |
| 19 | `ContractFeatures.cs:298` admin-bypass (list) | ✅ ĐÚNG | `:298` `if (!currentUser.Roles.Contains(AppRoles.Admin))` + `:302` IDOR where |
| 20 | `ContractFeatures.cs:440` admin-bypass (inbox) | ✅ ĐÚNG | `:440` `var eligiblePhases = isAdmin ? PhaseActorRoles.Keys.ToList() : …` |
| 21 | `ContractFeatures.cs:113-118` gen mã tại create + `:113-115` comment gap | ✅ ĐÚNG | `:113-115` "Trade-off: nếu HĐ bị TuChoi → mã bị 'wasted' (gap trong sequence). Acceptable"; `:118` `GenerateAsync(...)` |
| 22 | `ContractCodeGenerator.cs:25` MB | ✅ ĐÚNG | `ContractType.HopDongMuaBan => "MB"` |
| 23 | `ContractCodeGenerator.cs:33,:40-53` seq-per-prefix | ✅ ĐÚNG | `:33` `prefix = $"{scope}/{typeCode}/SOL&{supplierCode}"`; `:40-53` lookup/insert/increment/return trong tx SERIALIZABLE `:37` |
| 24 | `pe.ContractId` `:59-60,:145` idempotency | ✅ ĐÚNG | `CreateContractFromEvaluationFeatures.cs:59-60` `if (pe.ContractId is not null) throw ConflictException` · `:145` `pe.ContractId = contracts[0].Id` |
| 25 | `AuthorizePolicyRegressionTests` "ĐÃ TỒN TẠI" | ✅ CÓ THẬT (phủ sóng thì 0-bit — F-C5) | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs`; phép "policy-name có thật" `:120-143` **hardcode 4 action của `ApprovalWorkflowsV2Controller` `:123-129`** |
| 26 | plan §5 #1 nguồn "W3 `:362-365`" | ❌ **SAI FILE** | `spec-wave-w3-duyet-3-tram-29-07-2026.md` = **37 dòng**; đoạn thật ở `spec-ke-hoach-ky-ket-hd-28-07-2026.md:362-365` |

**Anchor spec-cũ khác trong plan — ĐÚNG HẾT:** `:27` = "OWNER CHỐT NHÃN @S159 … ranh GĐ2 b.7→12 · GĐ3 b.13→18 · GĐ4 b.19→21 upload-only · b.7 email BỎ (Q8 ĐÓNG)" ✅ · `:67-71` = "Rẽ nhánh b.12 — 4 đường ĐỀU CÓ SẴN … Q3 owner" ✅ · `:128` = row CẮT `ContractSigningPlanCodeSequences`, tái dùng `WorkflowAppCodeSequence` ✅ (đĩa: `IApplicationDbContext.cs:136` + dùng thật ở `LeaveOtApprovalFeatures.cs:35-39`).

## 2. FLAG F-C1..F-C12

### F-C1 [HIGH] `GET /api/contracts` KHÔNG nhận `type` — cả 7 leaf `Hdc_*` sẽ hiện CÙNG một danh sách
- Spec: W7 `:21` "route `/hard-copies?type=N` → list `GET /api/contracts?type=N&phase=9`" · `:27` anchor "list params `type`/`phase`" · `:32` acceptance "`?type=1&phase=9` → 200, mọi item `phase==9`".
- Đĩa: `ContractsController.cs:16-24` không khai `type`; handler `ContractFeatures.cs:305-307` chỉ lọc `Phase`/`SupplierId`/`ProjectId`. `type` CHỈ sống ở `/deleted` (`:31`).
- Model-binding ASP.NET bỏ qua param lạ ⇒ **200 + không lọc**. Acceptance `:32` **0-bit trên trục type** (đúng-bằng-xây-dựng).
- Prod đo 2026-07-29 (token admin, field `accessToken`): `GET /api/contracts?page=1&pageSize=1&type=1&phase=9` → **HTTP 200, `total:0`** — y hệt khi không truyền `type` (prod rỗng nên phép này còn không phân biệt nổi 2 thế giới).
- Khuôn được chỉ định (`MyContractsPage.tsx`) né bằng cách lọc CLIENT-side `:60` trên `pageSize:100` `:44` ⇒ copy nguyên khuôn thì "thầm đúng" nhưng trần 100 dòng/loại.
- Sửa: hoặc thêm `type` vào `ListContractsQuery` (BE, ~3 dòng), hoặc spec khai rõ "lọc client-side + trần 100"; acceptance đổi thành phép PHÂN BIỆT (2 loại HĐ khác nhau, gọi `type=1` chỉ trả loại 1).

### F-C2 [HIGH] Nguồn dữ liệu `phase=9` bị IDOR-filter chặn đúng những người phải thao tác
- Đĩa `ContractFeatures.cs:298-303`: non-admin chỉ thấy `DrafterUserId == userId || eligiblePhases.Contains(Phase)`.
- `GetEligiblePhases` `:334-350` liệt kê DangSoanThao/DangDamPhan/DangInKy/DangGopY/DangKiemTraCCM/DangTrinhKy/DangDongDau — **KHÔNG có `DaPhatHanh`(9)**, cũng **không có `ChoDuyet`(10)**.
- ⇒ Trang Bảng cứng (list `phase=9`) **RỖNG** với mọi non-admin không phải người soạn — gồm HrAdmin (đóng dấu b.20), Director/AuthorizedSigner (ký b.19), CostControl (b.21).
- Acceptance W7 `:32` chạy bằng token admin (bypass `:298`) ⇒ PASS mà không lộ. Thiếu hẳn ca "user KHÔNG phải drafter mở được trang".
- Cùng lớp ở inbox: `:440-452` lọc theo `PhaseActorRoles` (legacy-phase) ⇒ tập hiển thị của HĐ V2 chưa ai sửa. Plan §2 `:36` khai W7 chỉ cần W5/W6 "đẻ HĐ DaPhatHanh", KHÔNG khai phụ thuộc W6 phải mở lens `DaPhatHanh`/`ChoDuyet` ⇒ đồ thị phụ thuộc thiếu 1 cạnh.

### F-C3 [HIGH] Grant-list W8 bỏ sót đúng 2 vai mà W7 cần: `HrAdmin` + `AuthorizedSigner`
- W8 `:19` cấp `Contracts` Create/Update cho Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin (7 vai).
- W7 `:12-14` giao b.19 cho **BOD** (Director ∪ NĐUQ = `AuthorizedSigner`), b.20 cho **HRA/ISO**, b.21 cho **PB/BCH + CCM**.
- Đĩa: `AppRoles.cs:15` `AuthorizedSigner`, `:16` `HrAdmin` — cả 2 có thật, và chính hệ thống gán `AuthorizedSigner`→DangTrinhKy, `HrAdmin`→DangDongDau (`ContractFeatures.cs:348-349`) = đúng vai ký/đóng dấu.
- W8 `:18` gắn POST → `Contracts.Create`; upload là `[HttpPost("{id:guid}/attachments")]` (`ContractsController.cs:85`) ⇒ sau W8, HrAdmin/AuthorizedSigner **403 khi upload** = giết b.19/b.20 của W7 (gotcha #44 tái diễn — đúng rủi ro W8 `:33` tự nêu mà grant-list không đóng).
- Phụ: luật "theo verb" ở `:18` tự mâu thuẫn (POST→Create nhưng `POST {id}/transitions`→Update) ⇒ 8 POST `details/*` (`:123-165`) + POST comments (`:68`) + POST attachments (`:85`) bỏ ngỏ cho implementer đoán. Nên phát biểu theo NGỮ NGHĨA: chỉ `POST /api/contracts` (`:46`) là Create, còn lại Update.

### F-C4 [MEDIUM] Acceptance 403 của W8 không có chủ thể; tài khoản UAT duy nhất nằm TRONG tập được cấp
- W8 `:30` "user đã-cấp → 201; user chưa-cấp → 403" không nêu tài khoản. `AppRoles.cs:5-21` = 13 vai; cấp 7 ⇒ còn 6 (Finance/Accounting/Equipment/AuthorizedSigner/HrAdmin/CatalogManager). Tài khoản UAT sẵn có `nv.test` = Drafter ⇒ **thuộc tập được cấp** ⇒ vế ÂM không chạy được nếu không tạo tài khoản mới. Ghi tên tài khoản + vai vào acceptance.

### F-C5 [MEDIUM] Test mới W8 không khoá "policy-name có thật"; lưới hiện có 0-bit cho ContractsController
- `AuthorizePolicyRegressionTests:120-143` là phép duy nhất chặn bẫy "policy trỏ menu key ngoài `MenuKeys.All` ⇒ Program.cs không sinh ⇒ 500 lúc chạy" — nhưng `:123-129` hardcode 4 action của `ApprovalWorkflowsV2Controller`. ⇒ W8 `:27` "VẪN XANH" là **đúng mà vô nghĩa** cho wave này.
- W8 `:28` mô tả test mới chỉ kiểm "có `[Authorize(Policy=...)]`", thiếu vế `MenuKeys.All.Should().Contain(parts[0])` (`:138-139`) + `MenuKeys.Actions` (`:140`). Hôm nay `Contracts` ∈ All (`MenuKeys.cs:168`) nên an toàn, nhưng acceptance không khoá được điều đó cho lần sửa sau.

### F-C6 [MEDIUM] Badge "Đã ký GĐ" derive từ `ScannedSigned` = trùng nghĩa với chữ ký NCC (gotcha #71)
- Đĩa `ContractAttachment.cs:8`: `ScannedSigned = 2,   // Scan HĐ có chữ ký NCC ở phase DangInKy`.
- W7 `:12` gán chính value đó cho "b.19 Ký GĐ/NĐUQ … 'Đã ký GĐ' = has(ScannedSigned)" ⇒ file up theo nghĩa cũ (chữ ký NCC) làm badge GĐ sáng SAI, và không có cách phân biệt về sau.
- Bất đối xứng: W7 `:20` sẵn sàng đẻ `InitialSigned=4` cho ký-nháy b.17-18 nhưng lại tái dùng value mơ hồ cho b.19. Thêm `DirectorSigned=5` vẫn 0-mig.

### F-C7 [MEDIUM] "sửa DTO + projection" (số ít) — thực tế 3 site dựng `ContractListItemDto`
- `ContractFeatures.cs:323` (list) · `:401` (deleted) · `:454` (inbox). Record positional ⇒ compile-caught, KHÔNG im lặng; nhưng nếu 2 site kia điền `false` cho đủ chữ ký thì badge ở màn "Đã xóa"/inbox **luôn tắt** = sai-im-lặng. Ước lượng "BE nhỏ" (W7 `:20`) / "FE nặng + 1-2 BE" (plan §2 `:26`) hụt.

### F-C8 [MEDIUM] plan §4.4: đúng chữ, hụt phạm vi; và công thức admin-override không chạy cho HĐ đang `ChoDuyet`
- `ContractPhase.cs`: DangInKy=**5**, DaPhatHanh=**9**, ChoDuyet=**10**, TraLai=**98**, TuChoi=**99**. Guard `ContractFeatures.cs:632` `Phase >= DangInKy` so sánh SỐ ⇒ chặn xoá **mọi phiếu đã trình (10) / trả lại (98) / từ chối (99)**, không chỉ "HĐ test DaPhatHanh nằm lại" như §4.4 mô tả. Dry-run sẽ đẻ nhiều HĐ dở-dang không xoá được.
- Lối thoát §4.4 (`ContractWorkflowService.cs:191-198`) chỉ tới được nếu rơi khỏi các nhánh trước. Nhánh APPROVE `:92` `if (fromPhase == ChoDuyet && decision == Approve)` bắt TRƯỚC → `ApproveV2Async` `:100` ⇒ admin bấm "duyệt về Nháp" trên HĐ `ChoDuyet` sẽ **ĐẨY TỚI** chứ không lùi. Từ `DaPhatHanh`(9) thì `:92` không khớp ⇒ override chạy đúng. ⇒ công thức rollback đúng cho phase 9, **sai cho phase 10**.

### F-C9 [LOW-MED] plan §5 #7: đường-đảo "map MuaBan→NCC" không có căn cứ trong RG-001 v02
- Vế khẳng định của #7 ✅ ĐÚNG: `ContractCodeGenerator.cs:25` sinh "MB"; `src-RG-001-v02-ma-so-hd.txt:12-16` chỉ định đúng 4 viết tắt HĐTP/HĐGK/NCC/HĐDV — **không có MB**.
- Nhưng RG-001 `:33-42` + TABLE 3 `:57` có **format PO riêng** `A/PO/SOL&C/D`, và comment repo `ContractCodeGenerator.cs:18` tự khai "HĐ Mua bán: dùng PO format" ⇒ đường đảo đúng theo nguồn là **PO**, không phải NCC. Đảo sang NCC còn trộn chuỗi số với HĐ NCC thật (dùng chung prefix `{Project}/NCC/SOL&{NCC}` — `:33,:40-50`).

### F-C10 [LOW-MED] plan §5 #12 thiếu nghĩa vụ đồng bộ canonical mà chính mã dặn
- `MenuKeys.cs:37-40` + `:149-151` nói thẳng: thêm key vào `All` sẽ đổi 2 số canonical `Menu keys`/`Policies` ở `docs/STATUS.md` (Policies = |All| × |Actions|, DERIVED).
- Default #12 "All += KeHoachKyKet" không kèm bước cập nhật `docs/STATUS.md`; §7 `:85` chỉ nói "Menu/permission CHỈ qua seeder". ⇒ drift canonical ngay wave 1.

### F-C11 [LOW] plan §5 #1 anchor `W3 :362-365` trỏ ra ngoài file
- `spec-wave-w3-duyet-3-tram-29-07-2026.md` = **37 dòng** (`wc -l`). Văn bản thật ("2 trong 3 đường … anh phán **'PHÁ VỠ'** ở S155 ⇒ **W3 mặc định chỉ làm đường THƯỜNG**; 2 đường kia chờ anh xác nhận") nằm ở `spec-ke-hoach-ky-ket-hd-28-07-2026.md:362-365`. **Nội dung default #1 ĐÚNG** (không lấn quyền owner) — chỉ nhãn nguồn sai file.

### F-C12 [LOW] Acceptance grep của W7 có răng nhưng dễ Goodhart; note b.21 không có chốt máy
- Phép `grep "Hdc_" Layout.tsx | grep -c "coming-soon"` hiện = **7** ở CẢ 2 app (`fe-user:63-69`, `fe-admin:40-46`) ⇒ đích "=0" KHÔNG tautology ✅. Nhưng trỏ 7 leaf về BẤT KỲ chuỗi nào khác cũng ra 0 ⇒ nên kèm vế dương: 7 dòng chứa `/hard-copies?type=`.
- Note b.21: BE không ràng buộc gì ngoài `RuleFor(x => x.Purpose).IsInEnum()` (`ContractAttachmentFeatures.cs:51`); `Note` tự do ⇒ badge "Đã lưu bản cứng" bật kể cả note rỗng ⇒ default #9 (số bộ gốc / nơi lưu) không có ai canh. Nếu giữ option A thì acceptance phải là phép NGƯỜI đọc, đừng ghi như phép máy.

## 3. BẢNG 12-DEFAULT — default nào NGƯỢC ĐĨA?

| # | Phán | Bằng chứng |
|---|---|---|
| 1 | ✅ nội dung đúng · ❌ nhãn nguồn | F-C11 |
| 2 | ⚪ ngoài tầm đo lane C (chưa có bảng KHKK trên đĩa) | — |
| 3 | ⚪ ngoài tầm đo | — |
| 4 | ⚪ ngoài tầm đo | — |
| 5 | ⚪ ngoài tầm đo | — |
| 6 | ✅ ĐÚNG | `WorkflowAppCodeSequence` có thật (`IApplicationDbContext.cs:136`, dùng ở `LeaveOtApprovalFeatures.cs:35-39`); spec cũ `:128` đúng là row quyết định tái dùng |
| 7 | ✅ vế khẳng định · ⚠️ đường-đảo sai nguồn | F-C9 |
| 8 | ✅ ĐÚNG khả thi | enum `:5-11` + `IsInEnum` `:51` + tiền lệ PE `:10`; ⚠️ ngữ nghĩa value F-C6 |
| 9 | ✅ đúng "option A không có field" · ⚠️ 0 chốt máy | F-C12 |
| 10 | ✅ ĐÚNG | `ContractFeatures.cs:113-118` + comment gap `:113-115` |
| 11 | ✅ ĐÚNG số 22 + class-trần · ⚠️ grant-list thiếu 2 vai | F-C3 / F-C4 / F-C5 |
| 12 | ✅ tiền đề đúng · ⚠️ thiếu bước sync canonical | F-C10 |

**Không phát hiện default nào lấn quyền owner:** #1 giữ đúng chốt "PHÁ VỠ" S155 (spec cũ `:362-365`), #11 để O-C chờ anh gật (W8 `:12`, `:20`, plan `:90`), #8 để option B chờ anh. O-3 "chỗ hợp đồng cứ từ từ" được tôn trọng ở phần SỬA service.

## 4. BASELINE §3 — ĐO LẠI 6/6 KHỚP

| Claim plan §3 | Đo lại | Khớp |
|---|---|---|
| prod Contracts = 0 active + 0 deleted | curl admin 2026-07-29: `/api/contracts` → `total:0`; `/api/contracts/deleted` → `total:0` | ✅ |
| Mig 68 · 89 bảng · 562 test (45D+517I) | `docs/STATUS.md:6` | ✅ |
| Menu 142 (root 12) · 21 key `Ct_*` | `docs/STATUS.md:6` | ✅ |
| `Khkk_*` = 7 key (G1 + 6 leaf) | `DbInitializer.cs:1779` (G1) + `:1781-1786` (6 leaf) | ✅ |
| `Hdc_*` = 7 leaf per-ContractType | `DbInitializer.cs:1862-1867` × `MenuKeys.cs:34-35` (7 mã) | ✅ |
| bundle admin `D0sXA0fe`/`DWDbm5As` · user `Y6dW_5CM`/`6YIAufJR` | `docs/STATUS.md:6` | ✅ |
| `KeHoachKyKet` ngoài `All` ⇒ 0 policy | `MenuKeys.cs:41` vs `All :163-181` | ✅ |
| `scripts/backup-sql.ps1` (§4.6) | file tồn tại | ✅ |
| seed-permission ">1 site" (W8 `:19`) | 7 site `db.Permissions.Add` trong `DbInitializer.cs` (:2060,:2246,:2378,:2437,:2511,:2567,:2640) | ✅ (số thật = 7) |

## 5. LỆNH ĐÃ CHẠY

- `curl -X POST https://api.solutions.com.vn/api/auth/login` (admin, field `accessToken`, len 468) → 3 GET: `/api/contracts`, `/api/contracts/deleted`, `/api/contracts?type=1&phase=9` (HTTP 200, total 0).
- `grep -nE "\[Http(Post|Put|Delete|Patch)" ContractsController.cs` → 22 dòng.
- `grep -n "Authorize" ContractsController.cs` → 3 dòng (:13, :27 comment, :29).
- `grep -rn "new ContractListItemDto\(" src/Backend` → 3 site.
- `wc -l` toàn bộ spec run-dir + spec cũ S157 (496 dòng).
- `grep -rn "WorkflowAppCodeSequence" src/Backend` → có thật.
- `grep -n "public const string" AppRoles.cs` → 13 vai.
