# sub-style-fe-b3 — SPEC-LIST STYLE FE (YC-029 B3, /fable-real deep-pass)

> Vai: front-end-reviewer-style · Run: SOLTUON-12-08-2026 · Trạng thái: ĐANG GHI (append từng phần)
> Luật lõi owner S176: bản ĐÃ DEPLOY PROD = CHUẨN. Spec-list này = luật style ĐO ĐƯỢC cho worker Opus (B6) viết wave W1-W5 KHÔNG lệch khuôn.

## Khuôn nguồn (file nào là chuẩn cho việc gì)

Tất cả đều ĐÃ DEPLOY prod (origin/main, ngoài paths-ignore) — luật S176: đây là CHUẨN, lệch không có lời anh = FLAG.

| Việc trong wave | Khuôn ĐÃ CHẠY | Anchor |
|---|---|---|
| Host 3-panel + trục danh-tính URL (W2) | `KhkkListPage.tsx` | `fe-user/src/pages/khkk/KhkkListPage.tsx:156,171` — `h-[calc(100vh-4rem)]` + `lg:grid-cols-[400px_1fr_360px]`; trục URL đọc-only :62-68; override 3-trạng-thái :71-77; reset-in-render :81-88 |
| Panel danh sách + Select filter + banner thùng rác (W2 panel-3, Q2 Select) | `KhkkListPanel.tsx` | `fe-user/src/components/khkk/KhkkListPanel.tsx:326-360` — `<aside>` filter-block trên + thân cuộn; Select trạng-thái :352-359; banner đỏ deleted :347-350; ô Search icon-trong-Input :330-338 |
| Thanh nút đáy + nút Xóa + confirm (W5-FE) | `KhkkDetailContent.tsx` | `fe-user/src/components/khkk/KhkkDetailContent.tsx:1076-1137` — `sticky bottom-0 z-10 … border-t bg-white/95 backdrop-blur`; nút đỏ `bg-red-600` + `Trash2` + `window.confirm` :1087-1097 (KHÔNG Dialog); rào hiển-thị mirror BE-guard :1103-1119 |
| Mutation + invalidate + toast (W5-FE, W2) | `KhkkDetailContent.tsx` | :193-197 helper `invalidate()` (2 queryKey + `onChanged?.()`); `deletePlan` :316-320 (`onSuccess: toast + onDeleted?.()`; `onError: toast.error(getErrorMessage(e))`) |
| Đang-duyệt = ĐỔI ENDPOINT cùng trang (W3) | `PurchaseEvaluationsListPage.tsx` | `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:74-111` — queryKey gói MỌI trục lọc :75; nhánh `pendingMe` gọi `/inbox` rồi TỰ GÓI về shape Paged :91-99; nhánh deleted đứng TRƯỚC :80 |
| Cây toàn trình + opt-in callback (W2) | `PipelineTreePanel.tsx` + `usePipelineStages.ts` | `fe-user/src/components/pipeline/PipelineTreePanel.tsx:93-135` — props opt-in mỗi cái 1 khối comment lý-do + câu "không truyền ⇒ giữ nguyên"; `Pick<BuildStagesOptions,…>` :111. `fe-user/src/hooks/usePipelineStages.ts:228-230,244` — ternary `opts.onOpenX ? host-bắt : navigate-như-cũ`; GĐ3 navigate hiện tại :377 |
| Danh-tính menu ≠ bộ lọc (W1) | `fe-user/src/components/Layout.tsx` | :16-29 `TYPE_CODE_TO_INT`/`INT_TO_TYPE_CODE` (khuôn const-map); :141-154 nhánh `Ct_*`; :176-186 khuôn PE `view=approved` = KHOÁ ĐỊNH DANH trang bỏ qua; :350 `TRANSIENT_QUERY_KEYS`; :354-375 `queryMatches` pinned-by-target; accordion auto-expand :470-485 |
| Nhánh resolvePath Designer (W4) | `fe-admin/src/components/Layout.tsx` | :175-189 khối `awV2Match` — nhánh `KhkkN[1-8]` :188 là khuôn TRỰC TIẾP cho nhánh `findContractWfByTypeCode(code)`; :178 nhánh `Contract` = nhánh chết KHÔNG đụng |
| Empty-state panel giữa + hộp dashed panel phải | `KhkkListPage.tsx` | :195-203 `EmptyState icon/title/description` (import `@/components/EmptyState`); :257-269 `<aside>` hộp `border-dashed` khi chưa chọn |
| 404 thật ⟂ lỗi hệ thống (mọi detail-fetch mới) | `KhkkListPage.tsx` | :236-246 — `detail.isError && !isNotFound(detail.error)` ⇒ câu "Hệ thống đang chậm…" + nút thử lại; còn lại "Không tìm thấy phiếu." (YC-028(5) S189, 5 site ×2 app) |

