# B4 lane-1/2 — front-end-reviewer-style — lens ANCHOR + ĐÚNG-KHUÔN

> Review `sub-style-fe-b3.md` (25 SF + 13 ô C) TRƯỚC khi nó thành STYLE-GATE cho worker B6.
> Phương pháp: MỞ khuôn THẬT ra đọc, đối chiếu TỪNG mệnh đề (class string đúng từng chữ? message `window.confirm` đúng? `Pick<>` đúng key? số đếm đúng?). Không tin trích dẫn, chỉ tin dòng code.

## Verdict

**B4-FE-ANCHOR: PASS_WITH_FIXES — 11 findings** (2 MAJOR · 3 MODERATE · 6 MINOR)

- **Sức khoẻ anchor: ~35/37 chính xác** (xem §Anchor đã kiểm — ĐÚNG). Không có anchor CHẾT (trỏ file/vùng không tồn tại). Lệch duy nhất là ±1..3 dòng ở 1 chỗ (R-7).
- **(b) luật TỰ CHẾ kiểu "bỏ type=3" của B2: KHÔNG CÓ.** Không SF nào đòi thứ trái khuôn prod. 2 lỗi "tự chế nhẹ" là gọi sai TÊN (R-4 "shadcn Select") và trích chuỗi không literal (R-3/R-5).
- **(c) mâu thuẫn spec v2.1: 0.** Đo: `grep -n "Authorize\|policy"` trong `sub-style-fe-b3.md` = 0 (không đụng vế W5(d) đã đổi); SF-23 GIỮ `?type=3&ct=` đúng v2.1 (v1 đòi bỏ — đã gỡ).
- Rủi ro chính KHÔNG phải luật sai mà là **luật THIẾU MỆNH-ĐỀ so với khuôn** (R-1, R-2) — đúng lớp làm worker "đạt gate mà vẫn lệch prod".

## Findings (R-1..R-11)

### R-1 — MAJOR · khuôn ĐÃ-CHẠY của chính việc W2 làm bị BỎ SÓT khỏi bảng khuôn

`sub-style-fe-b3.md` không nhắc `ContractsListPage` lần nào (grep tên file trong spec-list = 0 hit), trong khi **fe-admin đang chạy prod một host 3-panel GĐ3 CÓ NHÚNG SẴN cây toàn trình**:

- `fe-admin/src/pages/contracts/ContractsListPage.tsx:141` — `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[340px_1fr_360px]`
- `fe-admin/src/pages/contracts/ContractsListPage.tsx:265` — `{!selectedId && <PipelineTreePanel currentStage={3} bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}`

Hệ quả đo được, 2 cái:

1. **Không luật nào bắt worker quyết `bodyMaxHClass`.** W2 nhúng cây vào CỘT CUỘN của host `h-[calc(100vh-4rem)]`; chính khuôn khai tại `fe-user/src/components/pipeline/PipelineTreePanel.tsx:112-115`: *"max-h mặc định 100vh−16rem là hiệu-chỉnh cho trang giấy-cuộn, vượt chỗ Panel-3 /inbox ~26px ⇒ 2 cuộn lồng nhau"*. SF-24 có liệt `bodyMaxHClass` như MẪU opt-in nhưng KHÔNG có luật "nhúng cây vào cột cuộn ⇒ PHẢI truyền/khai". Prod đã trả giá cho bài này (gate F-1 S168) — bỏ ra khỏi gate là mời nó quay lại.
2. **Trỏ khuôn đời sau, bỏ khuôn đời trước.** `fe-user/src/pages/khkk/KhkkListPage.tsx:7-8` tự khai: *"Khuôn code lấy từ `ContractsListPage.tsx` (255 dòng, gọn) — CỐ Ý KHÔNG chép `PurchaseEvaluationsListPage.tsx`"*. Spec-list lấy KhkkListPage làm khuôn 3-panel (đúng) nhưng không nói nó là bản SAO của ContractsListPage — worker W2 sửa `MyContractsPage` sẽ không biết bản GĐ3-anh-em đang đứng ở `340px` + đặt cây ở nhánh `!selectedId`.

