# B4 ensemble — front-end-reviewer-style lane-2/2 (lens Ô-ĐO CHẠY-ĐƯỢC + GOODHART)

> Soi lại `sub-style-fe-b3.md` §Checklist C-1..C-13 bằng cách **CHẠY THẬT** trên repo baseline (HEAD `b2f5d6f9`, 0 file `fe-*` dirty — `git status --porcelain` chỉ có `.claude/*` + `docs/*`).
> Mỗi ô: (a) baseline đo được (b) sau-wave có RĂNG không — **ca chết nào vẫn xanh?** (c) chạy được trên bash Windows không.
> Phép chứng răng = **fault-inject cây tạm** (scratchpad `c3lab/`, `lab7/9/13.tsx`), không phải happy-path.

## Verdict

**B4-FE-ODO: PASS_WITH_FIXES — 12 findings** (4 BLOCKER phải sửa TRƯỚC khi checklist đóng vai STYLE-GATE cho B6).

Phần **SPEC-LIST SF-1..25 + anchor**: 5/5 claim nặng em kiểm đều **ĐÚNG** (xem §Kiểm-chứng dưới) — không có ca "khuôn X sẵn" bịa như bài B2.
Phần **checklist C-1..C-13**: 1 ô **bất-khả-thi** (C-1), 3 ô **FALSE-GREEN đã chứng bằng fault-inject** (C-3 / C-7 / C-13), 1 ô **0 răng đúng trục nó sinh ra để canh** (C-9), 3 ô Goodhart-được (C-6 / C-11 / C-12), 1 ô hở phạm-vi (C-10), 1 ô đã-xanh-từ-baseline dễ đọc nhầm thành bằng-chứng (C-8 vế 2), 2 lỗi cơ-học (C-4/C-5 + exit-code).

## Kiểm-chứng claim của B3 (lens B2 "khuôn có sẵn" — MỞ RA ĐỌC)

| Claim trong `sub-style-fe-b3.md` | Đo được | Kết |
|---|---|---|
| SF-17 baseline SHA `1b9b5c61…`/`4ffc96be…`/`a2d486c5…`/`cd81508f…` | `sha256sum` fe-user: `1b9b5c611663` · `4ffc96beded5` · `a2d486c5c366` · `cd81508f97cb` | ✅ ĐÚNG 4/4 |
| SF-17 "`ContractDetailContent` identical nhưng **CHƯA tự khai MIRROR**" | 4 dòng header `:1-4` = mô-tả reusable body, **0 chữ MIRROR**; khuôn thật `KhkkDetailContent.tsx:2` = `// File MIRROR SHA256 identical với fe-admin counterpart.` | ✅ ĐÚNG |
| C-12 path `fe-user/src/components/contracts/ContractDetailContent.tsx` | file TỒN TẠI (21 359 B, ×2 app cùng byte); `window.confirm`=0; `<Dialog`=**1** đúng `:372` | ✅ path ĐÚNG |
| SF-22/C-13 anchor `fe-admin/Layout.tsx:178` nhánh chết · `:188` KhkkN | `:178 if (code === 'DuyetNcc' \|\| … \|\| 'Contract')` · `:188 if (/^KhkkN[1-8]$/…)`; khối `if (awV2Match)` mở `:176` đóng `:189`, `return null` `:191` | ✅ ĐÚNG |
| C-9 nguồn 7 token `ContractApprovalWorkflowFamily.cs:33-42` | `HD-TP · HD-GK · HD-NCC · HD-DV · HD-MB · HD-NT-NCC · HD-NT-DV` — khớp từng ký tự | ✅ ĐÚNG |
| C-1 "máy = recommended + react-hooks + react-refresh (`fe-user/eslint.config.js:12-17`)" | đúng 4 extends `:12-17`; `diff` 2 config = **IDENTICAL** | ✅ ĐÚNG |

## Findings (R-1..)

### R-1 🔴 BLOCKER — C-1 "eslint PASS" **BẤT KHẢ THI**: baseline đã ĐỎ 2 app, và 4/4 lỗi rơi ĐÚNG file W1 sửa
Đo (`npx eslint .`, flat-config, `npm run lint` = `eslint .` — cú pháp ô ĐÚNG, chạy được):
- `fe-user`: **47 problems (41 errors, 6 warnings)**
- `fe-admin`: **42 problems (37 errors, 5 warnings)**
- 6 file wave sẽ đụng (fe-user): **4 errors — TẤT CẢ trong `Layout.tsx`**: `325:10`, `416:8`, `453:8` (*Cannot create components during render*), **`478:17` (*Calling setState synchronously within an effect*) = ĐÚNG khối accordion `:470-485` mà W1(a) viết lại.**