## SPEC-LIST STYLE FE (đánh số SF-1..)

Mỗi luật = ĐO ĐƯỢC (grep/SHA/eslint). "Khuôn" = file ĐÃ DEPLOY prod. Worker lệch luật nào phải kèm trích lời anh cho phép — không có = FLAG khi review.

### A. Comment tự-khai (đặc sản repo — máy ESLint MÙ trục này)

- **SF-1 — Khối sửa lớn/file mới PHẢI mang comment header tự-khai:** `[W<n> S190 · YC-029]` + vai trò + quyết-định owner (nguyên văn khi có) + trade-off/mitigation ĐÁNH SỐ. Khuôn: `fe-user/src/components/khkk/KhkkListPanel.tsx:1-24` (M-1/M-2/B1/R-11 — mỗi mitigation nêu VÌ SAO + hệ-quả nếu bỏ), `KhkkListPage.tsx:9-26` (QĐ-1/M-4/R-5), `fe-admin/src/lib/contractWorkflowFamily.ts:6-18` (khối "VÌ SAO LÀ FILE RIÊNG"). Đo: `grep -n "W[1-5]\b.*S190\|YC-029" <file-wave>` ≥1 per file sửa.
- **SF-2 — Sửa hành-vi ⇒ LẬT comment cũ cùng diff; gỡ UI/code ⇒ để bia-mộ `🧊`:** khuôn bia-mộ `KhkkListPage.tsx:161-163` (chip đã gỡ + lời anh "Bỏ mấy dòng này đi"), `KhkkWorkflowPanel.tsx:117-118` (nút rời đi đâu). W1(d) chính là trả nợ luật này: comment `fe-user/Layout.tsx:141-142` "HĐ chung 1 bộ workflow V2" nay SAI (@S188 7 bộ per-loại) — sửa nhánh WfView mà không lật câu này = đẻ stale-comment mới. Đo: `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = 0 sau W1.
- **SF-3 — Comment nói WHY, trỏ anchor chéo dạng `file.tsx:line`; vùng hay trượt số dòng thì ghi "grep chuỗi X — đừng neo số dòng"** (khuôn `fe-user/Layout.tsx:202-207`). KHÔNG comment kể-lại code.

### B. Đặt tên

- **SF-4 — Callback opt-in mở leaf GĐ3 tại-chỗ đặt tên `onOpenContract`** — đúng họ `onOpenKhkk`/`onOpenPe`, FLAT trong `BuildStagesOptions` (KHÔNG bịa tầng lồng — F-4 lens-1). Site tiêu thụ = ternary y khuôn: `usePipelineStages.ts:228-230` (`opts.onOpenPe ? () => opts.onOpenPe!(id) : () => navigate(…)`) và `:244`; chỗ cắm GĐ3 = `:377`. `PipelineTreePanel.tsx:111` nới `Pick<BuildStagesOptions, …>` thêm `'onOpenContract'`. JSDoc trên prop theo khuôn `:109-110`: nêu đối-xứng họ + câu "host không truyền ⇒ giữ nguyên".
- **SF-5 — Const map module-level = SCREAMING_SNAKE `Record<>` + comment khai NGUỒN mirror**, đặt sau imports/trước component: khuôn `TYPE_CODE_TO_INT`/`INT_TO_TYPE_CODE` (`fe-user/Layout.tsx:16-29`, reverse qua `Object.fromEntries`), `PE_CODE_TO_INT :48` (comment "mirror BE … enum"), `FILTER_TO_PHASE` (`KhkkListPage.tsx:48-55`). Map W1(b) ContractType→mã WF: tên theo họ `INT_TO_*` (vd `CT_INT_TO_WF_CODE: Record<number, string>`), 7 entry chép TỪNG KÝ TỰ từ `ContractApprovalWorkflowFamily.cs:33-42` (cẩn thận token gạch-nối `HD-NT-NCC`/`HD-NT-DV`), comment tự khai "bản-sao-thứ-3 của họ (F-7)" + trỏ 2 bản kia (`.cs` + `fe-admin/src/lib/contractWorkflowFamily.ts`). 🔴 CẤM "vá" `contractWorkflowFamily.ts` thành mirror ×2 app — header `:16-18` cấm tường minh (fe-admin ONLY có chủ đích).
- **SF-6 — Named export `export function X()`** cho page/panel/component (repo-wide; CLAUDE.md §FE: không default trừ `App`). Đo: `grep -c "export default" <file-wave>` = 0.

### C. TanStack Query

- **SF-7 — Mọi biến queryFn đọc PHẢI vào queryKey — THEO SHAPE KEY ĐANG CÓ của file:** dạng positional `['my-contracts', typeFilter, phaseFilter, showDeleted]` (`MyContractsPage.tsx:47`) ⟂ dạng object-gói `['pe-list', {…}]` (`PurchaseEvaluationsListPage.tsx:75`), `['khkk-list', {…}]` (`KhkkListPanel.tsx:247`). Thêm trục (`pendingMe`, `search` server-side) = nối vào shape sẵn, KHÔNG đổi shape key cũ và KHÔNG tái dùng key của trang khác (bài S159 — `usePipelineStages.ts:88-89`: "đổi shape cache dùng chung sẽ vỡ trang khác").
- **SF-8 — Ngoại-lệ CÓ KHUÔN: lọc lát-cắt client trên payload đã tải trọn ⇒ KHÔNG thêm trục vào queryKey + PHẢI comment lý-do** — khuôn `WorkflowMatrixViewPage.tsx:88-95` (`group` không vào key: "thêm vào sẽ đẻ 8 lần gọi mạng cho cùng một payload"); khớp bằng `code`, không bằng tên/thứ tự (:92-95). W1(b) `ct` đi ĐÚNG khuôn này.
- **SF-9 — Detail-query gate `enabled: !!selectedId`** (`KhkkListPage.tsx:92-96`, `MyContractsPage.tsx:67-71`); query theo quyền gate `enabled: canX` (`usePipelineStages.ts:102`).
- **SF-10 — Mutation khuôn 3-mảnh:** `onSuccess: () => { toast.success('<câu VN>'); <invalidate()/callback host> }` + `onError: (e) => toast.error(getErrorMessage(e))` — 10/10 mutation `KhkkDetailContent.tsx:243-320` cùng dạng. Nút Xóa W5 trong `ContractDetailContent`: bộ invalidate lấy của CHÍNH FILE (khuôn `transition` `:57-61`: `['contract', c.id]` + `['my-contracts']` + `['inbox']`); đường về host qua callback optional `onDeleted?.()` khuôn `deletePlan :316-320` + JSDoc khuôn `KhkkWorkflowPanel.tsx:96-97`; host clear chọn khuôn `KhkkListPage.tsx:248` (`onDeleted={() => setId(null)}`).
- **SF-11 — "Đang duyệt" = ĐỔI ENDPOINT trong cùng queryFn (W3):** khuôn `PurchaseEvaluationsListPage.tsx:80-99` — nhánh `deleted` đứng TRƯỚC `pendingMe` (2 view loại trừ, comment :77-79); nhánh `pendingMe` gọi endpoint khác (`/inbox`) rồi TỰ GÓI kết quả về shape chung `{ items, total, page, pageSize }` (:98) để phần render không tách nhánh.

### D. shadcn/ui idiom

- **SF-12 — Empty-state 3 khuôn, dùng đúng chỗ:** panel giữa = `<EmptyState icon={} title description>` (`KhkkListPage.tsx:195-203`, `MyContractsPage.tsx:220-226`); panel phải chưa-chọn = hộp `border-dashed … text-center text-sm text-slate-500` (`KhkkListPage.tsx:257-269`); danh-sách rỗng phải TÁCH "rỗng-vì-lọc ⟂ rỗng-vì-chưa-có" thành 2 câu + đếm phần đang bị lọc khuất (`WorkflowMatrixViewPage.tsx:152-166`).
- **SF-13 — Badge/chip: full class string literal (Pattern 14 Tailwind JIT — `types/khkk.ts:57`), tra qua `Record<PhaseValue, string>` (`KHKK_PHASE_BADGE`); thang chữ 13/11/10, KHÔNG đẻ nấc mới** (bài `KhkkListPanel.tsx:155-158` gom nấc thứ 4 `text-[9px]` về 3 nấc). Chip "đang lọc theo X" đặt trong `PageHeader` title khuôn `WorkflowMatrixViewPage.tsx:110-115` + action "Xem tất cả" `:127-136`.
- **SF-14 — Select filter:** shadcn `Select`, option đầu `value=""` = "Tất cả …", `aria-label`, onChange ép kiểu qua `Number()` cast về `XValue` (`KhkkListPanel.tsx:352-359`). Q2 Select loại HĐ trên đầu panel cây W2: đặt trong khối filter TRÊN CÙNG panel trái (khuôn vị-trí `KhkkListPanel.tsx:326-338`: `<aside>` → khối `border-b p-3` chứa Search rồi Select).
- **SF-15 — 3-panel host:** `flex h-[calc(100vh-4rem)] flex-col` + header `shrink-0 border-b px-6 py-3` + `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]` (`KhkkListPage.tsx:156-171`). W2 đưa MyContractsPage 320px→400px cột trái (acceptance W2-i grep `grid-cols-[400px`). Hành-vi theo bề rộng: `window.matchMedia('(min-width: 1024px)')` — desktop chọn-tại-chỗ, `<lg` navigate trang đầy đủ (`KhkkListPage.tsx:127-131`, `MyContractsPage.tsx:97-103`).
- **SF-16 — Thanh nút đáy + nút Xóa (W5-FE):** port `KhkkDetailContent.tsx:1076-1137` — `{!<điều-kiện> && …}` bao ngoài; `<div className="sticky bottom-0 z-10 flex flex-wrap items-center gap-3 border-t border-slate-200 bg-white/95 px-5 py-3 backdrop-blur">`; nút Xóa = `<button type="button">` đỏ `bg-red-600 hover:bg-red-700 disabled:opacity-50` + `<Trash2 className="h-3.5 w-3.5" />` + nhãn pending "Đang xóa…"/"Xóa phiếu". **Confirm = `window.confirm(...)` với message chứa mã phiếu + đích đến** (":1090 — `Xóa phiếu ${mã}? Phiếu chuyển sang mục "Đã xóa".`") — 🔴 KHÔNG dựng Dialog confirm mới (Dialog xóa trong `KhkkWorkflowPanel.tsx:742-756` là đường CŨ đã gỡ trigger @S175 — xem Bẫy-1). Điều kiện hiện nút tính trước thành biến tên rõ (phase ∈ allow-list ∧ Drafter/Admin); khi có nút bị chặn-mềm thì nói lý-do qua `title` (khuôn rào hiển-thị `:1103-1125`).

### E. Kỷ luật mirror-pair

- **SF-17 — File đụng trong wave thuộc cặp SHA256-identical:** `hooks/usePipelineStages.ts` (baseline `1b9b5c61…`) · `components/pipeline/PipelineTreePanel.tsx` (`4ffc96be…`) · `components/contracts/ContractDetailContent.tsx` (`cd81508f…` — ĐO 2026-08-12: identical 403/403 dòng nhưng **CHƯA tự khai MIRROR ở header**; W5 sửa file này thì THÊM dòng tự-khai theo khuôn `KhkkDetailContent.tsx:2`, cùng bytes 2 bên) · `PipelineStageFolders.tsx` (`a2d486c5…`) nếu bị đụng. Luật thi-công: **một diff viết một lần, apply 2 bản trong CÙNG commit**; đo sau wave bằng `sha256sum` từng cặp (checklist C-3).
- **SF-18 — KHÔNG-CẶP CÓ CHỦ ĐÍCH, CẤM mirror bừa:** `Layout.tsx` mỗi app một bản (fe-admin không nới `Khkk_*`/`Pe_*_WfView` — khai tại `fe-admin/Layout.tsx:138-142,185-187`; fe-user không có `AwV2_*`); `MyContractsPage.tsx` + `WorkflowMatrixViewPage.tsx` fe-user-only; `contractWorkflowFamily.ts` fe-admin-only (`:16-18`). Dương-giả nếu bắt 2 Layout giống nhau.
- **SF-19 — Lệch-có-chủ-đích phải KHAI tại chỗ bằng khuôn "🔴 CỐ Ý KHÔNG … (lý-do + ngày/người chốt)"** — mẫu `fe-user/Layout.tsx:209-212`, `fe-admin/Layout.tsx:37-39`, `KhkkListPage.tsx:253-256` (QĐ-2). Câu tự-khai trong file mirror phải Y HỆT BYTES 2 bên — chấp nhận đọc "lệch ngữ" ở 1 app (vd cả 2 bản cùng ghi "với fe-admin counterpart"), CẤM sửa câu 1 bên cho xuôi tai (phá SHA).

### F. TS idiom repo

- **SF-20 — Enum-cấm (gotcha #3):** const object `as const` + `export type XValue = (typeof X)[keyof typeof X]` + nhãn `Record<XValue, string>` (`types/khkk.ts:40-55`). Không `enum`. Giá trị bắt đầu từ 1 (0 falsy JS — `:39`).
- **SF-21 — URL param số phải đỡ rác:** miền hữu hạn ⇒ clamp regex (`/^[1-8]$/` `KhkkListPage.tsx:67`, `WorkflowMatrixViewPage.tsx:84-86`; W1(b) ct dùng `/^[1-7]$/`) hoặc NaN-guard `Number.isFinite` (`MyContractsPage.tsx:30-36`, nguồn `HardCopiesPage.tsx:67-68` — "dùng lại nguyên, không đẻ khuôn thứ 2"). Rác ⇒ `null` GIỮ HÀNH-VI CŨ, không throw/redirect. Param axios optional: `?? undefined` để axios bỏ hẳn key (`MyContractsPage.tsx:59-60`).

### G. Ghim per-wave

- **SF-22 (W4):** nhánh mới đặt TRONG `awV2Match`, SAU nhánh `KhkkN[1-8]` (`fe-admin/Layout.tsx:188`), viết y khuôn khối `:181-188`: comment `[tag]` + "chỗ thứ 4 Pattern 16-bis" + gotcha #50 + return template route. KHÔNG đụng nhánh `code === 'Contract'` `:178` (nhánh chết 0 seed — spec H-2/H-3). Import named từ `@/lib/contractWorkflowFamily`.
- **SF-23 (W1):** `&view=approved` = KHOÁ ĐỊNH DANH trang bỏ qua — bê nguyên NGHĨA từ khuôn PE + giữ khối comment giải-thích (`fe-user/Layout.tsx:180-185`: vì sao `phase` không làm danh-tính được). 🔴 KHÔNG thêm `ct`/`view`/`pendingMe` vào `TRANSIENT_QUERY_KEYS` (`:350`) — chúng là danh-tính, vào TRANSIENT là leaf sáng nhầm (bug S155). Trang KHÔNG ghi ngược `ct`/`type` vào URL (M-4 `KhkkListPage.tsx:17-21` — chỉ `id` được ghi).
- **SF-24 (W2):** mọi prop/opts mới = opt-in default-TẮT + JSDoc có câu "Không truyền ⇒ giữ nguyên hành vi cũ" — 5 mẫu liền nhau `PipelineTreePanel.tsx:106-135` (`stage2Options`/`bodyMaxHClass`/`hideHeader`/`hideSearch`/`approvedPeOnly`). Control-âm fe-admin (không truyền `onOpenContract` ⇒ navigate cũ) là acceptance W2-v.
- **SF-25 (W2/W3):** đổi tầng lọc (client→server) phải GỠ vế client cũ + comment lật khai "vế X ĐÃ GỠ — BE lọc rồi; giữ lại = lọc 2 lần + che lỗi server" (khuôn W7 S187 `MyContractsPage.tsx:54-59,74-76` + cập deps useMemo `:87-88`). Chỗ CHỌN client-side (W3 lọc `type` trên inbox) phải comment khai lệch + tag carry (`[carry:w7b-inbox-param]` — spec W3).

## Bẫy style đã thấy trong repo (đã ĐO, không phải giả-định)

1. **Nút Xóa trong `KhkkWorkflowPanel` = xác-chết-còn-máy:** mutation `removePlan :231-237` + Dialog `:742-756` còn nguyên nhưng trigger ĐÃ GỠ @S175 (bia-mộ `:117-118`; `:438-440` khai "đã rời → thanh nút đáy"). v1 spec từng trỏ nhầm vào đây làm bằng-chứng — port khuôn xóa từ file này là bê CODE CHẾT + sai cả kiểu confirm (Dialog vs `window.confirm`).
2. **Mirror-header "lệch ngữ" là ĐÚNG:** `fe-user/pages/khkk/KhkkListPage.tsx:5` ghi "MIRROR … với fe-admin counterpart" và bản fe-admin cũng ghi Y HỆT (vì phải cùng bytes). Ai "sửa câu cho đúng" một bên là phá SHA-pair ngay lập tức.
3. **Nhánh `code === 'Contract'`** (`fe-admin/Layout.tsx:178`) trông như chỗ nối tự nhiên cho W4 — nhưng là nhánh CHẾT (0 seed, spec H-2/H-3 HELD). Nối vào = sửa nhánh không ai gọi, leaf mới vẫn DROP.
4. **Nhét key lọc mới vào `TRANSIENT_QUERY_KEYS` cho "đỡ mất highlight"** — làm leaf ghim key đó sáng nhầm; `queryMatches` đã có cơ chế pinned-by-target (`fe-user/Layout.tsx:358-369`, bài S155) — đừng tự chế thêm.
5. **Đổi shape/tái dùng queryKey trang khác** — `usePipelineStages.ts:88-89` khai thẳng bài S159: index cây phải đặt key MỚI (`pipeline-khkk-index`) thay vì mượn `khkk-list`.
6. **Tailwind JIT nội-suy class** (`bg-${color}-100`) = class không được generate — Pattern 14: full string literal trong `Record` (`types/khkk.ts:57`).
7. **`Number(rawParam)` không guard** ⇒ axios gửi `?type=NaN`, BE bind `ContractType?` → 400 (`MyContractsPage.tsx:30-33`). Cả 2 khuôn đỡ đã có sẵn (SF-21) — đừng đẻ khuôn thứ 3.
8. **resolvePath thiếu nhánh = leaf DROP IM LẶNG** (gotcha #50, Pattern 16-bis "chỗ thứ 4") — không đỏ build, không console error; chỉ thấy khi đếm leaf sidebar (acceptance W4-i đếm 17).
9. **Bê lời khai chưa chứng minh khi bê khuôn:** bài S176 banner PE "không còn tính vào lũy kế ngân sách" — KHKK cố ý CẮT vế đó vì `grep 'lũy kế'` trong KHKK = 0 hit (`KhkkListPanel.tsx:345-346`). W2/W5 bê banner/câu chữ nào thì kiểm từng MỆNH-ĐỀ SỰ-THẬT trong câu có đúng ở module HĐ không.
10. **Comment stale sau khi đổi hành-vi** — chính W1(d) sinh ra để trả nợ 1 ca (`Layout.tsx:141-142`); đừng vừa trả xong lại vay ca mới (SF-2).

## Checklist style ĐO ĐƯỢC cho worker

Chạy từ repo-root sau MỖI wave (C-1/C-2 bắt buộc trước khi khai xong wave):

- **C-1 máy trước:** `cd fe-user && npx eslint . ` PASS · W4/W2 thêm `cd fe-admin && npx eslint .` PASS (máy = recommended + react-hooks + react-refresh — `fe-user/eslint.config.js:12-17`; các trục A/B/D/E máy MÙ ⇒ mới cần list này).
- **C-2 build:** `npm run build` ở app bị đụng PASS (acceptance mỗi wave đã pin).
- **C-3 mirror (sau W2 + W5):**
  ```bash
  for f in hooks/usePipelineStages.ts components/pipeline/PipelineTreePanel.tsx \
           components/pipeline/PipelineStageFolders.tsx components/contracts/ContractDetailContent.tsx; do
    a=$(sha256sum "fe-admin/src/$f"|cut -c1-12); u=$(sha256sum "fe-user/src/$f"|cut -c1-12)
    [ "$a" = "$u" ] && echo "OK $f" || echo "LECH $f $a≠$u"; done   # 4/4 OK
  ```
- **C-4 named-export:** `grep -c "export default" <mỗi file wave>` = 0.
- **C-5 comment tự-khai:** `grep -ln "YC-029\|S190" <danh sách file sửa>` = đủ 100% file sửa.
- **C-6 stale-comment W1(d):** `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = 0.
- **C-7 danh-tính không rơi vào TRANSIENT:** dòng `TRANSIENT_QUERY_KEYS` (`grep -n "TRANSIENT_QUERY_KEYS = " fe-user/src/components/Layout.tsx`) KHÔNG chứa `ct`/`view`/`pendingMe`.
- **C-8 nhánh ct W1:** `grep -nF "get('ct')" fe-user/src/components/Layout.tsx` ≥1 · `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` ≥1 (spec W1-iv).
- **C-9 vòng 7 mã W1(b):** `grep -c "'HD-" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` = 7 và có đủ `HD-TP|HD-GK|HD-NCC|HD-DV|HD-MB|HD-NT-NCC|HD-NT-DV` khớp từng ký tự `ContractApprovalWorkflowFamily.cs:33-42`.
- **C-10 opt-in control-âm W2:** `grep -rn "onOpenContract" fe-admin/src/pages` = 0 (fe-admin không host nào truyền — định nghĩa trong hook/panel mirror thì CÓ, site truyền thì KHÔNG).
- **C-11 hoán-đổi W2:** `grep -F "grid-cols-[400px" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1 (spec W2-i).
- **C-12 confirm xóa W5:** `grep -c "window.confirm" fe-user/src/components/contracts/ContractDetailContent.tsx` ≥1 và KHÔNG thêm `<Dialog` mới cho việc xóa (số `<Dialog` giữ 1 — transition dialog `:372`).
- **C-13 W4 sau-nhánh-đúng:** `grep -n "findContractWfByTypeCode" fe-admin/src/components/Layout.tsx` ≥1 và nằm sau dòng match `KhkkN[1-8]` (`:188` baseline).

— HẾT (sub-style-fe-b3 · propose-only, lead verify trước khi dùng làm gate) —
