REVIEW-F1: PASS-WITH-FLAGS 7 — 3 MAJOR (G-1 default ngược PE-live · G-2 sơ đồ nói CEO "đã duyệt" · G-6 cây phình 5→7 file, K4a bị commit ké) / 4 MINOR; 0 blocker kỹ-thuật trên 5 file F-1, 1 blocker quy-trình (G-6: tách commit trước khi push)

# Reviewer F-1 — ô-tích opt-out `applyLevelFinalize` panel duyệt KHKK

Ghi-đĩa TRONG LÚC SOI (chống #53). Đo từ ĐĨA, HEAD `24ea71e`, diff CHƯA commit.
Phạm vi: `git diff -- src fe-user fe-admin` = **5 file M, +137 / −14**.

## §0 Mirror 2 app — chứng bằng blob-SHA của git (KHÔNG tin lời khai)

`git diff` in ra index-line của cả 2 app:

| Cặp file | blob TRƯỚC | blob SAU |
|---|---|---|
| `fe-{admin,user}/src/pages/khkk/KhkkWorkflowPanel.tsx` | `e2fa3bc` (cả hai) | `6913182` (cả hai) |
| `fe-{admin,user}/src/types/khkk.ts` | `a99658c` (cả hai) | `1ffd79b` (cả hai) |

⇒ 2 cặp **byte-identical cả trước lẫn sau** (git blob = sha1 nội dung) — mirror §3.9 ĐẠT, và
không có nhánh "sửa 1 app quên app kia". ✅

## §1 Ranh giới BE — cờ có tới FE bằng ĐÚNG một đường

- `ContractSigningPlanWorkflowLevelDto` (`ContractSigningPlanFeatures.cs:116`) +1 positional
  `bool AllowApproverFinalize`; **construction site = 1** (`:640`, grep toàn `src/Backend` cho
  đúng 3 hit: `:116` def · `:132` kiểu List · `:640` new) ⇒ không có site thứ 2 lỡ truyền `false` cứng.
- `l.AllowApproverFinalize` đọc thẳng từ `ApprovalWorkflowLevel` của **đúng workflow đã pin**
  (cùng bảng, cùng row mà `ResolveActingLevel` sẽ chấm lúc duyệt) ⇒ không có nguồn thứ hai.
- 0 đổi logic BE: `git diff` trên `src` chỉ 2 hunk, cả 2 nằm trong DTO + projection. ✅

## §2 (đề bài 1) `actingLevel` FE có mirror ĐÚNG `ResolveActingLevel` không

BE (`ContractSigningPlanWorkflowService.cs:463-468`):
`own = pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId)` → `if (own is not null) return own;`
→ `if (isAdmin) return pendingLevelGroup.First();` → còn lại **ném Forbidden**.
FE (`KhkkWorkflowPanel.tsx:135-137`):
`currentLevels.find(l => l.approverUserId === user?.id) ?? (isAdmin ? currentLevels[0] : undefined)`.

| Ca | BE chấm level nào | FE chấm level nào | Khớp? |
|---|---|---|---|
| Actor ∈ Cấp, cấp CÓ cờ | own (có cờ) | own (có cờ) → hiện ô-tích | ✅ |
| Actor ∈ Cấp OR-of-N, **cờ ở NGƯỜI KHÁC cùng Cấp** | own (KHÔNG cờ) ⇒ không finalize | own (KHÔNG cờ) ⇒ ẩn ô-tích, gửi `true` = no-op | ✅ (ca đề bài hỏi kỹ — không rò) |
| Admin **có** slot riêng trong Cấp | own | own (vì `find` chạy TRƯỚC nhánh isAdmin) | ✅ |
| Admin **không** có slot | `pendingLevelGroup.First()` | `currentLevels[0]` | ⚠️ xem §6 (thứ tự 2 truy vấn) |
| Không phải approver, không Admin | **Forbidden 403** | `undefined` ⇒ ẩn ô-tích | ✅ (và nút Duyệt đã `disabled` bởi `blockedByLevel` `:130`) |
| `currentLevels` **RỖNG** (phase ≠ ChoDuyet, hoặc con-trỏ null, hoặc idx ngoài biên) | không tới `ApproveV2Async` (409 `:250-251`) | `find`→undefined, `currentLevels[0]`→`undefined` ⇒ eligible=false | ✅ **không nổ** (index-out-of-range trên mảng rỗng trong JS = `undefined`, không throw) |
| `user` chưa nạp (`user?.id` undefined) | — | `find` không khớp (`approverUserId` là `string` non-null) ⇒ undefined; `isAdmin`=false ⇒ nút Duyệt disabled | ✅ |

Vế **so-khớp GUID dạng chuỗi**: `user.id` sinh từ `res.data.user` (`AuthContext.tsx:53-56`, DTO login
serialize `Guid` → chữ thường có gạch), `approverUserId` cũng là `Guid` serialize cùng kiểu ⇒ `===`
hợp lệ; và đây là **cùng phép so đã sống từ W3** (`:127` `actorIsCurrentApprover`), không phải phép mới.

⇒ **Mirror ĐÚNG 6/7 ca**; ca thứ 7 (Admin duyệt-thay trên Cấp **trộn cờ**) là finding §6/G-4.

## §3 (đề bài 2) Body — rò field ở action khác? khớp hợp-đồng `bool?`?

`KhkkWorkflowPanel.tsx:151-157`:
`{ action, comment, ...(a === Approve ? { applyLevelFinalize: eligible ? state : true } : {}) }`

| Action | Key có trong JSON? | Giá trị | BE nhận | Ảnh hưởng |
|---|---|---|---|---|
| `submit` | **KHÔNG** | — | `ApplyLevelFinalize = null` → `?? true` (`Controller:213`) | `SubmitAsync` **không nhận tham số** (`Service:122`) ⇒ vô hại |
| `return` / `reject` | **KHÔNG** | — | như trên | `ReturnOrRejectAsync` **không nhận tham số** (`:130-136`) ⇒ vô hại |
| `approve`, cấp thường | CÓ | `true` | `true` | `actingLevel.AllowApproverFinalize && true` = **false** ⇒ no-op (§4) |
| `approve`, cấp có cờ, giữ tick | CÓ | `true` | `true` | finalize (đúng ý người bấm) |
| `approve`, cấp có cờ, bỏ tick | CÓ | `false` | `false` | rơi xuống advance thường ⇒ trình tiếp (đúng mục tiêu F-1) |

⇒ **0 rò field** sang action khác; và ngay cả khi rò cũng không có chỗ đọc ở BE (3 nhánh kia không có tham số).

**absent vs null vs true** — hợp-đồng `ContractSigningPlanTransitionBody(string Action, string? Comment = null, bool? ApplyLevelFinalize = null)`:
key vắng ⇒ System.Text.Json để `bool?` = `null` (bằng ĐÚNG giá trị mặc định khai báo, nên **không phụ thuộc**
vào chuyện STJ có tôn trọng default-parameter-value của positional record hay không — cả hai đường đều ra `null`)
⇒ `?? true`. FE **không bao giờ** gửi `null` tường minh (spread bỏ hẳn key, không set `undefined`).
⇒ 3 trạng thái absent/null/true hội tụ về CÙNG một hành vi. ✅ Khớp hợp-đồng.

## §4 (đề bài 3) Regression cấp thường + rò ô-tích ở phase khác

**Cấp thường (không cờ), trước ⟂ sau vá:**
- trước: body `{action, comment}` ⇒ BE `?? true` ⇒ `applyLevelFinalize=true`
- sau: body `{action, comment, applyLevelFinalize: true}` ⇒ BE `true`
⇒ **cùng một giá trị đi vào cùng một biểu thức** `if (actingLevel.AllowApproverFinalize && applyLevelFinalize)`
(`Service:276`), mà vế trái = `false` ⇒ nhánh không vào ở CẢ HAI thế giới. Hành vi **y hệt**, không phải
"giống về mặt cảm tính". ✅
(Điểm yếu còn lại là của thiết kế BE, không phải của diff: no-op này chỉ đúng vì BE **AND** với cờ cấp —
FE gửi `true` cho người không có quyền finalize là dữ liệu thừa, không phải quyền thừa.)

**Rò ô-tích ở phase khác — 3 lớp chặn ĐỘC LẬP, hỏng 1 lớp vẫn không rò:**
1. `currentLevels` chỉ khác `[]` khi `isWaiting` (`:118-121`) ⇒ DaDuyet/TraLai/TuChoi/Nháp ⇒ `actingLevel=undefined` ⇒ `approverFinalizeEligible=false`.
2. Nút "Duyệt" (đường DUY NHẤT đặt `action=Approve`) chỉ render trong `{isWaiting && (…)}` (`:312-335`).
3. Chính JSX ô-tích đòi `action === Approve` (`:443`).
⇒ **0 rò**. Kiểm chéo: `KhkkTransitionAction.Approve` được gán ở đúng 1 site (`:323`), grep 2 app cho thấy
không có site thứ hai. ✅

**Reset trạng thái:** `setApplyLevelFinalize(true)` đặt ở `onClick` mở dialog (`:322`) ⇒ bỏ tick rồi **Huỷ**
rồi mở lại vẫn về mặc định. (PE chỉ reset trong `onSuccess` `:288` ⇒ PE **giữ** tick sau khi Huỷ — chỗ này
KHKK **chặt hơn** khuôn.) ✅

## §5 (đề bài 4) Khuôn-PE fidelity — lệch CỐ Ý vs SÓT

| # | Trục | PE (live) | KHKK (diff) | Phán |
|---|---|---|---|---|
| a | JSX ô-tích | `:772-791` | `:443-462` — cùng khung, cùng nhãn "Cấp này KẾT THÚC quy trình" | ✅ trung thành |
| b | Câu "Nhớ chọn 1 giá chốt" | có (`:786`) | bỏ | ✅ **CỐ Ý** — KHKK 0 price-picker, BE tự chốt ở `ApplyApprovedValuesOnFinalize` |
| c | Gửi field | luôn gửi, `!isReject && (...)` (`:259`) | chỉ gửi khi approve | ✅ tương đương (3 nhánh kia BE không đọc) |
| d | Reset state | `onSuccess` | mở-dialog | ✅ KHKK chặt hơn |
| e | **Default ô-tích** | **`useState(false)` = OPT-IN** (`:61`) | **`useState(true)` = OPT-OUT** (`:85`) | 🔴 **G-1 — lệch NGƯỢC khuôn LIVE** |
| f | Nguồn eligibility | BE precompute `currentLevelOptions` chọn level theo actor (`PurchaseEvaluationFeatures.cs:1083-1085`) | FE **tự mirror** `ResolveActingLevel` | 🟡 **G-4 — nợ drift** |
| g | Badge cấp mang cờ trong sơ đồ | có (`:441` `lv.allowApproverFinalize &&`) + làm mờ Bước/Cấp SAU (`:161-170`) | **không có** | 🔴 **G-2 — SÓT** |

### 🔴 G-1 (MAJOR) — default TICK ngược chiều khuôn PE ĐANG SỐNG; nhãn "khuôn PE S96" trích bản ĐÃ BỊ ĐẢO
`fe-*/src/pages/khkk/KhkkWorkflowPanel.tsx:15-18` (header) + `:83-85` (`useState(true)`).
Đo tại chỗ: `fe-*/src/components/pe/PeWorkflowPanel.tsx:61` = `useState(false)`, chú thích ngay trên nó:
*"[S97 2026-07-01 — anh Kiệt FDC] … = ô-tick **OPT-IN**: default UNCHECKED → trình tiếp CEO/cấp sau; TÍCH mới KẾT THÚC tại cấp này. **Đảo opt-out S96**."*
⇒ PE **đã đi qua đúng ngã ba này** và owner ĐẢO sang opt-IN một phiên sau S96. KHKK vừa nhặt lại **đúng vế PE đã bỏ**.
Vì sao lọt: chú-thích PE `:769-771` (ngay trên khối JSX được trích) vẫn viết *"NAY là ô-tích BỎ được (opt-out): default tick"* — **tự nó STALE** so với `:61` cùng file ⇒ implementer trích đúng dòng nhưng trúng **chú-thích chết**. Đây là tái phát lớp *đối-chứng-khuôn-nguồn* (S165 F-1): đo khuôn ở tầng CHÚ-THÍCH thay vì tầng **giá trị khởi tạo**.
Hệ quả hành vi: Chương bấm "Duyệt" → "Xác nhận" mà không đọc ⇒ phiếu **DaDuyet + chốt giá**, CEO không bao giờ thấy. Hướng-sai-lệch nghiêng về **ÍT phê duyệt hơn** ở đúng cổng tiền; chiều ngược lại chỉ tốn thêm 1 trạm và cứu được.
Vế bào chữa PHẢI ghi cùng: default=`true` **giữ nguyên** hành vi BE `?? true` ⇒ **0 regression** so với trước diff, và acceptance F-1 của gate K3 chỉ đòi "mirror `PeWorkflowPanel.tsx:778`" (JSX) — implementer làm đúng chữ.
⇒ Đây là **quyết định của owner, không phải bug**. Nhưng KHÔNG được để nhãn *"khuôn PE S96"* nói rằng PE đang làm thế.
**Acceptance (1 trong 2):** (i) owner ratify default=tick + sửa nhãn thành *"ĐẢO NGƯỢC PE-live (PE S97 = opt-IN) — owner chốt @S166, lý do: …"*; HOẶC (ii) đổi `useState(true)`→`useState(false)` + đổi 2 câu mô tả cho khớp.

### 🔴 G-2 (MAJOR — carry-over, KHÔNG do diff này đẻ) — sơ đồ nói CEO "đã duyệt" trong khi CEO chưa đụng
`KhkkWorkflowPanel.tsx:100-106` `stepStatus`: `if (isApproved) return 'Done'` cho **MỌI** Bước; `:108-114` `levelStatus` trả thẳng `s` khi `s !== 'Current'` ⇒ mọi Cấp = `'Done'`; `:234` render `ls === 'Done' && "đã duyệt"`.
⇒ Phiếu kết thúc ở CCM Cấp 2 ⇒ Bước 3 «Ban Giám đốc» + cấp của `truong.nguyen` hiện ✓ emerald **"đã duyệt"**. Dấu vết phân biệt duy nhất = **vắng** dòng "✓ ký {giờ}" (`:238-242`) — bắt người đọc suy từ sự vắng mặt.
PE đã giải đúng bài này: badge `allowApproverFinalize` trên chính Cấp (`:441`) + làm mờ Bước/Cấp SAU (`:161-170`), **gate bằng runtime `endsBeforeCeo`** (chú thích tại chỗ: *"bug 'CEO duyệt mà mờ'"*).
KHKK làm được **nửa rẻ ngay bây giờ** (badge theo `allowApproverFinalize` — cờ vừa được chính diff này đưa vào DTO); nửa còn lại cần `EndedByLevelFinalize` ra DTO = **F-7 gate K3 vẫn treo**.
Ghi thật: lỗi có từ K3 (nhánh BE + seed cờ đã land ở `15349e8`), **diff này không đẻ ra nó** — nhưng diff này biến đường kết-thúc-sớm thành đường **mặc định có UI**, nên tần suất từ hiếm → thường.

## §6 (đề bài 5) Gotcha #44 — có ca nào CÓ quyền mà ô-tích KHÔNG hiện?

Quét đủ 3 đường có thể giấu control khỏi đúng người:

1. **Cấp trộn cờ + Admin duyệt thay** — 🟡 **G-4 (MINOR, điều-kiện)**.
   BE lấy `pendingLevelGroup.First()` (IGrouping dựng trong `LoadStepsAsync`+`ResolvePointer`, `Service:410-442`);
   FE lấy `currentLevels[0]` (mảng dựng trong query detail `ContractSigningPlanFeatures.cs:621-643`).
   **Hai truy vấn khác nhau**, cả hai chỉ `OrderBy(l => l.Order)` — **không có khoá phụ** ⇒ thứ tự các row
   cùng `Order` do DB quyết, không có gì bảo đảm 2 lượt trả giống nhau. Lệch ⇒ FE ẩn ô-tích trong khi BE
   finalize = **mất opt-out im lặng** (đúng hình #44).
   **Vô hại trên 8 workflow seed** — đo `DbInitializer.cs:626-660`: cờ gán theo **CẤP**
   (`foreach (var (emails, finalize) in new[] { (r.Level1, false), (r.Level2, r.Level2Finalize) })` → mọi
   approver trong cùng Cấp nhận CÙNG `finalize`) ⇒ trong 1 Cấp cờ **đồng nhất** ⇒ `First()` chọn ai cũng ra
   cùng cờ; thêm nữa Cấp mang cờ chỉ có **1 người** (`chuong.phan`, `:576-580`).
   **Có hại khi admin dựng Cấp trộn cờ qua Designer** — schema cho phép (cờ là per-slot), và chính PE đã ghi
   thành chú thích cảnh báo: `PurchaseEvaluationFeatures.cs:1072-1075` *"OR-of-N approvers cùng Cấp = N rows
   cùng Order, mỗi row có Allow* riêng. Lookup KHÔNG discriminate → luôn lấy row đầu DB"*.
   Đối chiếu thiết kế: PE **không** mirror ở FE — BE trả `currentLevelOptions` đã chọn level theo actor. KHKK
   chọn mirror FE ⇒ mỗi lần `ResolveActingLevel` đổi, FE **im lặng lệch**, không có test nào kêu.

2. **Cấp CÓ cờ mà actor là người khác trong Cấp** — KHÔNG phải lỗi: BE cũng chấm `own` (không cờ) ⇒ 2 bờ cùng nói "không finalize" (§2 hàng 2). ✅

3. **Sợi dây DTO đứt** — 🟡 **G-3 (MINOR)**: `ContractSigningPlanFeatures.cs:643` `l.AllowApproverFinalize`
   là **điểm chết duy nhất** của tính năng, mà **0 test chạm nó**. Grep `tests/`: `AllowApproverFinalize` chỉ
   xuất hiện ở `ContractSigningPlanApprovalTests.cs:877-904` (hành vi service) và `:1065-1138` (create-definition);
   **0 hit** trên projection của `GetContractSigningPlanQuery`. Xoá đúng 1 argument đó ⇒ 614 test vẫn xanh,
   `tsc` vẫn xanh (field vẫn đúng kiểu, chỉ luôn `false` lúc chạy) ⇒ ô-tích **biến mất im lặng** với đúng người
   có quyền. Đúng lớp S161-W2 (*hợp-đồng FE↔BE vỡ mà build+test đều xanh*). Phase-9 test-after cho phép hoãn —
   nhưng phải hoãn **có ý thức**, vì đây là sợi dây duy nhất.

4. 🟡 **G-5 (MINOR) — chữ "(CEO)" là suy đoán theo seed, không theo cấu hình.**
   `:457` *"duyệt xong kế hoạch TRÌNH TIẾP lên cấp sau (CEO)"*. Đúng với 8 workflow seed (Bước 3 = Ban Giám đốc).
   Nếu admin cắm cờ ở **Cấp cuối của Bước cuối**: bỏ tick ⇒ BE rơi xuống nhánh terminal `Service:320-329` ⇒
   **vẫn `DaDuyet` ngay** (khác duy nhất: `EndedByLevelFinalize=false` + câu changelog) ⇒ ô-tích hứa "trình tiếp"
   mà phiếu đóng luôn. Test T-a có sanity chặn "cấp mang cờ = cấp cuối" **trong test**, nhưng BE **không có
   validator** cấm cấu hình đó ở Designer.

## §6b (đề bài 6) Header-comment — còn tự mâu thuẫn chỗ nào?

Quét toàn khối `:1-27` sau khi supersede:
- `:6-8` *"KHÔNG price-picker, KHÔNG ngân sách, KHÔNG file-khi-duyệt, KHÔNG 4 return-mode"* → vẫn ĐÚNG với diff (ô-tích không phải price-picker). ✅
- `:10-14` supersede-1 (BE đã port + Designer đã gỡ khoá) → kiểm chứng: `earlyFinalizeLocked` chỉ còn ở **2 chỗ bia-mộ** `ApprovalWorkflowsV2Page.tsx:153` + `:641` (đều là chú thích "ĐÃ BỎ HẲN"), 0 site dùng. ✅
- `:15-18` supersede-2 → nội dung mô tả ĐÚNG việc diff làm, nhưng **nhãn "khuôn PE S96"** sai với PE-live ⇒ G-1.
- `:20-27` *"KHÁC PE Ở CHỖ ĐẮT NHẤT: PE nhận `approvalFlow` đã có `status` … KHKK BE trả CÂY THÔ"* → vẫn đúng, và
  giờ **thiếu một vế**: từ diff này KHKK còn phải tự suy **cả level-của-actor** (thứ PE cũng nhận sẵn từ BE) —
  chính là G-4. Khối "KHÁC PE" nên nêu thêm vế đó, nếu không người sau đọc sẽ tưởng chỉ lệch ở `status`.
- Chú thích `:132-134` mô tả mirror đúng 2 vế BE, nhưng **không khai** rằng thứ tự nhóm đến từ 2 truy vấn khác nhau (G-4).
- Citation kiểm từng cái: `PeWorkflowPanel.tsx:259` = đúng dòng gửi `applyLevelFinalize` ✅; `:772-791` = đúng khối JSX ✅; `ContractSigningPlanWorkflowService.cs:455-469` = đúng `ResolveActingLevel` ✅. **Số dòng không bịa.**
⇒ 0 mâu thuẫn logic còn sót; 2 chỗ **thiếu-khai** (nhãn khuôn PE · vế mirror mới) đã tính vào G-1/G-4.

## §6c NGOÀI ĐỀ BÀI — cây làm việc PHÌNH GIỮA LƯỢT REVIEW (đo 2 lần, cách nhau ~40 tool-call)

| Mốc đo | `git status --porcelain -- src fe-user fe-admin tests` | `git diff --stat` |
|---|---|---|
| Lúc spawn (§0) | **5 file** | **+137 / −14** |
| Đo lại lúc viết §6c | **7 file** (+`Domain/Identity/MenuKeys.cs`, +`Infrastructure/Persistence/DbInitializer.cs`) | **+253 / −19** |

🔴 **G-6 (MAJOR — quy trình, chặn commit):** 2 file mới (+42 / +79 dòng) **KHÔNG thuộc spec F-1** và
**KHÔNG nằm trong phạm vi tôi soi**. Nội dung là **K4a** (khuôn (b) menu-key nhóm): `MenuKeys.cs:81-84`
thêm `KhkkGroupMenuKeys` rồi **spread `.. KhkkGroupMenuKeys` vào `All`** (`:244`).
Đo hệ quả canonical: `All` hiện gồm **64 định danh tĩnh + 1 spread**; spread = `KhkkGroupNumbers` (7 số
`[2..8]`) × (1 group + 6 `KhkkLeafActions`) = **49 key** — chính chú thích `MenuKeys.cs:79` cũng ghi *"49 key"*
⇒ `|MenuKeys.All|` = **113**, policies DERIVED = **452**.
⇒ Nếu commit "vá F-1" ngay bây giờ, nó **nuốt trọn** 121 dòng chưa qua cổng nào **và** đổi 2 con số canonical
(gấp ~1,8 lần). Đây đúng lớp đã ăn đòn ở gate K1 cùng chuỗi (*commit gộp = vượt quyền owner*).
**Acceptance:** tách 2 commit — (1) F-1 đúng 5 file; (2) K4a riêng, có gate riêng + cập nhật STATUS.
*(Ghi thật: tôi KHÔNG soi nội dung 2 file đó; con số 113/452 là DERIVE từ hình dạng mã + chú thích tại chỗ,
chưa chạy initializer — người ship phải tự đếm lại bằng máy trước khi ghi vào STATUS.)*

🟡 **G-7 (MINOR — carry-over K3 F-3, mới vá NỬA):** `docs/STATUS.md:6` (văn xuôi CURRENT) đã sửa
*"Policies **256** · Menu keys **64**"*, nhưng **2 row canonical** vẫn ở số S161: `:470` `| Policies | **220** |`
và `:471` `| Menu keys | **55** |`. Đó lại **đúng nơi máy đọc**: `scripts/governance-detectors.ps1:175-181`
`Get-StatusValue` khớp `^\|\s*<label>\s*\|\s*\*\*(\d+)`, `:202-208` map `'menu'→'Menu keys'`,
`'policy'→'Policies'` ⇒ detector đang resolve canonical **55 / 220** trong khi văn xuôi nói 64 / 256 và máy
(sau K4a) nói 113 / 452 — **ba nguồn, ba số**. Vá prose mà bỏ row = *fix áp NỬA spec* (bài S160).

## §7 VERDICT

**PASS-WITH-FLAGS — 7 finding (3 MAJOR / 4 MINOR), 0 blocker KỸ THUẬT trên 5 file F-1, 1 blocker QUY TRÌNH (G-6).**

Bản vá F-1 **làm đúng việc nó nhận**: sợi dây BE→FE nối bằng đúng 1 site (§1), mirror 2 app chứng bằng
blob-SHA (§0), `actingLevel` mirror `ResolveActingLevel` đúng 6/7 ca gồm cả OR-of-N cờ-ở-người-khác và
`currentLevels` rỗng (§2), body không rò field sang 3 action kia và hội tụ absent/null/true (§3), cấp
thường **bất biến từng bit đường đi** (§4), ô-tích không rò sang phase khác nhờ 3 lớp chặn độc lập (§4).

| # | Mức | Một dòng | Địa chỉ |
|---|---|---|---|
| G-1 | MAJOR | default TICK ngược chiều PE-**live** (PE S97 = opt-IN); nhãn "khuôn PE S96" trích bản đã bị owner ĐẢO — cần owner ratify hoặc lật default | `KhkkWorkflowPanel.tsx:85` + `:15-18` ⟂ `PeWorkflowPanel.tsx:61` |
| G-2 | MAJOR (carry-over K3) | phiếu kết-thúc-sớm ⇒ sơ đồ hiện Bước «Ban Giám đốc» ✓ "đã duyệt" dù CEO chưa đụng; PE đã giải bằng badge + làm mờ | `KhkkWorkflowPanel.tsx:100-114` + `:234` ⟂ `PeWorkflowPanel.tsx:441` `:161-170` |
| G-6 | MAJOR (quy trình) | cây phình 5→7 file giữa lượt review; K4a (+121 dòng, `All` 64→113, policies →452) sẽ bị commit ké | `MenuKeys.cs:81-84` `:244` · `DbInitializer.cs` |
| G-3 | MINOR | 0 test chạm `l.AllowApproverFinalize` trong projection — xoá 1 argument là ô-tích biến mất mà 614 test + tsc vẫn xanh | `ContractSigningPlanFeatures.cs:643` |
| G-4 | MINOR | FE tự mirror `ResolveActingLevel` (PE để BE precompute); Admin-fallback so `First()` giữa **2 truy vấn** không khoá phụ — vô hại trên seed (cờ đồng nhất trong Cấp), hại khi Designer trộn cờ | `KhkkWorkflowPanel.tsx:135-136` ⟂ `Service:463-465` |
| G-5 | MINOR | chữ "(CEO)" suy theo seed; cắm cờ ở Cấp cuối Bước cuối ⇒ bỏ tick vẫn `DaDuyet` ngay, ô-tích hứa sai; BE không có validator cấm | `KhkkWorkflowPanel.tsx:457` ⟂ `Service:320-329` |
| G-7 | MINOR (carry-over K3 F-3) | STATUS mới vá văn xuôi, 2 row canonical detector đọc vẫn 55/220 | `docs/STATUS.md:470-471` |

**Khuyến nghị thứ tự:** (1) tách commit theo G-6 → (2) hỏi owner G-1 (1 câu: *"Chương bấm Duyệt mà không
đọc thì mặc định KẾT THÚC hay LÊN CEO?"*) → (3) ship F-1 → (4) G-2/G-3 vào nhịp K4 → (5) G-7 kèm lần đụng STATUS kế.

END sub-reviewer-f1-checkbox — TOTAL: 7 finding (3 MAJOR / 4 MINOR) · 0 blocker kỹ-thuật · 1 blocker quy-trình (G-6) · verdict PASS-WITH-FLAGS





