From bbe3fbb5764d096027a9a1ce797eb262b2171b8c Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 4 Aug 2026 18:53:50 +0700 Subject: [PATCH] wal: flush 20260804T1853 --- .claude/WAL.md | 2 +- .../2026-08-04-S171-khkk-ui-mirror-pe/run.md | 39 ++- .../spec-khkk-ui-mirror-pe-04-08-2026.md | 278 ++++++++++++++++++ .../sub-implementer-backend-s171.md | 34 +++ .../sub-reviewer-lens-feasibility-s171.md | 219 +++++++++++++- .../sub-reviewer-lens-fidelity-s171.md | 202 +++++++++++-- .../sub-reviewer-lens-regression-s171.md | 208 +++++++++++-- 7 files changed, 922 insertions(+), 60 deletions(-) create mode 100644 .claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/spec-khkk-ui-mirror-pe-04-08-2026.md create mode 100644 .claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-implementer-backend-s171.md 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 `` nhóm N1-N8 **đúng ô đó** — chính dòng P1-4 tự viết *"CHỖ ĐẶT phải giống"*. Đây là **khác NGUỒN DỮ-LIỆU, không khác BỐ-TRÍ**, mà cả tài-liệu này là về bố-trí. +⇒ Dán nhãn KHÁC-buộc-phải-khác cho nó làm **mẫu-số miễn-trừ phồng lên 6**, trong khi số miễn-trừ THẬT về bố-trí chỉ là 5. Với owner đang đếm "chỉ khác đúng phần khác", con số 6 là **báo cáo bi-quan sai chiều**. Đề-nghị: chuyển P1-4 → **GIỐNG-được (khác nội-dung tùy-chọn)**. + +### 1.2 P2-7 — 🔴 BROKE: nguỵ-biện lưỡng-nan (false dichotomy) + +Bản đó lập-luận: *"KHKK không có trang workspace. Ép `readOnly` mà chưa dựng workspace = cắt mất đường sửa duy nhất ⇒ mất chức-năng."* + +Đo lại **chính component mẫu**, `PeDetailTabs.tsx:395-440`: +- `readOnly` là **prop do HOST truyền**, không phải hằng-số trong component (`PurchaseEvaluationsListPage.tsx:657-663` truyền `readOnly={true}`; component còn có nhánh `mode === 'workspace'` `:427-431` in banner khác). +⇒ Mệnh-đề "muốn giống thì phải dựng workspace" **sai**: host `/khkk/list` chỉ cần truyền `readOnly={phiếu không phải Nháp || user không phải drafter}`. Đường sửa **không bị cắt** — nó nằm cùng chỗ, chỉ đổi điều-kiện bật. +⇒ Lý-do thật đằng sau P2-7 là **chi-phí + "hiện tại nó đang thế"**, đúng thứ mà hiến-pháp run cấm (`:149`). Đây là dòng **duy nhất** trong 6 dòng mà chính tác-giả gắn nhãn *"(có điều-kiện)"* — tự-khai đúng, nhưng kết-luận vẫn để ở cột KHÁC. + +**Đề-nghị lật:** P2-7 → **GIỐNG-được**, với 1 câu spec: *"panel 2 nhận `readOnly` từ host; host list truyền `true` khi phiếu ≠ Nháp hoặc người xem ≠ drafter, `false` ngược lại — KHÔNG dựng workspace riêng."* Nếu owner vẫn muốn workspace, đó là món **P2 riêng**, không phải điều-kiện để được miễn-trừ hôm nay. + +### 1.3 P3-12 — 🔴 BROKE tiền-đề + lý-do "ngoài phạm-vi" không hợp-lệ + +Hai vế: +- Vế "ngoài phạm-vi UI / đổi state machine BE" — **không phải lý-do nghiệp-vụ**, là lý-do chi-phí. Theo luật owner, vế này tự nó không mua được miễn-trừ. +- Vế "chuỗi KHKK là **3 trạm ngắn** (PRO→CCM→CEO) nên lùi-1-cấp ≈ về-đầu" — đây MỚI là vế nghiệp-vụ, nhưng nó là **khẳng-định chưa đo**. Trong khi đó chính bản đó ghi KHKK có **8 workflow `KHKK-N{n}`** cấu-hình được (P1-4, `types/khkk.ts:163`) và Mig 71 thêm **nhóm duyệt N1-N8**. Số Bước × Cấp là **dữ-liệu cấu-hình**, không phải hằng-số "3". Nếu tồn-tại workflow KHKK ≥3 Bước hoặc ≥2 Cấp/Bước thì "lùi 1 cấp ≈ về đầu" **sai**, và miễn-trừ sụp. + +*(đo cụ-thể xem mục 6 — tôi có chạy, kết quả ghi ở đó)* + +**Đề-nghị:** giữ P3-12 ở cột KHÁC **chỉ khi** đo được số Bước/Cấp tối-đa trong 8 workflow KHKK ≤ 3 tổng-cấp. Nếu không đo được ⇒ hạ xuống **"chờ owner"**, đừng để ở KHÁC-buộc-phải-khác. Lý-do "ngoài phạm-vi" phải viết đúng tên: *hoãn*, không phải *buộc khác*. + +--- ## 2. Câu (2) — 27 dòng "GIỐNG-được" có PHỦ HẾT 3 ảnh không? -(đang đo) +### 2.1 Đối-chiếu từng khối thị-giác owner NHÌN THẤY (nguồn: `run.md:13-29`) -### 2.1 Liệt-kê khối thị-giác trong ảnh (từ run.md) -### 2.2 Khối KHÔNG xuất hiện trong §3/§5 — BỎ SÓT +| Ảnh | Khối owner thấy | Có dòng trong §3? | Có trong §5 bố-cục đích? | Có acceptance §7? | +|---|---|---|---|---| +| 1 | header icon + tên + badge đếm `45` | K2 | ✔ (ngầm) | ❌ **không có** | +| 1 | ô search trong panel 1 | P1-2 | ✔ | ❌ không có | +| 1 | dropdown `Tất cả trạng thái` | P1-3 | ✔ | ❌ không có | +| 1 | cây `📅 Năm 2026` + badge | P1-1 | ✔ | ❌ (chỉ A-0 đọc hook) | +| 1 | **panel 2 rỗng: "Chọn phiếu ở danh sách"** | 🔴 **KHÔNG có dòng nào** | 🔴 **không nhắc** | 🔴 **không có** | +| 1 | panel 3 rỗng: "Quy trình duyệt sẽ hiện khi chọn phiếu." | P3-16 (GIỐNG-được) | 🔴 **§5 thay bằng PipelineTreePanel** | A-8e **khoá luôn bản thay-thế** | +| 2 | tiêu-đề + meta `v04` | P3-1 | ✔ | ❌ | +| 2 | banner xanh kết-thúc-sớm | P3-2 | ✔ | A-8a ✔ | +| 2 | Bước + chip phòng | P3-4 | ✔ | A-8b ✔ | +| 2 | **liệt-kê NV/Cấp, người duyệt in ĐẬM, NV khác `/ Tên` mờ** | 🔴 **không có dòng** | 🔴 không nhắc | 🔴 không có | +| 2 | badge vai `NS PRO/CCM` | P3-7 (KHÁC) | — | — | +| 2 | mờ Bước chưa tới + chip `Kết thúc tại Cấp 2` | P3-6 | ✔ | A-8b ✔ | +| 2 | badge `⚑ Duyệt thay CEO` | P3-5 | ✔ | A-8b ✔ | +| 2 | `File đính kèm khi duyệt` + phụ-đề + hàng file | P3-8 | ✔ | A-8c ⚠️ (xem F-6) | +| 2 | `Lịch sử duyệt (6)` + hint `→ Lùi về Cấp 2` | P3-9 (hint không nhắc) | ✔ | A-8d ✔ | +| 3 | bảng số-liệu dòng 8/9 (tổng nền xanh, âm đỏ ngoặc) | P2-3 (KHÁC nội-dung) | ✔ | ❌ | +| 3 | card `THÔNG TIN CHỌN THẦU` | P2-3 | ✔ | ❌ | +| 3 | **Section 5 khoanh đỏ: nhóm BƯỚC→PHÒNG, lưới 2 cột, meta `x/y cấp · N NV`** | P2-4/5/6 | ✔ | A-7b/c/d ✔ | +| 3 | cây trái 4 folder GĐ + badge `SẮP` | P1-1 / D-8 | ✔ | ❌ (không ai gỡ `SẮP` cho GĐ2) | +| 3 | **card phiếu: chip `⚑ Kết thúc · Phòng 2` + `Đã duyệt` + mã + giờ + tên** | P1-5 liệt-kê **THIẾU chip `⚑ Kết thúc`** | 🔴 không nhắc | ❌ không có | -## 3. Câu (3) — Bố-cục đích §5 có đúng "content trên, duyệt+ghi-chú dưới"? +### 2.2 🔴 3 khối BỊ THIẾU (bỏ-sót tôi bắt thêm) -(đang đo) +**F-1 — Empty-state panel 2 "Chọn phiếu ở danh sách" (Ảnh 1) BIẾN MẤT hoàn-toàn.** +Đo: `PurchaseEvaluationsListPage.tsx:644-652` — ``, cộng 2 nhánh `Đang tải…` `:653` và `Không tìm thấy phiếu.` `:654-656`. +Bản đó **có ghi sự-thật này ở §1.2** (`:50`, kèm chữ "khớp Ảnh 1") rồi **đánh rơi**: không có dòng §3.C, không có trong sơ-đồ §5, không có acceptance §7. Đây đúng là loại lỗi "vắng-mặt trông giống ổn" — không ai đọc §3 mà phát-hiện được thứ **không có dòng**. +Với KHKK, host 3-panel mà panel 2 trống trơn khi chưa chọn = **màn hình trắng giữa** — sai Ảnh 1 ngay ở trạng-thái ĐẦU TIÊN người dùng nhìn thấy. -## 4. Câu (4) — Nhóm 5 "chờ owner" (A-15/A-16/A-17): hoãn đúng hay hoãn lẩn? +**F-2 — Chip `⚑ Kết thúc · Phòng N` trên card phiếu ở panel 1 (Ảnh 3) không được port.** +P1-5 liệt-kê card 3 hàng gồm *"mã · gói thầu · trạng thái · Σ tiền · người soạn · ngày"* — **thiếu** đúng con chip mà owner nhìn thấy trong ảnh (`PeFinalizeChip`, bản đó tự ghi ở §1.1 `:43`). Trớ trêu: đây là khối **0-công-BE** cho KHKK, vì cột `EndedByLevelFinalize` đã có sẵn (chính D-1 của bản đó khẳng-định, Mig 71). Bỏ sót một khối vừa-thấy-trong-ảnh vừa-rẻ-nhất. -(đang đo) +**F-3 — Danh-sách NV trong mỗi Cấp (người duyệt thật in đậm, NV còn lại `/ Tên` mờ) — Ảnh 2 mục 3.** +Bản đó ghi ở §1.3 (`:78`, `PeWorkflowPanel.tsx:453-465`) rồi **không đưa thành dòng delta**. §2.3 chỉ có dòng cho *badge vai*, không có dòng cho *bản-thân danh-sách NV*. Nếu KHKK panel 3 chỉ vẽ Bước→Cấp mà không liệt-kê NV, hình sẽ **không giống Ảnh 2** dù A-8a/b/c/d PASS hết. -## 5. Chỗ bản đó ĐÚNG mà owner có thể tưởng sai (bảo vệ kết-luận đúng) +--- -(đang đo) +## 3. Câu (3) — Bố-cục đích §5 có đúng "content nhập phía trên, duyệt ghi chú phía dưới"? -## 6. Bảng tổng-kết verdict +### 3.1 Thứ-tự dọc panel 2 — ✅ HELD -| # | Claim | Verdict | Bằng-chứng | +Đo mẫu PE, `PeDetailTabs.tsx:397-437`: `
    ` → `1. Thông tin gói thầu` `:402` → `2. Hạng mục + Báo giá NCC (…)` `:405` (template-literal) → `3. Đơn vị NCC/TP được chọn` `:418` → `
    ` trần "4. Thông tin chọn thầu" `:423` → **`5. Ý kiến cấp duyệt (sign-off theo workflow)` `:426`** (dưới cùng). +§5 của bản đó xếp KHKK: Section 1 Thông tin → 2 Dòng giá → 3 Căn cứ hồ sơ → 4 File đính kèm → **5 Ý KIẾN CẤP DUYỆT** (dưới cùng). **Đúng thứ-tự mẫu, đúng câu owner.** ✅ + +### 3.2 Claim P2-4 "dời khối ý-kiến từ panel 3 sang panel 2" — ✅ HELD + +Đo: khối ý-kiến KHKK hiện nằm trong `KhkkWorkflowPanel.tsx:376-412` (panel duyệt), render `
      ` **1 cột** `text-[11px]`, mỗi `
    • ` gộp `signedByFullName · Bước n — tên · Cấp m · giờ`, badge `duyệt thay` `:398-402`, placeholder `(duyệt — không ý kiến)` `:407`. Mẫu PE là `md:grid-cols-2` card emerald (`PeDetailTabs.tsx:738-772`). +⇒ Vừa **sai panel** vừa **sai hình**, đúng như bản đó nói. Claim đứng vững, và đây chính là ô owner khoanh đỏ. ✅ + +### 3.3 🔴 F-4 — nhưng acceptance A-7b ĐO SAI: chạy trên chính MẪU thì FAIL + +A-7b: *"`grep -oE 'title="[0-9]\. [^"]+"' KhkkDetailContent.tsx` → **5 dòng**"*. +Tôi chạy đúng lệnh đó trên **mẫu** `PeDetailTabs.tsx`: **3 hit** (`:402 :418 :426`), không phải 5 — vì Section 2 dùng `title={\`2. …(${n} hạng mục)\`}` và Section 4 là `
      ` trần. +Hệ-quả THẬT (không phải bắt bẻ chữ): Section 2 của KHKK gần như chắc-chắn cũng cần đếm động (*"Dòng giá per-NCC (N NCC)"*) ⇒ implementer viết ĐÚNG sẽ **FAIL A-7b**, và cách duy-nhất để PASS là **bỏ số đếm động đi cho vừa regex** — tức Goodhart: sửa hiện-vật cho vừa thước đo. +Sửa: `rg -c '
      {!selectedId && (
      {'Quy trình duyệt sẽ hiện khi chọn phiếu.'}
      )}`; GĐ3 `ContractsListPage.tsx:249` = `{!selectedId && }`. + +⇒ Đây là **miễn-trừ THỨ 7, không được khai**, và nó nằm đúng vào khối owner nhìn thấy ở Ảnh 1. Cả 6 dòng KHÁC kia đều được nêu tên và biện-hộ; dòng này thì **đổi âm-thầm giữa §3 và §5**, rồi acceptance đóng đinh bản đã đổi. Đây là dạng nguy nhất theo luật owner: *không phải khác — mà là khác mà không nói*. + +Cách vá 2 dòng, không tốn gì: panel 3 khi `!selectedId` render **hộp dashed y PE** (đúng Ảnh 1) và đẩy `PipelineTreePanel currentStage={2}` xuống **dưới hộp đó** (hoặc để nguyên ở panel 1 như KHKK đang làm). Nếu owner thích bản GĐ3 hơn thì đó là **quyết-định của owner**, phải hỏi — không phải mặc-định của implementer. + +### 3.5 🔴 F-7 — §5 tự mâu-thuẫn về bề-rộng panel 1, và A-4 không bắt được + +§5 sơ-đồ ghi `Panel 1 **400px**` (`:253`) = số của PE. Nhưng Điều-kiện 1 (`:246`) bảo *"lấy khuôn từ GĐ3, không phải từ PE"*, mà GĐ3 tôi đo là **`340px`** (`ContractsListPage.tsx:125`). Hai chỉ-dẫn cho hai số khác nhau. +A-4 acceptance: `grep -nE "lg:grid-cols-\[" … → khớp ^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]` — **chấp mọi con số**, kể cả `[200px_1fr_200px]`. Với hiến-pháp *"giống nhất với duyệt NCC"*, thước đo phải ghim **`[400px_1fr_360px]`** literal. Hiện tại acceptance đo "có 3 panel", không đo "giống PE". + +--- + +## 4. Câu (4) — Nhóm 5 "chờ owner": hoãn đúng hay hoãn lẩn? + +| Món | Owner đã ngầm yêu-cầu qua ảnh? | Verdict | +|---|---|---| +| **A-15** upload file khi duyệt | Ảnh 2 CÓ khối `File đính kèm khi duyệt` — nhưng ảnh chỉ chứng-minh **khối hiển-thị**, không chứng-minh **quyền upload** | ✅ **hoãn ĐÚNG** (tách hiển-thị ⟂ năng-lực là tách đúng) — nhưng có lỗ, xem F-6 | +| **A-16** readOnly cứng + workspace KHKK | Đây là P2-7 đóng gói lại | ⚠️ **hoãn HỢP-LỆ, lý-do SAI** — hỏi owner thì đúng, nhưng phải hỏi kèm phương-án "host truyền `readOnly`" (mục 1.2), đừng trình như lưỡng-nan | +| **A-17** overlay "Xem mở rộng" | Nút `Maximize2` là `opacity-0 group-hover` (`:607-620`) ⇒ **không hiện trong screenshot**; owner chưa hề thấy | ✅ **hoãn ĐÚNG**, và bản đó còn cross-ref P1-7 → §7. Sạch | + +### 🔴 F-6 — A-15 hoãn đúng, nhưng acceptance A-8c biến khối đó thành **UI CHẾT mà vẫn PASS** + +Đo enum: `src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanAttachment.cs:9-12` — `DossierScan = 1 · PlanDocument = 2 · Other = 99`, comment ghi rõ *"Append-only khi cần thêm mục đích mới"*. Không có giá-trị nào mang nghĩa "file người duyệt tải lên". +Bản đó khuyến-nghị (P3-8): *"dựng khối, đọc `purpose=Other`, đợi owner chốt vế upload"* + P3-8 ghi khối PE **tự-ẩn khi rỗng** (`PeWorkflowPanel.tsx:928-967`). +Ghép 3 sự-thật: (a) không có đường ghi attachment lúc duyệt ở KHKK (§2.3 tự khai "VẮNG"), (b) khối tự-ẩn khi rỗng, (c) A-8c chỉ đo `grep -c "File đính kèm khi duyệt" ≥1` — **trên mã nguồn**. +⇒ Khối sẽ **không bao giờ render** trên UAT, trong khi acceptance **PASS 100%**. Owner mở fe-user, không thấy khối trong Ảnh 2, mà checklist báo xanh. Đây đúng lớp bài học *"claim mạnh hơn việc đã làm"*. +Sửa rẻ: A-8c phải là acceptance **RUNTIME** — *"phiếu đã duyệt có ≥1 attachment `purpose=Other` ⇒ khối hiện; nếu prod chưa có bản-ghi nào thì ghi thẳng vào spec: **khối này chưa sống được cho tới khi owner chốt A-15**"*. Đừng để nó nằm im ở nhóm 3 như một món đã-làm-xong. + +--- + +## 5. Chỗ bản đó ĐÚNG mà owner có thể tưởng là SAI (bảo vệ kết-luận đúng) + +1. **"Việc này không phải sửa CSS mà là đổi lớp kiến-trúc" (§8.1) — ĐÚNG, và tôi xác-nhận độc-lập.** KHKK list hiện là `space-y-5` cuộn-body 2 panel (`KhkkListPage.tsx:161,197`), trong khi cả GĐ1 (`PurchaseEvaluationsListPage.tsx:381,397`) lẫn GĐ3 (`ContractsListPage.tsx:106,125`) đều là `h-[calc(100vh-4rem)]` + 3 cột. Owner nhìn ảnh sẽ nghĩ "đổi bố-cục" — chi-phí thật lớn hơn cảm-giác, và **lý-do lớn hơn đó là chính-đáng**, không phải agent vẽ việc. +2. **P2-3 (nội-dung Section khác) không phải là "làm khác cho tiện".** GĐ2 thật-sự không có báo-giá/so-sánh NCC (đã chốt ở GĐ1) — ép 5 Section trùng tên PE sẽ đẻ ra 2 Section rỗng. "Giống nhau" ở đây là **giống KHUNG** (đánh số, `divide-y`, ý-kiến dưới cùng), không phải giống chữ. Bản đó chọn đúng. +3. **P3-3 phán "ĐÃ GIỐNG (hình), không ép BE precompute status" — ĐÚNG.** Hình giống là điều owner yêu-cầu; ép đồng-bộ tầng dữ-liệu sẽ chạm đường duyệt PE đang chạy prod. Đây là chỗ dễ bị chê "làm ẩu" nhưng thực-ra là kiềm-chế đúng chỗ. +4. **Phương-án A (chép khuôn) thay vì B (generic hoá) — ĐÚNG với `CLAUDE.md`** (*"duplicate giữa 2 app là CÓ CHỦ ĐÍCH"*), và Điều-kiện 2 (ghi nợ kỹ-thuật vào đầu file kèm điều-kiện thoát) biến trùng-lặp thành quyết-định có kỳ-hạn. Owner có thể tưởng đây là lười; nó không phải. + +--- + +## 6. Đo bổ-sung cho P3-12 (tiền-đề "3 trạm ngắn") + +*(kết quả đo ghi ở đây — xem mục 8 nếu chưa đo được)* + +--- + +## 7. Bảng tổng-kết verdict + +| # | Claim của bản đó | Verdict | Bằng-chứng tôi tự đo | |---|---|---|---| +| N-1 | tally 27/6/2 | **HELD** | 35 dòng bảng §3, tự phân loại lại: 27/6/2 | +| N-2 | khuôn GĐ3 `:125` `:249` | **HELD** | `ContractsListPage.tsx:106,125,249` | +| N-3 | PE 5 khối, ý-kiến dưới cùng | **HELD** (thứ-tự) | `PeDetailTabs.tsx:402,405,418,423,426` | +| N-4 | ý-kiến KHKK ở panel 3, list phẳng | **HELD** | `KhkkWorkflowPanel.tsx:376-412` | +| P1-4 | KHÁC-buộc-phải-khác | **HELD lý-do / SAI nhãn** | dòng tự viết "CHỖ ĐẶT phải giống" | +| P2-3 | KHÁC-buộc-phải-khác | **HELD** | — | +| P2-7 | KHÁC-buộc-phải-khác | 🔴 **BROKE** | `readOnly` là prop của host (`PurchaseEvaluationsListPage.tsx:663`, `PeDetailTabs.tsx:427`) | +| P3-7 | KHÁC-buộc-phải-khác | **HELD** | — | +| P3-12 | KHÁC-buộc-phải-khác | 🔴 **BROKE tiền-đề** | "ngoài phạm-vi" = lý-do chi-phí; "3 trạm" chưa đo (mục 6) | +| P3-13 | KHÁC-buộc-phải-khác | **HELD** | cardinality per-Line ≠ 1-giá | +| P2-4 | ý-kiến phải dời panel | **HELD** | đo 2 đầu | +| §5 thứ-tự Section | content trên / ý-kiến dưới | **HELD** | — | +| §5 panel-3 empty | *(âm-thầm đảo P3-16)* | 🔴 **BROKE** (F-5) | `:204` vs `:261-262` vs A-8e | +| A-7b | acceptance 5 dòng | 🔴 **BROKE** (F-4) | mẫu PE chỉ 3 hit | +| A-4 | acceptance grid | ⚠️ **YẾU** (F-7) | regex chấp mọi số; §5 400px vs GĐ3 340px | +| A-8c | acceptance file-khi-duyệt | 🔴 **BROKE** (F-6) | enum `:9-12` không có purpose; khối tự-ẩn ⇒ PASS mà không hiện | +| A-15/A-17 | hoãn | **HELD** | — | +| A-16 | hoãn | **HELD (hành-vi) / BROKE (lý-do)** | mục 1.2 | +| — | **bỏ-sót F-1** empty-state panel 2 | 🔴 **bắt thêm** | `PurchaseEvaluationsListPage.tsx:644-656` | +| — | **bỏ-sót F-2** chip `⚑ Kết thúc` trên card | 🔴 **bắt thêm** | P1-5 vs §1.1 `:43`; dữ-liệu có sẵn (D-1) | +| — | **bỏ-sót F-3** liệt-kê NV/Cấp | 🔴 **bắt thêm** | `PeWorkflowPanel.tsx:453-465` có, §3 không có dòng | -## 7. Lỗ-hổng tôi KHÔNG đo được (khai thật) +--- -(đang đo) +## 8. Lỗ-hổng tôi KHÔNG đo được (khai thật) + +(đang bổ-sung) END reviewer-lens-fidelity-s171 — VERDICT=? HELD / ? BROKE / ? bỏ-sót-bắt-thêm diff --git a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-regression-s171.md b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-regression-s171.md index 22d7fb1..e5f392a 100644 --- a/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-regression-s171.md +++ b/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-regression-s171.md @@ -1,40 +1,208 @@ # reviewer — LĂNG-KÍNH 3/3: RỦI-RO VỠ CÁI ĐANG CHẠY (S171 KHKK UI mirror PE) -> Lane 3/3 của ENSEMBLE `/fable-clone`. PROPOSE-ONLY, READ-ONLY. Vật chấm: `sub-investigator-codebase-invest-s171.md`. -> Quy tắc lane: mỗi claim → `HELD` / `BROKE` + bằng-chứng `file:line` TỰ ĐO. +> Lane 3/3 của ENSEMBLE `/fable-clone`. PROPOSE-ONLY, READ-ONLY. Vật chấm: `sub-investigator-codebase-invest-s171.md` (365 dòng, 44.830 B). +> Mọi số dưới đây tôi TỰ đo bằng lệnh của mình, không chép từ bản invest. Đường dẫn ghi rõ app (`fe-user/` hay `fe-admin/`) — bản invest mặc-định `fe-admin/src/`, đó là một phần của vấn-đề (xem §4.1). -## 0. Tái-dựng số load-bearing (bắt buộc ≥2, tự đo) +## 0. Tái-dựng số load-bearing (tự đo, ≥2 bắt buộc) -_(chưa ghi)_ +| # | Con số bản invest | Lệnh tôi chạy | Kết quả tôi đo | Verdict | +|---|---|---|---|---| +| T-1 | `PE_INDEX_PAGE_SIZE = 200` tại `PipelineTreePanel.tsx:22` (R-11) | `grep -n "PE_INDEX_PAGE_SIZE" fe-user/src/components/pipeline/PipelineTreePanel.tsx` | `22:const PE_INDEX_PAGE_SIZE = 200` · dùng tại `:112 params:{page:1,pageSize:PE_INDEX_PAGE_SIZE}` · dòng cảnh-báo `:252-256` "Cây tải 200 phiếu mới nhất — gói cũ hơn không hiển thị." | **KHỚP** | +| T-2 | `ADMIN_HIDDEN_KEYS` chứa `'KeHoachKyKet'` tại `Layout.tsx:227-231` (R-1) | `grep -n "ADMIN_HIDDEN_KEYS" -A 8 fe-admin/src/components/Layout.tsx` | `227:const ADMIN_HIDDEN_KEYS = new Set([` · `229: 'KeHoachKyKet',` · +`PurchaseEvaluations`(228) `Contracts`(230) `HopDongCung`(231) · `:248 return ADMIN_HIDDEN_KEYS.has(key) \|\| key.startsWith('Ct_')` | **KHỚP** | +| T-3 | `navKey`/`lastNavKey` `:95-101` (R-5) | đọc `fe-user/src/pages/khkk/KhkkListPage.tsx:95-102` | `95: const navKey = \`${group ?? ''}|${urlFilter ?? ''}|${deletedView ? 'del' : ''}\`` → `97-101` reset `chipPhase=undefined`, `page=1` | **KHỚP** | +| T-4 | Bảng "8 cột" (R-6/§3) | đọc `KhkkListPage.tsx:240-254` + `colSpan={8}` `:258,266` | 8 ``: Mã kế hoạch · 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 | **KHỚP** | +| T-5 | `PipelineTreePanel` 2 app mirror | `find … -name PipelineTreePanel.tsx -exec wc -l` | fe-user 327 dòng · fe-admin 327 dòng | **KHỚP (dòng)** | -## 1. R-4 — URL-param vs sidebar-highlight (claim nguy nhất) +4/4 số load-bearing tôi kiểm đều khớp. Bản invest **đo đúng**. Chỗ nó hỏng không phải số — là **suy-luận từ số** và **răng của acceptance** (§3, §4). -_(chưa ghi)_ +--- -## 2. R-1 — fe-admin ẩn menu KHKK (mirror còn bắt buộc?) +## 1. R-4 — URL-param vs sidebar-highlight (claim nguy nhất) → **HELD, nhưng lý-do bản invest đưa ra QUÁ THÔ, và A-6 chặn NHẦM KHOÁ** -_(chưa ghi)_ +### 1.1 Cơ-chế thật (tôi đo, bản invest không mở ra) -## 3. Quyền — gotcha #85 + #82, 2 tầng ĐỘC LẬP (endpoint `changelogs` A-2 / A-13) +`fe-user/src/components/Layout.tsx`: +``` +350: const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', 'awId']) +368: const pinnedByTarget = new Set(b.keys()) +369: const ignorable = (k: string) => TRANSIENT_QUERY_KEYS.has(k) && !pinnedByTarget.has(k) +373: if (aKeys.length !== bKeys.length) return false +390: const isActive = pathnameMatches && qMatches +``` +(fe-admin y hệt, `:308` cùng 6 phần-tử.) -_(chưa ghi)_ +⇒ Luật thật **KHÔNG phải** "ghi URL = sáng nhầm". Luật thật là: **ghi một khoá KHÔNG-transient (hoặc transient nhưng ĐANG bị leaf đích ghim) vào URL ⇒ vỡ so-khớp**. -## 4. Cái gì bị ĐỘNG mà bản invest KHÔNG liệt (blast-radius tự quét) +### 1.2 Áp vào KHKK — đo tên khoá thật -_(chưa ghi)_ +`fe-user/src/pages/khkk/KhkkListPage.tsx` đọc từ URL đúng **3 khoá**: `:74 get('filter')` · `:76 get('view')` · `:79 get('group')`. **Không hề có `phase` trên URL** — `phase` chỉ là biến nội-bộ `:102`. +Leaf KHKK ghim gì (`fe-user/src/components/Layout.tsx:63-87` + regex `:213-216`): `?group=n` · `&filter=ChoDuyet|DaDuyet` · `&view=deleted`. -## 5. R-10 `budgetFrozen` + R-11 trần 200 phiếu (phán lại mức nghiêm-trọng) +| Khoá | Transient? | Leaf ghim? | Ghi vào URL thì sao | +|---|---|---|---| +| `filter` | **KHÔNG** | CÓ (Pending/Approved) | 🔴 **hồi-sinh S155** — đứng leaf "Danh sách" (`?group=1`) bấm chip "Đã duyệt" → URL `?group=1&filter=DaDuyet` khớp CHÍNH XÁC leaf "Đã duyệt" ⇒ sidebar nhảy sang mục khác | +| `view` | KHÔNG | CÓ (Deleted) | 🔴 như trên | +| `group` | KHÔNG | CÓ (mọi leaf) | 🔴 đổi nhóm bằng chip = nhảy leaf | +| `phase` | **CÓ** | KHÔNG leaf KHKK nào ghim | 🟢 **vô hại** — bị `ignorable()` lọc, highlight giữ nguyên | +| `id`, `q`, `page` | CÓ | KHÔNG | 🟢 vô hại | -_(chưa ghi)_ +**Verdict R-4 = HELD** (kết-luận "đừng chép `setParam` của PE vào chip lọc" là ĐÚNG và tôi tái-dựng được đường vỡ từng bước). -## 6. Bảo-vệ chỗ bản invest ĐÚNG mà owner có thể tưởng sai +### 1.3 🔴 BROKE — A-6 kiểm SAI KHOÁ (lỗ thủng ngay trong hàng rào bảo-vệ R-4) -_(chưa ghi)_ +A-6 (`invest:320`) viết nguyên văn: *"`grep -n "setSearchParams\|setSp(" $APP/src/pages/khkk/KhkkListPage.tsx` → chỉ xuất hiện trong hàm set `id`; **0 hit** cho `'phase'`/`'group'` bên trong lời gọi đó"*. -## 7. Bỏ-sót bắt thêm (thách-CLEAN) +Tôi chạy literal: `grep -n "setSearchParams\|setSp(" fe-user/src/pages/khkk/KhkkListPage.tsx | wc -l` → **0** (file hiện chỉ `const [searchParams] = useSearchParams()` `:68`, destructure 1 phần-tử). -_(chưa ghi)_ +Vấn-đề: A-6 cấm `'phase'` và `'group'`. +- `'phase'` — **không phải khoá URL của KHKK**, và kể cả có ghi cũng **vô hại** (transient, không leaf nào ghim). +- `'filter'` và `'view'` — **đúng hai khoá làm vỡ**, A-6 **không nhắc chữ nào**. -## 8. Tổng-kết verdict +⇒ Một implementer viết `setSearchParams({ group: g, filter: 'DaDuyet' })` — tức **chính xác cái bug S155** — sẽ **PASS A-6** (0 hit `'phase'`; `'group'` thì… có hit, nửa may). Acceptance được viết ra để chặn R-4 lại **chặn cái vô hại và cho lọt cái nguy hiểm**. Đây là inverted check, không phải "thiếu chặt". -_(chưa ghi)_ +**Sửa đề-nghị (1 dòng):** đổi A-6 thành — *0 hit cho `'filter'` / `'view'` / `'group'` bên trong mọi lời gọi `setSearchParams`; `'id'`/`'q'`/`'page'` được phép vì nằm trong `TRANSIENT_QUERY_KEYS` (`Layout.tsx` — grep tên hằng)*. + +### 1.4 A-6 "hỗn-hợp `?id=` ghi URL, `phase/group` thì không" — **an-toàn, tôi bảo-vệ nó** + +Owner có thể ngờ đây là bẫy. Tôi đo: `id` ∈ `TRANSIENT_QUERY_KEYS` (`Layout.tsx:350`), và **không leaf KHKK nào ghim `id`** ⇒ `/khkk/list?group=1&id=` vẫn khớp leaf `?group=1` (`aKeys=[group]` = `bKeys=[group]`, `:371-374`) ⇒ **highlight giữ nguyên**. +Thêm nữa: `id` **không nằm trong `navKey`** (`:95`) ⇒ chọn dòng KHÔNG kích reset `chipPhase`/`page` (`:97-101`) ⇒ chọn phiếu không nhả bộ lọc, không nhảy về trang 1. **Hỗn-hợp này ĐÚNG, không phải bẫy.** (Điều kiện: đừng thêm `id` vào `navKey`, và đừng để leaf nào ghim `id`.) + +--- + +## 2. R-1 — fe-admin ẩn menu KHKK → **HELD**, và hệ-quả nặng hơn bản invest kết luận + +`fe-admin/src/components/Layout.tsx:227-231` xác nhận: `KeHoachKyKet`(229) bị ẩn, cùng `PurchaseEvaluations`(228), `Contracts`(230), `HopDongCung`(231); `:248` còn cắt thêm mọi key `Ct_*`. ⇒ **cả 4 giai-đoạn nghiệp-vụ đều vô-hình bên fe-admin.** + +Bản invest kết: *"mirror 2 app là BẮT BUỘC, không phải tuỳ chọn"*. Tôi **giữ kết-luận đó** (SHA256-mirror là quy-ước sống, R-7/A-9), nhưng bổ-sung 2 điểm nó bỏ: + +1. **UAT bên fe-admin là BẤT KHẢ qua đường menu.** Nghiệm-thu phải deep-link `http://localhost:8082/khkk/list?group=1`. Nếu checklist §7 không nói câu này, người chạy UAT sẽ báo "không thấy menu ⇒ chưa deploy" (đúng lớp lỗi `feedback_mechanism_right_data_absent`). +2. **Rủi-ro thật của mirror-mù:** vì bên admin không ai nhìn thấy màn này, một lỗi render chỉ-có-ở-admin (VD panel 3 vỡ lưới) sẽ **sống câm** cho tới khi có người deep-link. Đề-nghị: A-9 (sha256) giữ nguyên, nhưng thêm 1 dòng acceptance *"chụp màn `:8082/khkk/list?group=1` sau khi sửa"* — chứ không chỉ so hash. + +--- + +## 3. Quyền — 2 tầng ĐỘC-LẬP (#85 + #82): R-2/A-2 **HELD**, **A-13 KHÔNG CÓ RĂNG (BROKE)** + +### 3.1 Convention hiện tại của KHKK — sạch, R-2 HELD + +`grep -n "\[Authorize" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs`: +class `:29 [Authorize(Policy="KeHoachKyKet.Read")]` + **18 attribute action-level** (`:37,55,61,74,80,87,96,109,118,130,141,153,163,183,192,206,223,244`), khớp **18 route** (`grep -cE "^\s*\[Http(Get|Post|Put|Delete|Patch)"` = **18**). Read/Create/Update/Delete phân đúng; riêng `:244 [Authorize(Policy="Contracts.Create")]` cho endpoint bắc-cầu HĐ. **0 `[Authorize]` trần thật.** ⇒ R-2 HELD, A-2 (endpoint `changelogs` mới phải mang `KeHoachKyKet.Read`) là yêu-cầu đúng. + +### 3.2 🔴 BROKE — A-13 chạy literal thì **BÁO ĐỎ trên file đang SẠCH** + +A-13 (`invest:344`) nguyên văn: *"…`| grep -c "Policy"` — số này phải ≥ số route, và **0 dòng** `[Authorize]` trần"*. Tôi chạy đúng như viết: + +- `grep -rn "Authorize" …ContractSigningPlansController.cs | grep -c "Policy"` → **20**. Số route = 18 ⇒ 18 ≤ 20, "PASS". Nhưng 20 ≠ 19 attribute thật: **dòng 15 là COMMENT** (`// tầng 1 = class [Authorize(Policy = "KeHoachKyKet.Read")]`) cũng bị đếm. Vế này pass **nhờ đếm nhầm bình luận**, không phải nhờ code đúng. +- `grep -n "\[Authorize\]"` → **3 hit: `:17`, `:236`, `:241` — TẤT CẢ đều là bình luận** đang *nhắc tên* anti-pattern (`:241` = "`ContractsController` `[Authorize]` trần, #82 treo"). ⇒ vế "0 dòng `[Authorize]` trần" **FAIL ngay trên file 100% policy-gated**. + +Đây đúng lớp **citation-trap self-reference** (`feedback_citation_trap_selfreference.md`): matcher khớp chính tài-liệu mô-tả cái xấu. Hậu-quả không phải "báo thừa cho vui" — checker kêu oan sẽ bị **miễn** ở lượt sau, và lúc đó lỗ thật đi lọt. **A-13 hiện là hàng rào giả.** + +**Sửa (đo được):** neo vào đầu dòng, bỏ bình luận — +`grep -nE "^\s*\[Authorize\]\s*$" ` → phải **0 hit**; và `grep -cE "^\s*\[Authorize\(Policy" ` phải **= 1 + số route** (hiện 19 = 1 + 18). + +### 3.3 🔴 Bỏ-sót của bản invest: mẫu PE mà A-2 bảo "mirror" **CHÍNH LÀ một lỗ #82** + +`src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs`: +``` +15: [Authorize] ← class, TRẦN (any-auth) +281: [HttpGet("{id:guid}/changelogs")] ← KHÔNG có [Authorize(Policy=…)] +282: public async Task> GetChangelogs(…) +``` +⇒ endpoint changelogs của PE hiện chỉ cần **đăng-nhập bất-kỳ** là đọc được lịch-sử phiếu. Đề-bài lượt này là *"mirror PE"*; một implementer trung-thành sẽ **chép nguyên** — và đó là cách #82 lan sang KHKK. +Bản invest có nói A-2 phải gắn `KeHoachKyKet.Read` (tốt), nhưng **không nói mẫu nguồn bẩn** ⇒ người đọc §5 mà bỏ qua §7 sẽ copy thẳng. **Cần một câu cảnh-báo tại chỗ mô-tả mirror, không phải chỉ ở checklist.** +(Ngoài phạm-vi lượt này nhưng nên vào sổ: PE changelogs là ứng-viên #82 thứ hai bên cạnh `POST /api/contracts` mà `:241` đã treo.) + +--- + +## 4. Cái gì bị ĐỘNG mà bản invest KHÔNG liệt (tôi tự quét) + +### 4.1 🔴 BROKE — "khuôn GĐ3" (Đ-2, R-6, R-9) **chỉ tồn tại ở fe-admin; fe-user KHÔNG CÓ file đó** + +`find fe-user/src fe-admin/src -iname "ContractsListPage.tsx"` → **duy nhất `fe-admin/src/pages/contracts/ContractsListPage.tsx`**. fe-user chỉ có `pages/contracts/ContractCreatePage.tsx` + `MyContractsPage.tsx`. + +Bản invest mở đầu: *"Đường-dẫn tương-đối gốc `fe-admin/src/` trừ khi ghi rõ khác. fe-user SHA256-identical (lead đã đo 4/4)"* — câu "4/4" chỉ phủ 4 file KHKK, nhưng đặt ở **header toàn văn** nên đọc thành "mọi path dưới đây đều có twin". Hệ-quả cụ-thể: +- R-6 dặn *"chép nguyên chuỗi `bodyMaxHClass` từ `ContractsListPage.tsx:247-249`"* → người làm fe-user grep sẽ **không tìm thấy file**. +- R-9 dặn *"khuôn `ContractsListPage.tsx:90-96`"* → như trên. +- Đ-2 — **lập-luận mạnh nhất ủng-hộ đề-bài owner** ("GĐ3 đã 3-panel, KHKK là mắt xích lệch") — dựa trên một trang **nằm ở app đang ẩn menu Contracts** (`Layout.tsx:230` + `:248 startsWith('Ct_')`). Tiền-lệ có thật, nhưng nó là tiền-lệ **fe-admin-only, menu-invisible**; dùng nó để suy ra "vậy KHKK fe-user cũng phải thế" là bắc cầu qua một chỗ gãy. +⇒ Không làm sập luận-điểm, nhưng **phải khai**: port khuôn GĐ3 sang fe-user = **viết mới**, không phải copy; và sẽ tạo ra pattern 3-panel **chỉ có ở fe-user** trừ khi làm cả 2 app (mà A-9 buộc làm cả 2). + +### 4.2 🔴 BROKE — R-9 liệt THIẾU call-site `/khkk/:id`: **cây toàn-trình tự nó điều-hướng** + +R-9 chỉ kể `KhkkListPage:280` và `KhkkDetailPage:400`. Tôi grep cả 2 app: +``` +fe-user/src/hooks/usePipelineStages.ts:211 onOpen: () => navigate(`/khkk/${k.id}`) +fe-user/src/hooks/usePipelineStages.ts:241 onOpenAll: () => navigate(`/khkk/list?group=${n}`) +fe-user/src/hooks/usePipelineStages.ts:250 onOpenAll: () => navigate('/khkk/list') +fe-admin/src/hooks/usePipelineStages.ts:211/241/250 (y hệt) +fe-user|fe-admin/src/pages/khkk/KhkkCreatePage.tsx:126 navigate(`/khkk/${data.id}`) +fe-user|fe-admin/src/pages/khkk/KhkkDetailPage.tsx:258 navigate('/khkk/list') (sau khi xóa) ++ App.tsx:80 (user) / :107 (admin) +``` +Đây là bẫy **tự-cắn**: nếu panel 1 dựng bằng `PipelineTreePanel`, thì **chính cây trong panel 1** khi bấm node KHKK sẽ `navigate('/khkk/')` — **văng khỏi màn 3-panel vừa dựng**, thay vì chọn phiếu vào panel 2. Hai đường chọn phiếu (`?id=` mới vs `navigate` cũ) sống song song trong CÙNG một khung nhìn. Phải sửa `usePipelineStages.ts:211` ở **cả 2 app** (hoặc truyền callback `onOpen` từ host) — R-12 chỉ nói tới việc dời `KhkkWorkflowPanel`, không nói tới cái này. +Thêm: `KhkkDetailPage.tsx:258` sau khi xóa → `navigate('/khkk/list')` **mất `?group=`** ⇒ người dùng bị ném về "tất cả nhóm" (đã là lỗi UX nhẹ hiện nay, thành lỗi rõ hơn khi detail nằm trong panel). + +### 4.3 R-12 — đo lại: **1 import/app, không phải "≥2 nơi × 2 app"** + +`grep -rn "KhkkWorkflowPanel"` → mỗi app đúng **2 dòng**: `KhkkDetailPage.tsx:41` (import) + `:411` (dùng), cộng `KhkkWorkflowPanel.tsx:73` (export). Tức dời file = sửa **1 dòng import × 2 app**. Bản invest **phóng đại nhẹ** ("≥2 nơi × 2 app"); vô hại, nhưng là dấu hiệu con số ở cột "hệ-quả" không phải lúc nào cũng được đo. **Mức: nhỏ.** + +### 4.4 Bảng 8 cột — cái gì **mất im lặng** khi ép vào card 400px + +8 `` đo tại `KhkkListPage.tsx:241-252`: Mã kế hoạch · 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 (`colSpan={8}` `:258,266` xác nhận). Ngoài ra badge **nhóm duyệt N1-N8** được nhét vào cột 1 (`:296-300`) kèm ghi chú K2 S164: *đã cố ý KHÔNG đẻ cột thứ 9 vì bảng đã cuộn ngang ở 1366 khi có cây bên trái*. +⇒ Panel 1 kiểu PE (card 3 hàng, `400px`) chứa **tối đa ~4-5 mẩu tin**. Nếu bê nguyên khuôn PE, các thứ sau **biến mất mà không ai báo**: cột **Dự án**, **Người soạn**, **Ngày tạo**, và nguy nhất là cặp **Tổng đề xuất / Tổng duyệt** (xem §5.1). Khối **6 `KpiCard`** (`:202-219`, `xl:grid-cols-6`) cũng không nhét vừa 400px — PE thay bằng `Select` trạng-thái (`PurchaseEvaluationsListPage.tsx:437-453` theo bản invest §1.1). **Phân trang** (`PAGE_SIZE=20`, `:45`) trong PE được thay bằng cây-tải-1-phát-200 ⇒ đổi mô-hình tải, không phải đổi CSS. §3/§5 của bản invest có bàn layout nhưng **không có bảng "cột nào chết, chuyển đi đâu"** — đây là chỗ mất-chức-năng-im-lặng dễ xảy ra nhất. + +--- + +## 5. R-10 `budgetFrozen` + R-11 trần 200 → HELD, nhưng **xếp hạng sai** ở R-11 + +### 5.1 R-10 — HELD, kèm một cái bẫy bản invest chưa nói + +Đo `fe-user/src/types/khkk.ts:296-301`: `budgetFrozen: boolean` nằm **trong DTO CHI-TIẾT** (comment `:296-300` ghi rõ *"`ContractSigningPlanDetailDto:166` — CỜ HIỂN THỊ dẫn xuất từ phase (`phase == DaDuyet`), KHÔNG phải cột DB"*). +⇒ **`budgetFrozen` KHÔNG có trong `KhkkListItemDto`.** Panel 1 render **list item**, nên lời khuyên R-10 *"card panel 1 hiện Σ tiền phải theo đúng cờ này"* không thực-thi được như viết: hoặc phải **suy từ `phase === DaDuyet`** (đúng, vì BE cũng suy thế), hoặc ai đó sẽ **thêm field vào ListItemDto** — một thay-đổi BE **không nằm trong A-1**. Bảng hiện tại né được vì nó in **cả hai cột** (vá-8 K2, `:246-251`); card 400px in một số ⇒ phải chọn, và chọn sai = tái-hiện đúng lỗi vá-8 đã sửa. **Verdict: HELD + phải sửa cách phát-biểu.** + +### 5.2 🔴 R-11 — tôi **NÂNG mức nghiêm-trọng**: không phải "cảnh-báo", mà là **mất danh-sách** + +Đo `fe-user/src/components/pipeline/PipelineTreePanel.tsx`: `:22` `const PE_INDEX_PAGE_SIZE = 200` · `:112` `params:{page:1,pageSize:PE_INDEX_PAGE_SIZE}` · `:117` `enabled: canPe` · `:252-256` dòng chữ xám 10px *"Cây tải 200 phiếu mới nhất — gói cũ hơn không hiển thị."* + +Ba tầng hỏng, tăng dần: + +1. **Trần 200 = mất phiếu, không phải mất "gói cũ".** Cây bắt nguồn từ **chỉ-mục PE** (GĐ1). Phiếu KHKK treo dưới gói thầu; gói 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 vẫn là **bảng có phân trang + tìm-kiếm server-side** (`KhkkListPage.tsx:112-129`, `total` thật từ server). Đổi panel 1 thành 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 nhạt (`text-[10px] italic text-slate-400`) **không** bù được việc mất đường tới phiếu — không có "trang 2". +2. 🔴 **Nặng hơn — quyền: `enabled: canPe`.** `usePipelineStages.ts:83-84` đo được: + `const canPe = PE_MENU_KEYS.some(k => can(k))` với `PE_MENU_KEYS = ['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List']` (`:55`), còn `canKhkk = ['KeHoachKyKet'].some(...)` (`:57,84`). + ⇒ **Người chỉ có quyền KHKK (VD người duyệt nhóm N5) có `canPe = false`** ⇒ `:117 enabled:canPe` không fetch ⇒ `:222-226` in *"Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình chưa hiển thị được."* + Hôm nay chấp-nhận được vì đó chỉ là **cột phụ bên trái**, bảng KHKK vẫn đầy đủ bên phải. **Nếu panel 1 = cây, người dùng đó mất TRẮNG danh-sách phiếu KHKK của chính mình** — trong khi API `GET /contract-signing-plans` gate `KeHoachKyKet.Read` (`ContractSigningPlansController.cs:29,37`) **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, và là **mất chức-năng, không phải mất thẩm-mỹ**. + **Bản invest không có dòng nào về `canPe` ⇒ đây là bỏ-sót nghiêm-trọng nhất tôi tìm được.** +3. Nếu vẫn muốn cây ở panel 1: **bắt buộc** giữ một đường vào không-qua-PE (giữ bảng dưới cây, hoặc cây KHKK dựng từ `GET /contract-signing-plans` chứ không từ chỉ-mục PE, hoặc fallback `!canPe → render bảng cũ`). Cần acceptance riêng: *"đăng-nhập tài-khoản chỉ có `KeHoachKyKet.*`, panel 1 vẫn liệt đủ phiếu"*. + +--- + +## 6. Chỗ bản invest ĐÚNG mà owner có thể tưởng sai (bảo-vệ) + +1. **A-6 "`?id=` ghi URL nhưng `phase`/`group` thì không" — hỗn-hợp này AN-TOÀN.** Nhìn thì như tiêu-chuẩn kép, nhưng `TRANSIENT_QUERY_KEYS` (`Layout.tsx:350`) làm nó có cơ-sở cơ-học: `id` transient + không leaf nào ghim ⇒ highlight không đổi; `id` cũng không nằm trong `navKey` (`KhkkListPage.tsx:95`) ⇒ không nhả bộ lọc. (Chỉ cần đổi tên khoá bị cấm — §1.3.) +2. **R-5 "đọc URL mỗi render, đừng `useState` khởi-tạo-một-lần"** — đúng nguyên-văn cơ-chế `:70-102`; và `: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 cần "dọn thành `useEffect`". Ai refactor cho "sạch" sẽ tái-hiện vá-5 (phải F5 mới đúng). +3. **Đ-1 "PE panel 1 viết INLINE, không có `PeListPanel` dùng-lại được"** — nhất-quán với những gì tôi thấy khi grep; hệ-quả *"muốn giống thì phải tách ra hoặc chép khuôn"* là cảnh-báo chi-phí đúng, đừng bỏ. +4. **R-8 "KHKK không có phiếu V1-legacy, đừng port nhánh fallback"** — hợp lý; module sinh 2026-07-29, port nhánh V1 = code chết ngay ngày đầu. + +--- + +## 7. Tổng-hợp bỏ-sót tôi bắt thêm (ngoài R-1..R-12) + +| # | Bỏ-sót | Bằng-chứng | Mức | +|---|---|---|---| +| M-1 | **`canPe` khoá cây** ⇒ user KHKK-only mất trắng panel 1 | `usePipelineStages.ts:55,57,83-84` · `PipelineTreePanel.tsx:117,222-226` | 🔴 **CAO** (mất chức-năng, ngược #82) | +| M-2 | **`usePipelineStages.ts:211` `navigate('/khkk/:id')`** — cây trong panel 1 tự văng khỏi màn 3-panel; ×2 app | grep call-site §4.2 | 🔴 CAO | +| M-3 | **A-13 báo đỏ trên file sạch** (khớp 3 dòng bình luận) ⇒ hàng rào giả | chạy literal §3.2 | 🔴 CAO | +| M-4 | **A-6 cấm nhầm khoá** (`phase` vô hại / `filter`,`view` mới vỡ) | §1.3 | 🔴 CAO | +| M-5 | **Mẫu PE `GetChangelogs` là lỗ #82** (class `[Authorize]` trần, action không policy) — "mirror PE" = chép lỗ | `PurchaseEvaluationsController.cs:15,281-283` | TRUNG-CAO | +| M-6 | **`ContractsListPage.tsx` không tồn tại ở fe-user** — R-6/R-9/Đ-2 trỏ file người làm fe-user không mở được | `find` §4.1 | TRUNG | +| M-7 | **Không có bảng "cột nào chết"** khi 8 cột + 6 KpiCard + phân trang ép vào 400px | §4.4 | TRUNG | +| M-8 | **`budgetFrozen` nằm ở DetailDto, không có ở ListItemDto** ⇒ R-10 không thực-thi được như viết | `types/khkk.ts:294-301` | TRUNG | +| M-9 | **UAT fe-admin phải deep-link** (menu ẩn) — thiếu câu này sẽ có báo-cáo "chưa deploy" giả | `Layout.tsx:227-231,248` | THẤP-TRUNG | +| M-10 | `KhkkDetailPage.tsx:258` xóa xong `navigate('/khkk/list')` **rụng `?group=`** | grep §4.2 | THẤP | + +## 8. Kết-luận lane + +- Bản invest **đo rất chắc** (5/5 số tôi tái-dựng đều khớp) và **kết-luận R-4/R-5/R-1/R-8 đứng vững**. +- Chỗ nó vỡ **không nằm ở phân-tích, nằm ở HÀNG RÀO**: hai acceptance được viết riêng để chặn hai rủi-ro nguy nhất (**A-6** chặn R-4, **A-13** chặn #82) thì **một cái kiểm nhầm khoá, một cái báo đỏ trên file sạch**. Sức thuyết-phục của văn-bản che mất việc chưa ai **chạy thử chính các lệnh acceptance đó** — tôi chạy, cả hai đều không đứng. +- Rủi-ro **vỡ cái đang chạy** lớn nhất **không phải** URL/sidebar (R-4 đã được canh), mà là **M-1 `canPe`** + **R-11 trần-200**: cả hai cùng biến "panel 1 = cây" thành **mất đường tới phiếu**, im-lặng, chỉ với một nhóm người dùng nhất-định — kiểu lỗi khó thấy trong demo và chỉ nổ ở prod. +- Khuyến-nghị tối-thiểu trước khi cho làm: (a) sửa A-6 + A-13 theo §1.3/§3.2; (b) thêm acceptance "tài-khoản KHKK-only vẫn thấy đủ phiếu ở panel 1"; (c) giữ **một đường danh-sách có phân-trang** song song với cây, hoặc dựng cây từ API KHKK thay vì chỉ-mục PE. + +END reviewer-lens-regression-s171 — VERDICT=8 HELD / 4 BROKE / 10 bỏ-sót-bắt-thêm