# LENS-3 ACCEPTANCE + VẬN HÀNH — review đối-kháng spec SOLTUON 12-08-2026 (YC-029)

Reviewer lane-3/3 (ensemble /fable-clone). Lăng-kính: **acceptance ô-nào-gãy-khi-chạy-thật** + **vận hành W5 delete-path**.

## Verdict

**FAIL** — 2 BLOCKER (tiền-đề spec SAI, làm đúng theo spec ⇒ đẻ regression nặng hơn bug đang chữa),
4 MAJOR, 4 MINOR. Không phải "sửa chữ" — W1 và W3 phải viết lại phần ② trước khi cho wave chạy.

## Findings (F-1..)

### F-1 🔴 BLOCKER — W1: "trang tự suy applicableType=3 theo path" LÀ SAI. Bỏ `type=3` ⇒ leaf 7 loại HĐ hiện quy trình **Duyệt NCC**

Spec §② Đ2+Đ3: *"WfView → `/contracts/workflow-matrix?ct=N` (BỎ `type=3` khỏi URL — trang tự suy
applicableType=3 theo path, khuôn alias-path khkk sẵn)"*.

Đo trên đĩa:
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:57` `const rawType = Number(searchParams.get('type'))`
- `:61` `const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1`

⇒ **KHÔNG có một dòng nào map pathname → applicableType.** `pathname` (`:56`) chỉ dùng để dựng link
"xem tất cả" (`:131` ``${pathname}?type=${typeInt}``). Bỏ `type` khỏi URL ⇒ `searchParams.get('type')`
= `null` ⇒ `Number(null)` = `0` ⇒ rơi vào **fallback `: 1`** = ApplicableType **1 (Duyệt NCC)**.
Hệ quả cụ thể: leaf "Luồng duyệt" của cả 7 loại HĐ gọi `/approval-workflows-v2?applicableType=1`
(`:67`) → hiện **quy trình PE**; rồi nhánh lọc `ct` mới (`code === 'HD-*'`) lọc trên tập PE → **0 dòng,
trang trắng**. Cả 2 nhánh đều tệ hơn bug A/B đang chữa, và bản "hiện quy trình PE" trông *hợp lý* nên
qua mắt được người click thử.

Control-dương cho claim "khuôn alias-path khkk sẵn": leaf KHKK **VẪN truyền type tường minh** —
`fe-user/src/components/Layout.tsx:86` `'/khkk/workflow-matrix?type=10&group=1'` và `:216`
``/khkk/workflow-matrix?type=10&group=${group}``. Tức alias-path KHKK **không** chứng minh khả năng
tự-suy; nó chứng minh điều NGƯỢC LẠI. Đây là lỗi khớp-khuôn-chéo (mượn UI của khuôn A, gán năng-lực
của khuôn B) — cùng lớp lỗi S187.

Thêm: `Layout.tsx:151` **hiện tại đã có `ct`** — nguyên văn
``/contracts/workflow-matrix?type=3&ct=${typeInt}``. Nên delta thật của W1 chỉ là (a) trang ĐỌC `ct`,
(b) accordion ưu tiên `ct`. Việc "bỏ `type=3`" là nửa RỦI RO và **không cần** cho cả bug A lẫn bug B.

**Đề nghị:** xoá mệnh lệnh "BỎ `type=3`" khỏi §②. Giữ URL `?type=3&ct=N`. Nếu vẫn muốn bỏ thì spec
phải giao thêm việc: thêm map `pathname → applicableType` trong `WorkflowMatrixViewPage` **và** sửa
`:131` (link "xem tất cả" đang tự re-add `?type=`) **và** đổi luôn leaf KHKK cho nhất quán — 3 việc,
không phải 0.

### F-2 🔴 BLOCKER — W3: `pendingMe` KHÔNG tồn tại phía BE Contract. Acceptance W3 là ô "gõ đúng chữ là đạt"

Spec §② Đ4-W3: *"(khuôn PE `:176`; BE param `pendingMe` sẵn)"*.

Đo: `grep -rn "pendingMe" src/Backend/SolutionErp.Application/Contracts/ src/Backend/SolutionErp.Api/Controllers/ContractsController.cs`
= **0 hit**. Danh sách tham số `ContractsController.cs:16-27` có `page, pageSize, search, sortDesc,
phase, supplierId, projectId, type` — **không có `pendingMe`**.
Control-dương (chứng thước không hỏng): cùng token có hit ở `ContractSigningPlansController.cs` (KHKK)
và `fe-user/src/components/Layout.tsx:176` (PE) ⇒ 0-hit phía Contract là **0 thật**.

Vì sao đây là BLOCKER chứ không phải "thiếu 1 dòng": ASP.NET Core **im lặng bỏ qua** query-param không
bind được ⇒ `/my-contracts?type=N&pendingMe=1` sẽ trả **TOÀN BỘ** HĐ loại N (không lọc đến-lượt-tôi),
**HTTP 200, không lỗi**. Trong khi acceptance W3(i) là `grep "pendingMe=1" Layout.tsx ≥1` — **gõ chuỗi
vào URL là xanh**. Đúng lớp bẫy S171: ô đo HÌNH-DẠNG-CHUỖI, không đo HÀNH-VI.

**Đề nghị:** W3 đổi nhãn từ "FE nhỏ" → **BE + FE**; §② phải giao rõ: thêm `pendingMe` vào
`ListContractsQuery` + handler (khuôn precompute V2 `ResolveV2InboxIdsAsync` phía PE), hoặc thay bằng
route `/inbox?type=N` nếu inbox đã có lọc loại. Acceptance thay ô grep bằng ô so-số:
`GET /api/contracts?type=N&pendingMe=1` trả **ít hơn** `GET /api/contracts?type=N` với user có ít nhất
1 HĐ không-đến-lượt (control-dương: user Admin/ngoài luồng phải ra **0 dòng**, không phải cả list).

### F-3 🟠 MAJOR — W5 mirror khuôn PE mà bỏ mất 3 trong 4 cột trụ của khuôn: đường-TÁCH, policy, và VẾT changelog

Khuôn PE (`PeSoftDeleteFeatures.cs`) không chỉ là "3 điều kiện" như spec §② trích (`:110-114`):
1. **TÁCH ĐƯỜNG có chủ đích** — `:25-29` nói thẳng: đường xóa nháp cũ **GIỮ NGUYÊN không policy**, vì
   gắn policy vào đó làm `Drafter` mất quyền xóa nháp của chính mình (*đo thật trên DB Dev: 11/13 role
   `CanDelete=0`*). Đường mới là **endpoint RIÊNG** mang `[Authorize(Policy = "PurchaseEvaluations.Delete")]`.
   Spec W5 lại đi hướng **NGƯỢC**: "nới delete-guard `ContractFeatures.cs:896-897`" = nhét cả 2 nghiệp
   vụ vào MỘT handler `DeleteContractCommand` — đúng thứ khuôn PE đã cân nhắc rồi loại bỏ.
2. **VẾT changelog** — `:147-158` ghi `ContractChangelog`-tương-đương với `Action=Delete`, `PhaseAtChange`,
   `UserId`, `ContextNote=lý do`. Spec W5 **không nhắc chữ changelog nào**. Đối tượng bị xóa ở đây là
   **hợp đồng** (chứng từ pháp lý) mà lại xóa không vết, trong khi phiếu PE thì có vết. Hạ tầng đã sẵn:
   `src/Backend/SolutionErp.Domain/Contracts/ContractChangelog.cs` tồn tại +
   `IApplicationDbContext.cs:40 DbSet<ContractChangelog> ContractChangelogs`.
3. **Bẫy thứ-tự T26** — `:127-144`: phải `Remove()` **TRƯỚC**, `Add(changelog)` **SAU**; đảo lại thì
   cascade client-side (`CascadeDeleteTiming.Immediate`) DETACH changelog ⇒ **ghi 0 row, im lặng, không
   lỗi**. Nếu W5 có changelog mà spec không chép cảnh báo này sang, wave sẽ dẫm lại đúng bug đã trả giá.
4. Chỉ có "3 rào" là được mirror.

**Đề nghị:** §② W5 viết lại theo khuôn TÁCH ĐƯỜNG: command + endpoint riêng
(`DeleteContractByApproverCommand`, `POST/DELETE /api/contracts/{id}/by-approver`) mang policy, GIỮ
`DeleteContractCommand` cũ nguyên vẹn cho nháp; + ghi `ContractChangelog` với đúng thứ tự Remove→Add
(chép nguyên chú thích T26); + acceptance thêm ô: sau khi xóa, `GET` lịch sử đổi của HĐ đó phải còn
**đúng 1 dòng** action Delete (đọc NGƯỢC từ DB — nếu đọc ra 0 dòng là đã dẫm T26).

### F-4 🟠 MAJOR — W5 chạm đường DELETE đang **không có policy per-action** (#82) mà acceptance không có ô authz nào

`src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:11-14` = `[ApiController] [Route] [Authorize]`
**trần** (không Policy). `:78-83` `[HttpDelete("{id:guid}")]` **không** có `[Authorize(Policy=...)]`.
Đối chứng ngay trong cùng file: màn "Đã xóa" (GET) lại tự khai *"policy root `Contracts.Read` (chặt hơn
`[Authorize]` trần của class)"* ⇒ tác giả cũ **biết** class-level là trần, và đã siết cho GET nhưng
**chưa siết cho DELETE**.

Nghĩa là hôm nay: **bất kỳ user đã đăng nhập nào** cũng xóa được HĐ ở phase < `DangInKy` (kể cả HĐ của
người khác — handler `ContractFeatures.cs:889-901` không đọc `ICurrentUser`, không kiểm chủ sở hữu).
W5 **nới** guard này ra phase `ChoDuyet/TraLai/TuChoi` ⇒ mở rộng đúng cái lỗ đó sang HĐ đang chạy duyệt.
Acceptance W5 (i)-(iv) **không có một ô nào** về authz.

**Đề nghị:** thêm ô acceptance test-before: (a) user thường (không role, không trong Cấp) `DELETE` HĐ
nháp của người khác ⇒ phải 403 (hôm nay 204 — ghi rõ đây là **sửa lỗ có sẵn**, không phải regression do
W5 đẻ ra); (b) đường xóa-bởi-approver có `[Authorize(Policy = "Contracts.Delete")]`; (c) grep chứng
policy đã gắn. Nếu owner muốn giữ nguyên hành vi cũ cho nháp thì phải khai **thành lời** trong spec
(accepted-risk), không để im lặng.

### F-5 🟠 MAJOR — W5 acceptance thiếu ca `DaPhatHanh(9)` — chỗ duy nhất mà "V2-aware" dễ mở nhầm

`ContractPhase`: `DangInKy=5 … DaPhatHanh=9, ChoDuyet=10, TraLai=98, TuChoi=99`
(`src/Backend/SolutionErp.Domain/Contracts/ContractPhase.cs`). Guard cũ `Phase >= DangInKy` chặn
**5→99** (chặn thừa V2, đúng như finding S160). Acceptance W5(i) liệt: TraLai/TuChoi cho xóa · ChoDuyet
có điều kiện · "V1 `Phase>=DangInKy` VẪN chặn".

Lỗ: `DaPhatHanh = 9` vừa nằm trong dải "V1 legacy ≥5" **vừa là terminal THÀNH CÔNG của V2** (chú thích
enum: *"last step done ──► DaPhatHanh (terminal + gen mã HĐ)"*). Một bản viết lại "V2-aware" kiểu
`if (v2) { allow ChoDuyet/TraLai/TuChoi/DangSoanThao } else { block >= DangInKy }` sẽ **không còn** ô
nào chặn V2-`DaPhatHanh` một cách tường minh — HĐ **đã duyệt, đã gen mã** có thể bị xóa. Không ô
acceptance nào bắt được.

**Đề nghị:** thêm ô bắt buộc: "HĐ V2 phase `DaPhatHanh(9)` (đã gen `MaHopDong`) ⇒ mọi đường xóa đều
409/403" + ô đối xứng "HĐ V1 phase `DangSoanThao(2)` vẫn xóa được như cũ" (chống vá quá tay).

### F-6 🟡 MINOR-nhưng-chắc-gãy — W1(iv): lệnh grep viết như trong spec **đo trượt** dưới ripgrep / `grep -E`

Acceptance W1(iv): `grep -rn "workflow-matrix?type=3" fe-user/src` = 0-hit + control `workflow-matrix?ct=` ≥1.

Chạy thật, cùng cây, cùng lúc:
- `grep -rn "workflow-matrix?type=3" fe-user/src` (BRE, `?` là ký tự thường) → **1 hit**
  `fe-user/src/components/Layout.tsx:151`.
- `rg -n "workflow-matrix?type=3" fe-user/src` → **0 hit** (rg mặc định regex: `x?` = "x có thể vắng"
  ⇒ mẫu đòi chuỗi `workflow-matritype=3`/`workflow-matrixtype=3`, không có thật).
- `rg -n "workflow-matrix?ct=" fe-user/src` → **0 hit** — tức control-dương cũng gãy theo.

⇒ Nếu wave dùng `rg` (mặc định của Grep-tool) hoặc `grep -E`, ô W1(iv) **xanh ngay trước khi sửa gì**.
May là control-dương cũng 0 ⇒ người cẩn thận sẽ thấy thước hỏng; nhưng spec đang mời gọi đọc "control
0-hit" thành "chưa sửa xong" thay vì "thước hỏng".

**Đề nghị:** ghi lệnh có `-F`: `grep -rnF 'workflow-matrix?type=3' fe-user/src` (hoặc escape `\?`), và
ghi rõ **kỳ vọng của control** (`≥1 ngay từ trước khi sửa` — vì `ct=` ĐÃ có ở `:151` hôm nay).

### F-7 🟠 MAJOR — W1 đẻ **bản sao thứ BA** của họ `HD-*` trong fe-user, và acceptance chỉ phủ **1/7** mã

Spec §②: *"map ContractType→mã: const cục bộ 7-entry — fe-user KHÔNG mirror lib fe-admin"*.
Sau W1 sẽ có 3 bản danh sách họ: BE `src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs`
(nguồn thật — seeder `DbInitializer.cs:775 foreach (…Family.Members)` sinh mã từ đây) ·
`fe-admin/src/lib/contractWorkflowFamily.ts` · const mới trong fe-user.

Lệch **CÂM**: sai 1 ký tự trong mã ⇒ `allWorkflows.filter(wf => wf.code === …)` ra **mảng rỗng** ⇒
trang trắng, trông y hệt "loại này chưa cấu hình quy trình". Chính lib fe-admin đã ghi cảnh báo này
(`contractWorkflowFamily.ts:2-8`) và còn ghi rõ bẫy chẻ chuỗi: *"`HdNTNCC` chẻ ra `NT-NCC` hay `N-TNCC`
là hai mã khác nhau"* (`:70-73`).

Acceptance W1(iii) chỉ đo **`ct=1` → `HD-TP`** = 1/7. Hai mã dễ sai nhất (`HD-NT-NCC`, `HD-NT-DV` —
token gạch nối) **không có ô nào**.

**Đề nghị:** đổi ô (iii) thành vòng 7 giá trị: với mọi `ct ∈ 1..7`, trang hiện **≥1** quy trình và
**0** dòng mã khác họ; ghi kèm control-dương `ct` rác (`ct=99`) ⇒ **không lọc** (giữ hành vi cũ, không
trang trắng) — đúng khuôn `group` rác đã chốt ở `WorkflowMatrixViewPage.tsx:82-83`.

### F-8 🟠 MAJOR — 9/20 ô acceptance là **hành-vi**, mà spec không ghi **đo ở đâu** khi CI hỏng P0 + deploy CHỜ

Ô hành-vi: W1(i)(ii)(iii) · W2(ii)(iii) · W3(ii) · W4(i)(ii) · W5(iii-render). Spec §① tự khai
*"DEPLOY CHỜ (CI runner workspace còn hỏng P0)"* ⇒ **không có prod để bấm**, và `npm run build` (ô
duy nhất chạy được) **không chứng minh gì** về hành vi. Đây đúng lớp S171: ô đọc thì hợp lý, tới lúc
chạy mới lộ là không có chỗ chạy ⇒ wave sẽ tick bằng suy luận.

Nấc đo local **có thật, rẻ** — nên ghi thẳng vào spec thành bước bắt buộc trước khi tick:
1. `dotnet run --project src/Backend/SolutionErp.Api` (LocalDB `SolutionErp_Dev`). Seeder chạy lúc
   khởi động và **idempotent per-code** (`DbInitializer.cs:762-764,775`) ⇒ 7 `HD-*` tự có,
   `IsUserSelectable = true` (`:797`) — thoả tiền-đề lọc `isUserSelectable: true` của trang
   (`WorkflowMatrixViewPage.tsx:67`). Sample cũ `QT-HD-V2-001` (`:257`) vẫn còn ⇒ ô "0 dòng
   QT-HD-V2-001" mới có ý nghĩa (nếu Dev sạch trơn thì ô đó xanh vì **vắng**, không phải vì **lọc**).
2. `npm run dev` ở `fe-user` (proxy `/api → localhost:5443`) — bấm đủ W1/W2/W3; `fe-admin` cho W4.
3. **Chuẩn bị DỮ LIỆU** (spec đang thiếu hẳn): W2(ii) cần ≥1 HĐ hiện trên cây GĐ3; W3(ii) cần ≥1 HĐ
   `ChoDuyet` mà user đăng nhập **đúng lượt** + ≥1 HĐ **không** đến lượt (nếu không có vế thứ hai thì
   ô "list khớp inbox" xanh cả khi lọc **không chạy** — xem F-2); W5(iii) cần 1 Cấp bật cờ
   `AllowApproverDelete` và 1 Cấp tắt.

**Đề nghị:** thêm mục "④ Nấc đo local" vào spec với 3 bước trên; ô nào không đo được local thì đánh
dấu **DEFERRED-TO-DEPLOY** công khai, KHÔNG tick.

### F-9 🟡 MINOR-cao — W2 sửa cặp hook/panel **dùng chung cho GĐ1/GĐ2/GĐ4** mà không có ô hồi-quy

Đo baseline (bất biến mirror còn NGUYÊN hôm nay — tiền-đề W2(iv) HỢP LỆ):
`fe-user|fe-admin/src/hooks/usePipelineStages.ts` = `1b9b5c61…52ff48` (khớp) ·
`fe-user|fe-admin/src/components/pipeline/PipelineTreePanel.tsx` = `4ffc96be…c6c334` (khớp).

Nhưng cặp này là cây toàn-trình 4 GĐ (S162), fe-admin **cũng** render nó trong khi fe-admin **không có**
`MyContractsPage` ⇒ tuỳ chọn `onOpenContract` phải **mặc định TẮT**. Acceptance W2 chỉ có SHA-pair +
build — cả hai đều xanh y nguyên nếu opt-in lỡ thành opt-out (SHA vẫn khớp vì sửa **cả 2 bản**!).

**Đề nghị:** thêm ô control-âm: "trên fe-admin (không truyền `onOpenContract`), click leaf GĐ3 vẫn
`navigate('/contracts/{id}')` như cũ" + ô "leaf GĐ1/GĐ2/GĐ4 hành vi không đổi ở CẢ 2 app".

### F-10 🟡 MINOR-cao — W5: đường xóa HĐ hiện **0 test**, và acceptance không pin số test / không nhắc STATUS

- `grep -rl "DeleteContract" tests/` → chỉ trúng **file .dll** trong `bin/obj`, **0 file nguồn** ⇒
  `DeleteContractCommand` chưa từng có test. File duy nhất lân cận là
  `tests/SolutionErp.Domain.Tests/Contracts/WorkflowPolicyTests.cs` (có nhắc `DangInKy`) nhưng nó test
  **policy chuyển phase**, không test guard xóa ⇒ **không có lưới nào** đỡ nếu W5 nới sai.
- Số canonical hiện tại: **680** (`docs/STATUS.md:6`, S187→S188 "test 668→680"). ⚠️ Dòng `:20` còn ghi
  **644** của khối cũ hơn — người đo vội dễ lấy nhầm; spec nên ghi rõ mốc lấy từ `:6`.
- Acceptance W5(i) chỉ nói "`dotnet test` PASS + test mới …" — không pin **680 → 680+N**, không nhắc
  cập nhật **DUY NHẤT** row test trong `docs/STATUS.md` (B1). Theo `docs/rules.md §7` guard mới =
  test-before BẮT BUỘC (spec có ghi ✔) nhưng "PASS" là ô 0-bit nếu số không tăng.

**Đề nghị:** ô (i) ghi rõ: "trước: 680 · sau: 680+N (liệt N ca)" + "cập nhật đúng 1 row test ở
`docs/STATUS.md`, không chép số ra chỗ khác".

### F-11 🟡 MINOR — W2 đổi trục điều hướng, thiếu ô **tương-thích-ngược deep-link**

W2 chuyển chọn HĐ sang `?id=` trong `/my-contracts`. Đường cũ `/contracts/{id}` đang là đích của
`usePipelineStages.ts:377` **và** của link/thông báo cũ (bookmark, notification). Acceptance W2 không có
ô nào chứng đường cũ còn sống.
**Đề nghị:** thêm ô: "dán thẳng `/contracts/{id}` vào thanh địa chỉ ⇒ vẫn mở detail như cũ (không 404,
không redirect mất id)".

### Held — 5 tiền-đề tôi cố bẻ mà KHÔNG gãy (ghi để wave khỏi đo lại)

| # | Claim spec | Phép đo | Kết |
|---|---|---|---|
| H-1 | Cặp mirror ×2 app đang khớp (tiền-đề W2-iv/W5-iii) | `sha256sum` 4 file + 4 file panel | **HELD** — `usePipelineStages` `1b9b5c61…` · `PipelineTreePanel` `4ffc96be…` · `ContractWorkflowPanel` `9cdf9ffe…` · `KhkkWorkflowPanel` `e7b294c9…`, từng cặp KHỚP |
| H-2 | W4 một dòng `findContractWfByTypeCode(code)` đúng chỗ | đọc `fe-admin/src/components/Layout.tsx:175-189` | **HELD** — chèn *sau `:188`* nằm TRONG khối `awV2Match`, ở đó `code = awV2Match[1]` = phần sau `AwV2_` (`HdTP`…), đúng thứ `findContractWfByTypeCode` tra (`contractWorkflowFamily.ts:69,77-80`). Ngoài khối là sai — spec nên ghi "trong khối `if (awV2Match)`" cho chắc |
| H-3 | Designer/route đã sẵn cho `:typeCode` họ HĐ | `App.tsx:80` route `:typeCode` + `ApprovalWorkflowsV2Page.tsx:39,314` gọi `findContractWfByTypeCode` | **HELD** — W4(ii) khả thi |
| H-4 | `view=approved` chữa được bug B (2 leaf cùng sáng) | đọc `queryMatches` `fe-user/Layout.tsx:350,368-374` | **HELD** — `view` KHÔNG nằm trong `TRANSIENT_QUERY_KEYS`, `phase` chỉ được bỏ qua khi **đích không ghim** ⇒ đích "Danh sách" `[type]` không còn khớp URL `[type,phase,view]`. Đúng khuôn PE `:180-185` |
| H-5 | W4 "17 leaf" | `MenuKeys.cs:118-119` (2 NCC) + `:129-136` (8 KHKK) + `:151` `AwV2_Hd{token}` × 7 thành viên họ | **HELD** — 2+8+7 = 17 |
| H-6 | Lead-note "soft hay hard delete?" | `AuditingInterceptor.cs:56-62` | **ĐÃ TRẢ LỜI, đừng để wave đi đo lại:** `Contract : AuditableEntity` (`Contract.cs:8`) ⇒ interceptor đổi `Deleted → Modified` + set `IsDeleted/DeletedAt/DeletedBy` ⇒ `db.Contracts.Remove` **là XÓA MỀM**, giống hệt PE. Màn `/contracts/deleted` an toàn. **Nhưng** vẫn dính bẫy T26 nếu W5 thêm changelog — xem F-3 |

## Đề nghị sửa spec

Theo thứ tự phải-làm-trước:

1. **§② Đ2+Đ3: xoá mệnh lệnh "BỎ `type=3` khỏi URL"** (F-1). Giữ `?type=3&ct=N`; delta của W1 =
   trang đọc `ct` + accordion ưu tiên `ct` + comment stale. Nếu owner vẫn muốn URL sạch thì spec phải
   liệt thêm 3 việc (map pathname→type · sửa link `:131` · đồng bộ leaf KHKK) và nhận rủi ro.
2. **§② Đ4-W3: bỏ chữ "BE param `pendingMe` sẵn"** (F-2) — đổi W3 thành wave **BE+FE**, hoặc chuyển
   sang `/inbox?type=N` nếu inbox đã lọc loại. Ô grep thay bằng ô so-số 2 chiều.
3. **§② W5: viết lại theo khuôn TÁCH ĐƯỜNG của PE** (F-3): command/endpoint riêng + policy
   (`Contracts.Delete`) + `ContractChangelog` với thứ tự `Remove()` → `Add()` (chép nguyên cảnh báo
   T26) + GIỮ `DeleteContractCommand` cũ cho nháp.
4. **§③ W5 thêm 3 ô:** authz (403 cho người ngoài luồng — F-4) · `DaPhatHanh(9)` vẫn chặn (F-5) ·
   changelog đọc-ngược ra đúng 1 dòng (F-3).
5. **§③ W1(iv): `grep -rnF` (hoặc `\?`)** + khai kỳ vọng control-dương là "≥1 **ngay từ trước** khi
   sửa" (F-6); **W1(iii) mở thành vòng `ct ∈ 1..7`** + ca `ct` rác (F-7).
6. **§ mới "④ Nấc đo local"** — `dotnet run` + `npm run dev` + 3 dòng chuẩn-bị-dữ-liệu; ô nào không đo
   được thì đánh **DEFERRED-TO-DEPLOY**, cấm tick (F-8).
7. **§③ W2 thêm ô control-âm fe-admin + ô deep-link `/contracts/{id}`** (F-9, F-11).
8. **§③ W5(i) pin số test 680 → 680+N + nhắc cập nhật đúng 1 row `docs/STATUS.md`** (F-10).