**Sửa:** thêm 1 dòng khuôn `ContractsListPage.tsx:141,265` vào bảng + 1 luật SF mới: *"Nhúng `PipelineTreePanel` vào cột có thanh cuộn riêng ⇒ BẮT BUỘC quyết `bodyMaxHClass` và khai lý do (khuôn `:112-115` + host prod `ContractsListPage.tsx:265`)"*. Ô đo: `grep -n "bodyMaxHClass" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1 HOẶC comment khai vì sao không cần.

### R-2 — MAJOR · SF-21/SF-8 chép THIẾU một vế của guard, vế thiếu chính là vế chống trang-trắng

Khuôn thật `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:84-86`:

```ts
const rawGroup = searchParams.get('group')
const khkkGroup =
  typeInt === 10 && rawGroup !== null && /^[1-8]$/.test(rawGroup) ? Number(rawGroup) : null
```

Guard có **2 vế**: `typeInt === 10 &&` (GATE THEO TYPE) rồi mới tới clamp regex. SF-21 chỉ chép vế clamp (*"miền hữu hạn ⇒ clamp regex … W1(b) ct dùng `/^[1-7]$/`"*), SF-8 cũng chỉ nói "W1(b) `ct` đi ĐÚNG khuôn này" mà không nêu vế gate.

Vì sao vế đó load-bearing: **1 file phục vụ 3 route** — `fe-user/src/App.tsx:71` (`/purchase-evaluations/workflow-matrix`), `:73` (`/contracts/workflow-matrix`), `:83` (`/khkk/workflow-matrix`). Thiếu `typeInt === 3 &&`, một URL `?type=1&ct=5` (route PE) sẽ đem mã `HD-*` đi lọc bộ workflow Duyệt-NCC ⇒ 0 dòng — đúng ca mà comment prod `:81-83` dựng ra để chặn (*"thà hiện đủ còn hơn lọc theo một con số vô nghĩa rồi ra trang trắng"*). Không ô C nào bắt được: C-9 chỉ **đếm 7 chuỗi `'HD-`**, đếm đủ vẫn có thể thiếu gate.

**Sửa SF-21:** thêm mệnh đề *"gate `typeInt === <type của trang>` đứng TRƯỚC clamp (khuôn `:86`) — trang này gắn 3 route, thiếu gate là lọc chéo module"*. Ô đo thêm: `grep -nE "typeInt === 3 &&" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` ≥1.

### R-3 — MODERATE · SF-24 trích một câu KHÔNG TỒN TẠI trong khuôn ⇒ gate literal sẽ trượt trên chính file khuôn

SF-24: *"JSDoc có câu **'Không truyền ⇒ giữ nguyên hành vi cũ'**"* (đóng ngoặc kép = đọc như literal).

ĐO trên khuôn `fe-user/src/components/pipeline/PipelineTreePanel.tsx`:
- `grep -c "Không truyền ⇒ giữ nguyên hành vi cũ"` = **0**
- `grep -c "hông truyền"` = **6** — 6 biến thể khác nhau: `:108` *"Không truyền ⇒ cây giữ NGUYÊN hành vi cũ"* · `:110` *"4 host không truyền giữ nguyên"* · `:119` *"6 host còn lại KHÔNG truyền ⇒ giữ nguyên hình cũ"* · `:127` *"…giữ nguyên ô lọc cây như cũ"* · `:132` *"5 host còn lại không truyền ⇒ cây giữ NGUYÊN mọi phase như cũ"* · `:135` *"Không truyền ⇒ vẽ đủ 4 giai-đoạn như cũ"*. Riêng `:115` còn dạng *"Mặc định giữ nguyên"* (không có chữ "truyền").

⇒ Khuôn đòi **khớp NGHĨA**, không phải khớp chuỗi. Để nguyên ngoặc kép: hoặc gate 0-hit oan, hoặc worker dán câu cứng nhắc khác giọng 6 mẫu đang có.
**Sửa:** bỏ ngoặc kép literal, ghi *"câu chốt cùng nghĩa: không truyền ⇒ giữ hành vi cũ (6 biến thể `:108-135`)"*; ô đo `grep -c "hông truyền" <file>` ≥1 per prop mới.