Hai đường chết, cả hai đều tệ: worker đọc "PASS" ⇒ đi refactor hook-logic prod trong `Layout.tsx` (ngoài scope, đúng vùng bug A/B); hoặc thấy đỏ ⇒ **bỏ luôn C-1** ⇒ tầng máy chết câm — phá chính tiền-đề "máy trước, vai sau".
**Sửa (ô delta, neo số đã đo):**
```bash
# C-1a (app): npx eslint .  ⇒ fe-user ≤ 47 problems (41E/6W) · fe-admin ≤ 42 (37E/5W) — KHÔNG TĂNG
# C-1b (file wave, ô CÓ RĂNG): chạy eslint CHỈ trên file wave đụng ⇒ ≤ 4 errors, và
#   tập ruleId KHÔNG có mục mới; neo theo (file, ruleId), KHÔNG neo số dòng (dòng sẽ trượt)
```
🔴 kèm bẫy đo: `npx eslint . | tail -5; echo $?` in ra **0** dù eslint 41 errors (đo thật lượt này) — `$?` là của `tail`. Ô phải dùng `${PIPESTATUS[0]}` hoặc bỏ pipe.

### R-2 🔴 BLOCKER — C-3 in **"OK" khi CẢ HAI bản mất** (false-green; đã fault-inject)
```
FAULT-INJECT (scratchpad/c3lab): file không tồn tại 2 bên
→ sha256sum: No such file or directory (×2, ra stderr)  →  in "OK hooks/khong-co-file-nay.ts"
```
`$a` rỗng = `$u` rỗng ⇒ `[ "$a" = "$u" ]` TRUE. Ca chết thật: worker **đổi tên/di chuyển** file mirror, hoặc path trong checklist gõ sai (`components/contract/…`) ⇒ ô báo 4/4 OK trong khi **không đo gì cả**. (Ca mất 1 bên thì ô ĐÚNG — ra `LECH`.) Đây đúng lớp "vắng-mặt trông y hệt sạch".
**Sửa:** thêm guard tồn-tại trước khi so:
```bash
for f in …; do A="fe-admin/src/$f"; U="fe-user/src/$f"
  { [ -f "$A" ] && [ -f "$U" ]; } || { echo "MISSING $f"; continue; }
  a=$(sha256sum "$A"|cut -c1-12); u=$(sha256sum "$U"|cut -c1-12)
  [ "$a" = "$u" ] && echo "OK $f" || echo "LECH $f $a != $u"; done
```
Baseline sau khi sửa: **4/4 OK** (đã chạy — đúng kỳ vọng, `ContractDetailContent` identical từ trước).

