# reviewer — LĂNG-KÍNH 2/3: KHẢ-THI KỸ-THUẬT + SỰ-THẬT DỮ-LIỆU

> 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 — tôi làm 4)

| # | 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?

### 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

**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)

### 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<peId, KhkkListItemDto[]>` (`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

### 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?

### 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?

### 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))' <file> | wc -l      # R = số route
  grep -nE '^\s*\[Authorize\(Policy'          <file> | wc -l      # P
  grep -nE '^\s*\[Authorize\]\s*$'            <file> | 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 "<cụm tiếng Việt>" ≥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)

**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

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 | 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=8 HELD / 6 BROKE / 6 bỏ-sót-bắt-thêm
