# B5 — PLAN FINAL "SOLTUON-12-08-2026" (reviewer /fable-real, tổng hợp cuối YC-029)

> Nguồn: spec v2.2 (`spec-soltuon-dieu-chinh-hd-12-08-2026.md`) ⟂ `style-gate-v2.md` ⟂ `anh-mo-ta.md` + bối cảnh B1 §PROPOSE / B2-synthesis; đối chiếu nền `sub-style-{fe,be}-b3.md` chỗ delta chạm. Mọi neo `file:line` trong file này B5 ĐÃ MỞ ĐĨA ĐO TƯƠI 2026-08-12 (không chép từ artifact).

## Verdict tổng

**B5-PLAN: GO_WITH_NOTES — 9 mâu thuẫn/lỗ soi ra (6 CHỐT tại chỗ, 3 ghi-nhận, 0 treo 50/50), plan 5 wave đủ worker-cầm-làm.**

| Wave | Verdict | Ghi chú 1 dòng |
|---|---|---|
| W4 | **GO** | Neo :176/:188/:189 đo tươi ĐÚNG; 1 file ~6 dòng; C-13 RANGE có răng |
| W1 | **GO** | Gate `ct` phải ĐỦ 3 vế (X-4); +1 việc mới comment stale thứ 2 (X-5) |
| W2 | **GO** | Q1/Q2 nhánh-đã-chọn; bổ quyết định `bodyMaxHClass` (X-3) + cơ chế lọc cây `stage3TypeFilter` opt-in |
| W3 | **GO** | Đổi-endpoint `/contracts/inbox` (0-param, đo đĩa :41-43); CẤM gửi param `pendingMe` lên server |
| W5 | **GO** (test-before) | T-matrix nguồn duy nhất (X-1); T-13 Fact riêng (X-9); STATUS 2-ô, canonical treo cicd-verify (X-2) |

Điểm mấu chốt cho lead: (i) spec §③ W5(i) danh-sách ca + câu "ĐÚNG 1 row STATUS" là **dư-lượng v2.1** — plan này thay bằng T-matrix + 2-ô; đề nghị lead đánh dấu 2 chỗ đó trong spec khi rảnh (B5 không sửa spec — ranh). (ii) Toàn bộ Q3/Q4/Q5 + W8/W9 nằm NGOÀI plan — Đ-8 chặn worker chạm. (iii) Deploy CHỜ ⇒ mọi ô prod-only = DEFERRED-TO-DEPLOY, wave dừng ở local-verify + commit, push gom sau.

## Mâu thuẫn còn sót (X-1..)

> Soi CHÉO 3 tầng {spec v2.2 ⟂ style-gate-v2 ⟂ T-matrix} + đối chiếu ĐĨA (mọi neo dưới đây B5 đã mở file đo lại, không chép từ artifact). 9 mục: **6 CHỐT** (đổi hành-động, đã nhập vào PLAN) · **3 ghi-nhận** (không đổi hành-động).