### R-3 🔴 BLOCKER — C-7 đọc **XANH** khi key danh-tính ĐÃ nằm trong `TRANSIENT_QUERY_KEYS` (reformat xuống dòng)
Baseline: `fe-user/src/components/Layout.tsx:350` = `new Set(['id','q','editHeader','page','phase','awId'])` — 1 dòng, sạch.
```
FAULT-INJECT (lab7.tsx): Set tách 4 dòng, có 'ct','view'
grep -n "TRANSIENT_QUERY_KEYS = " → chỉ ra dòng `new Set([`  ⇒ đếm 'ct|view|pendingMe' = 0  ⇒ XANH (SAI)
ô đề xuất  grep -A6 …  ⇒ 1  ⇒ ĐỎ (đúng)
```
Set hiện 6 phần tử; W1 thêm bất kỳ thứ gì là prettier/tay xuống dòng rất dễ. Bug S155 (leaf sáng nhầm) sẽ **lọt gate**.
**Sửa:** `grep -A6 "TRANSIENT_QUERY_KEYS = " fe-user/src/components/Layout.tsx | grep -cE "'(ct|view|pendingMe)'"` = **0**.

### R-4 🔴 BLOCKER — C-13 đọc **XANH** khi nhánh đặt NGOÀI khối `awV2Match` (đúng ca gotcha #50 mà W4 sinh ra để diệt) + lẫn dòng import
```
FAULT-INJECT (lab13.tsx): nhánh findContractWfByTypeCode đặt SAU khi khối awV2Match đã đóng
grep -n "findContractWfByTypeCode" → 2 hit: dòng 1 (import) + dòng 9 (nhánh)
"sau KhkkN?" head-1 ⇒ KHÔNG (false-alarm) · tail-1 ⇒ CÓ (false-green: leaf VẪN DROP)
```
Hai lỗi trong một ô: (a) không loại dòng `import` ⇒ kết quả phụ thuộc `head`/`tail` mà ô không nói; (b) **không có cận trên** ⇒ "sau `KhkkN`" thoả cả khi nhánh rơi ra ngoài `}` `:189` — khi đó `code` không còn là phần sau `AwV2_`, leaf drop im lặng đúng như #50.
**Sửa (ô range, dùng dạng GỌI + 2 mốc động):**
```bash
F=fe-admin/src/components/Layout.tsx
open=$(grep -n "if (awV2Match) {" $F | cut -d: -f1)
close=$(awk -v s=$open 'NR>s && /^  }$/{print NR; exit}' $F)
hit=$(grep -n "findContractWfByTypeCode(code)" $F | cut -d: -f1)
k=$(grep -n "KhkkN\[1-8\]" $F | cut -d: -f1)
[ -n "$hit" ] && [ "$hit" -gt "$k" ] && [ "$hit" -lt "$close" ] && echo OK || echo FAIL
```
Baseline: `hit` rỗng ⇒ FAIL (đúng — chưa làm). Mốc verify: `open=176 · KhkkN=188 · close=189 · return null=191`.

### R-5 🔴 BLOCKER — C-9 **0 RĂNG đúng trục nó sinh ra để canh** (drift 7 token) + 2 đường false-alarm
```
FAULT-INJECT (lab9*.tsx)
ca A map viết 1 DÒNG (đúng)      : grep -c "'HD-" = 1  ⇒ ≠7  ⇒ BÁO ĐỎ OAN
ca B worker dùng "HD-TP" (đúng)  : grep -c "'HD-" = 0  ⇒ BÁO ĐỎ OAN
ca C typo 'HD-NTNCC' (SAI)       : grep -c "'HD-" = 2… và nếu 7 dòng thì =7 ⇒ XANH DÙ SAI
ô đề xuất (quote-agnostic, sort -u): A=7 ✅ · B=7 ✅ · C=6 ⇒ ĐỎ ✅
```
`grep -c` đếm **DÒNG** không đếm **lần**; và giả định nháy-đơn **không có gì cưỡng chế**: repo **không có `.prettierrc`** (`ls` 0 hit) và eslint config **không có rule `quotes`** (chỉ 4 `recommended`) — tỉ lệ nháy đơn chỉ là thói quen (`Layout.tsx` 40:4, `WorkflowMatrixViewPage.tsx` 22:2).
**Sửa:**
```bash
grep -oE "['\"]HD-(NT-NCC|NT-DV|TP|GK|NCC|DV|MB)['\"]" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx | sort -u | wc -l   # = 7
```
(alternation để token DÀI trước — đã chạy, bắt đúng typo.) Baseline: `grep -rn "'HD-" fe-user/src` = **0 hit** ⇒ 0 → 7 vẫn là ô tiến-độ tốt, chỉ cần đổi cách đếm.

### R-6 🟠 C-10 hở phạm-vi — `fe-admin/src/pages` **bỏ sót 2 host thật** đang truyền callback kiểu này
Đo: fe-admin host `PipelineTreePanel` ở **4 chỗ**, trong đó 2 chỗ NGOÀI `pages/` và **đang truyền callback qua `stage2Options`**:
`fe-admin/src/components/khkk/KhkkListPanel.tsx:387` (`stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect, onOpenPe }}`) · `fe-admin/src/components/khkk/KhkkWorkListPanel.tsx:176`. (GĐ3 host thì đúng là ở `pages/`: `ContractsListPage.tsx:265` — nên scope cũ không sai hoàn toàn, chỉ **thủng**.)
**Sửa:** đo cả `fe-admin/src`, trừ đúng 2 file mirror có quyền chứa token:
```bash
grep -rn "onOpenContract" fe-admin/src | grep -vE "hooks/usePipelineStages\.ts|components/pipeline/PipelineTreePanel\.tsx" | wc -l   # = 0
```
Baseline: 0 (toàn `fe-admin/src` cũng 0). Ô này còn CHẶT hơn: sau W2 nó chứng "định-nghĩa CÓ ở mirror, site truyền KHÔNG có ở fe-admin" = đúng control-âm W2-v.

### R-7 🟠 C-6 Goodhart — **xoá sạch** câu stale cũng cho 0
Baseline: `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = **1** (`:142` `// đều ?type=3 (HĐ chung 1 bộ workflow V2) — thêm &ct=N để active-state riêng từng leaf.`); toàn `fe-user/src`+`fe-admin/src` chỉ 1 hit ⇒ ô có răng, chạy được, `grep`/`rg` cho cùng kết quả (diacritic UTF-8 OK trên Git Bash).
Nhưng SF-2 đòi **LẬT** comment, mà `=0` thoả luôn khi worker **xoá cả dòng** ⇒ mất WHY của `&ct=N`, đẻ vùng câm mới.
**Sửa:** thêm ô sinh-đôi dương: `grep -cE "7 bộ|per-loại|S188" fe-user/src/components/Layout.tsx` **≥1** (baseline: cần đo lại sau wave; hiện khối `:141-142` không có chữ nào trong 3 chữ đó).

### R-8 🟠 THIẾU ô cho acceptance W5-iv (dòng tự-khai MIRROR) — C-3 **không thể** bắt
SHA-pair vẫn khớp khi **cả 2 bên đều thiếu** header tự-khai ⇒ spec ③ W5-iv + SF-17 yêu-cầu "thêm dòng tự-khai MIRROR cùng bytes" mà checklist **không có ô nào đo**.
**Thêm:** `grep -c "File MIRROR SHA256 identical" fe-admin/src/components/contracts/ContractDetailContent.tsx fe-user/src/components/contracts/ContractDetailContent.tsx` ⇒ **1 mỗi bên** (baseline **0/0** — đã đo).

### R-9 🟠 C-11 Goodhart — đổi con số 320→400 **không chứng minh HOÁN ĐỔI**
Baseline: `MyContractsPage.tsx:153` = `lg:grid-cols-[320px_1fr_360px]`; `grep -F "grid-cols-[400px"` = 0 hit ⇒ ô có răng cho *bề rộng*, nhưng W2 là **đổi chỗ 2 khung**: sửa mỗi token vẫn xanh.
**Thêm ô đồng-hành (đã đo baseline = 1):** `grep -c "{!selectedId && <PipelineTreePanel" fe-user/src/pages/contracts/MyContractsPage.tsx` ⇒ **0** sau W2 (cây thành panel-1 thường-trực, không còn treo vào `!selectedId`).

### R-10 🟡 C-8 — vế 2 **ĐÃ XANH TỪ BASELINE**, và vế 1 brittle theo nháy
Đo: `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` = **1 hit sẵn** (`Layout.tsx:151`) ⇒ đây là **ô canh-thoái-lui** (spec W1-iv ghi "GIỮ NGUYÊN"), phải LABEL rõ trong checklist, kẻo đọc thành bằng-chứng W1 đã làm.
Vế 1 `grep -nF "get('ct')"` baseline = 0 (grep -F và `rg -F` **cùng 0** — không tái hiện lệch grep/rg của lens-3 B2). Nhưng dạng viết `get("ct")` / `params.get( 'ct' )` sẽ trượt ⇒ dùng `grep -nE "get\(['\"]ct['\"]\)"`. (Khuôn hiện có `Layout.tsx:472` `params.get('type')` — nháy đơn, nhưng không có máy nào cưỡng chế, xem R-5.)

### R-11 🟡 C-4/C-5 lỗi cơ-học + nguồn danh-sách file
- `grep -c` khi 0 hit in `0` nhưng **exit 1** ⇒ chuỗi `&&` / `set -e` của runner **đứt giữa chừng** đúng lúc ô ĐANG PASS (C-4 kỳ vọng 0!). Ô phải viết `grep -c … || true` hoặc so bằng `[ "$(grep -c …)" = 0 ]`.
- C-5 nói "`<danh sách file sửa>`" mà **không nêu lấy từ đâu**. Theo bài S179 của chính vai này: `git diff` **mù file untracked** ⇒ file MỚI (nếu wave đẻ) thoát ô comment-header. Nguồn đúng:
```bash
{ git diff --name-only origin/main...HEAD; git diff --name-only HEAD; git ls-files -o --exclude-standard; } \
  | sort -u | grep -E '^fe-(admin|user)/src/'
```
(baseline cửa-sổ này = 0 file `fe-*` ⇒ mọi hit sau wave đều là của wave.)

### R-12 🟡 C-2 build — nửa `tsc` XANH baseline; nửa `vite build` CHƯA đo
`npm run build` = `tsc -b && vite build`. Đo được: `npx tsc -b` (fe-user) **exit 0, 0 dòng lỗi** ⇒ tầng type baseline SẠCH — nên C-2 **không** dính bệnh R-1 (khác hẳn eslint). Chưa đo: `vite build` (bundle) và `tsc -b` của `fe-admin` ⇒ 2 ô này để **CHƯA ĐO**, cấm đọc thành xanh.
Hệ quả cho gate: C-2 là ô **có răng thật** (baseline sạch ⇒ đỏ sau wave = do wave) — ngược với C-1. Ghi rõ cặp này trong checklist để worker không gộp "máy" thành một cục.

## Ô đo phải sửa (bảng gọn cho B6)

| Ô | Baseline ĐO ĐƯỢC | Bệnh | Ô thay thế (đã chạy thử) |
|---|---|---|---|
| C-1 | fe-user 47 (41E/6W) · fe-admin 42 (37E/5W) · 6 file wave = 4E toàn `Layout.tsx` {325,416,453,478} | PASS bất-khả-thi | delta: app không tăng · file-wave ≤4E, 0 ruleId mới; dùng `${PIPESTATUS[0]}` |
| C-3 | 4/4 OK | mất-2-bên ⇒ in OK | thêm `[ -f A ] && [ -f U ] \|\| echo MISSING` |
| C-6 | `chung 1 bộ` = 1 (`Layout.tsx:142`) | xoá-trắng cũng =0 | + ô dương `grep -cE "7 bộ\|per-loại\|S188"` ≥1 |
| C-7 | Set 1 dòng `:350`, 0 key danh-tính | reformat ⇒ xanh giả | `grep -A6 … \| grep -cE "'(ct\|view\|pendingMe)'"` = 0 |
| C-8 | vế1 = 0 · **vế2 = 1 (đã xanh)** | vế2 là canh-thoái-lui; vế1 brittle nháy | label rõ + `grep -nE "get\(['\"]ct['\"]\)"` |
| C-9 | 0 hit `'HD-` trong `fe-user/src` | đếm DÒNG · giả định nháy · typo vẫn xanh | `grep -oE "['\"]HD-(NT-NCC\|NT-DV\|TP\|GK\|NCC\|DV\|MB)['\"]" \| sort -u \| wc -l` = 7 |
| C-10 | 0 (cả `fe-admin/src`) | scope `pages/` bỏ sót 2 host `components/khkk/` | quét `fe-admin/src` trừ 2 file mirror ⇒ 0 |
| C-11 | `grid-cols-[320px_1fr_360px]` `:153`; 400px = 0 | đổi số ≠ hoán đổi | + `{!selectedId && <PipelineTreePanel` : 1 → **0** |
| C-12 | `window.confirm`=0 · `<Dialog`=1 (`:372`) | ≥1 xanh dù message thiếu mã/đích (SF-16) | + `grep -A2 "window.confirm" \| grep -c 'Đã xóa'` ≥1 |
| C-13 | 0 hit (`open=176·KhkkN=188·close=189`) | ngoài-khối vẫn xanh · lẫn import | ô range `open < hit < close` + dạng gọi `(code)` |
| — | `File MIRROR SHA256 identical` = 0/0 | **thiếu ô** cho W5-iv | thêm ô = 1 mỗi bên |
| C-4/C-5 | 0 file `fe-*` trong cửa-sổ | exit-1 khi PASS · thiếu nguồn danh-sách | `|| true` + union `git ls-files -o` |

— HẾT (`sub-style-review-b4-fe2.md` · read-only lens, propose-only) —
