Files
solution-erp/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-regression-s171.md
2026-08-04 18:53:50 +07:00

24 KiB
Raw Blame History

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<string>([ · 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 ?? ''}
T-4 Bảng "8 cột" (R-6/§3) đọc KhkkListPage.tsx:240-254 + colSpan={8} :258,266 8 <th>: 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ố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 URLphase 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 KHÔNG leaf KHKK nào ghim 🟢 vô hại — bị ignorable() lọc, highlight giữ nguyên
id, q, page 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 -l0 (file hiện chỉ const [searchParams] = useSearchParams() :68, destructure 1 phần-tử).

Vấn-đề: A-6 cấm 'phase''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''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: idTRANSIENT_QUERY_KEYS (Layout.tsx:350), và không leaf KHKK nào ghim id/khkk/list?group=1&id=<guid> 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*$" <file> → phải 0 hit; và grep -cE "^\s*\[Authorize\(Policy" <file> 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<List<PurchaseEvaluationChangelogDto>> 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: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:280KhkkDetailPage: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)  <Route path="/khkk" → Navigate /khkk/list>

Đâ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/<id>')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:211cả 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 <th> đ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-101set-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