- **X-1 [CHỐT — T-matrix thắng] spec §③ W5(i) còn giữ danh-sách ca CŨ đã bị §C thay.** Spec :121-125 vẫn liệt "V1 nháp (2) vẫn xoá được…" trong khi chính spec §② :87-88 tuyên *"Ca test W5 = T-matrix T-1..T-13 + phản-ca T-14 (thay danh sách ca ở ✅W5(i) cũ — ca 'V1 nháp 2' đã bị chứng TRÙNG)"* và be2-R7 (style-gate §B.5) chứng phase-2 TRÙNG T-1 → thay bằng **T-4 DangGopY(3)**. Chốt: worker W5 CHỈ dùng T-1..T-14; câu-chữ §③ W5(i) coi là dư-lượng v2.1. *(Trả lời câu hỏi giao việc: T-4 phase 3 KHÔNG đá allow-list — đo đĩa `ContractPhase.cs:17-27`: DangGopY=**3** < DangInKy=**5** ⇒ nằm TRONG `<DangInKy ∪ {TraLai=98, TuChoi=99}`; T-5 ChoDuyet=10 · T-6 DaPhatHanh=9 đều NGOÀI dải ⇒ 409 khớp. T-matrix ⟂ allow-list: 0 ca đá nhau.)*
- **X-2 [CHỐT — style-gate thắng] spec §③ W5(i) "cập nhật ĐÚNG 1 row test `docs/STATUS.md`" ⟂ style-gate §C:77-78 + §D C16 "pin **2 ô**".** Nguyên văn be2 `sub-style-review-b4-be2.md:221`: *"lật 2 ô: dòng session mới (DELTA 680→680+N) VÀ row canonical Tests `:476` (số tuyệt đối, **chỉ chép từ cicd-verify**)"* + R-12 :164-170. Chốt: 2 ô; vì DEPLOY CHỜ (CI runner P0) ⇒ commit W5 ghi **dòng session DELTA ngay**, row canonical `:476` **KHÔNG lật đợt này** — ghi `DEFERRED-TO-DEPLOY` vào `run.md` theo đúng cơ chế spec §④.4 (đây không phải quên: row `:476` tự khai kỷ luật chỉ chép số đã cicd-verify).
- **X-3 [CHỐT — lỗ spec, delta SF-26 chưa được W2 nhắc] spec §② W2 KHÔNG quyết `bodyMaxHClass` cho host mới.** style-gate §A.1 (fe1-R1/SF-26): đặt cây vào host PHẢI quyết tường minh — mặc định panel đẻ "2 cuộn lồng" (`PipelineTreePanel.tsx:112-115` tự khai: *"Host 3-panel (Panel-3 TỰ CUỘN) truyền class trần riêng — vd `lg:max-h-none`"*). Host mới = cột trái 400px trong grid `overflow-hidden` (đúng ca host-3-panel S168 F-1). Chốt: panel-1 truyền `bodyMaxHClass="lg:max-h-none"` + cột wrapper tự cuộn; ô đo THÊM: `grep -c "bodyMaxHClass" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1 sau khi xoá render cũ `:242` + mắt-đo local "đúng 1 thanh cuộn trong panel cây".
- **X-4 [CHỐT — delta fe1-R2 chưa được spec chép đủ] spec §② W1(b) chỉ ghi "clamp `/^[1-7]$/`" — THIẾU 2 vế `typeInt === 3 && rawCt !== null`.** Khuôn thật `WorkflowMatrixViewPage.tsx:85-86` là gate 3 vế (`typeInt === 10 && rawGroup !== null && /^[1-8]$/`); thiếu vế `typeInt === 3` thì `?type=1&ct=5` lọc HD-* trên bộ Duyệt-NCC = trang trắng (style-gate §A.2). Chốt: PLAN W1 ghi ĐỦ 3 vế.
- **X-5 [CHỐT — phát hiện MỚI của B5] comment stale THỨ HAI ngoài tầm C-6.** Spec W1(d) + C-6 chỉ vá/đo comment `Layout.tsx:141-142`; nhưng `WorkflowMatrixViewPage.tsx:58-59` cũng đang khai *"HĐ dùng CHUNG 1 bộ workflow V2 cho mọi ContractType ⇒ 7 leaf 'Luồng duyệt' … đều trỏ về matrix này"* — sau W1 câu này thành nói dối (mỗi leaf slice per-loại). Cùng file worker đang đụng (nhánh `ct` nằm ngay dưới). Chốt: W1 thêm việc sửa comment này; ô đo THÊM: `grep -ci "chung 1 bộ" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` = 0.
- **X-9 [CHỐT — mâu thuẫn NỘI BỘ style-gate §C] T-13 ghi "(gộp vào T-1 được)" ⟂ chú cuối bảng đòi "`[Fact]` ≥13".** Gộp T-13 vào T-1 thì chỉ còn 12 Fact (T-14 là phản-ca không viết) ⇒ vỡ ô ≥13. Chốt: **T-13 viết Fact RIÊNG** — thoả cả hai câu, và giữ được assert trim-to-null tách bạch (ContextNote==null khi Reason toàn khoảng trắng).
- **X-6 [ghi nhận] spec §② W5(d) viện "tiền lệ PE S155 đo 11/13 role CanDelete=0" ⟂ be2-R2 đo tươi key `Contracts` LocalDB = 12/13, Drafter=0.** Hai phép đo KHÁC key/thời điểm, CÙNG chiều kết luận (không gắn policy đợt này). PLAN cite số tươi 12/13 (be2-R2); điều kiện mở W8 giữ nguyên: policy + grant Drafter CÙNG commit + đo lại **prod** trước khi gắn.
- **X-7 [ghi nhận] các ô grep §③ W1(iv)/W2(i) và ô SF-17 W5(iv) đã có bản SIẾT trong §D** (C-8 2 vế + nhãn "canh-thoái-lui" cho vế đã-xanh-baseline; C-11 thêm vế hoán-đổi `=0` baseline 1 — B5 đo `MyContractsPage.tsx:242` đúng 1 hit; C-MIRROR-KHAI baseline 0/0 — B5 đo cặp `ContractDetailContent` SHA `cd81508f97cb` identical 2 app; W4 thêm ô RANGE C-13 mà §③ không có). Không mâu thuẫn giá trị — PLAN hợp nhất: **§D là chuẩn ô-máy, §③ giữ vai ô HÀNH-VI local ④**.
- **X-8 [ghi nhận] wave order W4→W1→W2→W3→W5 KHÔNG bị delta nào đổi tiền-đề.** B5 đo tươi neo W4: `fe-admin/Layout.tsx` open `:176` · KhkkN `:188` · close `:189` = ĐÚNG mốc C-13. W3 đứng SAU W2 (chỗ hiển thị list pendingMe = panel-3 do W2 quyết) — giữ. W1 ⟂ W3 đụng cùng vùng `Layout.tsx:148-153` ⇒ tuần tự (đã đúng thứ tự). SHA-pair baseline W2(iv) khớp đĩa: `usePipelineStages` = `1b9b5c611663…` · `PipelineTreePanel` = `4ffc96beded5…` (KHỚP 2 app, đo sha256sum hôm nay).

## PLAN FINAL SOLTUON-12-08-2026 (per-wave)

> **Thứ tự BẮT BUỘC: W4 → W1 → W2 → W3 → W5** (X-8: không delta nào đổi tiền-đề). Mỗi wave: local-verify + commit riêng; **PUSH GOM SAU** (deploy CHỜ — CI runner P0). Q1/Q2 = nhánh-đã-chọn (làm theo, anh bác thì đổi); **Q3/Q4/Q5 CẤM đụng** (chờ anh). Ô hành-vi đo theo spec §④ nấc-đo-local; ô không dựng được dữ liệu ⇒ `DEFERRED-TO-DEPLOY` ghi `run.md`, CẤM tick. Ô máy: nối `;` không `&&`, `grep -c … || true`, exit qua `${PIPESTATUS[0]}` (§D đầu bảng).

### W4 — Admin Designer 7 leaf (fe-admin, ≈6 dòng)

- **INPUT đọc:** spec §② W4 + style-gate §D ô C-13, C-1a, C-2, C-5. Không cần file B3.
- **File đích (DUY NHẤT):** `fe-admin/src/components/Layout.tsx`.
- **Việc theo thứ tự:**
  1. Import named `findContractWfByTypeCode` từ `@/lib/contractWorkflowFamily` (hàm SẴN `fe-admin/src/lib/contractWorkflowFamily.ts:77-80`; family const `:45`).
  2. TRONG khối `if (awV2Match) {` (baseline `:176`), SAU nhánh KhkkN `:188` và TRƯỚC `}` `:189`, thêm: ``if (findContractWfByTypeCode(code)) return `/system/approval-workflows-v2/${code}` `` — `code` = phần sau `AwV2_` (vd `HdTP`), GIỮ NGUYÊN casing.
  3. Kèm comment 2-3 dòng kiểu Pattern 16-bis như khối `:181-187` sẵn có (BE seed leaf mà resolvePath thiếu nhánh ⇒ MenuLeaf DROP im lặng — gotcha #50).
  4. KHÔNG đụng: nhánh `code === 'Contract'` `:178` (nhánh chết, 0 seed) · `DbInitializer` · họ `Khkk_*` (fe-admin cố ý không có WorkflowMatrixViewPage — `:185-187`).
- **Ô acceptance:** C-13 RANGE (`open < hit < close` và `hit > dòng KhkkN`; mốc 176/188/189) · spec ✅W4(i) sidebar đủ **17 leaf** (2 NCC + 8 KHKK + 7 "QT Duyệt HĐ — …" Order 11..17, đo local ④) · (ii) click "QT Duyệt HĐ — Thầu phụ" → Designer khoá mã `HD-TP`, history CHỈ HD-TP · (iii) control-âm: `KhkkN1` + `DuyetNcc` mở đúng trang cũ · (iv) C-2 build fe-admin PASS + C-1a eslint fe-admin ≤42 (37E/5W) KHÔNG TĂNG.
- **Bẫy né:** chèn NGOÀI khối awV2Match = leaf chết câm (#50) · đừng "tiện tay" xoá nhánh chết `:178` (ngoài lệnh).
- **DoD:** build + eslint delta 0 + commit `[CLAUDE] FE-Admin: YC-029 W4 — sidebar Designer 7 leaf QT Duyệt HĐ (resolvePath AwV2_Hd*)`.

### W1 — Danh-tính menu + slice per-loại (fe-user)

- **INPUT đọc:** spec §② W1 (a-d) + style-gate §A.2 (fe1-R2), §A.7 (fe1-R9) + §D ô C-1a/b, C-2, C-6, C-7, C-8, C-9 + X-4/X-5 trên đây.
- **File đích (2):** `fe-user/src/components/Layout.tsx` · `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx`.
- **Việc theo thứ tự:**
  1. **(Layout — accordion `:470-485`)** GIỮ nhánh Pe (`startsWith('/purchase-evaluations')`, `:476-478`) NGUYÊN VỊ TRÍ ĐẦU. THÊM ưu tiên `ct`: đọc `params.get('ct')`, nếu khớp `/^[1-7]$/` → `INT_TO_TYPE_CODE[Number(ct)]` (map SẴN `:27-29`) → `setExpandedCtCode` rồi return. Nhánh `?type=` cũ (`:479-484`) CHỈ còn chạy khi `['/my-contracts','/contracts/new','/inbox'].includes(location.pathname)` — allowlist EXACT (diệt bug A `?type=3` của workflow-matrix + cross-talk `Hdc_*` `/hard-copies?type=1..7`).
  2. **(Layout `:152`)** Approved: `?type=${typeInt}&phase=9` → thêm `&view=approved` (khuôn PE; trang bỏ qua param lạ — H-4 HELD).
  3. **(Layout `:141-142`)** viết lại comment stale: "cả 7 loại đều ?type=3 (HĐ chung 1 bộ workflow V2)" → nêu 7 bộ per-loại `HD-*` @S188, `ct` = danh-tính leaf (ô dương C-6 cần chứa ≥1 token `7 bộ|per-loại|S188`).
  4. **(Layout)** 🔴 KHÔNG thêm `ct`/`view`/`pendingMe` vào `TRANSIENT_QUERY_KEYS` `:350` — 3 param là DANH-TÍNH leaf, cho vào set là leaf mất phân biệt active (C-7 canh =0). Đây chính là cơ chế diệt bug B: `view` nằm NGOÀI set ⇒ leaf List tắt khi URL có `view=approved`.
  5. **(WfViewPage)** const cục-bộ `INT_TO_HD_CODE: Record<number, string>` 7-entry — **MỞ `src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs:33-42` CHÉP ĐÚNG** mapping ContractType→mã (⚠️ bản-sao-thứ-3 của họ; token gạch-nối `HD-NT-NCC`/`HD-NT-DV`; comment ghi nguồn + "drift-check = C-9"). KHÔNG import từ fe-admin (`contractWorkflowFamily.ts:16-18` tự khai fe-admin-ONLY).
  6. **(WfViewPage)** nhánh `ct` mirror khuôn `group` `:84-99`: `const rawCt = searchParams.get('ct')`; gate **ĐỦ 3 VẾ (X-4)**: `typeInt === 3 && rawCt !== null && /^[1-7]$/.test(rawCt)` → `hdCode = INT_TO_HD_CODE[Number(rawCt)]`, else `null`; `workflows = khkkGroup !== null ? …(giữ nguyên)… : hdCode !== null ? allWorkflows.filter(wf => wf.code === hdCode) : allWorkflows`. `ct` rác/thiếu ⇒ KHÔNG lọc (giữ hành vi cũ). 🔴 KHÔNG thêm `ct` vào `queryKey` (lý-do ghi sẵn `:88-91` — client-side slice của cùng payload).
  7. **(WfViewPage `:58-60` — X-5)** sửa comment stale thứ 2 "HĐ dùng CHUNG 1 bộ workflow V2…" → 7 bộ per-loại, `ct` slice.
- **Ô acceptance:** spec ✅W1(i) tái-hiện A local ④ (mở nhóm 01 → "Luồng duyệt" → accordion GIỮ 01; control-âm `ct=5` → nhóm 05) · (ii) tái-hiện B (`?type=1&phase=9&view=approved` → CHỈ leaf Đã-duyệt sáng) · (iii) vòng `ct ∈ 1..7` ≥1 quy trình ĐÚNG HỌ + 0 dòng khác họ + `ct=1` VẮNG `QT-HD-V2-001` + control `ct=99` KHÔNG lọc *(mapping int→code SAI thì C-9 vẫn xanh — chính vòng (iii) này mới khoá mapping)* · C-8 vế-1 `get('ct')` ≥1 (mới) + vế-2 `workflow-matrix?type=3&ct=` ≥1 (canh-thoái-lui, đã xanh baseline) · C-9 = 7 mã distinct · C-6 2 vế · ô X-5: `grep -ci "chung 1 bộ" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` = 0 · C-7 = 0 · C-1a/b eslint fe-user ≤47 (41E/6W), `Layout.tsx` ≤4E **0 ruleId mới** (lỗi sẵn :478 nằm đúng khối accordion — đổi hình thì lỗi phải ≤ cũ) · C-2 build fe-user.
- **Bẫy né:** allowlist pathname phải EXACT — `startsWith('/contracts')` sẽ nuốt luôn `/contracts/workflow-matrix` (tái sinh bug A) · KHÔNG sửa `:151` (WfView ĐÃ đúng `type=3&ct=` — v2 GIỮ type=3, chỉ THÊM logic đọc) · grep có `?` phải `-F` (B2 cụm 5).
- **DoD:** build + eslint + 3 ô hành-vi local + commit `[CLAUDE] FE-User: YC-029 W1 — danh-tính menu 7 loại HĐ (accordion ct + view=approved + WfView slice HD-*)`.

### W2 — HOÁN ĐỔI 2 khung: cây trái 400px + list phải 360px (host fe-user; cặp mirror ×2 app)

- **INPUT đọc:** spec §② W2 + Q1/Q2 (nhánh-đã-chọn) + style-gate §A.1 (SF-26), §A.3 (fe1-R3 JSDoc), §A.4 (fe1-R4 Select native), §A.5 (fe1-R5 header) + §D ô C-2, C-3, C-5, C-10, C-11 + X-3 trên đây.
- **File đích (3 + 2 copy):** host `fe-user/src/pages/contracts/MyContractsPage.tsx` (CHỈ fe-user — fe-admin không có trang này) · cặp mirror `fe-user/src/hooks/usePipelineStages.ts` + `fe-user/src/components/pipeline/PipelineTreePanel.tsx`, sửa xong **COPY NGUYÊN BYTE** sang `fe-admin/src/...` cùng đường dẫn.
- **Việc theo thứ tự:**
  1. **(hook — cặp mirror)** `BuildStagesOptions` thêm 2 opt-in **FLAT trong opts** (CẤM bịa tầng `stage3Options` — F-4 lens-1):
     - `onOpenContract?: (id: string) => void` — JSDoc theo khuôn `:185-192` sẵn, BẮT BUỘC có mệnh đề *"hông truyền ⇒ giữ nguyên `navigate('/contracts/…')` — 6 host còn lại 0 đổi hành vi"* (fe1-R3, gate `grep -cE "hông truyền"` ≥1 per prop mới);
     - `stage3TypeFilter?: number` — lọc leaf GĐ3 theo ContractType; JSDoc cùng chuẩn ("hông truyền ⇒ đủ mọi loại").
     Site leaf GĐ3 `:368-378`: `onOpen: opts.onOpenContract ? () => opts.onOpenContract!(contractId) : () => navigate(\`/contracts/${contractId}\`)` (khuôn `:228-230`/`:244`); điều kiện push leaf thêm vế `(!opts.stage3TypeFilter || !c || c.type === opts.stage3TypeFilter)` — **`c` vắng (HĐ ngoài chỉ mục 200, `:371-373`) thì leaf VẪN HIỆN khi filter bật** (không đủ dữ liệu phân loại — thà thừa; ghi comment quyết định này).
  2. **(panel — cặp mirror)** `PipelineTreePanel.tsx:111` mở rộng Pick: `'stage2Content' | 'stage2Count' | 'onOpenKhkk' | 'onOpenPe' | 'onOpenContract' | 'stage3TypeFilter'` (GIỮ tên prop `stage2Options`).
  3. **(host — grid)** `MyContractsPage.tsx:153`: `lg:grid-cols-[320px_1fr_360px]` → `lg:grid-cols-[400px_1fr_360px]` (khuôn host `KhkkListPage.tsx:171`). **Panel-1 TRÁI = khối MỚI:** thanh đầu = Select loại HĐ (wrapper `ui/select.tsx` NATIVE `:4-19` — fe1-R4, CẤM kéo radix; option đầu "Tất cả loại HĐ" cho ca URL trần; default = `?type=N` của URL, sync khi URL đổi; đổi Select CHỈ đổi state lọc cây — KHÔNG navigate/không đổi list) + `<PipelineTreePanel currentStage={3} hideHeader hideSearch bodyMaxHClass="lg:max-h-none" stage2Options={{ onOpenContract: id => setSelectedId qua ?id=, stage3TypeFilter: selectLoại || undefined }} />`; cột wrapper TỰ CUỘN (X-3/SF-26 — mặc định panel = 2 cuộn lồng). Nếu panel-1 cần header bar riêng: dùng đúng chuỗi class `KhkkListPage.tsx:157` (fe1-R5).
  4. **(host — panel-3 PHẢI 360px)** = khối DANH-SÁCH HĐ hiện tại CHUYỂN CHỖ NGUYÊN CHỨC NĂNG (search + empty-state + lọc `phase=9`/`deleted=1` hiện có — Q1: KHÔNG xoá khối UI anh không bảo xoá; HĐ tạo-trực-tiếp-ngoài-gói luôn thấy được ở đây). **Search ĐỔI client→server**: nối ô tìm vào param `search` của `GET /contracts` (SẴN `ContractsController.cs:19` — nói thẳng: đây là ĐỔI hành-vi, F-10 lens-2).
  5. **(host — panel-2 GIỮA)** giữ nguyên detail/placeholder; click leaf cây → chọn vào panel-2 qua `?id=` — URL Ở LẠI `/my-contracts`. Xoá render cây cũ `:242` (`{!selectedId && <PipelineTreePanel …}`) — C-11 vế-2 về 0.
  6. Copy 2 file cặp sang fe-admin (fe-admin KHÔNG truyền 2 opt-in ⇒ control-âm: hành-vi cũ giữ nguyên, kể cả `ContractsListPage.tsx:265` fe-admin).
- **Ô acceptance:** C-11 2 vế (`grid-cols-[400px` ≥1 **VÀ** `{!selectedId && <PipelineTreePanel` = 0 — baseline B5 đo `:242` = 1) · ô X-3: `grep -c "bodyMaxHClass" …MyContractsPage.tsx` ≥1 + mắt-đo local đúng 1 thanh cuộn trong panel cây · C-3 vòng 4 cặp + guard MISSING, 2 cặp W2 SHA MỚI khớp nhau 2 app (baseline cũ `1b9b5c61…`/`4ffc96be…` SẼ đổi — khớp-cặp là chuẩn, không phải khớp-baseline) · C-10 = 0 **+ ô THÊM cùng dạng cho token `stage3TypeFilter`** (đổi token trong đúng lệnh C-10) = 0 · spec ✅W2(ii) click leaf → panel-2, URL ở lại (local ④) · (iii) Network tab `/contracts?search=` (server-side) · (v) control-âm fe-admin: leaf GĐ3 vẫn `navigate('/contracts/{id}')`; leaf GĐ1/GĐ2/GĐ4 không đổi ở CẢ 2 app · (vi) deep-link `/contracts/{id}` dán thẳng vẫn mở detail · (vii) HĐ ngoài-gói hiện panel-3 · (viii) C-2 build ×2 app + eslint C-1a/b KHÔNG TĂNG.
- **Bẫy né:** SF-26 hai-thanh-cuộn-lồng (S168 F-1) · `hideHeader` nuốt nút "Hiện/Ẩn cây" mobile — panel đã tự ép thân hiện (`PipelineTreePanel.tsx:117-122`), vẫn PHẢI kiểm viewport `<lg` local còn thấy cây · sửa tay 2 bản mirror thay vì copy-byte = SHA lệch · Select đổi loại mà navigate = đổi cả list (SAI — chỉ lọc cây).
- **DoD:** build ×2 + SHA cặp khớp + hành-vi local + commit `[CLAUDE] FE-User: YC-029 W2 — hoán đổi cây toàn trình sang trái + Select loại + list phải (mirror cặp pipeline ×2 app)`.

### W3 — Đang-duyệt ở lại trang (fe-user, FE-only — khuôn PE đổi-endpoint)

- **INPUT đọc:** spec §② W3 + §D ô C-2, C-7. Khuôn: `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:91-98`.
- **File đích (2):** `fe-user/src/components/Layout.tsx` (`:150`) · `fe-user/src/pages/contracts/MyContractsPage.tsx`.
- **Việc theo thứ tự:**
  1. Layout `:150`: `if (action === 'Pending') return \`/my-contracts?type=${typeInt}&pendingMe=1\`` (thay `/inbox?type=`). GIỮ route `/inbox` + `InboxPage` nguyên trạng — hộp thư tổng.
  2. MyContractsPage: đọc `pendingMe` từ URL. Khi `= '1'` ⇒ panel-3 ĐỔI ENDPOINT: `GET /contracts/inbox` (0-param — SẴN `ContractsController.cs:41-43`, trả `List<ContractListItemDto>`) + lọc `type` CLIENT-SIDE (`c.type === Number(typeParam)`), ép shape về dạng list thường (khuôn PE `:98`: `{ items, total: length, page: 1, pageSize: length }`); ẩn/khoá phân trang + search-server ở chế độ này (inbox không phân trang — payload nhỏ, đúng hiện trạng `InboxPage.tsx:103`). Comment khai lệch-W7 CHẤP NHẬN + `[carry:w7b-inbox-param]` (nâng BE là việc khác đợt).
- **Ô acceptance:** spec ✅W3(i) so-số 2 chiều local ④ (user có ≥1 HĐ đến-lượt + ≥1 không-đến-lượt: bật `pendingMe=1` ⇒ list ÍT HƠN + HĐ không-đến-lượt VẮNG; control-dương: user ngoài luồng ⇒ **0 dòng**, không phải cả list) · (ii) leaf "Đang duyệt" ở lại `/my-contracts` · C-7 = 0 (pendingMe KHÔNG vào TRANSIENT — danh-tính leaf) · C-2 build fe-user + eslint không tăng.
- **Bẫy né:** 🔴 CẤM gửi `?pendingMe=1` lên `GET /contracts` — BE KHÔNG có param này, ASP.NET NUỐT im lặng ⇒ list y hệt list thường (B2 BLOCKER ❷; đổi-endpoint là khuôn đúng) · queryKey phải tách trục `pendingMe` (2 nguồn dữ liệu khác nhau — đừng dùng chung key gây cache lẫn).
- **DoD:** build + đo 2 chiều + commit `[CLAUDE] FE-User: YC-029 W3 — leaf Đang duyệt ở lại /my-contracts (pendingMe đổi-endpoint /contracts/inbox)`.

### W5 — Đường XOÁ đồng-khuôn KHKK (BE nhỏ + FE, 0-mig, TEST-BEFORE)

- **INPUT đọc:** spec §② W5 (a)-(e) + style-gate §B TRỌN (be2-R1/R2/R7/R13/R14) + §C T-matrix + §D khối BE (**MỞ `sub-style-review-b4-be2.md` mục "Ô đo phải sửa" `:198` chạy đúng bảng C1-C21 đã sửa**) + §D ô FE C-12, C-MIRROR-KHAI, C-3, C-5 + X-1/X-2/X-9 trên đây.
- **File đích (4):** `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` (khối DELETE `:885-902`) · `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` (`:78-81`) · test MỚI `tests/SolutionErp.Infrastructure.Tests/Application/ContractDeleteGuardTests.cs` (tên CỤ THỂ — CẤM glob) · cặp FE `fe-{user,admin}/src/components/contracts/ContractDetailContent.tsx` (sửa fe-user → copy-byte fe-admin).
- **Hiện trạng đã đo (B5, đừng đo lại):** `DeleteContractCommand(Guid Id)` KHÔNG Reason/owner-check/changelog; guard cũ `:896` `Phase >= DangInKy`; controller DELETE `:78` KHÔNG policy (class `[Authorize]` trần `:13`); `Remove` = SOFT-delete qua `AuditingInterceptor.cs:54-62` (H-6 — chỉ assert); FE DTO SẴN `drafterUserId` (`fe-user/src/types/contracts.ts:154`); EF `ContractChangelogConfiguration.cs:20` ContextNote HasMaxLength(2000).
- **Việc theo thứ tự (test-before):**
  1. **Viết `ContractDeleteGuardTests.cs` TRƯỚC** — fixture private-per-file theo nếp Contract-family (`ContractSigningPlanCrudTests.cs:77` `KhkkFixture` / `ContractCatalogTests.cs:83` — be2-R14, cái thứ 6 cùng nếp là ĐÚNG khuôn), fixture **PHẢI wire `AuditingInterceptor`** (thiếu = assert soft-delete/changelog RỖNG-NGHĨA). Ca = **T-1..T-13 ĐÚNG BẢNG §C, mỗi ca 1 `[Fact]` — T-13 viết RIÊNG (X-9: gộp vào T-1 là vỡ ô `[Fact] ≥13`)**. Chi tiết bẫy per-ca: T-2/T-3 Dev 0-row TraLai/TuChoi ⇒ fixture seed THẲNG `Phase` (§D dữ-liệu đo tay) · T-9 fake `ICurrentUser` UserId=null ⇒ expect `UnauthorizedException` (nấc-0) · T-10/T-11 khoá THỨ TỰ ném exception 404→403→409 · T-1 assert 3-trong-1 (khuất query thường · `IgnoreQueryFilters` thấy IsDeleted/DeletedBy · changelog đúng 1 dòng ContextNote/PhaseAtChange/UserId). Chạy chứng ĐỎ (guard chưa có) — KHÔNG commit lúc đỏ. **T-14 phản-ca: KHÔNG VIẾT test reflection-authz cho DELETE** (2 chiều đều cấm — chiều phủ-định pin lỗ, ĐỎ oan khi W8 mở).
  2. **BE handler viết lại (`ContractFeatures.cs:889-902`):** thứ tự trong handler =
     (0) nấc-0 actor-guard khuôn `PeSoftDeleteFeatures.cs:56-58`: `!currentUser.IsAuthenticated || currentUser.UserId is null → UnauthorizedException` — 🔴 khuôn KHKK `:1763` KHÔNG có nấc này (`DrafterUserId == UserId` để null==null lọt), ĐỪNG chép nguyên hình;
     (1) load + `NotFoundException` (giữ `:893-894`);
     (2) owner-check khuôn KHKK `:1763-1766`: `isOwner = entity.DrafterUserId == currentUser.UserId` (an toàn vì nấc-0 đã chặn UserId null) · `isAdmin = currentUser.Roles.Contains(AppRoles.Admin)` · `!isOwner && !isAdmin → ForbiddenException` (T-7/T-8; KHAI trong comment: sửa lỗ sẵn có #82 — trước đây user thường xoá được HĐ người khác);
     (3) phase guard THAY `:896-897`: allow `Phase < DangInKy(5)` **∪** `{TraLai(98), TuChoi(99)}`, còn lại `ConflictException` message liệt rõ trạng thái cấm (chặn ChoDuyet=10 kể cả Drafter — khuôn KHKK, approver-delete = Q4; chặn DaPhatHanh=9 mọi đường — đã gen mã; T-4/T-5/T-6; 🔴 bẫy vá-quá-tay: ĐỪNG chép pattern `is not (DangSoanThao or TuChoi or TraLai)` của KHKK — allow-list HĐ RỘNG HƠN, gồm cả 1/3/4 legacy `<DangInKy`, T-4 bắt);
     (4) command nở `DeleteContractCommand(Guid Id, string? Reason = null)` + validator MỚI khuôn `:1743-1752` (`RuleFor(Reason).MaximumLength(2000)` MATCH EF `:20` — T-12) + trim-to-null khuôn `:1785` (T-13);
     (5) 🔴 `db.Contracts.Remove(entity)` **TRƯỚC** → `db.ContractChangelogs.Add(new ContractChangelog { ContractId, EntityType = Contract, EntityId = null, Action = Delete, PhaseAtChange = <phase snapshot>, UserId, UserName, ContextNote = reason })` **SAU** (bẫy T26 — đảo là cascade DETACH ⇒ 0 row im lặng, nguyên lý ghi tại KHKK `:1787-1790`; field-set mirror 2 site ghi-tay `CreateContractFromSigningPlanFeatures.cs:247` / `CreateContractFromEvaluationFeatures.cs:121`). 🔴 **GHOST-WIRE be2-R13:** comment `ContractChangelog.cs:10-12` nói "AuditBehavior tự populate" — **file KHÔNG TỒN TẠI**, changelog là GHI TAY; nhân tiện sửa/chú thích comment đó (cùng lượt chạm, hết nói dối).
  3. **Controller `:78-81`:** thêm `[FromQuery] string? reason = null` → `new DeleteContractCommand(id, reason)` (khuôn KHKK controller `:153-159`) — 🔴 **KHÔNG chép `[Authorize(Policy = "….Delete")]`** từ khuôn (be2-R1 LẬT: GIỮ shape class-`[Authorize]`-trần, C10 kỳ vọng **= 0**; SB-14 → comment khai VÌ SAO CHƯA gắn: chốt (82) W8 HOÃN + BS-3/be2-R2 đo LocalDB key `Contracts` = 12/13 role CanDelete=0, Drafter=0 ⇒ gắn = khoá oan mất nút xoá nháp; đánh `[carry:w8-delete-policy]`; điều kiện mở W8 = policy + grant Drafter CÙNG commit + đo lại prod).
  4. **FE `ContractDetailContent.tsx` (fe-user rồi copy-byte fe-admin):** port khuôn THANH NÚT ĐÁY `fe-user/src/components/khkk/KhkkDetailContent.tsx:1076-1097` (sticky bottom-0 + `window.confirm` message CÓ ĐÍCH nhắc chuyển sang mục "Đã xóa" + `Trash2` + `isPending` disable) — 🔴 KHÔNG lấy `KhkkWorkflowPanel` làm khuôn (phần xoá trong đó = CODE CHẾT S175). Điều kiện render nút = `phase ∈ allow-list` (mirror BE: `<5 || 98 || 99`) **VÀ** (`isDrafter` khuôn null-safe `PeDetailTabs.tsx:141`: `currentUser?.id != null && contract.drafterUserId === currentUser.id` **||** `isAdmin`). Mutation `DELETE /contracts/{id}?reason=…`; xong: toast + rời detail về list. GIỮ `<Dialog` hiện có = 1 (C-12 vế 3).
  5. **Tự-khai MIRROR (SF-17):** thêm 1 dòng header `// File MIRROR SHA256 identical — …` CÙNG BYTES 2 bên cặp `ContractDetailContent` (C-MIRROR-KHAI baseline 0/0 → 1/1; SHA cặp B5 đo hôm nay `cd81508f97cb` identical — sau sửa SHA mới phải tiếp tục khớp-cặp).
  6. **STATUS (X-2):** dòng session DELTA `680→680+N` ghi ngay tại commit; row canonical `Tests` `:476` **KHÔNG lật** (chỉ chép từ cicd-verify — be2 C16 `:221`); ghi `DEFERRED-TO-DEPLOY: STATUS row :476` vào `run.md`.
- **Ô acceptance:** `dotnet test SolutionErp.slnx` PASS **680 → 680+N, N ≥ 13** (baseline 680 đo lại TRƯỚC khi sửa — Đ-9) · 13 `[Fact]` T-1..T-13 xanh + T-14 tôn trọng · bảng BE be2 C1-C21 đã sửa (scope `awk '/class DeleteContractCommandHandler/,/^}/'` TRƯỚC grep — C3/C5 từng dương-giả 7-hit/4-hit; **C10 = 0**; C13 BỎ; C14 neo tên file cấm glob; C17 viết `"A|B|C"` + control-dương 292; C18 union ls-files; C19 `--include=*.cs`; C21 sqlcmd ma-trận CanDelete trước mọi kết luận policy) · changelog đọc-ngược DB đúng 1 dòng Action=Delete (0 dòng = dẫm T26) · C-12 (`window.confirm` ≥1 + message 'Đã xóa' ≥1 + `<Dialog` = 1) · C-MIRROR-KHAI = 1 mỗi bên + SHA cặp khớp (C-3 guard MISSING) · C-2 build ×2 app · C-5 union danh-sách file (git diff MÙ untracked — test file MỚI phải hiện qua `ls-files -o`).
- **Bẫy né (gom):** T26 thứ-tự Remove/Add · ghost-wire AuditBehavior (be2-R13) · chép policy khuôn KHKK controller (NGƯỢC be2-R1) · chép owner-check KHKK thiếu nấc-0 (T-9 bắt) · vá-quá-tay pattern phase KHKK (T-4 bắt) · fixture thiếu AuditingInterceptor (T-1 rỗng-nghĩa) · sửa STATUS row canonical (X-2).
- **DoD:** test 680+N xanh local + build ×2 + commit **MỘT phát** BE+FE+tests+STATUS-delta: `[CLAUDE] Contract: YC-029 W5 — đường xoá đồng-khuôn KHKK (owner-check + phase allow-list + changelog, test-before 13 ca)`.

## Điều kiện dừng/leo thang cho worker (Đ-1..)

Gặp bất kỳ điều nào dưới đây: **DỪNG wave đang làm, ghi hiện trạng vào `run.md`, hỏi lead** — KHÔNG tự chế nhánh xử lý.

- **Đ-1 — eslint delta vỡ:** `npx eslint .` app đụng TĂNG so baseline (fe-user >47 · fe-admin >42) hoặc file-wave `Layout.tsx` >4E hoặc xuất hiện **ruleId MỚI** ⇒ DỪNG. CẤM `eslint-disable` để ép xanh (rời tập-đo).
- **Đ-2 — SHA-pair không khớp được:** copy-byte rồi mà 2 bản mirror vẫn lệch SHA (nghi BOM/CRLF/format-on-save) ⇒ DỪNG báo kèm `sha256sum` 2 bên. CẤM sửa `.gitattributes`/editor-config để ép.
- **Đ-3 — T-matrix FAIL không rõ nguyên nhân:** đặc biệt T-1 changelog 0-row DÙ đã Remove-trước-Add-sau (nghi cascade timing khác KHKK) hoặc T-9 không ném đúng exception ⇒ DỪNG kèm log. CẤM nới assert cho xanh.
- **Đ-4 — đụng file NGOÀI danh sách wave:** kể cả "tiện tay" refactor/format file lân cận ⇒ DỪNG hỏi trước khi commit (reviewer gate đánh scope-drift = FAIL).
- **Đ-5 — spec/plan LỆCH so đĩa:** neo `file:line` lệch >±15 dòng, hàm/khuôn không tồn tại, hình khuôn khác mô tả ⇒ DỪNG báo (mọi neo trong plan này B5 đã đo tươi 2026-08-12 — lệch nghĩa là cây đã trôi giữa chừng).
- **Đ-6 — mất control W1(iii):** Dev VẮNG `QT-HD-V2-001` (ô "vắng-vì-lọc" mất nghĩa) ⇒ đánh dấu ô mất-control trong `run.md` + DỪNG hỏi (dữ liệu này dính Q5 đang chờ anh — CẤM tự tạo/xoá quy trình tay).
- **Đ-7 — ô hành-vi không dựng được dữ liệu local:** áp đúng spec §④.4: đánh `DEFERRED-TO-DEPLOY` công khai trong `run.md`, CẤM tick, CẤM lặng lẽ bỏ ô. (Không phải leo thang — nhưng là đường thoát HỢP LỆ duy nhất.)
- **Đ-8 — chạm ranh CHỜ-ANH:** mọi việc dính Q3 (port 3 món KHKK/DTO nở) · Q4 (approver-delete `AllowApproverDelete`) · Q5 (QT-HD-V2-001) · W8 (policy DELETE/authz diện rộng) · W9 (đã HỦY) ⇒ DỪNG ngay — spec đã tách các món này KHỎI wave.
- **Đ-9 — baseline test lệch:** `dotnet test` TRƯỚC khi sửa ra số ≠ 680 ⇒ DỪNG báo (số pin từ B4 có thể trôi bởi commit khác chen giữa; cần lead chốt lại mốc trước khi tính 680+N).
- **Đ-10 — build/test đỏ vì nguyên nhân NGOÀI wave** (localdb/seed hỏng, package restore fail) ⇒ DỪNG báo, CẤM `--no-verify` mọi tình huống.
