diff --git a/.claude/WAL.md b/.claude/WAL.md
index 97b1366..8980a6e 100644
--- a/.claude/WAL.md
+++ b/.claude/WAL.md
@@ -12,7 +12,7 @@ chain:
- [x] V3 trio TRỌN — eval MIXED 19-trục/14-signal → refine 1-action/14-BÁC (E1 impl-fe drain −62,5% dưới low-water, A4 0-sàn → CHỜ-ANH) → audit 55Đ/5T re-chạy-177-số/172-byte-exact + CHẶN-A1 (2 mục MISS = trivial<30min + build-gap ⇒ 0 đích reinject; 11/13 đứng-yên trừ khi anh đổi chữ/matcher → CHỜ-ANH) · #53: eval-return + audit-×2 (resume ×2 cứu trọn), refine sạch
- [x] V4 nén-ngủ (⚠️ marker lật `[!]`→`[x]` @S171 ground-truth: `last_sleep_at=2026-08-03` + gist trong archive + `sub-ring4-audit-sleep-S170.md`; thân dòng đã tự khai XONG từ S170, chỉ ô chưa lật) — P0✓(17 shard/238.849B) · P1✓ 2 curator XONG (A: 9f/45c 113.797B resume-1× · B: 8f/48c 123.454B sạch) + 🔐 2 secret THÔ redact (xem dòng CHỜ-ANH) + đề-nghị skip cicd/2026-08 (đã-chưng-cất) CHẤP NHẬN ⇒ 16 gist · P2✓ 2 drafter sạch (A 8-draft/98KB 38/38-pointer · B 8-draft+note/121KB) · P3(a)✓ coverage-máy 16/16 PASS (77 token-literal bổ sau soi-tay 4-ca: MISS = biến-thể dải/range/bold-split, KHÔNG mất nội-dung) · P3(b)✓ FIDELITY PASS-WITH-FIXES (reviewer 38KB ×2-resume: P1 4-cờ [3-drift+1-MA AllowDelegate pickaxe-0] · P2a-d · P3-lưới) → 9 nhóm fix ÁP TRỌN (kể cả bug máy-append cut-byte → vá cắt-ký-tự) · P4✓ 16 gist → archive/ (233.858B ≈ 100,4% nguồn — KHÔNG claim giảm-byte; additive hash-assert 16/16; last_sleep 08-03) · P4.5✓ ring4 8Đ/3T (3T=sai-số-đo 13B-tiền-redact+làm-tròn; ADDITIVE vững; T-1..T-5 tồn-dư → vá TRỌN vào archive @S170; caveat "gate-PASS ≠ 0-mất-load-bearing") ⇒ V4 [x] XONG → còn 3.5 đuôi + P5 report
- [x] Phase 3.5 đuôi TRỌN — MIND-1 (mind-check 10Đ/0T ×2) + synthesis (orphan-4 ĐÓNG) + ctx-audit TRUOT-9-FLAG liền-mạch (THẬT/ĐỦ/SẠCH đạt, §E 10/10 khen) → lead vá 4 cùng lượt: WAL-next-stale · HANDOFF (47)(48) thước-tươi · §E +2 ring-verdict + neo MIND-1 · persona ctx-audit [p,p+3] (ENV-DEP) + seed diary on-behalf. Mồi @close: port refresh-bookend sang _context (lỗ FLOW đường tiep→closeout) + **7**-dir-rỗng (S171 sửa từ "6" — ctx-verifier cờ⑧: closeout đọc mồi sẽ dọn 6, SÓT đúng cái mới `sleep-drafts/.claude/agent-memory/reviewer`) + sleep-drafts + deep-JUMP 7/11
-- [!] wf: `2026-08-04-S171-khkk-ui-mirror-pe` — TASK SẢN-PHẨM anh giao @S171: bố-trí lại 3 panel màn **KHKK** đồng-nhất với **Duyệt NCC** (P1 list+cây · P2 content-trên/ý-kiến-dưới · P3 quy-trình+history). A✓ `/fable-real investigator-codebase` (`wf_b76692d4-3a4`, 756s, return SẠCH) → artifact 44.830 B **27 giống-được/6 buộc-khác/8 data-gap**; 2 đính-chính: PE-panel-1 INLINE `:399-639` KHÔNG phải `PeListPanel` · **GĐ3 `ContractsListPage.tsx:125` đã là PE-3-panel gọt gọn (S168)** ⇒ KHKK = mắt-xích lệch DUY NHẤT, chép khuôn từ GĐ3; + đảo ước-lượng: approvals+changelog **ĐANG GHI THẬT** (`WorkflowService.cs:534/:547`) chỉ thiếu ~5 field DTO + 1 endpoint. B `/fable-clone reviewer` ×3 lăng-kính (fidelity·feasibility·regression) **ĐANG CHẠY** `wf_832dadb2-93f` → C lead refute+synthesize→ghi spec 3-mục → D Opus 5 MAX thực-thi. Delta gốc đã đo: PE = 1 màn 3-panel (`PurchaseEvaluationsListPage.tsx:397` grid 400/1fr/360) ⟂ KHKK = list 2-panel (`KhkkListPage.tsx:197`) + detail trang riêng có workflow XẾP CHỒNG (`KhkkDetailPage.tsx:411`)
+- [!] wf: `2026-08-04-S171-khkk-ui-mirror-pe` — TASK SẢN-PHẨM anh giao @S171: bố-trí lại 3 panel màn **KHKK** đồng-nhất với **Duyệt NCC** (P1 list+cây · P2 content-trên/ý-kiến-dưới · P3 quy-trình+history). A✓ `/fable-real investigator-codebase` (`wf_b76692d4-3a4`, 756s, return SẠCH) → artifact 44.830 B **27 giống-được/6 buộc-khác/8 data-gap**; 2 đính-chính: PE-panel-1 INLINE `:399-639` KHÔNG phải `PeListPanel` · **GĐ3 `ContractsListPage.tsx:125` đã là PE-3-panel gọt gọn (S168)** ⇒ KHKK = mắt-xích lệch DUY NHẤT, chép khuôn từ GĐ3; + đảo ước-lượng: approvals+changelog **ĐANG GHI THẬT** (`WorkflowService.cs:534/:547`) chỉ thiếu ~5 field DTO + 1 endpoint. B✓ `/fable-clone reviewer` ×3 (`wf_832dadb2-93f`) — 🔴 **3/3 lane CHẾT "hit session limit · resets 6:10pm"**, engine trả null×3, **NHƯNG ĐĨA CỨU 2,95/3** (74.070 B): feasibility 8Đ/6BROKE · regression 8Đ/4BROKE/10-bỏ-sót **HOÀN TẤT** · fidelity DỞ (§6+§8 trống, VERDICT `?` — lead derive ≈12Đ/6BROKE/3, tự chạy nốt phép §6). C✓ **spec `spec-khkk-ui-mirror-pe-04-08-2026.md` 35.017 B** đủ ①②③, 0 residual. 🔴 **3 chốt lead ĐẢO bản invest**: (1) panel-1 KHÔNG dùng `PipelineTreePanel` — `canPe` khoá cây ⇒ user KHKK-only **mất TRẮNG danh-sách** (+trần-200 +cây tự-navigate +`content` nuốt `groups`) ⇒ dựng cây từ danh-sách CỦA CHÍNH KHKK · (2) miễn-trừ **6→3** (P1-4·P2-7 lật giống-được · P3-12 hạ chờ-anh: lead đo `DbInitializer.cs:554` = 3 Bước×≤2 Cấp, seed "sao từ workflow Duyệt NCC prod" ⇒ tiền-đề "3 trạm ngắn" SAI) + **miễn-trừ thứ 7 KHÔNG khai** (F-5 panel-3 empty-state) · (3) giữ P/A **thay lý-do** (nhị-phân giả). **6 acceptance bẻ gãy khi chạy thật** (A-13 citation-trap báo đỏ file sạch · A-6 kiểm NHẦM KHOÁ để lọt đúng bug S155 · A-10 sai trục · A-7b/A-7d/A-8c Goodhart) — đã thay bản sửa. ✅ **3 QĐ ANH chốt 04-08** (spec→43.087 B): (1) panel-1 = **`PipelineTreePanel`** — anh **BÁC** đề-xuất đảo của em, đọc đủ 4 rủi-ro trên thẻ rồi vẫn chọn ⇒ QĐ có-thông-tin; em chuyển 4 rủi-ro thành **mitigation CƯỠNG-HÀNH** C-5..C-8 (*chọn HÌNH ≠ chấp-nhận VỠ*) · (2) panel-3 rỗng = **CHỈ hộp dashed y PE** ⇒ đóng miễn-trừ-thứ-7 · (3) file-khi-duyệt = **upload THẬT** ⇒ BE-7 `+ApprovalAttachment=3` policy `KeHoachKyKet.Update`. **D ĐANG CHẠY** `wf_6fd33d7a-a20` (giới-hạn reset 18:10, phóng 18:50): `implementer-backend`→`implementer-frontend`, opus+effort:max, TUẦN TỰ, spec bơm qua con-trỏ file (B6). 🔸 Treo không-chặn: P3-12 4-return-mode = **hoãn** (tiền-đề "3 trạm" SAI), hỏi anh khi tới. Delta gốc đã đo: PE = 1 màn 3-panel (`PurchaseEvaluationsListPage.tsx:397` grid 400/1fr/360) ⟂ KHKK = list 2-panel (`KhkkListPage.tsx:197`) + detail trang riêng có workflow XẾP CHỒNG (`KhkkDetailPage.tsx:411`)
- [ ] monthly drift-audit 2026-08-01 DUE quá hạn (session đầu-tháng = L10) + re-sync measured{} 17/23 — xếp thứ-tự vs task anh giao
- [ ] 🔴 CHỜ-ANH slot (65) ZZTEST hạn 01-08 QUÁ 2 ngày (chỉ anh có kênh) · (64) user TEST · (66) dọn HĐ ZZTEST · thư day-wake-probe nấc adopt (ctx-verifier cờ ⑩: món 65 từng ngoài bản-đồ)
- [ ] 🔴🔴 CHỜ-ANH MỚI @S170: XOAY password SQL prod `vrapp` — 2 secret THÔ trong agent-memory (cicd-runs:48 + reviewer-note:22, 2 giá-trị KHÁC nhau) đã redact `[đã che 71b7740a/4c3c67d5]` NHƯNG nằm trong git remote từ ~13-05 → rotate là bắt buộc thực-chất; + A4 low-water 0-sàn (refine E1) + đổi-chữ/matcher MFE impl-fe (audit CHẶN-A1)
diff --git a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/run.md b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/run.md
index 692e768..40204fc 100644
--- a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/run.md
+++ b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/run.md
@@ -73,10 +73,41 @@ Opus 5 MAX theo `spec-khkk-ui-mirror-pe-04-08-2026.md` (lead ghi SAU khi verify
- [x] A — `/fable-real investigator-codebase` deep-pass (Fable 5, `wf_b76692d4-3a4`, 756s, 33 tool-use, return SẠCH 0-garble) → `sub-investigator-codebase-invest-s171.md` **44.830 B** · **VERDICT=27 giống-được / 6 buộc-khác / 8 data-gap**
🔴 **2 đính-chính vào Ground-truth của lead** (bảng trên SAI 1 dòng): (Đ-1) PE panel 1 **KHÔNG** phải `PeListPanel` — viết INLINE `PurchaseEvaluationsListPage.tsx:399-639`; `PeListPanel` chỉ 1 call-site = `PurchaseEvaluationWorkspacePage.tsx:84` ⇒ **không có component list dùng-lại được**. (Đ-2) **Đã có tiền-lệ mới hơn:** `ContractsListPage.tsx:125` (GĐ3, S168, 255 dòng) = bản PE-3-panel gọt gọn ⇒ KHKK là **mắt-xích lệch duy nhất** giữa GĐ1 và GĐ3; chép khuôn từ GĐ3, KHÔNG chép từ PE 807 dòng.
🔴 **Đảo ước-lượng:** `ContractSigningPlanWorkflowService.cs:534/:547` ĐANG GHI approvals + changelog THẬT ⇒ "Lịch sử duyệt" không phải xây-lại, chỉ thiếu **~5 field DTO + 1 endpoint**.
-- [ ] B — `/fable-clone reviewer` ensemble 3 lăng-kính
-- [ ] C — lead verify + refute/synthesize → ghi spec-file 3-mục
-- [ ] D — giao Opus 5 MAX thực-thi
+- [x] B — `/fable-clone reviewer` ×3 lăng-kính (`wf_832dadb2-93f`, Opus, 470s, 52 tool-use) — 🔴 **CẢ 3 LANE CHẾT** *"You've hit your session limit · resets 6:10pm"*, engine trả `verdict: null` ×3. **NHƯNG ĐĨA CỨU 2,95/3**: khung-rỗng-lượt-1-2 giữ trọn 74.070 B.
+ · `lens-feasibility` 26.461 B **HOÀN TẤT** — `8 HELD / 6 BROKE / 6 bỏ-sót`
+ · `lens-regression` 24.432 B **HOÀN TẤT** — `8 HELD / 4 BROKE / 10 bỏ-sót`
+ · `lens-fidelity` 23.177 B **DỞ** — §6 (phép đo P3-12) + §8 (lỗ tự khai) trống, VERDICT còn `?`. Lead derive từ §7 bảng của chính nó ≈ 12 HELD / 6 BROKE / 3 bỏ-sót (**nhãn lead-derive, KHÔNG phải lane tự kết**); lead **tự chạy phép §6 còn thiếu** → lật P3-12
+- [x] C — lead verify + refute/synthesize → **`spec-khkk-ui-mirror-pe-04-08-2026.md` 35.017 B** (utf8-strict OK, đủ 3 heading ①②③). Post-wave `git status`: **0 residual-write** ngoài run-folder
+- [x] C-bis — ✅ **3 QUYẾT-ĐỊNH OWNER chốt 2026-08-04**, spec cập-nhật **43.087 B**: (1) panel-1 = **`PipelineTreePanel`** — owner **BÁC** đề-xuất đảo của lead, đọc đủ 4 rủi-ro trên thẻ chọn rồi vẫn chọn ⇒ quyết-định có thông-tin đầy-đủ; lead chuyển 4 rủi-ro thành **mitigation CƯỠNG-HÀNH** + acceptance C-5..C-8 (*chọn HÌNH ≠ chấp-nhận VỠ*) · (2) panel-3 rỗng = **CHỈ hộp dashed y PE** ⇒ **đóng miễn-trừ-thứ-7** · (3) file-khi-duyệt = **upload THẬT** ⇒ BE-7 `+ApprovalAttachment=3` policy `KeHoachKyKet.Update`, bẫy F-6 "UI chết" tự tan
+- [ ] D — giao Opus 5 MAX thực-thi (giới-hạn phiên reset 18:10, phóng 18:50)
## Synthesis
-(chờ)
+### Lead refute — 3 chốt lead ÁP LÊN bản invest sau phản-biện
+
+1. 🔴 **Panel 1 KHÔNG dùng `PipelineTreePanel`** (đảo §5 bản invest). 4/5 phát-hiện nghiêm nhất hội tụ đúng chỗ này: `canPe` khoá cây ⇒ user **chỉ có quyền KHKK mất TRẮNG danh-sách** (display-layer khoá authz-layer đang mở — mặt còn lại #82) · trần 200 = **mất đường tới phiếu**, không có "trang 2" · `usePipelineStages.ts:211` cây **tự navigate rời màn 3-panel** · `content` **nuốt `groups`** im lặng (`PipelineStageFolders.tsx:285-287`). Mirror trung-thành = **KHKK dựng cây từ danh-sách CỦA CHÍNH KHKK** (đúng như PE dùng `yearGroups` từ query `pe-list`, KHÔNG dùng `PipelineTreePanel`) ⇒ 3 rủi-ro tan hết.
+2. 🔄 **Miễn-trừ 6 → 3.** P1-4 lật GIỐNG-được (khác nguồn dữ-liệu ≠ khác bố-trí) · P2-7 lật GIỐNG-được (`readOnly` là prop HOST, `PurchaseEvaluationsListPage.tsx:663` — không cần workspace) · P3-12 hạ xuống CHỜ-ANH (**lead đo `DbInitializer.cs:554`: 3 Bước × ≤2 Cấp = tới 6 trạm, seed `:620` tự ghi "sao từ workflow Duyệt NCC prod"** ⇒ tiền-đề *"3 trạm ngắn"* SAI đo được). **+ miễn-trừ THỨ 7 không khai** (F-5: §5 âm-thầm đảo P3-16 panel-3 empty-state) → đẩy thành CÂU HỎI 2.
+3. ✅ **Giữ quyết-định A, THAY lý-do.** Lý-do cũ (*"cả hai đường generic đều đụng PE prod"*) là **nhị-phân giả** — có đường (iii) adapter 1 chiều nằm trọn trong KHKK, 0 dòng PE. Lý-do đúng: trích `StepOpinionsBox` khỏi `components/pe/` **mâu-thuẫn trực-tiếp acceptance A-10 của chính nó**. *Kết-luận đúng, chứng-minh hỏng* — giữ kết-luận, sửa chứng-minh.
+
+### 6 acceptance của bản invest bị bẻ gãy khi CHẠY THẬT (đã thay bản sửa trong spec)
+
+| Gốc | Vỡ thế nào |
+|---|---|
+| **A-13** (canh QUYỀN) | Vỡ **cả hai đầu**: `:15` comment chứa "Authorize"+"Policy" ⇒ **bơm tử-số**; `:17 :236 :241` comment *nhắc tên* `[Authorize]` trần ⇒ vế "0 dòng trần" **FAIL trên file 100% sạch** (citation-trap). → thay bằng neo `^\s*\[` + fault-inject chứng răng |
+| **A-6** (canh bug S155) | **Kiểm NHẦM KHOÁ** — cấm `phase` (vô hại, transient) và bỏ sót `filter`/`view` (đúng 2 khoá làm vỡ) ⇒ `setSearchParams({group,filter:'DaDuyet'})` = **đúng bug S155** vẫn PASS. Inverted check |
+| **A-10** (canh hồi-quy) | **Đo sai trục** — thiếu `hooks/usePipelineStages.ts` + `components/pipeline/` ⇒ sửa 2 path đó có thể làm **trang PE mất 8 ngăn GĐ2** mà A-10 PASS 100% |
+| **A-7b** | Chạy trên **chính mẫu PE** ra **3 hit**, không phải 5 ⇒ implementer viết ĐÚNG sẽ FAIL; cách duy nhất PASS = **bỏ số đếm động cho vừa regex** = Goodhart |
+| **A-7d** | Đo **chuỗi chữ**, mục-tiêu là **vị-trí khối** ⇒ đổi tiêu-đề "Ý kiến duyệt" là PASS trong khi khối vẫn ở panel 3 |
+| **A-8c** | Grep-trên-source ⇒ khối **UI CHẾT vẫn PASS 100%** (không có đường ghi attachment + khối tự-ẩn khi rỗng ⇒ không bao giờ render trên UAT) |
+
+### 3 khối owner NHÌN THẤY trong ảnh mà bản invest đánh rơi
+
+**F-1** EmptyState panel 2 *"Chọn phiếu ở danh sách"* (Ảnh 1 — trạng-thái ĐẦU TIÊN người dùng thấy; bản invest ghi ở §1.2 rồi rơi khỏi §3/§5/§7) · **F-2** chip `⚑ Kết thúc · Phòng N` trên card panel 1 (Ảnh 3 — **0 công BE**, cột Mig 71 đã có) · **F-3** liệt-kê NV mỗi Cấp, người duyệt in đậm + NV khác `/ Tên` mờ (Ảnh 2).
+
+### Tin tốt được verify độc-lập (đừng cắt ngân-sách vì nghi ngờ)
+
+Lane feasibility **vào để lật D-2 và không lật được**: `LogSigningPlanTransitionAsync` **6 call-site phủ TRỌN** Pending/Approve/**Reject-Return** (`:204 :283 :296 :311 :325 :393`), `Approvals.Add` ở `:534` **không `if` nào bọc** ⇒ entry *"Trả lại → Lùi về Cấp 2"* của Ảnh 2 **SẼ CÓ dữ-liệu**. Và **A-0 chặn-cửa GỠ ĐƯỢC** — `usePipelineLinkage()` đã có sẵn `khkkByPeId` (`:123-134,147`), **0 query mới**.
+
+### Ngược lại — 1 chỗ RẺ bị ước-lượng sai chiều
+
+`FinalizeStepName`/`FinalizeLevelName` **không phải "field append"**: `ContractSigningPlan.cs:51` chỉ có `bool EndedByLevelFinalize`, **không cột nào lưu Bước/Cấp nào**. Phải **dẫn xuất** — port ~20 dòng projection **3 subquery** từ `PurchaseEvaluationFeatures.cs:665-683`. Bản invest cảnh-báo *"dễ sai đắt nhất theo cả hai chiều"* rồi **tự dính chiều còn lại**.
diff --git a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/spec-khkk-ui-mirror-pe-04-08-2026.md b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/spec-khkk-ui-mirror-pe-04-08-2026.md
new file mode 100644
index 0000000..28287b6
--- /dev/null
+++ b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/spec-khkk-ui-mirror-pe-04-08-2026.md
@@ -0,0 +1,278 @@
+# spec — KHKK UI mirror Duyệt NCC (3 panel đồng-nhất)
+
+> **Pipeline H21:** engine propose → **lead verify** → **lead ghi (file này)** → worker thực-thi.
+> **Nguồn:** `/fable-real investigator-codebase` (Fable 5) + `/fable-clone reviewer` ×3 lăng-kính (Opus).
+> **Trạng-thái review:** feasibility **8 HELD/6 BROKE** · regression **8 HELD/4 BROKE/10 bỏ-sót** · fidelity **~12 HELD/6 BROKE/3 bỏ-sót** *(2 mục cuối lane fidelity chết dở — verdict do lead derive từ §7 bảng của chính nó, KHÔNG phải lane tự kết; phép đo §6 còn thiếu thì lead đã tự chạy, kết-quả ở ②-D dưới)*.
+> **Đọc kèm:** `sub-investigator-codebase-invest-s171.md` (bản-đồ đầy-đủ) · 3 file `sub-reviewer-lens-*.md` (phản-biện).
+
+---
+
+## ① TÍNH-NĂNG / ĐỀ-BÀI
+
+Đưa màn **Kế hoạch Ký kết HĐ (KHKK — giai-đoạn 2)** về **cùng bố-cục 3 panel** với **Duyệt NCC (PE — giai-đoạn 1)**.
+
+**Owner (verbatim):**
+> "điều chỉnh lại giao diện chỗ KKHĐ lại nhé, lấy của Duyệt NCC làm mẫu: Panel 1: Hình gửi 1 · Panel 3: Duyệt và History tương tự (Hình 2) · Panel 2 - Giữa: Cấu trúc tương tự như duyệt NCC (Phần content nhập phía trên, duyệt ghi chú phía dưới) - Hình 3."
+> "cách bố trí toàn diện cho tất cả, **giống nhất** với duyệt NCC, **chỉ khác đúng phần khác, tương tự thì giống nhau, đồng nhất**."
+
+**Vì sao việc này chính-đáng (lý-do KHÔNG nằm trong ảnh):** GĐ1 (`PurchaseEvaluationsListPage.tsx:381,397`) và GĐ3 (`ContractsListPage.tsx:106,125`) đều đã là `h-[calc(100vh-4rem)]` + 3 cột. GĐ2 là `space-y-5` cuộn-body 2 panel (`KhkkListPage.tsx:161,197`) ⇒ **mắt-xích lệch duy nhất trong 4 giai-đoạn**. Đây là **đổi lớp kiến-trúc**, không phải sửa CSS.
+
+### Phân-loại cuối (sau phản-biện) — 29 GIỐNG-được · **3** KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG · **2 CHỜ-ANH**
+
+Bản invest báo 6 miễn-trừ. Sau khi áp đúng luật owner (*"chỉ khác đúng phần khác"* ⇒ mỗi dòng KHÁC là **một lần xin miễn-trừ**, gánh nặng chứng-minh thuộc người xin), **3 dòng không đứng vững**:
+
+| Dòng | Bản invest phán | Chốt cuối | Vì sao |
+|---|---|---|---|
+| P1-4 nhóm N1-N8 | KHÁC-buộc | 🔄 **GIỐNG-được** | Khác **nguồn dữ-liệu**, KHÔNG khác **bố-trí** — chính nó tự viết *"CHỖ ĐẶT phải giống"*. PE để `Select` quy-trình ở khối filter panel 1; KHKK để `Select` nhóm **đúng ô đó**. Dán nhãn KHÁC làm phồng mẫu-số miễn-trừ |
+| P2-7 inline-edit vs readOnly | KHÁC-buộc | 🔄 **GIỐNG-được** | `readOnly` là **prop do HOST truyền** (`PurchaseEvaluationsListPage.tsx:663` truyền `readOnly={true}`; `PeDetailTabs.tsx:427` có nhánh `mode`). Mệnh-đề *"muốn giống thì phải dựng workspace"* **sai** — host chỉ cần truyền `readOnly={phiếu ≠ Nháp \|\| user ≠ drafter}`. Đường sửa KHÔNG bị cắt |
+| P3-12 bốn return-mode | KHÁC-buộc | 🔄 **CHỜ-ANH (hoãn)** | Tiền-đề *"KHKK là 3 trạm ngắn nên lùi-1-cấp ≈ về-đầu"* — **lead đo, SAI**: `DbInitializer.cs:554` roster = `(StepName, DeptCode, Level1[], Level2[], Level2Finalize)` ⇒ **3 Bước × tối đa 2 Cấp = tới 6 trạm**, và seed `:620` tự ghi *"đội hình **sao từ workflow Duyệt NCC prod**"* ⇒ hình dạng **y hệt PE**. Vế còn lại (*"đổi state machine BE / ngoài phạm-vi"*) là lý-do **chi-phí**, không mua được miễn-trừ ⇒ gọi đúng tên: **hoãn**, không phải buộc-khác |
+
+**3 miễn-trừ THẬT còn lại** (đều có lý-do nghiệp-vụ, không dòng nào lấy *"code đang thế"* làm bằng-chứng):
+- **P2-3** nội-dung Section: GĐ2 không có báo-giá/so-sánh NCC (đã chốt ở GĐ1), có căn-cứ hồ-sơ TVGS. Ép 5 Section trùng tên PE sẽ đẻ 2 Section rỗng. *"Giống nhau"* ở đây = giống **KHUNG** (đánh số · `divide-y` · ý-kiến dưới cùng), không phải giống chữ.
+- **P3-7** badge `✎ NS PRO/CCM`: quyền nhập ngân-sách per-role là khái-niệm riêng PE (`PeWorkItemBudgets`, Mig 50). KHKK không có cột đó ⇒ badge vô nghĩa.
+- **P3-13** bộ chọn giá chốt: KHKK chốt giá **per-Line** ở choke-point BE `ApplyApprovedValuesOnFinalize` — không có "1 giá" để người duyệt chọn (khác **cardinality**, không khác ý-thích).
+
+### 🔴 Miễn-trừ THỨ 7 — KHÔNG ĐƯỢC KHAI (fidelity F-5), phải hỏi anh
+
+§3 bản invest phán **P3-16** *"empty-state panel 3 (hộp dashed + `X` + 'Quy trình duyệt sẽ hiện khi chọn phiếu.') = **GIỐNG-được**, Ảnh 1 có"*. Nhưng §5 bố-cục đích lại ghi *"panel 3 chưa chọn → `PipelineTreePanel currentStage={2}`"*, và acceptance A-8e **đóng đinh bản thay-thế**. ⇒ Một dòng đã phán GIỐNG-được bị **đảo âm-thầm giữa §3 và §5**, đúng vào khối anh nhìn thấy ở Ảnh 1.
+Theo luật owner, dạng này nguy nhất: **không phải khác — mà là khác mà không nói.**
+✅ **ĐÃ ĐÓNG — owner chốt 2026-08-04 (QĐ-2):** panel 3 khi chưa chọn = **CHỈ hộp dashed y PE**, quay về đúng Ảnh 1. Xem bảng *3 QUYẾT-ĐỊNH OWNER* cuối file.
+
+---
+
+## ② CÁCH IMPLEMENT
+
+### ②-A. Quyết-định kiến-trúc: **Phương-án A (tạo `components/khkk/`, chép khuôn)** — GIỮ, nhưng **THAY LÝ-DO**
+
+Bản invest bác phương-án B (generic-hoá component PE dùng chung) bằng lập-luận: *"phải **hoặc** ép BE KHKK precompute `status`, **hoặc** nhồi adapter 2 chiều — **cả hai đều đụng đường duyệt PE đang chạy prod"*.
+
+🔴 **Lập-luận này BROKE (feasibility §5.2) — nhị-phân giả.** Có đường (iii) nó không xét: KHKK **đã có sẵn** hàm suy status 15 dòng thuần-tuý không side-effect (`KhkkWorkflowPanel.tsx:106-120`); chỉ cần KHKK **tự chuẩn-hoá cây thô → shape có `status`** trước khi truyền vào component generic — adapter **MỘT chiều, nằm TRỌN trong nhánh KHKK, 0 dòng PE bị sửa**.
+
+✅ **Nhưng QUYẾT-ĐỊNH A vẫn đứng, vì một lý-do khác mạnh hơn:** trích `StepOpinionsBox` ra khỏi `PeDetailTabs.tsx:705-774` **buộc phải sửa file trong `components/pe/`** — mâu-thuẫn trực-tiếp với acceptance **A-10** của chính bản đó (*"`git diff --name-only` → 0 path khớp `components/pe/`"*). Cộng ≥6 nhánh riêng của PE (NS PRO/CCM · price-picker · 4 return-mode · CCM-done · skip-to-final · DeptApprovals legacy) ⇒ generic sẽ đầy `if (module === …)`.
+
+> 🔴 **Ghi vào đầu mỗi file `components/khkk/` (điều-kiện thoát, chống nợ im-lặng):**
+> *"Khối ý-kiến cấp duyệt là bản thứ 3 của cùng một hình (PE `PeDetailTabs.tsx:705-774` · HĐ `components/contracts/WorkflowHistoryPanel.tsx`). Gộp được khi và chỉ khi BE 3 module thống-nhất shape `status` precompute."*
+
+### ②-B. 🔴 Panel 1 = **`PipelineTreePanel`** — OWNER CHỐT 2026-08-04 (bác đề-xuất đảo của lead)
+
+> **QUYẾT-ĐỊNH OWNER (ratified):** anh chọn **giữ đúng đề-xuất bản invest** — panel 1 = cây toàn-trình `PipelineTreePanel currentStage={2}`. Lead đã trình phương-án đảo (danh-sách riêng KHKK) **kèm đủ 4 rủi-ro đo được ngay trên thẻ chọn**; anh đọc và vẫn chọn cây. ⇒ **Đây là quyết-định có thông-tin đầy-đủ, thi-hành nguyên vẹn.**
+>
+> 🔴 **NHƯNG: chọn HÌNH ≠ chấp-nhận VỠ.** 4 rủi-ro dưới đây **không biến mất vì owner chọn cây** — chúng chuyển từ *"lý-do đổi thiết-kế"* thành ***việc kỹ-thuật BẮT BUỘC phải xử***. Mỗi cái có mitigation cưỡng-hành + acceptance riêng. **CẤM** thi-hành cây mà bỏ mitigation — làm thế là biến quyết-định của owner thành cái cớ cho một hồi-quy im-lặng.
+
+#### 4 rủi-ro + mitigation CƯỠNG-HÀNH (đi kèm, không tách rời)
+
+| Rủi-ro | Bằng-chứng | 🔧 Mitigation BẮT BUỘC |
+|---|---|---|
+| **M-1 🔴 CAO — `canPe` khoá cây** ⇒ user **chỉ có quyền KHKK** (VD người duyệt nhóm N5) thấy panel 1 **TRỐNG**, trong khi API `GET /contract-signing-plans` gate `KeHoachKyKet.Read` **vẫn cho họ đọc**. Display-layer khoá authz-layer đang mở — mặt còn lại gotcha #82 | `usePipelineStages.ts:55,57,83-84` (`canPe` = 3 key PE) · `PipelineTreePanel.tsx:117 enabled:canPe` · `:222-226` in *"Bạn chưa có quyền xem phiếu Duyệt NCC…"* | 🔴 **FALLBACK `!canPe` → render DANH-SÁCH KHKK phẳng** (card, cùng khuôn, không cây) trong đúng ô panel 1, nguồn `GET /contract-signing-plans`. Cây là **lớp làm giàu**, KHÔNG phải **điều-kiện để thấy phiếu**. ⇒ acceptance **C-5** (runtime, tài-khoản KHKK-only) |
+| **R-11 trần 200 phiếu** ⇒ gói thầu rơi khỏi 200 mới nhất thì **mọi phiếu KHKK của gói đó biến mất**, và cây **không có "trang 2"** | `PipelineTreePanel.tsx:22 PE_INDEX_PAGE_SIZE=200` · `:112` · cảnh-báo `:252-256` | 🔴 Giữ **ô search + Select trạng-thái server-side** trong khối filter panel 1 → khi lọc, đi thẳng `GET /contract-signing-plans` (**không qua cây**) ⇒ luôn có đường tới phiếu cũ. **Và** dòng cảnh-báo `:252-256` **PHẢI hiện**, không được nuốt |
+| **M-2 🔴 CAO — cây tự văng khỏi màn 3-panel**: bấm node KHKK ⇒ `navigate('/khkk/'+id)` rời trang vừa dựng | `usePipelineStages.ts:211` (×2 app) | 🔴 Đổi `onOpen` thành **callback do host truyền**: `onOpen: opts?.onSelectKhkk ?? (() => navigate(…))` — host `/khkk/list` truyền `id => setParam({id})`. Giữ nguyên hành-vi cũ cho **mọi call-site khác** (mặc-định navigate) ⇒ 0 hồi-quy trang PE/GĐ3 |
+| **B1 🔴 `content` nuốt `groups` im lặng**: `PipelineStageFolders.tsx:285-287` đặt `content` **TRƯỚC** `groups` trong chuỗi tam-nguyên ⇒ truyền `stage2Content` thì **8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" biến mất, không lỗi, không cảnh-báo**; `:95-97` quên `stage2Count` ⇒ badge về **0** | tự đo `grep -n -C4` | 🔴 **KHAI TRADE-OFF, không im:** trong cây của KHKK, folder GĐ2 hiện **card phiếu** thay **8 ngăn nhóm** — chấp nhận được **vì `Select` nhóm N1-N8 đã có ở khối filter panel 1** (P1-4). **BẮT BUỘC** truyền `stage2Count` cùng lúc. **Additive**: `PurchaseEvaluationsListPage` không truyền `stage2Content` ⇒ **trang PE giữ nguyên 8 ngăn** ⇒ acceptance **E-2** |
+
+#### 🔸 Ghi lại cho người đọc sau (bối-cảnh quyết-định, KHÔNG phải để lật lại)
+
+Lead từng đề-xuất đảo sang *"panel 1 = danh-sách riêng của KHKK"* với lập-luận: panel 1 của PE **không phải** `PipelineTreePanel` mà là **danh-sách của chính PE** tự gom cây `Năm > Dự án > Hạng mục` từ `yearGroups` (`PurchaseEvaluationsListPage.tsx:220-262`, nguồn = query `pe-list`), tầng 4 mới gọi `buildStages(wg.items, {stage1Content})` (`:524`). Owner **đã nghe và chọn cây** — nên hướng thi-hành là cây + 4 mitigation trên. Đoạn này giữ để người sau hiểu **vì sao có mitigation**, đừng đọc thành lời mời mở lại tranh-luận.
+
+---
+
+*(Phần dưới giữ nguyên phân-tích gốc để tra-cứu — 4 rủi-ro đã có mitigation ở bảng trên.)*
+
+| Nếu panel 1 = `PipelineTreePanel` (bản invest) | Hậu-quả đo được |
+|---|---|
+| **M-1 🔴 `canPe` khoá cây** — `usePipelineStages.ts:55,57,83-84`: `canPe = ['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List'].some(can)`; `PipelineTreePanel.tsx:117 enabled:canPe`, `:222-226` in *"Bạn chưa có quyền xem phiếu Duyệt NCC…"* | Người **chỉ có quyền KHKK** (VD người duyệt nhóm N5) ⇒ `canPe=false` ⇒ **mất TRẮNG danh-sách phiếu của chính mình**, trong khi API `GET /contract-signing-plans` gate `KeHoachKyKet.Read` **vẫn cho phép**. Đây là **display-layer khoá cái authz-layer đang mở** — mặt còn lại của gotcha #82. **Mất chức-năng, không phải mất thẩm-mỹ.** |
+| **R-11 trần 200** — `PipelineTreePanel.tsx:22 PE_INDEX_PAGE_SIZE=200`, cây dựng từ **chỉ-mục PE** | Gói thầu rơi khỏi 200 ⇒ **mọi phiếu KHKK của gói đó biến mất khỏi panel 1**. Hôm nay vô hại vì panel 1 là **bảng có phân-trang + tìm-kiếm server-side** (`KhkkListPage.tsx:112-129`, `total` thật từ server). Đổi sang cây = **thay danh-sách-có-phân-trang bằng cây-cắt-cụt**; dòng cảnh-báo 10px xám **không có "trang 2"** |
+| **M-2 🔴 cây tự văng khỏi màn** — `usePipelineStages.ts:211 onOpen: () => navigate('/khkk/'+k.id)` (×2 app) | Bấm node KHKK trong panel 1 ⇒ **rời màn 3-panel vừa dựng**, thay vì chọn vào panel 2 |
+| **B1 🔴 `content` nuốt `groups`** — `PipelineStageFolders.tsx:285-287` chuỗi tam-nguyên đặt `content` **TRƯỚC** `groups`; `:95-97` `stageCount` khi có `content` đọc `count ?? 0` | Truyền `stage2Content` ⇒ **8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" biến mất, không lỗi, không cảnh-báo**; quên `stage2Count` ⇒ badge về **0** trong khi folder đầy phiếu |
+
+🔴 **Nhưng đọc kỹ mẫu PE thì hoá ra bản invest chép nhầm ĐỐI-TƯỢNG:** panel 1 của PE **KHÔNG phải** `PipelineTreePanel`. Nó là **danh-sách CỦA CHÍNH PE**, tự gom cây `Năm > Dự án > Hạng mục` từ `yearGroups` (`PurchaseEvaluationsListPage.tsx:220-262`, nguồn = query `pe-list`), rồi tầng 4 mới gọi `buildStages(wg.items, {stage1Content:
…
})` (`:524`) để re-parent phiếu vào folder GĐ1.
+
+⇒ **Mirror TRUNG-THÀNH = KHKK panel 1 dựng cây từ DANH-SÁCH CỦA CHÍNH KHKK**, đổ card vào folder **GĐ2** — đúng đối-xứng với PE đổ vào GĐ1. Và làm đúng thì **M-1 · R-11 · M-2 tan hết**, vì nguồn dữ-liệu là `GET /contract-signing-plans` (đã gate `KeHoachKyKet.Read`, có phân-trang server, có `total` thật), **không đụng `canPe`, không đụng trần 200, không có `navigate` của hook**.
+
+**Về B1 (`content` nuốt `groups`) — trade-off PHẢI KHAI, không được để im:**
+- Thêm `stage2Content`/`stage2Count` vào `BuildStagesOptions` là **additive**: `PurchaseEvaluationsListPage` không truyền nên **giữ nguyên 8 ngăn** ⇒ **trang PE KHÔNG bị đụng**.
+- Trong **cây của chính KHKK**, truyền `stage2Content` ⇒ folder GĐ2 hiện **card phiếu** thay cho **8 ngăn nhóm**. Chấp nhận được **vì bộ lọc nhóm N1-N8 đã có mặt dạng `Select` ở khối filter panel 1** (P1-4) — không mất đường tới nhóm nào.
+- 🔴 **BẮT BUỘC:** truyền `stage2Count` cùng lúc (nếu không badge = 0), và **acceptance A-10 phải mở rộng path** (xem ③).
+
+### ②-C. Bố-cục đích
+
+```
+/khkk/list ← host 3-panel; khuôn ContractsListPage.tsx (GĐ3, 255 dòng) — KHÔNG chép
+ PurchaseEvaluationsListPage.tsx (807 dòng, mang theo deleted/pendingMe/awId/overlay)
+ 🔸 CẢNH-BÁO: ContractsListPage.tsx CHỈ TỒN-TẠI Ở fe-admin (regression M-6).
+ Port sang fe-user = VIẾT MỚI, không phải copy.
+
+├─ Panel 1 400px components/khkk/KhkkListPanel.tsx ← OWNER CHỐT: cây PipelineTreePanel
+│ khối filter: search icon-trong-ô · Select trạng-thái · Select nhóm N1-N8
+│ (search/trạng-thái đi SERVER-SIDE, KHÔNG qua cây — mitigation R-11)
+│ cây: PipelineTreePanel currentStage={2}
+│ 📅 Năm > 📁 Dự án > 🧱 Hạng mục > folder GĐ2 = card phiếu KHKK
+│ onOpen = callback host (KHÔNG navigate — mitigation M-2)
+│ stage2Content + stage2Count truyền CẶP (mitigation B1)
+│ 🔴 FALLBACK !canPe → danh-sách KHKK phẳng cùng ô (mitigation M-1)
+│ 🔴 GIỮ dòng cảnh-báo "Cây tải 200 phiếu mới nhất…" (PipelineTreePanel.tsx:252-256)
+│ card 3 hàng: [chip ⚑ Kết thúc·Phòng N] + tên gói thầu + badge trạng-thái
+│ / mã font-mono + timestamp / 👤 người soạn + Σ tiền
+│
+├─ Panel 2 1fr components/khkk/KhkkDetailContent.tsx
+│ chưa chọn → EmptyState "Chọn phiếu ở danh sách" 🔴 F-1, xem ②-E
+│
+│
+│ Section 1. Thông tin kế hoạch
+│ Section 2. Dòng giá per-NCC trúng thầu (N NCC)
+│ Section 3. Căn cứ hồ sơ (b.8-9)
+│ Section 4. File đính kèm
+│ Section 5. Ý KIẾN CẤP DUYỆT (sign-off theo workflow) ← Ảnh 3 khoanh đỏ
+│ readOnly = prop TỪ HOST (P2-7 đã lật): true khi phiếu ≠ Nháp HOẶC user ≠ drafter
+│
+└─ Panel 3 360px components/khkk/KhkkWorkflowPanel.tsx (dời từ pages/)
+ chưa chọn → ✅ OWNER CHỐT: **CHỈ hộp dashed y PE** (khuôn PurchaseEvaluationsListPage.tsx:669-676
+ — border-dashed + icon X + "Quy trình duyệt sẽ hiện khi chọn phiếu.")
+ 🔴 KHÔNG nhét PipelineTreePanel vào đây (khác GĐ3 — CÓ CHỦ ĐÍCH, vì cây
+ đã sống ở panel 1 theo QĐ owner câu 1 ⇒ nhét lần 2 = trùng)
+ ⇒ miễn-trừ-thứ-7 mà bản invest không khai nay đã ĐÓNG: quay về đúng Ảnh 1
+ đã chọn → 1 "Quy trình duyệt" + meta {code} v{NN} · {name}
+ 2 Banner kết-thúc-sớm "… không qua CEO"
+ 3 Sơ-đồ Bước → Cấp → NV (+chip phòng · +⚑ Duyệt thay CEO
+ · +mờ sau finalize · +liệt-kê NV 🔴 F-3)
+ 4 Nút Duyệt / Trả lại / Từ chối + Dialog + nút Xóa
+ 5 📎 File đính kèm khi duyệt (🔴 F-6 — xem ②-E)
+ 6 Lịch sử duyệt (N) (🔴 B4 — xem ②-E)
+ 7 Lịch sử thay đổi
+```
+
+`/khkk/:id` **GIỮ NGUYÊN route** (deep-link · bookmark · fallback ` Approvals` | 🟢 Chỉ **1 call-site** `new ContractSigningPlanDetailDto` (`:784`) ⇒ append RẺ |
+| BE-1b | 🔴 **`FinalizeStepName`/`FinalizeLevelName` KHÔNG phải "field append"** — `ContractSigningPlan.cs:51` chỉ có `bool EndedByLevelFinalize`, **không có cột nào lưu Bước/Cấp nào đã kết-thúc** ⇒ phải **DẪN XUẤT**: port ~20 dòng projection có **3 subquery** + nhánh 3-ngả từ `PurchaseEvaluationFeatures.cs:665-683` | Bản invest ước "+3 field" = **sai chiều RẺ**. Nó tự cảnh-báo *"dễ ước-lượng sai đắt nhất theo cả hai chiều"* rồi dính chiều còn lại |
+| BE-2 | 🔴 **`ContractSigningPlanApprovalDto` CHƯA TỒN-TẠI** (`grep` → **0 hit**) ⇒ đẻ record mới + projection + mirror `types/khkk.ts` ×2 app | A-1 viết như thể chỉ "thêm field" |
+| BE-3 | `ApprovedByUserId` **không kèm tên** (`:534-544` 8 field, không có tên) trong khi hàng changelog ngay dưới (`:546-557`) **CÓ** `UserName` denorm | 🔸 Rẻ hơn join Users: 2 bảng ghi **cặp 1-1 cùng unit-of-work cùng `now`** ⇒ lấy tên từ changelog cùng transition, **hoặc** denorm thêm cột tên vào Approval cho khớp anh-em |
+| BE-4 | `ContractSigningPlanWorkflowStepDto` **+`DepartmentName`** (join `Departments`) | Nuôi **2 chỗ**: chip phòng panel 3 (P3-4) **và** header nhóm Section 5 (P2-5) |
+| BE-5 | `GET /contract-signing-plans/{id}/changelogs` | 🔴 **`[Authorize(Policy = "KeHoachKyKet.Read")]`** — gotcha #85 (đúng key endpoint, KHÔNG OR key con) |
+| BE-6 | `.Include`/join `ApprovalWorkflowLevels` cho `Approvals` nếu muốn hiện "Cấp nào" (Ảnh 2 có *"Lùi về **Cấp 2**"*) — `ApprovalWorkflowLevelId` là Guid trần (`:541`) | |
+| **BE-7** ✅ | **OWNER CHỐT — file đính kèm khi duyệt, LÀM THẬT:** `ContractSigningPlanAttachment.cs:9-12` **+`ApprovalAttachment = 3`** (append-only, **KHÔNG** đánh số lại `Other=99`) + **endpoint upload** gắn vào luồng dialog duyệt + lưu `purpose=ApprovalAttachment` | 🔴 Endpoint mới ⇒ **`[Authorize(Policy = "KeHoachKyKet.Update")]`** (ghi, không phải Read) — gotcha #85 dùng ĐÚNG key của endpoint, **KHÔNG** OR key con. 🔴 **KHÔNG** chép khuôn authz của `PurchaseEvaluationsController` (xem cảnh-báo BE-5) |
+
+> 🔴 **BE-5 — CẢNH-BÁO TẠI CHỖ, không chỉ ở checklist (regression M-5):** mẫu PE mà lệnh *"mirror PE"* trỏ tới **CHÍNH LÀ một lỗ #82**:
+> `PurchaseEvaluationsController.cs:15 [Authorize]` **trần (any-auth)** + `:281 [HttpGet("{id:guid}/changelogs")]` **KHÔNG có** `[Authorize(Policy=…)]` ⇒ endpoint changelogs của PE hiện chỉ cần **đăng-nhập bất-kỳ** là đọc được lịch-sử phiếu.
+> **Implementer trung-thành sẽ chép nguyên — đó là cách #82 lan sang KHKK. ĐỪNG CHÉP.** (Vào sổ riêng: PE changelogs = ứng-viên #82 thứ hai bên cạnh `POST /api/contracts` đã treo.)
+
+### ②-E. 5 khối bị bỏ sót / bẫy hiển-thị — PHẢI làm, không được im
+
+| # | Việc | Vì sao |
+|---|---|---|
+| **F-1** 🔴 | **EmptyState panel 2 "Chọn phiếu ở danh sách"** + 2 nhánh `Đang tải…` / `Không tìm thấy phiếu.` — mẫu `PurchaseEvaluationsListPage.tsx:644-656` | Bản invest **ghi sự-thật này ở §1.2 rồi đánh rơi**: 0 dòng §3, 0 chỗ §5, 0 acceptance §7. Host 3-panel mà panel 2 trống trơn = **màn hình trắng giữa**, sai Ảnh 1 ở đúng trạng-thái **ĐẦU TIÊN** người dùng thấy |
+| **F-2** 🔴 | **Chip `⚑ Kết thúc · Phòng N` trên card panel 1** (`PeFinalizeChip`) | Anh **nhìn thấy nó trong Ảnh 3**; P1-5 liệt-kê card thiếu đúng con chip này. Và nó **0 công BE** — cột `EndedByLevelFinalize` đã có (Mig 71). Bỏ sót khối vừa-thấy-trong-ảnh vừa-rẻ-nhất |
+| **F-3** 🔴 | **Liệt-kê NV trong mỗi Cấp**: người duyệt thật **in đậm**, NV cùng cấp còn lại `/ Tên` mờ — mẫu `PeWorkflowPanel.tsx:453-465` | Ảnh 2 mục 3 có. §3 chỉ có dòng cho *badge vai*, **không có dòng cho bản-thân danh-sách NV** ⇒ panel 3 sẽ **không giống Ảnh 2** dù A-8a/b/c/d PASS hết |
+| **F-6** ✅ | **📎 File đính kèm khi duyệt — OWNER CHỐT: LÀM THẬT, cho upload.** ⇒ **BE-7** (dưới): `ContractSigningPlanAttachment.cs:9-12` **+`ApprovalAttachment = 3`** (enum append-only, đúng comment nhà *"Append-only khi cần thêm mục đích mới"*) + **endpoint upload trong dialog duyệt** + FE khối đọc `purpose=ApprovalAttachment`, tự-ẩn khi rỗng (khuôn `PeWorkflowPanel.tsx:928-967`) | Chọn đường này ⇒ khối **SỐNG THẬT** trên UAT, và bẫy *"UI chết mà acceptance vẫn xanh"* **tự tan** — vì nay có đường ghi. Acceptance **D-9** đổi sang runtime: upload 1 file lúc duyệt ⇒ khối phải HIỆN |
+| **B4** 🔴 | **`Decision = Pending` của hành-vi GỬI duyệt (`:204-207`) nằm THẲNG trong `Approvals`** ⇒ "Lịch sử duyệt (N)" của KHKK sẽ có **nhiều dòng hơn** số lần thật sự bấm Duyệt/Trả/Từ-chối. PE chống bằng merge + dedupe 5s-bucket + synthetic-reject (`PeDetailTabs.tsx:3152-3192`) | Không xử ⇒ số `(N)` của KHKK **không cùng nghĩa** với số `(N)` của PE — trong khi cả đề-bài là *"cho đồng nhất"*. **Phải quyết: lọc `Pending` (giống PE) hay hiện đủ (nhật-ký).** Khuyến-nghị: **lọc**, cho khớp mẫu |
+| **B3** | Changelog ghi **10 chỗ** (bản invest kể 8). Chỗ bị bỏ đáng kể: **`CreateContractFromSigningPlanFeatures.cs:226`** = bắc-cầu **KHKK→HĐ (K7)** | ⇒ "Lịch sử thay đổi" sẽ hiện **entry tạo Hợp đồng** — loại dòng FE **chưa có nhãn**. Thiếu ⇒ hiện chuỗi thô hoặc rơi im lặng |
+
+### ②-F. 4 bẫy hồi-quy — làm sai là vỡ cái đang chạy
+
+| # | Bẫy | Luật |
+|---|---|---|
+| **M-4** 🔴 | **A-6 của bản invest kiểm SAI KHOÁ.** Cơ-chế thật: `Layout.tsx:350 TRANSIENT_QUERY_KEYS = {id,q,editHeader,page,phase,awId}`; `:369 ignorable = transient && !pinnedByTarget`. KHKK đọc URL đúng **3 khoá**: `filter`(`:74`) · `view`(`:76`) · `group`(`:79`) — **`phase` KHÔNG hề nằm trên URL** | Ghi `filter`/`view`/`group` vào URL ⇒ **hồi-sinh bug S155** (đứng leaf `?group=1` bấm chip "Đã duyệt" → URL khớp CHÍNH XÁC leaf "Đã duyệt" ⇒ sidebar nhảy). Ghi `phase` ⇒ **vô hại** (transient, 0 leaf ghim). ⇒ Acceptance phải cấm **`filter`/`view`/`group`**, KHÔNG phải `phase` — bản cũ **chặn cái vô hại, cho lọt cái nguy hiểm** |
+| **M-2** 🔴 | `usePipelineStages.ts:211 onOpen: () => navigate('/khkk/'+k.id)` (×2 app) | Nếu còn dùng hook cho panel 1 ⇒ đổi thành **callback do host truyền** (`onSelect(id)` → `?id=`), nếu không cây tự văng khỏi màn 3-panel |
+| **M-8** | `budgetFrozen` nằm ở **DetailDto**, **KHÔNG có** ở `KhkkListItemDto` (`types/khkk.ts:296-301` — *"CỜ HIỂN THỊ dẫn xuất từ phase, KHÔNG phải cột DB"*) | Card panel 1 in **một** số Σ tiền ⇒ phải **suy từ `phase === DaDuyet`** (đúng như BE suy), **đừng** thêm field vào ListItemDto. Bảng cũ né được vì in **cả hai cột** (vá-8 K2 `:246-251`); chọn sai = tái-hiện đúng lỗi đã sửa |
+| **M-7** | Bảng 8 cột (`:241-252`: Mã · Gói thầu · **Dự án** · Trạng thái · **Tổng đề xuất** · **Tổng duyệt** · **Người soạn** · **Ngày tạo**) + 6 `KpiCard` + phân-trang ép vào card 400px | Card 3 hàng chứa **~4-5 mẩu tin** ⇒ **PHẢI có bảng "cột nào chết, chuyển đi đâu"** trong PR. Không có bảng đó = mất-chức-năng-im-lặng |
+| **M-10** | `KhkkDetailPage.tsx:258` xóa xong `navigate('/khkk/list')` **rụng `?group=`** | Ném người dùng về "tất cả nhóm". Giữ `?group=` khi quay lại |
+| **R-5** | `:97-101` là **set-state-trong-render CÓ CHỦ ĐÍCH** (khuôn React "reset state khi input đổi"), **không phải** code lỗi | Ai refactor cho "sạch" thành `useEffect` sẽ **tái-hiện vá-5** (phải F5 mới đúng). ĐỪNG ĐỘNG |
+| **R-8** | KHKK **không có phiếu V1-legacy** (module sinh 2026-07-29) | **KHÔNG** port nhánh fallback V1 của PE (`PeDetailTabs.tsx:434-436`, `PeWorkflowPanel.tsx:481-485`) — code chết ngay ngày đầu |
+
+---
+
+## ③ CHECKLIST (vai · deliverable · acceptance ĐO ĐƯỢC)
+
+**Vai thực-thi:** `implementer-backend` (nhóm 1) → `implementer-frontend` (nhóm 2-3) → `reviewer` (gate) → `cicd-monitor` (deploy).
+`APP` ∈ {`fe-admin`, `fe-user`} — **mọi mục FE phải PASS CẢ HAI**.
+
+> 🔴 **4 acceptance của bản invest đã bị bẻ gãy khi CHẠY THẬT — dùng bản sửa dưới đây, KHÔNG dùng bản gốc.** Bài học: *chưa ai chạy thử chính các lệnh acceptance đó*; hai cái được viết riêng để canh hai rủi-ro nguy nhất thì **một cái kiểm nhầm khoá, một cái báo đỏ trên file sạch**.
+
+### Nhóm 0 — trước khi viết dòng code đầu tiên
+
+| # | Việc | Acceptance |
+|---|---|---|
+| ~~A-0~~ | 🟢 **GỠ CHẶN — feasibility đã trả lời dứt.** `usePipelineLinkage()` **đã có sẵn** query `khkkIndex` + `khkkByPeId: Map` (`usePipelineStages.ts:123-134,147`); `buildStages` gom bằng `:204`; `toKhkkLeaf` `:205-212`; `stage2` `:265-273` | **Không cần query mới, không cần endpoint mới cho panel 1.** 🔸 Nếu ai vẫn đọc theo A-0 gốc: khoảng đúng là **`:100-320`**, không phải `:150-320` — nguồn thật ở `:123-134` nằm **NGOÀI** khoảng cũ |
+
+### Nhóm 1 — BE
+
+| # | Acceptance |
+|---|---|
+| **B-1** | `grep -n "record ContractSigningPlanDetailDto" -A 80 src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` → thấy đủ **5** tên mới |
+| **B-2** | `grep -c "ContractSigningPlanApprovalDto" src/Backend` **≥ 2** (định-nghĩa + call-site) — hôm nay là **0** |
+| **B-3** | 🔴 **RUNTIME, không phải đọc code:** gọi thật 1 phiếu `DaDuyet` → `approvals[]` có **≥1 phần tử `Decision=Reject`** (chứng chuỗi Trả lại được ghi) **và** `approvedByFullName ≠ null` ∀ phần tử |
+| **B-4** | `grep -c "DepartmentName" …WorkflowStepDto` ≥ 1 |
+| **B-5** | `grep -n "changelogs" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` → 1 hit, **và** dòng `[Authorize(Policy = "KeHoachKyKet.Read")]` đứng ngay trên |
+| **B-6** 🔴 | **A-13 THAY BẢN GỐC** (bản gốc **vỡ cả hai đầu**: `:15` là **comment** chứa "Authorize"+"Policy" nên bơm tử-số; và `:17` `:236` `:241` là **comment nhắc tên** `[Authorize]` trần nên vế "0 dòng trần" **FAIL trên file 100% sạch** — citation-trap). Bản sửa **neo đầu dòng, loại comment**: `grep -cE '^\s*\[Http(Get\|Post\|Put\|Delete\|Patch)' ` = **R** `grep -cE '^\s*\[Authorize\(Policy' ` phải = **1 + R** (hiện 19 = 1+18) `grep -cE '^\s*\[Authorize\]\s*$' ` phải = **0** 🧪 **Chứng thước có RĂNG:** cắm tạm 1 action `[Authorize]` trần vào **cây tạm** ⇒ số thứ 3 phải nhảy lên 1. Không nhảy = thước mù |
+
+### Nhóm 2 — FE cấu-trúc
+
+| # | Acceptance |
+|---|---|
+| **C-1** | `ls $APP/src/components/khkk/` → **≥3** file; `grep -rn "pages/khkk/KhkkWorkflowPanel" $APP/src` → **0 hit** |
+| **C-2** 🔴 | **A-4 THAY BẢN GỐC** (gốc chấp **mọi con số** kể cả `[200px_1fr_200px]` ⇒ đo "có 3 panel", không đo "giống PE"; và **FAIL nếu ai dùng `rem`** như KHKK đang dùng `19rem/21rem`). Bản sửa **ghim literal**: `grep -c 'lg:grid-cols-\[400px_1fr_360px\]' $APP/src/pages/khkk/KhkkListPage.tsx` = **1**, **và** `grep -c 'h-\[calc(100vh-4rem)\]'` = **1** 🔸 *Giải mâu-thuẫn F-7: §5 bản invest ghi `400px` (số PE) nhưng Điều-kiện-1 bảo chép GĐ3 = `340px`. Hiến-pháp owner là **"giống nhất với duyệt NCC"** ⇒ **chốt `400px`** (số PE), khuôn code lấy GĐ3* |
+| **C-3** | `grep -c "matchMedia('(min-width: 1024px)')" $APP/src/pages/khkk/KhkkListPage.tsx` ≥1 · `grep -c 'path="/khkk/:id"' $APP/src/App.tsx` = **1** (route CÒN SỐNG) |
+| **C-4** 🔴 | **A-6 THAY BẢN GỐC** (gốc cấm `'phase'`/`'group'` — `phase` **không phải khoá URL của KHKK** và **vô hại**; `filter`/`view` **mới là 2 khoá làm vỡ** thì gốc **không nhắc chữ nào** ⇒ `setSearchParams({group, filter:'DaDuyet'})` = **đúng bug S155** vẫn PASS). Bản sửa: **0 hit** `'filter'` / `'view'` / `'group'` bên trong **mọi** lời gọi `setSearchParams`/`setSp`; chỉ `'id'`/`'q'`/`'page'` được phép (∈ `TRANSIENT_QUERY_KEYS`, `Layout.tsx:350`) 🧪 **Negative-control:** cắm tạm `setSearchParams({filter:'DaDuyet'})` ⇒ acceptance PHẢI TRƯỢT |
+| **C-5** 🔴 | **Mitigation M-1 — acceptance QUAN-TRỌNG NHẤT của cả spec.** Owner chốt panel 1 = cây ⇒ `canPe` **sẽ** khoá fetch cho user KHKK-only ⇒ **BẮT BUỘC có fallback**. 🔴 **RUNTIME:** đăng-nhập tài-khoản **chỉ có `KeHoachKyKet.*`** (KHÔNG có `PurchaseEvaluations`) ⇒ panel 1 **vẫn liệt ĐỦ phiếu** (dạng danh-sách phẳng, không cây). 🧪 **Negative-control bắt buộc:** gỡ tạm fallback ⇒ acceptance PHẢI TRƯỢT (panel 1 trống). Không trượt = thước mù, vì `canPe` của tài-khoản test có thể đang `true` do thừa quyền |
+| **C-6** 🔴 | **Mitigation B1** — truyền `stage2Content` thì **BẮT BUỘC** kèm `stage2Count` (`PipelineStageFolders.tsx:95-97` — thiếu ⇒ badge **0** trong khi folder đầy phiếu). Và **khai trade-off trong PR**: folder GĐ2 của cây KHKK hiện **card phiếu** thay **8 ngăn N1-N8** (lối vào nhóm giữ ở `Select` panel 1) |
+| **C-7** 🔴 | **Mitigation M-2** — `grep -c "navigate(\`/khkk/" $APP/src/hooks/usePipelineStages.ts` phải cho thấy `onOpen` nhận **callback tuỳ chọn từ host**, mặc-định giữ `navigate` cũ. 🧪 **Chứng 0 hồi-quy:** mở **trang PE** và **trang GĐ3**, bấm node KHKK trên cây ⇒ **vẫn navigate như cũ** (chỉ host `/khkk/list` mới chặn lại thành `?id=`) |
+| **C-8** 🔴 | **Mitigation R-11** — ô search + Select trạng-thái panel 1 đi **server-side** thẳng `GET /contract-signing-plans`, **KHÔNG lọc trên cây**: `grep -c "PE_INDEX_PAGE_SIZE\|200" …/KhkkListPanel.tsx` không được là đường duy nhất tới phiếu. **Và** dòng cảnh-báo `PipelineTreePanel.tsx:252-256` *"Cây tải 200 phiếu mới nhất…"* **PHẢI còn hiện** (`grep -c "200 phiếu mới nhất"` ≥1) — nuốt cảnh-báo = giấu giới-hạn |
+
+### Nhóm 3 — FE nội-dung (từng khối của ảnh)
+
+| # | Acceptance |
+|---|---|
+| **D-1** | `grep -c "PePipelineStrip" …/KhkkDetailContent.tsx` = 1 **kèm** `current={2}` |
+| **D-2** 🔴 | **A-7b THAY BẢN GỐC** (gốc đòi **5 hit** `title="N. …"` nhưng chạy trên **chính mẫu PE** chỉ ra **3** — vì Section 2 dùng template-literal `` title={`2. …(${n} hạng mục)`} `` và Section 4 là `` trần ⇒ implementer viết ĐÚNG sẽ **FAIL**, và cách duy nhất để PASS là **bỏ số đếm động cho vừa regex** = Goodhart thuần). Bản sửa **chấp cả literal lẫn template**: `grep -c '🧪 Và kiểm **vế tự-ẩn**: phiếu duyệt **không** kèm file ⇒ khối **KHÔNG hiện** (khuôn PE `:928-967`) |
+| **D-10** | `grep -c "Lịch sử duyệt"` ≥1 · `grep -c "Lịch sử thay đổi"` ≥1 **và** `grep -c "/changelogs"` ≥1 (khối vô nghĩa nếu không fetch) |
+| **D-11** 🔴 | **B4** — `Approvals` lọc bỏ `Decision=Pending` (hoặc khai rõ quyết-định ngược lại): số `(N)` của KHKK phải **cùng nghĩa** với số `(N)` của PE |
+
+### Nhóm 4 — chống hồi-quy
+
+| # | Acceptance |
+|---|---|
+| **E-1** | Mirror 2 app: `sha256sum fe-admin/src/
` = `sha256sum fe-user/src/
` ∀ file mới/sửa → **PASS 100%**, không trừ file nào |
+| **E-2** 🔴 | **A-10 THAY BẢN GỐC** (gốc liệt 4 path `components/pe/` `pages/pe/` `components/contracts/` `pages/contracts/` — **thiếu đúng 2 path nguy hiểm**: sửa `hooks/usePipelineStages.ts` hoặc `components/pipeline/PipelineStageFolders.tsx` có thể làm **cây trang PE mất 8 ngăn GĐ2** mà A-10 **vẫn PASS 100%**). Bản sửa: giữ 4 path cũ = **0 hit**; **thêm** `hooks/usePipelineStages.ts` + `components/pipeline/` với nghĩa **"chạm ⇒ BẮT BUỘC mở trang PE và trang HĐ đếm lại folder GĐ2 bằng mắt"** (không cấm chạm, nhưng cấm chạm-mà-không-kiểm) |
+| **E-3** | `npm --prefix fe-admin run build` **và** `npm --prefix fe-user run build` → exit 0. 🔴 Chỉ tin build chạy **SAU** khi mọi agent nền dừng (gotcha #68) |
+| **E-4** | `dotnet test SolutionErp.slnx` → 0 failed |
+| **E-5** 🔴 | **UAT trên `fe-user:8080`** (R-1: `Layout.tsx:227-231` `ADMIN_HIDDEN_KEYS` chứa `KeHoachKyKet` ⇒ **menu KHKK không tồn tại bên admin**). Chụp 3 màn khớp Ảnh 1/2/3, **cửa-sổ ẩn-danh** (bài S96 #77: cache ≠ deploy) |
+| **E-6** | **fe-admin phải UAT bằng DEEP-LINK** `http://localhost:8082/khkk/list?group=1` — menu ẩn nên không vào được bằng sidebar. Thiếu câu này ⇒ sẽ có báo-cáo **"không thấy menu ⇒ chưa deploy"** giả |
+| **E-7** | PR có **bảng "cột nào chết, chuyển đi đâu"** (M-7): 8 cột bảng cũ + 6 KpiCard → card 400px |
+
+---
+
+## ✅ 3 QUYẾT-ĐỊNH OWNER — chốt 2026-08-04, THI-HÀNH NGUYÊN VẸN
+
+| # | Câu hỏi | Owner chốt | Hệ-quả lên spec |
+|---|---|---|---|
+| **1** | Panel 1 lấy nguồn nào? | 🔴 **`PipelineTreePanel`** (giữ đề-xuất bản invest) — **bác** phương-án đảo của lead | ②-B viết lại: cây + **4 mitigation CƯỠNG-HÀNH** (fallback `!canPe` · search server-side · `onOpen` callback · `stage2Count` cặp) + acceptance **C-5 · C-6 · C-7 · C-8**. 🔴 Lead đã trình đủ 4 rủi-ro **ngay trên thẻ chọn**; owner đọc và vẫn chọn ⇒ **quyết-định có thông-tin đầy-đủ**. Nhưng *chọn HÌNH ≠ chấp-nhận VỠ* — mitigation là **bắt buộc**, không phải tuỳ chọn |
+| **2** | Panel 3 khi chưa chọn hiện gì? | ✅ **CHỈ hộp dashed y PE** (giống Ảnh 1 nhất) | Đóng **miễn-trừ-thứ-7** mà bản invest không khai: §5 gốc âm-thầm thay bằng `PipelineTreePanel`, nay quay về đúng Ảnh 1. **Không** nhét cây vào panel 3 (cây đã sống ở panel 1 theo QĐ-1 ⇒ nhét lần 2 là trùng). Khác GĐ3 ở điểm này = **CÓ CHỦ ĐÍCH**, ghi rõ trong PR |
+| **3** | File đính kèm khi duyệt — hiện hay upload? | ✅ **Cho upload THẬT** (thêm enum + endpoint) | **BE-7** mới: `+ApprovalAttachment = 3` (append-only) + endpoint upload trong dialog duyệt, policy **`KeHoachKyKet.Update`**. Bẫy *"UI chết mà acceptance vẫn xanh"* (F-6) **tự tan** vì nay có đường ghi; acceptance **D-9** đổi sang runtime 2 chiều (có file ⇒ hiện · không file ⇒ tự ẩn) |
+
+### 🔸 Còn treo (KHÔNG chặn thi-hành, hỏi khi tới)
+
+**P3-12 — 4 return-mode (Cấp / Bước / NV / Drafter).** Bản invest xếp "buộc phải khác" với tiền-đề *"KHKK là 3 trạm ngắn"*; lead đo `DbInitializer.cs:554` ⇒ **3 Bước × ≤2 Cấp = tới 6 trạm**, seed `:620` tự ghi *"sao từ workflow Duyệt NCC prod"* ⇒ **tiền-đề sai, hình dạng y hệt PE**. ⇒ Nó là **hoãn**, không phải buộc-khác. Đợt này **giữ 1 mode** (về Bước 1 · Cấp 1) như hiện tại; nếu anh muốn đủ 4 mode như PE thì đó là **món riêng** (đổi state machine BE), không nhét vào đợt UI này.
+
+---
+
+## Nợ kỹ-thuật khai ngay (không giấu)
+
+- `ContractsListPage.tsx` (khuôn được chép) **CHỈ TỒN-TẠI Ở fe-admin** — port sang fe-user = **viết mới**, không phải copy (M-6).
+- Trích-dẫn hỏng trong bản invest, đã sửa ở spec này: path `WorkflowService` thiếu lớp `Infrastructure/Services/` (B6) · `:552` `ResolveActorFullNameAsync` **không trỏ vào gì** (call-site thật `:485`/`:554`, định-nghĩa ở file khác) · `:547` changelog lệch 1 dòng (thật `:546`) · A-0 sai khoảng dòng (B5).
+- Lane `reviewer lens-fidelity` **chết dở** (hết giới-hạn phiên): §6 (phép đo P3-12) và §8 (lỗ-hổng tự khai) còn trống, dòng VERDICT còn `?`. **Lead đã tự chạy phép đo §6** (kết-quả ở ①, lật P3-12) nhưng **§8 thì chưa ai làm** ⇒ **danh-sách "chưa đo được" của lăng-kính fidelity hiện KHÔNG TỒN-TẠI**. Đừng đọc bản đó thành "đã soi hết trục trung-thành".
+- Chưa đo: `fe-user` twin của `usePipelineStages.ts` / `PipelineStageFolders.tsx` (feasibility C-1) · số phiếu KHKK thật ở prod để biết trần 200 có cắn không (C-4) · `hideEmpty` tương-tác với `content`/`groups` (C-3).
diff --git a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-implementer-backend-s171.md b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-implementer-backend-s171.md
new file mode 100644
index 0000000..90cc187
--- /dev/null
+++ b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-implementer-backend-s171.md
@@ -0,0 +1,34 @@
+# sub-implementer-backend-s171 — nhật-ký lane BE (Nhóm 1, BE-1 → BE-7)
+
+> Run: `2026-08-04-S171-khkk-ui-mirror-pe` · vai `implementer-backend` · spec §②-D + §③ Nhóm 1
+> Luật ghi: sau MỖI file sửa xong ⇒ append ngay 1 dòng ` — <đã làm gì> — `.
+
+## 0. Trạng thái
+
+- [ ] BE-1 DetailDto +5 field
+- [ ] BE-1b FinalizeStepName/FinalizeLevelName DẪN XUẤT (3 subquery + 3-ngả)
+- [ ] BE-2 ContractSigningPlanApprovalDto (đẻ mới) + projection
+- [ ] BE-3 tên người duyệt cho Approval
+- [ ] BE-4 WorkflowStepDto +DepartmentName
+- [ ] BE-5 changelogs endpoint policy KeHoachKyKet.Read
+- [ ] BE-7 enum +ApprovalAttachment=3 + endpoint upload
+- [ ] Mirror `types/khkk.ts` ×2 app
+- [ ] `dotnet build` sạch
+
+## 1. Đọc / khảo sát
+
+(chưa ghi)
+
+## 2. Nhật-ký file sửa
+
+(chưa ghi)
+
+## 3. Tự kiểm B-1 · B-2 · B-4 · B-5 · B-6
+
+(chưa ghi)
+
+## 4. Chưa làm / lý do
+
+(chưa ghi)
+
+END implementer-backend-s171 — 0/7 mục BE xong · build CHƯA CHẠY
diff --git a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-feasibility-s171.md b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-feasibility-s171.md
index b25448f..e538f92 100644
--- a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-feasibility-s171.md
+++ b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-feasibility-s171.md
@@ -3,44 +3,239 @@
> Lane 2/3 của ENSEMBLE `/fable-clone` S171. Chấm bản `sub-investigator-codebase-invest-s171.md`.
> PROPOSE-ONLY / READ-ONLY. Mọi con số dưới đây do lane này TỰ đo, không chép từ bản invest.
-## 0. Tái-dựng số load-bearing (bắt buộc ≥2)
+## 0. Tái-dựng số load-bearing (bắt buộc ≥2 — tôi làm 4)
-_(chưa đo)_
+| # | Số bản invest nêu | Lệnh tôi chạy | Đo được | Phán |
+|---|---|---|---|---|
+| R1 | `WorkflowService.cs:534` ghi `ContractSigningPlanApprovals` | `grep -n "ContractSigningPlanApprovals\.Add"` | `:534` **đúng y** | **KHỚP** |
+| R2 | changelog ghi **≥9 chỗ** (liệt kê 8: `:547` + `Features.cs:497,562,639,1078,1116,1224,1304`) | `grep -n "ContractSigningPlanChangelogs\.Add" src/Backend` | **10 chỗ**: `WorkflowService.cs:546` · `Features.cs:497,562,639,1078,1116,1224,1304,1372` · `CreateContractFromSigningPlanFeatures.cs:226` | **KHỚP** về mệnh-đề (10 ≥ 9) — nhưng **liệt-kê thiếu 2** (xem §7-B3) |
+| R3 | `BuildStagesOptions` chỉ `stage1Content`/`stage1Count` (`:163`) | đọc `usePipelineStages.ts:161-165` | đúng 2 field, đúng dòng | **KHỚP** |
+| R4 | `DetailDto` ở `Features.cs:145-181` | đọc `:145-181` | record đóng đúng tại `:181` (`bool BudgetFrozen);`), **37 tham-số** | **KHỚP** |
+
+⇒ 4/4 con-số load-bearing của bản invest **tái-dựng được**. Bản đó **không bịa số**. Chỗ nó vỡ là **suy-luận từ số**, không phải số. Ghi rõ để lead đừng hạ tín-nhiệm nhầm chỗ.
## 1. D-2 — `ContractSigningPlanWorkflowService.cs:534` ghi Approvals: LUÔN hay 1-NHÁNH?
-_(chưa đo)_
+### 1.0 Đính-chính ĐƯỜNG-DẪN (bản invest ghi thiếu, người sau sẽ mở trượt)
+
+File **KHÔNG** nằm ở `Application/ContractSigningPlans/`. Đường-dẫn thật:
+`src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs`
+(`find src -iname "*ContractSigningPlan*"` → chỉ 1 file mang tên đó). Bản invest trích `ContractSigningPlanWorkflowService.cs:534` trần, không path; ai đoán theo hàng-xóm `ContractSigningPlanFeatures.cs` (Application) sẽ tìm không ra. Lỗi nhẹ, nhưng A-1/A-2 là checklist thi-công nên phải đúng path.
+
+### 1.1 🟢 Giả-thuyết "chỉ ghi 1 nhánh" của lane tôi: **SAI**. Claim D-2 **HELD**.
+
+Tôi vào săn kẽ này và **không có kẽ**. Đo bằng `grep -n "LogSigningPlanTransitionAsync"` — **6 call-site**, phủ TRỌN mọi transition:
+
+| call-site | Ngữ-cảnh | `Decision` truyền vào |
+|---|---|---|
+| `:204` | Gửi duyệt / gửi lại sau Trả lại | `ApprovalDecision.Pending` |
+| `:283` | Duyệt + level-finalize (kết thúc sớm) | `Approve` |
+| `:296` | Duyệt → chuyển Cấp kế | `Approve` |
+| `:311` | Duyệt → hoàn tất Bước, sang Bước kế | `Approve` |
+| `:325` | Duyệt Cấp cuối → `DaDuyet` | `Approve` |
+| **`:393`** | **Trả lại VÀ Từ chối** — `targetPhase` động, `comment`, `isReturn ? … : …` chọn summary | **`ApprovalDecision.Reject`** |
+
+`:534` `db.ContractSigningPlanApprovals.Add(...)` nằm **thẳng trong thân** `LogSigningPlanTransitionAsync` (`:521-544`), **không có `if` nào bọc** — mọi tham-số quyết-định (`fromPhase/toPhase/decision/actingLevelId/summary`) đều là **đối-số**, không phải hằng.
+⇒ **Entry "Trả lại → Lùi về Cấp 2" của Ảnh 2 SẼ CÓ.** Kẽ mà lệnh của lead nghi ngờ **không tồn tại**; ước-lượng chi-phí D-2 **không bị lật**.
+🔸 Ghi rõ để lead khỏi mất công: đây là chỗ **bản invest đúng mà owner/lead có thể tưởng sai** — nó đúng vì tác-giả `LogSigningPlanTransitionAsync` đã cố-ý gom 2 bảng vào **một unit-of-work chung** (docstring `:515-520`), chứ không phải vì may.
+
+### 1.2 Nhưng có 1 điều D-2 **không nói** mà ảnh-hưởng đến hình hiển-thị
+
+`Approvals` cũng chứa dòng `Decision = Pending` của **hành-vi GỬI duyệt** (`:204-207`), tức "Lịch sử duyệt (N)" của KHKK sẽ có **nhiều dòng hơn** số lần ai đó thật sự bấm Duyệt/Trả/Từ-chối. PE xử việc này bằng lớp merge + dedupe 5s-bucket + synthetic-reject (`PeDetailTabs.tsx:3152-3192`, bản invest có ghi ở §1.3 mục 10 nhưng **không kéo hệ-quả** sang §4/§7). ⇒ FE KHKK phải quyết: hiện dòng `Pending` (nhật-ký đầy-đủ) hay lọc (giống PE hơn). Đây là **việc không có trong bảng D**, và nó nằm trên đường tới A-8d.
+
+### 1.3 Tái-dựng số load-bearing #1: **KHỚP tuyệt-đối**
+
+`:534` = `db.ContractSigningPlanApprovals.Add(...)` ✅ · `:547` = `db.ContractSigningPlanChangelogs.Add(...)` **lệch 1 dòng** — đo thật là `:546` (`:547` là `{`). Sai-số 1 dòng, vô-hại, ghi ra cho đủ.
+Còn `ResolveActorFullNameAsync` bản invest ghi `WorkflowService.cs:552`: đo thật trong file KHKK là **call-site `:485` và `:554`**; **định-nghĩa** hàm nằm ở file KHÁC (`PurchaseEvaluationWorkflowService.cs:1301`, `ContractWorkflowService.cs:446`). `:552` **không trỏ vào cái gì cả** → trích-dẫn hỏng (dạng nhẹ). Nội-dung claim ("BE đã có sẵn helper") vẫn **HELD**.
## 2. D-3 — changelog ≥9 chỗ ghi + `UserName` denorm + 0 endpoint đọc
-_(chưa đo)_
+**Cả 3 vế: HELD.**
+
+1. **≥9 chỗ ghi → HELD (thật 10).** Xem R2 §0.
+2. **`UserName` đã denorm → HELD.** `ContractSigningPlanWorkflowService.cs:554`: `UserName = Truncate(await ResolveActorFullNameAsync(actorId, ct), 200)` — tên **ghi thẳng vào hàng changelog**, không cần join lúc đọc. Đúng như bản invest nói.
+3. **0 endpoint đọc → HELD.** `grep -in "changelog"` trên `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` → **0 hit**. Liệt-kê đủ 18 route (`HttpGet/Post/Put/Delete` tại `:36,54,60,73,79,86,95,108,117,129,140,152,162,182,191,205,222,243`), **không route nào** là `changelogs`. Cửa đọc thật sự chưa có.
+
+**Kèm 1 dữ-kiện làm A-13 vỡ (xem §6):** đếm thật trên controller — **19** dòng `[Authorize(Policy = …)]` (1 class-level `:29` + 18 action) / **18** route ⇒ tỷ-lệ 19 ≥ 18 và **0** `[Authorize]` trần THẬT. Tức hôm nay file này **sạch theo gotcha #82**; nhưng thước A-13 mà bản invest viết lại **không đo được điều đó** (§6-G1).
## 3. D-9 / D-8 — `usePipelineStages.ts:150-320` (lỗ hổng bản invest TỰ KHAI)
-_(chưa đo)_
+### 3.1 Trả lời DỨT câu hỏi D-9 (unknown → known)
+
+**Đã đọc `fe-admin/src/hooks/usePipelineStages.ts:120-350`.** Trả lời:
+
+**Phiếu KHKK cho folder GĐ2 lấy từ QUERY ĐÃ CÓ SẴN — KHÔNG phải thêm fetch mới.**
+- Hook `usePipelineLinkage()` đã chạy query `khkkIndex` và trả `khkkByPeId: Map` (`usePipelineStages.ts:123-134`, `:147`).
+- `buildStages` gom phiếu bằng `peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? [])` (`:204`) — **đúng tập phiếu KHKK của gói thầu**, kiểu `KhkkListItemDto` (đủ `maKeHoach` `peTenGoiThau` `phase` `approvalGroup` `contractIds`).
+- `toKhkkLeaf` (`:205-212`) đã dựng sẵn leaf. `stage2` (`:265-273`) đã có `leaves` + `groups` + `locked/failed/loading/truncated`.
+⇒ **Chặn-cửa A-0 GỠ ĐƯỢC NGAY.** Không cần query mới, không cần endpoint mới cho panel 1. Đây là điểm bản invest bỏ ngỏ và nó rơi về phía **RẺ HƠN** ước-lượng.
+
+### 3.2 D-8 — claim `BuildStagesOptions` chỉ có stage1: **HELD** (tự đo)
+
+`usePipelineStages.ts:161-165`:
+```ts
+export type BuildStagesOptions = {
+ /** GĐ1 tự vẽ (trang Duyệt NCC truyền khối leaf phiếu CŨ vào đây). */
+ stage1Content?: ReactNode
+ stage1Count?: number
+}
+```
+Đúng 2 field, đúng số dòng bản invest nêu (`:163`). **HELD.**
+
+### 3.3 🔴 D-8 claim *"sửa 1 hook, 0 đụng component trình-bày"*: **BROKE** — bản invest bỏ sót `groups`
+
+Đây là chỗ vỡ **thật**, không phải bắt bẻ chữ:
+
+- Stage 1 (PE) là folder **PHẲNG**: `{ n:1, leaves:[], content, count }` (`:183-186`) — không có `groups`. Cho nên `stage1Content` **thay chỗ** `leaves` là an-toàn.
+- Stage 2 (KHKK) **KHÔNG PHẲNG**: nó có `groups: khkkGroups` = **8 ngăn cố định N1-N8 + ngăn "(chưa phân nhóm)"** (`:220-252`, `:268`), là feature K5 S166 cố ý *"HIỆN ĐỦ 8 NGĂN KỂ CẢ RỖNG"* (comment `:232-234`), mỗi ngăn có `onOpenAll → /khkk/list?group=n` khớp URL leaf sidebar (`:239-241`), và có **bất-biến Σ** cảnh báo DEV khi tổng ngăn ≠ tổng folder (`:254-263`).
+- ⇒ Nhét `stage2Content` theo khuôn đối-xứng stage1 (`content` thay `leaves`) sẽ đặt **card phiếu KHKK** và **8 ngăn nhóm** vào tranh-chấp cùng một ô. Hoặc mất 8 ngăn, hoặc phải quyết-định thứ-tự content ⟂ groups — **đó là đụng vào `PipelineStageFolders`**, và đụng vào một bất-biến có chuông báo.
+- Đối-xứng "stage1 ↔ stage2" mà bản invest dựa vào là **đối-xứng giả**: hai stage khác CARDINALITY (phẳng vs 2 tầng). Đây đúng lớp bài học `feedback_cardinality_change_grep_consumers`.
+
+**Hệ quả chi-phí:** D-8 không phải "sửa 1 hook". Tối thiểu là 1 hook + 1 quyết-định trình-bày (giữ 8 ngăn hay bỏ) + đo lại bất-biến Σ. Vẫn RẺ, nhưng **không phải 0 đụng component**.
+
+### 3.3-bis 🔴 BẰNG-CHỨNG CỨNG: `content` THẮNG `groups` — 8 ngăn bị nuốt IM LẶNG
+
+`fe-admin/src/components/pipeline/PipelineStageFolders.tsx:285-295` (tự đo, `grep -n -C4`):
+```
+285: ) : stage.content !== undefined ? (
+286: stage.content
+287: ) : stage.groups !== undefined ? (
+...
+292: {stage.groups.map(g => (
+```
+`content` đứng **TRƯỚC** `groups` trong chuỗi tam-nguyên ⇒ hễ truyền `stage2Content` thì nhánh `groups` **không bao giờ chạy**. 8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" **biến mất, không lỗi, không cảnh-báo**.
+Và `stageCount` (`:95-97`) khi có `content` thì đọc `count ?? 0` ⇒ quên truyền `stage2Count` là badge đếm về **0** trong khi folder đầy phiếu — lại một dạng "vắng-mặt trông giống ổn".
+⇒ Claim D-8 *"0 đụng component trình-bày"*: **BROKE** (2/2 bằng-chứng tại `:285-287` và `:95-97`).
+🔸 Đường ra rẻ: **đừng dùng `content` cho stage 2**. Panel 1 KHKK có thể giữ nguyên `leaves`/`groups` sẵn có và chỉ làm giàu `PipelineLeaf` (thêm badge Σ tiền / người soạn), hoặc thêm ô `groupContent` per-ngăn. Cả hai đều KHÔNG giẫm lên nhánh `content` của GĐ1.
+
+### 3.4 Sai địa-chỉ dòng của A-0 (nhỏ nhưng làm người sau đọc trượt)
+
+A-0 và D-9 bảo đọc `:150-320` và mô-tả *"stage 2 dựng bằng `leaves` (link) `:265`"*. Đúng dòng `:265` là `const stage2: PipelineStage = {`, nhưng **nguồn dữ-liệu thật** nằm ở `:204` (`khkkByPeId`) và `:123-134` (map dựng trong `usePipelineLinkage`) — tức **NGOÀI** khoảng `:150-320` mà A-0 chỉ định (`:123-134` nằm trước `:150`). Ai đọc đúng khoảng A-0 vẫn có thể không thấy query nguồn. Sửa A-0 thành `:100-320`.
## 4. Chi-phí BE thật (`ContractSigningPlanFeatures.cs:145-181`) + D-6 join Users
-_(chưa đo)_
+### 4.1 Chỗ RẺ HƠN ước-lượng (nói ra cho công bằng)
+
+`grep -n "new ContractSigningPlanDetailDto"` → **đúng 1 call-site**: `ContractSigningPlanFeatures.cs:784`. Record **positional** 37 tham-số ⇒ thêm field bình thường sẽ vỡ mọi call-site, nhưng ở đây chỉ có **một**. ⇒ Chi-phí "append field vào DetailDto" thật sự **thấp**, đúng như bản invest hàm-ý. **HELD.**
+
+### 4.2 D-6 *"entity chỉ có `ApprovedByUserId`, không có tên ⇒ phải join Users"*: **HELD**
+
+Đọc thân `ContractSigningPlanApproval` được khởi-tạo tại `WorkflowService.cs:534-544`: 8 field `ContractSigningPlanId · FromPhase · ToPhase · Decision · Comment · ApprovalWorkflowLevelId · ApprovedByUserId · ApprovedAt` — **không có** field tên. Đối-chiếu: hàng changelog ngay dưới (`:546-557`) **CÓ** `UserName`. Hai bảng ghi cùng lúc, **một denorm tên một không** ⇒ D-6 đúng, và đúng vì lý-do cấu-trúc chứ không phải suy đoán.
+🔸 Gợi-ý rẻ hơn "join Users" mà bản invest không nêu: `Approvals` và `Changelogs` **ghi cặp 1-1 trong cùng unit-of-work** (`:534` + `:546`, cùng `now`) ⇒ có thể lấy tên từ changelog cùng transition, hoặc **denorm thêm cột tên vào Approval** cho khớp anh-em. Cả hai đều tránh join lúc đọc.
+
+### 4.3 🔴 **BROKE** — D-1 *"+3 field (append cuối)"* ước-lượng THIẾU phần đắt nhất
+
+`ContractSigningPlan.cs:51` chỉ có **`public bool EndedByLevelFinalize`** — một cờ boolean. **Không có** cột nào lưu *Bước nào / Cấp nào* đã kết-thúc.
+⇒ `FinalizeStepName` và `FinalizeLevelName` **không phải "field append"**, chúng là **giá-trị phải DẪN XUẤT**. PE làm việc đó bằng **subquery `where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == …` lặp 3 lần** trong projection (`PurchaseEvaluationFeatures.cs:665-683`), cộng nhánh 3-ngả `phase == DaDuyet ? runtime-flag : suy-config` (`:665-673`).
+⇒ D-1 thật sự = **1 field append (`EndedByLevelFinalize`) + port ~20 dòng projection có 3 subquery**. Vẫn làm được, nhưng gọi nó là "+3 field" là **ước-lượng sai chiều RẺ** — đúng loại sai mà §8-2 của chính bản invest cảnh-báo ("dễ ước-lượng sai đắt nhất theo **cả hai** chiều"). Nó cảnh-báo đúng rồi tự dính chiều còn lại.
+
+### 4.4 Chỗ phải sửa mà bản invest **không đếm** (BE)
+
+| # | Việc bị bỏ khỏi bảng D / A-1 | Bằng-chứng |
+|---|---|---|
+| U-1 | **`ContractSigningPlanApprovalDto` CHƯA TỒN TẠI** — phải tạo record mới + projection | `grep -n "ContractSigningPlanApprovalDto" src/Backend` → **0 hit** |
+| U-2 | Projection `Approvals` phải `.Include`/join `ApprovalWorkflowLevels` nếu muốn hiện "Cấp nào duyệt" (Ảnh 2 có "Lùi về **Cấp 2**") — `ApprovalWorkflowLevelId` là Guid trần | `WorkflowService.cs:541` |
+| U-3 | `types/khkk.ts` phải mirror **×2 app** cho mọi field mới; A-9 có phủ, nhưng A-1 (BE) không nhắc ⇒ dễ làm 1 app rồi tick A-1 xong | `R-7` của chính bản invest |
+| U-4 | Hàng `Decision = Pending` (hành-vi **gửi duyệt**, `:204-207`) sẽ nằm trong `Approvals` ⇒ phải quyết lọc hay không trước khi dựng "Lịch sử duyệt (N)" | `:204-207` |
+
+---
## 5. §5 Phương-án A vs B — lý-do bác B có đứng vững?
-_(chưa đo)_
+### 5.1 Nửa DỮ-KIỆN: **HELD** (tôi tự mở 2 file)
+
+- `fe-admin/src/components/pe/PeWorkflowPanel.tsx:384` — `const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'` ⇒ PE **đọc** `step.status` có sẵn trên DTO. ✅
+- `fe-admin/src/pages/khkk/KhkkWorkflowPanel.tsx:106-120` — `stepStatus(idx)` / `levelStatus(idx, order)` **tự tính** từ `currentWorkflowStepIndex` + `currentApprovalLevelOrder`. ✅
+⇒ "shape KHÁC NHAU THẬT" là **sự-thật đo được**, không phải cái cớ. **HELD.**
+
+### 5.2 Nửa LẬP-LUẬN: **BROKE** — nhị-phân giả (false dilemma)
+
+Bản invest viết: *"Generic hoá = phải **hoặc** (i) ép BE KHKK precompute, **hoặc** (ii) nhồi adapter 2 chiều. Cả hai đều **đụng vào đường duyệt của PE đang chạy prod**."*
+
+Hai lỗi:
+1. **Có đường (iii) mà nó không xét:** KHKK **đã có sẵn** hàm suy status — 15 dòng thuần-tuý, không side-effect (`:106-120`). Chỉ cần KHKK **tự chuẩn-hoá cây thô → shape có `status`** ngay trước khi truyền vào component generic. Đó là adapter **MỘT chiều, nằm TRỌN trong nhánh KHKK**, và **0 dòng PE bị đọc lại hay sửa**.
+2. **Câu "cả hai đều đụng vào đường duyệt PE" SAI với (ii):** adapter ở phía KHKK không chạm PE. Câu này gán rủi-ro của (i) cho cả (ii) rồi dùng nó để đóng cửa B.
+
+### 5.3 Nhưng **KẾT-LUẬN chọn A vẫn ĐỨNG** — vì một lý-do khác, mạnh hơn
+
+Lý-do thật sự để bác B **không phải** shape, mà là: trích `StepOpinionsBox` ra khỏi `PeDetailTabs.tsx:705-774` **buộc phải sửa file trong `components/pe/`**, mà chính bản invest đặt A-10 = *"`git diff --name-only` → **0 đường-dẫn** khớp `components/pe/`"*. ⇒ **B mâu-thuẫn trực-tiếp với acceptance của chính nó.** Cộng thêm ≥6 nhánh riêng của PE (đúng, và đây là lý-do có sức nặng).
+🔸 Khuyến-nghị lead: **giữ quyết-định A, thay lý-do.** Lý-do hiện tại sai kỹ-thuật; nếu owner (hoặc reviewer sau) bẻ được lý-do, quyết-định đúng sẽ bị lật oan. Đây là dạng "kết-luận đúng, chứng-minh hỏng".
+
+---
## 6. §7 Checklist — mục nào GOODHART-able?
-_(chưa đo)_
+### G1 🔴 **A-13 VỠ CẢ HAI ĐẦU** (nghiêm nhất — nó là mục canh **quyền**)
+
+Thước viết: `grep -rn "Authorize" …Controller.cs | grep -c "Policy"` phải **≥ số route**, và **0 dòng `[Authorize]` trần**.
+
+- **Đầu 1 — tử-số bơm bằng COMMENT.** Dòng `:15` là **chú-thích**: ``// tầng 1 = class `[Authorize(Policy = "KeHoachKyKet.Read")]` (mọi endpoint tối thiểu Read)`` — chứa cả "Authorize" lẫn "Policy" ⇒ **được đếm**. Viết thêm vài dòng chú-thích là con-số tự tăng. Ai xoá 1 `[Authorize(Policy=…)]` thật rồi thêm 1 dòng comment ⇒ **A-13 vẫn PASS, API mở toang**.
+- **Đầu 2 — vế "0 dòng `[Authorize]` trần" FAIL NGAY HÔM NAY.** `:17` và `:241` chứa **literal** ``[Authorize]` trần`` trong chú-thích cảnh-báo. Đo thật: file **KHÔNG có** `[Authorize]` trần nào (19 dòng Policy / 18 route, §2). ⇒ thước báo đỏ trên một file **đang sạch** — đây đúng lớp bẫy `feedback_citation_trap_selfreference` (doc/nguồn **nói VỀ** anti-pattern bị matcher bắt như **là** anti-pattern).
+- **Thước thay thế (đề-xuất):** bỏ grep chuỗi, đếm trên **AST-lite** bằng cách chỉ soi dòng **bắt đầu bằng `[`** sau khi cắt comment:
+ ```
+ grep -nE '^\s*\[(Http(Get|Post|Put|Delete))' | wc -l # R = số route
+ grep -nE '^\s*\[Authorize\(Policy' | wc -l # P
+ grep -nE '^\s*\[Authorize\]\s*$' | wc -l # T phải = 0
+ ```
+ Neo `^\s*\[` loại sạch comment `//`. **Chứng thước có RĂNG:** thêm tạm 1 action `[Authorize]` trần vào cây tạm ⇒ `T` phải nhảy lên 1 (fault-injection, `feedback_faultinjection_proves_teeth`).
+
+### G2 🔴 **A-10 đo SAI TRỤC** — đo *đường-dẫn chạm*, không đo *hành-vi đổi*
+
+A-10: *"`git diff --name-only` → 0 path khớp `components/pe/` · `pages/pe/` · `components/contracts/` · `pages/contracts/`"*.
+Đo thật, `buildStages` có **3 consumer** trong fe-admin:
+```
+components/pipeline/PipelineTreePanel.tsx:309 stages={buildStages(wg.items)}
+pages/pe/PurchaseEvaluationsListPage.tsx:524 stages={buildStages(wg.items, { stage1Content: … })}
+```
+(và `PipelineTreePanel` lại là thứ KHKK dùng ở panel-1 lẫn GĐ3 dùng ở panel-3).
+⇒ Việc D-8 **sửa `hooks/usePipelineStages.ts`** hoặc `components/pipeline/PipelineStageFolders.tsx` — **hai path đều KHÔNG nằm trong danh-sách A-10** — có thể làm **cây của trang PE mất 8 ngăn GĐ2** (§3.3-bis: `content` thắng `groups`) mà **A-10 vẫn PASS 100%**. Hồi-quy đi vòng qua cửa mà thước không canh.
+**Thước thay thế:** thêm 2 path vào A-10 (`hooks/usePipelineStages.ts`, `components/pipeline/`) với ý nghĩa **"chạm ⇒ phải mở trang PE và trang HĐ đếm lại folder GĐ2 bằng mắt"**, chứ không cấm chạm.
+
+### G3 **A-7d Goodhart-able trực-diện**
+`grep -c "Ý kiến cấp duyệt" …KhkkWorkflowPanel.tsx` = **0**. Đổi tiêu-đề thành "Ý kiến duyệt" / "Ý kiến các cấp" ⇒ **PASS** trong khi khối vẫn nằm nguyên panel 3. Thước đo **chuỗi chữ**, mục-tiêu là **vị-trí khối**.
+**Thay bằng:** `grep -c "opinions" …KhkkWorkflowPanel.tsx` = 0 **và** `grep -c "levelOpinions" …KhkkDetailContent.tsx` ≥ 1 — đo **luồng dữ-liệu**, thứ không đổi được bằng cách sửa nhãn.
+
+### G4 A-7c / A-8a / A-8b / A-8c / A-8d — cùng một lớp, nhẹ hơn
+Đều là `grep -c "" ≥1`. Gõ đúng cụm chữ vào JSX **chết** (khối render sau `return null`, hoặc điều-kiện luôn false) vẫn PASS. Không cần thay hết — nhưng **A-8d** ("Lịch sử duyệt" + "Lịch sử thay đổi") nên buộc thêm **1 dòng gọi API**: `grep -c "/changelogs" …` ≥1, vì khối đó **vô nghĩa nếu không fetch**.
+
+### G5 A-4 regex sai cú-pháp — sẽ FAIL trên chính bố-cục nó đề-xuất
+A-4 đòi khớp `^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]`. Nhưng §5 "Bố-cục đích" ghi Panel 1 = **400px** … trong khi khuôn nó bảo chép là `ContractsListPage.tsx:125` dùng `lg:grid-cols-[340px_1fr_360px]` — khớp. **Rủi ro thật:** nếu ai dùng `rem` (như KHKK đang dùng `19rem/21rem`, `KhkkListPage.tsx:197`) thì regex `[0-9]+px` **FAIL** dù bố-cục đúng. Nới thành `\[[^]]*_1fr_[^]]*\]`.
+
+---
## 7. Bỏ-sót lane này bắt thêm (thách-CLEAN)
-_(chưa đo)_
+**B1 🔴 `content` nuốt `groups` — 8 nhóm N1-N8 biến mất im lặng.** §3.3-bis. Đây là món nặng nhất: nó vừa lật claim D-8, vừa là **hồi-quy chéo sang trang PE**, vừa **không có thước nào trong §7 bắt được** (A-10 miss, A-11 build vẫn xanh, A-14 chụp màn KHKK chứ không chụp PE).
+
+**B2 `ContractSigningPlanApprovalDto` chưa tồn tại** (0 hit) — A-1 viết như thể chỉ cần "thêm field", thực tế phải **đẻ record + projection + mirror types ×2 app**. (U-1 §4.4)
+
+**B3 D-3 liệt-kê thiếu 2 chỗ ghi changelog, một trong đó có nghĩa nghiệp-vụ.** Đo được 10, bản invest kể 8. Chỗ bị bỏ đáng kể nhất: **`CreateContractFromSigningPlanFeatures.cs:226`** — đó là **bắc-cầu KHKK→HĐ (K7)**. Nghĩa là "Lịch sử thay đổi" của KHKK sẽ hiện **entry tạo Hợp đồng**, một loại dòng mà spec §7 không hề nhắc và FE chưa có nhãn cho nó. Thiếu ⇒ hoặc hiện chuỗi thô, hoặc rơi im lặng.
+
+**B4 Dòng `Decision = Pending` (gửi duyệt) vào thẳng "Lịch sử duyệt (N)".** PE chống bằng merge + dedupe 5s + synthetic-reject (`PeDetailTabs.tsx:3152-3192`); bản invest **có mô-tả** lớp đó ở §1.3 nhưng **không kéo hệ-quả** vào §4/§7 ⇒ số `(N)` của KHKK sẽ **không cùng nghĩa** với số `(N)` của PE, trong khi cả đề-bài là "cho đồng nhất".
+
+**B5 A-0 chỉ sai khoảng dòng.** Nguồn dữ-liệu thật ở `:123-134` + `:204`, **nằm ngoài** `:150-320` mà A-0 bắt đọc (§3.4). Người làm đúng theo A-0 vẫn có thể không thấy query nguồn ⇒ chặn-cửa tự vô-hiệu.
+
+**B6 Path `ContractSigningPlanWorkflowService.cs` ghi thiếu lớp.** Nó ở **`Infrastructure/Services/`**, không phải `Application/ContractSigningPlans/` như ngữ-cảnh xung quanh gợi ý (§1.0). Và `:552` (`ResolveActorFullNameAsync`) **không trỏ vào gì** — call-site thật `:485`/`:554`, định-nghĩa ở file khác.
+
+---
## 8. Chỗ bản invest ĐÚNG mà owner có thể tưởng sai
-_(chưa đo)_
+1. 🟢 **"Lịch sử duyệt của KHKK ĐÃ được ghi đầy đủ, kể cả Trả lại"** — tôi vào để lật và **không lật được**. 6/6 call-site phủ trọn Pending/Approve/Reject-Return (§1.1). Entry `Trả lại → Lùi về Cấp 2` ở Ảnh 2 **sẽ có dữ-liệu**. Đừng cắt ngân-sách mục này vì nghi ngờ.
+2. 🟢 **"Không ép BE KHKK precompute `status`" (P3-3)** — đúng. Hàm suy FE `:106-120` là 15 dòng thuần-tuý, đọc `currentWorkflowStepIndex`/`currentApprovalLevelOrder` **đã có trên DTO** (`Features.cs:162-163`). Ép BE precompute là đổi hợp-đồng API để lấy về một thứ FE tính được miễn phí.
+3. 🟢 **Đ-2 (GĐ3 đã 3-panel ⇒ GĐ2 là mắt-xích lệch duy nhất)** — tôi không kiểm trực-tiếp `ContractsListPage.tsx:125` (ngoài lăng-kính, lane 1/3 nên chấm), nhưng **gián-tiếp KHỚP**: `PipelineTreePanel` được cả KHKK và GĐ3 dùng, và `buildStages` chỉ có 3 consumer đúng như bố-cục đó hàm-ý.
+4. 🟢 **Chi-phí append DetailDto thật sự RẺ** — 1 call-site duy nhất `:784` (§4.1). Owner có thể nghĩ "sửa record 37 tham-số = đụng khắp nơi"; không phải.
+
+---
## 9. Chưa đo được / lệnh cần chạy
-_(chưa đo)_
+| # | Chưa đo | Lệnh cần chạy |
+|---|---|---|
+| C-1 | **Toàn bộ đo của tôi chỉ trên `fe-admin/`.** Không xác-nhận `fe-user/` identical | `for f in hooks/usePipelineStages.ts components/pipeline/PipelineStageFolders.tsx pages/khkk/KhkkWorkflowPanel.tsx; do sha256sum fe-admin/src/$f fe-user/src/$f; done` |
+| C-2 | A-2b (tên người trong `Approvals` ≠ null) — cần gọi API thật | `curl -H "Authorization: Bearer …" https://api…/api/contract-signing-plans/{id}` trên 1 phiếu `DaDuyet` |
+| C-3 | `hideEmpty` tương-tác thế nào với `content`/`groups` khi lọc bật (PE truyền `hideEmpty={filterActive}`) | đọc `PipelineStageFolders.tsx:230-311` |
+| C-4 | Số phiếu KHKK thật ở prod (bản invest nói PE 45 phiếu, không nói KHKK) — quyết-định trần 200 (R-11) có cắn không | đếm `SELECT COUNT(*) FROM ContractSigningPlans WHERE IsDeleted=0` |
+| C-5 | `ContractsListPage.tsx` (khuôn được đề-xuất chép) — tôi **không đọc**, để lane khác chấm | `sed -n '80,260p' fe-admin/src/pages/contracts/ContractsListPage.tsx` |
-END reviewer-lens-feasibility-s171 — VERDICT=? HELD / ? BROKE / ? bỏ-sót-bắt-thêm
+END reviewer-lens-feasibility-s171 — VERDICT=8 HELD / 6 BROKE / 6 bỏ-sót-bắt-thêm
diff --git a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-fidelity-s171.md b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-fidelity-s171.md
index beb1d33..c6d964e 100644
--- a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-fidelity-s171.md
+++ b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-fidelity-s171.md
@@ -2,49 +2,205 @@
> Vật chấm: `sub-investigator-codebase-invest-s171.md` (44.830 B)
> Hiến-pháp owner: *"cách bố trí toàn diện cho tất cả, **giống nhất** với duyệt NCC, **chỉ khác đúng phần khác, tương tự thì giống nhau, đồng nhất**."*
-> Vai: cố làm bản đó SAI. Sức thuyết phục ≠ bằng chứng.
+> Vai: cố làm bản đó SAI. Sức thuyết phục ≠ bằng chứng. Mọi `file:line` dưới đây do TÔI tự đo.
-## 0. Tự tái-dựng con số load-bearing (≥2, đo bằng lệnh của TÔI)
+## 0. Tự tái-dựng con số load-bearing (đo bằng lệnh của TÔI)
-(đang đo)
+| # | Con số bản đó khẳng-định | Tôi đo lại bằng gì | Kết quả |
+|---|---|---|---|
+| N-1 | "**27 GIỐNG-được · 6 KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG**" (`:206`) | `rg -o '^\| \*?\*?(K[0-9]+\|P[123]-[0-9]+)\*?\*? \|.*\| \*\*([^*]+)\*\*[^\|]*\|'` → 35 dòng, tôi tự phân loại từng dòng | **KHỚP.** 35 dòng = K1-4 (4G) + P1-1..8 (7G+1K) + P2-1..7 (5G+2K) + P3-1..16 (11G+3K+2ĐÃ) ⇒ **27 / 6 / 2**. `grep -c` dòng-bảng = 35. ✅ HELD |
+| N-2 | `ContractsListPage.tsx:125` = `lg:grid-cols-[340px_1fr_360px]`, `:249` PipelineTreePanel ở panel 3 | `grep -nE 'lg:grid-cols-\[\|PipelineTreePanel\|100vh' fe-admin/src/pages/contracts/ContractsListPage.tsx` | **KHỚP CHÍNH XÁC** — `:106` `flex h-[calc(100vh-4rem)] flex-col` · `:125` `…lg:grid-cols-[340px_1fr_360px]` · `:249` `{!selectedId && }`. ✅ HELD |
+| N-3 | PE panel 2 có "**5 khối đánh số**", Section 5 = ý-kiến (`PeDetailTabs.tsx:397-437`) | `sed -n '395,440p'` + `grep -nE 'title="[0-9]'` | **HELD về THỨ-TỰ** (1→2→3→4→5, ý-kiến dưới cùng `:426`) nhưng **con số hình-thức lệch**: chỉ **3** hit `title="N.` (`:402 :418 :426`); Section 2 dùng template-literal `title={\`2. Hạng mục + Báo giá NCC (${…})\`}` `:405`, Section 4 là `` trần `:423`. ⇒ xem F-4 |
+| N-4 | Khối ý-kiến KHKK ở panel 3, `KhkkWorkflowPanel.tsx:376-412` | `sed -n '370,415p' fe-admin/src/pages/khkk/KhkkWorkflowPanel.tsx` | **KHỚP** — `:376` comment, `:377` `{opinions.length > 0 && (`, `
` 1 cột `text-[11px]` `:384-410`, placeholder `(duyệt — không ý kiến)` `:407`. ✅ HELD |
+
+**Kết luận mục 0:** 4/4 con số load-bearing tôi tự đo đều đứng (N-3 lệch ở dạng-thức, không lệch ở kết-luận). Bản đó **không bịa số**. Vấn-đề của nó nằm chỗ khác — ở **đoạn từ §3 sang §5/§7**, xem dưới.
+
+---
## 1. Câu (1) — 6 dòng "KHÁC-buộc-phải-khác" có THẬT SỰ buộc không?
-(đang đo)
+Luật chấm: mỗi dòng KHÁC = **một lần xin miễn-trừ**; gánh nặng chứng-minh thuộc người xin. Lý-do hợp-lệ = **nghiệp-vụ** (trục dữ-liệu/quy-tắc GĐ2 không tồn-tại ở GĐ1). Lý-do KHÔNG hợp-lệ = "code đang thế" · "làm sẽ tốn" · "ngoài phạm-vi".
-### P1-4 — nhóm N1-N8
-### P2-3 — nội-dung Section
-### P2-7 — inline-edit vs readOnly
-### P3-7 — badge NS PRO/CCM
-### P3-12 — 4 return-mode
-### P3-13 — price-picker
+| Dòng | Lý-do bản đó nêu | Loại lý-do | Verdict |
+|---|---|---|---|
+| **P1-4** nhóm N1-N8 | 8 nhóm × 8 workflow SP-002, rào 409 BE | **nghiệp-vụ THẬT** | ⚠️ **HELD nhưng DÁN NHÃN SAI** — xem 1.1 |
+| **P2-3** nội-dung Section | GĐ2 không có báo-giá NCC, có căn-cứ TVGS | **nghiệp-vụ THẬT** | ✅ HELD |
+| **P2-7** inline-edit vs readOnly | "KHKK không có workspace ⇒ ép readOnly = cắt đường sửa" | **"code đang thế" ngụy-trang** | 🔴 **BROKE** — xem 1.2 |
+| **P3-7** badge NS PRO/CCM | quyền nhập ngân-sách per-role là khái-niệm riêng PE | **nghiệp-vụ THẬT** | ✅ HELD |
+| **P3-12** 4 return-mode | "3 trạm ngắn" + "đổi state machine BE, ngoài phạm-vi UI" | **nửa nghiệp-vụ / nửa phạm-vi** | 🔴 **BROKE tiền-đề** — xem 1.3 |
+| **P3-13** price-picker | KHKK chốt giá per-Line ở choke-point BE | **nghiệp-vụ THẬT (cardinality)** | ✅ HELD |
+
+### 1.1 P1-4 — HELD về lý-do, SAI về nhãn (làm phồng số miễn-trừ)
+
+Lý-do nghiệp-vụ có thật. Nhưng **kết-quả thị-giác thì GIỐNG HỆT**: PE đặt `