### R-4 — MODERATE · SF-14 gọi sai tên công nghệ ⇒ mời worker kéo Radix vào

SF-14: *"shadcn `Select`"*. ĐO:
- `fe-user/src/components/ui/select.tsx:1-21` = wrapper **`<select>` NATIVE**: `forwardRef<HTMLSelectElement, SelectHTMLAttributes<HTMLSelectElement>>` (`:4,:6`), render `<select className={cn(...)}>{children}</select>` (`:7-19`).
- `grep -rn "radix-ui/react-select" fe-user/package.json fe-user/src` = **0 hit** ⇒ repo KHÔNG có shadcn/Radix Select.

Worker "làm cho đúng shadcn" sẽ dựng `SelectTrigger/SelectContent/SelectItem` + thêm dependency = lệch khuôn, và mất luôn idiom đang đo ở `KhkkListPanel.tsx:352-359` (`value={phase ?? ''}` · `aria-label="Lọc trạng thái"` · `onChange={e => … Number(e.target.value) as KhkkPhaseValue}` · `<option value="">Tất cả trạng thái</option>`).
**Sửa:** *"`Select` của repo (`@/components/ui/select` — wrapper `<select>` native, KHÔNG Radix)"*.

### R-5 — MODERATE · SF-15 trích class KHÔNG literal trong đúng luật nói về class literal

SF-15 ghi header khuôn là `shrink-0 border-b px-6 py-3`. Chuỗi THẬT `fe-user/src/pages/khkk/KhkkListPage.tsx:157`:

```
flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3
```

Hai vế còn lại của SF-15 lại literal chính xác (`:156` `flex h-[calc(100vh-4rem)] flex-col` ✓ · `:171` `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]` ✓) ⇒ đọc như thể cả 3 đều literal. Worker chép nguyên vế giữa mất `border-slate-200 bg-white` + toàn bộ căn `flex/items-center/justify-between/gap-3`.
**Sửa:** chép đủ chuỗi, hoặc chèn `…` như SF-12 đã làm đúng (`"border-dashed … text-center text-sm text-slate-500"`).

### R-6 — MINOR · SF-10 sai số đếm + sai khoảng anchor

SF-10: *"10/10 mutation `KhkkDetailContent.tsx:243-320` cùng dạng"*. ĐO `fe-user/src/components/khkk/KhkkDetailContent.tsx`:
- `grep -n "= useMutation"` → **11** khai báo: `:243, :253, :266, :286, :296, :307, :316, :322, :340, :350, :364`
- `grep -n "onError"` → **11** dòng `onError: (e) => toast.error(getErrorMessage(e))` (`:247,263,271,293,299,313,319,337,343,361,367`)
- Khoảng `:243-320` chỉ chứa **7** mutation.

Số đúng: **11/11, `:243-367`**. Nội dung luật không đổi, nhưng đây là con-số kiểm được sai — worker đối chiếu 1 lần là hạ tín nhiệm cả list.

### R-7 — MINOR · Bẫy-1 lệch anchor ±1..3 dòng (nhưng phần NẶNG thì ĐÚNG, và mạnh hơn mô tả)

- Bẫy-1 ghi mutation `removePlan :231-237`; thật là **`:230-240`** (`const removePlan = useMutation({` ở `:230`, đóng `})` ở `:240`).
- Dialog `:742-756` ✓ (thật `:742-758`, đủ gần).
- **Phần quan trọng ĐÚNG và đo được cứng hơn lời khai:** `grep -n "confirmDelete\|setConfirmDelete" fe-user/src/components/khkk/KhkkWorkflowPanel.tsx` → `:107` useState · `:234`/`:745`/`:748` đều set **`false`** · `:742` đọc. **0 caller `setConfirmDelete(true)`** ⇒ Dialog bất-khả-đạt = CODE CHẾT THẬT. Bia-mộ `:117-118` ✓ và `:438-442` ✓ đúng nguyên văn.

Đề nghị thay anchor số dòng bằng chính phép đo 0-caller (bền hơn, và chính SF-3 dạy vậy).

