Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-f1-checkbox.md
2026-08-01 09:00:14 +07:00

230 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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``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`
**đ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**
**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** |`
`: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