# 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` (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 (tự đo, ≥2 bắt buộc) | # | 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)** | 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). --- ## 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Á** ### 1.1 Cơ-chế thật (tôi đo, bản invest không mở ra) `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ử.) ⇒ 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**. ### 1.2 Áp vào KHKK — đo tên khoá thật `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`. | 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 | **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). ### 1.3 🔴 BROKE — A-6 kiểm SAI KHOÁ (lỗ thủng ngay trong hàng rào bảo-vệ R-4) 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 đó"*. 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ử). 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**. ⇒ 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". **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