### R-8 — MINOR · Bẫy-9 chứa citation-trap: chạy lại đúng lệnh nó ghi sẽ ra kết quả NGƯỢC

Bẫy-9 ghi *"`grep 'lũy kế'` trong KHKK = 0 hit"*. ĐO hôm nay: `grep -rn "lũy kế" fe-user/src/components/khkk fe-user/src/pages/khkk` = **1 hit** — chính comment `KhkkListPanel.tsx:345-346` khai điều đó. Worker chạy lại lệnh, thấy 1 hit, có thể kết luận ngược ("KHKK có lũy kế ⇒ bê banner PE được") — đúng lớp lỗi mà Bẫy-9 sinh ra để chặn.
**Sửa:** phát biểu lại *"0 hit NGOÀI comment tự-khai"*, ô đo loại trừ dòng comment.

### R-9 — MINOR · C-6 xoá-là-đạt (mất WHY vẫn xanh)

C-6 = `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = 0. Control-dương ✓ (hôm nay **1 hit** tại `:142`: *"WfView cả 7 loại đều ?type=3 (HĐ chung 1 bộ workflow V2)"*). Nhưng **xoá trọn khối `:141-142` cũng cho 0** ⇒ đạt gate mà mất luôn phần WHY, trái chính SF-2 (*"LẬT comment cũ"*, không phải xoá).
**Sửa:** thêm ô dương ghép đôi — `grep -cE "7 bộ|per-loại|S188" fe-user/src/components/Layout.tsx` ≥1.

### R-10 — MINOR · 2/13 ô C là guard-giữ-nguyên (không delta), nên dán nhãn để khỏi đọc thành "đã kiểm 13 việc"

Đo trạng thái HÔM NAY:
- **C-4** `export default` = 0 ở 7/7 file wave (`fe-user` Layout · WorkflowMatrixViewPage · MyContractsPage · usePipelineStages · PipelineTreePanel · ContractDetailContent · `fe-admin` Layout) ⇒ luôn xanh.
- **C-8 vế 2** `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` — đã TRUE sẵn tại `fe-user/src/components/Layout.tsx:151` (spec W1-iv cũng ghi "GIỮ NGUYÊN").

Ô CÓ RĂNG (0 → mục tiêu), đã đo control-âm hôm nay: **C-9** (`grep -rn "'HD-" fe-user/src` = **0** ⇒ 0→7 thật) · **C-11** (`grid-cols-[400px` = 0; `MyContractsPage.tsx:153` đang `lg:grid-cols-[320px_1fr_360px]`) · **C-12** (`<Dialog` trong `ContractDetailContent` = **1**, tại `:372` ✓ đúng như C-12 khai) · **C-13** · **C-10** (fe-admin CÓ host thật để control-âm: `fe-admin/src/pages/contracts/ContractsListPage.tsx` + `HardCopiesPage.tsx` + `pages/khkk/KhkkListPage.tsx` đều dựng `PipelineTreePanel` ⇒ ô này không rỗng nghĩa).

### R-11 — MINOR · C-1 tóm tắt "máy phủ tới đâu" thiếu 1 preset

C-1 ghi máy = *"recommended + react-hooks + react-refresh (`fe-user/eslint.config.js:12-17`)"*. Anchor ✓ đúng, nhưng đọc ra thì `:12-17` có **4** preset: `js.configs.recommended` (`:13`), **`tseslint.configs.recommended` (`:14`)**, `reactHooks.configs.flat.recommended` (`:15`), `reactRefresh.configs.vite` (`:16`). Bỏ sót `tseslint` = bỏ sót họ `no-explicit-any` / `no-unused-vars` TS ⇒ worker tưởng trục TS chưa có máy gác và đi đẻ luật tay chồng lên máy.

## Anchor đã kiểm — ĐÚNG (không cần sửa; liệt để lead khỏi kiểm lại)

| Luật | Anchor | Kết quả đo |
|---|---|---|
| SF-16 thanh nút đáy | `KhkkDetailContent.tsx:1085,1086,1090,1093,1095,1096` | ✓ ĐÚNG TỪNG CHỮ: `{!readOnly && (` · `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` · `window.confirm(\`Xóa phiếu ${plan.maKeHoach ?? ''}? Phiếu chuyển sang mục "Đã xóa".\`)` · `bg-red-600 … hover:bg-red-700 disabled:opacity-50` · `<Trash2 className="h-3.5 w-3.5" />` · `'Đang xóa…' : 'Xóa phiếu'` |
| SF-16 rào hiển-thị | `:1103-1135` (list ghi 1103-1119 / 1103-1125) | ✓ khối IIFE có thật, `title={why ?? …}` ở `:1125` |
| SF-10 invalidate/deletePlan/host-clear | `:193-197` · `:316-320` · `KhkkListPage.tsx:248` | ✓ 2 queryKey + `onChanged?.()` · `onSuccess: toast + onDeleted?.()` · `onDeleted={() => setId(null)}` |
| SF-10 bộ invalidate của CHÍNH file W5 | `ContractDetailContent.tsx:57-61` | ✓ đúng 3 key: `['contract', c.id]`, `['my-contracts']`, `['inbox']` |
| SF-17 mirror baseline | 4 cặp | ✓ **4/4 khớp và đúng prefix đã ghi**: `1b9b5c611663` · `4ffc96beded5` · `a2d486c5c366` · `cd81508f97cb` |
| SF-17 "ContractDetailContent chưa tự khai MIRROR" | header `:1-4` | ✓ ĐÚNG — 403 dòng, header nói "Reusable detail body…", 0 dòng MIRROR |
| SF-4 | `usePipelineStages.ts:228-230,:244,:377` · `PipelineTreePanel.tsx:109-110,:111` | ✓ ternary `opts.onOpenPe ? … : navigate` · `opts.onOpenKhkk ? …` · `onOpen: () => navigate(\`/contracts/${contractId}\`)` · JSDoc đối-xứng họ · `Pick<BuildStagesOptions,'stage2Content'\|'stage2Count'\|'onOpenKhkk'\|'onOpenPe'>` |
| SF-7 / SF-9 | `MyContractsPage.tsx:47,:67-71` · `KhkkListPanel.tsx:247` · `PurchaseEvaluationsListPage.tsx:75` · `usePipelineStages.ts:102` | ✓ positional ⟂ object-gói · `enabled: !!selectedId` · `enabled: canKhkk` |
| SF-8 | `WorkflowMatrixViewPage.tsx:88-95` | ✓ nguyên văn "đẻ 8 lần gọi mạng cho cùng một payload" + khớp bằng `code` |
| SF-11 | `PurchaseEvaluationsListPage.tsx:80,:91,:98` | ✓ deleted TRƯỚC pendingMe · `/inbox` · tự gói `{ items, total, page, pageSize }` |
| SF-12 | `KhkkListPage.tsx:195-203,:257-269` · `WorkflowMatrixViewPage.tsx:152-166` | ✓ cả 3 khuôn, kể cả vế "đếm phần bị lọc khuất" `:161` |
| SF-13 / SF-20 | `types/khkk.ts:39-55,:57` · `KhkkListPanel.tsx:155-158` | ✓ `as const` + `(typeof X)[keyof typeof X]` + bắt đầu từ 1 · Pattern 14 · thang 13/11/10 |
| SF-21 | `MyContractsPage.tsx:30-36,:60` · `WorkflowMatrixViewPage.tsx:84-86` | ✓ NaN-guard · `type: typeFilter ?? undefined` · clamp |
| SF-22 | `fe-admin/Layout.tsx:175-189` | ✓ `awV2Match` `:175`, nhánh chết `code === 'Contract'` `:178`, `KhkkN[1-8]` `:188`, khối comment `:181-187` |
| SF-23 | `fe-user/Layout.tsx:151,:180-185,:350,:358-369` | ✓ `?type=3&ct=` đã có sẵn · khối WHY `view=approved` · `TRANSIENT_QUERY_KEYS = new Set(['id','q','editHeader','page','phase','awId'])` (không chứa ct/view/pendingMe) · pinned-by-target |
| SF-18 / SF-19 | `fe-admin/Layout.tsx:37-39,:138-142,:185-187` · `KhkkListPage.tsx:253-256` · `contractWorkflowFamily.ts:16-18` | ✓ 5/5 khai-lệch-có-chủ-đích tồn tại đúng chỗ |
| SF-5 | `contractWorkflowFamily.ts:6-18,:69,:77-80` · `fe-user/Layout.tsx:16-29,:48` | ✓ khối "VÌ SAO LÀ FILE RIÊNG" · `findContractWfByTypeCode` · const-map |
| SF-5 "bản-sao-thứ-3" | `grep -rn "'HD-" fe-user/src` | ✓ **0 hit** ⇒ đúng là copy thứ 3 (BE .cs → fe-admin lib → fe-user mới) |
| C-9 đúng FILE | `fe-user/src/App.tsx:71,73,83` | ✓ `/contracts/workflow-matrix` render **chính** `pages/pe/WorkflowMatrixViewPage` ⇒ path trong C-9 KHÔNG sai |
| Bẫy-2 | `KhkkListPage.tsx:5` + sha | ✓ *"File MIRROR SHA256 identical với fe-admin counterpart"* và 2 bản **cùng sha `af8d121a5940`** |
| Bẫy-4/5/6/7 | `Layout.tsx:358-369` · `usePipelineStages.ts:88-89` · `types/khkk.ts:57` · `MyContractsPage.tsx:30-33` | ✓ cả 4 đúng nguyên văn |
| SF-16 câu confirm dùng được cho HĐ? (Bẫy-9 self-test) | `fe-user/Layout.tsx:153` | ✓ HĐ CÓ mục "Đã xóa" (`Ct_*_Deleted` → `/my-contracts?type=N&deleted=1`) ⇒ câu *"Phiếu chuyển sang mục 'Đã xóa'"* là mệnh-đề ĐÚNG trong ngữ cảnh HĐ (chỉ cần đổi "Phiếu" → "Hợp đồng") |

## Luật phải sửa/gỡ (đơn cho lead — thứ tự ưu tiên)

1. **THÊM** (R-1): dòng khuôn `fe-admin/src/pages/contracts/ContractsListPage.tsx:141,265` + luật *"nhúng `PipelineTreePanel` vào cột cuộn ⇒ quyết `bodyMaxHClass`, khai lý do"* + ô đo `grep -n "bodyMaxHClass" MyContractsPage.tsx`.
2. **SỬA SF-21** (R-2): thêm vế **gate theo type** trước clamp (khuôn `WorkflowMatrixViewPage.tsx:86`) + ô đo `grep -nE "typeInt === 3 &&"` ≥1.
3. **SỬA SF-24** (R-3): bỏ ngoặc kép literal → "khớp NGHĨA"; ô đo `grep -c "hông truyền"` ≥1.
4. **SỬA SF-14** (R-4): "shadcn `Select`" → "`Select` repo (`ui/select.tsx` — native `<select>`, KHÔNG Radix)".
5. **SỬA SF-15** (R-5): chép đủ chuỗi header `:157` hoặc chèn `…`.
6. **SỬA SF-10** (R-6): "10/10 … `:243-320`" → "**11/11 … `:243-367`**".
7. **SỬA Bẫy-1** (R-7): `removePlan :231-237` → `:230-240`; thay bằng phép đo bền: *"0 caller `setConfirmDelete(true)`"*.
8. **SỬA Bẫy-9** (R-8): "0 hit" → "0 hit NGOÀI comment tự-khai" (hôm nay grep ra 1).
9. **THÊM ô C-6b** (R-9): `grep -cE "7 bộ|per-loại|S188" fe-user/src/components/Layout.tsx` ≥1.
10. **DÁN NHÃN** (R-10): C-4 và C-8-vế-2 = `[regression-guard, no-delta]`.
11. **SỬA C-1** (R-11): thêm `tseslint.configs.recommended` (`eslint.config.js:14`) vào phần "máy đã phủ".

**KHÔNG gỡ luật nào.** 25/25 SF đều có khuôn thật đứng sau; không có luật tự chế trái prod, không có luật mâu thuẫn spec v2.1.

— HẾT (sub-style-review-b4-fe1 · lane-1/2 ANCHOR+KHUÔN · read-only, không sửa code) —
