REVIEWER-GATE-K3: PASS-WITH-FLAGS — 8 finding (3 MAJOR / 5 MINOR, 0 blocker build-test)

# Reviewer gate K3 — 8 workflow nhóm + port finalize + designer

Ghi-đĩa TRONG LÚC LÀM (chống #53). Đo từ ĐĨA, HEAD `2967c43`, 25 file M / +908 −102; 2 untracked chỉ ở `.claude/auto-memory/`.

## §1 Build / test / npm — số NGUYÊN VĂN

| Phép | Lệnh | Kết quả nguyên văn |
|---|---|---|
| BE build | `dotnet build SolutionErp.slnx` | `Build succeeded.` · `0 Warning(s)` · `0 Error(s)` |
| BE test #1 | `dotnet test SolutionErp.slnx` | 🔴 `Failed!  - Failed: 1, Passed: 568, Skipped: 0, Total: 569 - SolutionErp.Infrastructure.Tests.dll` |
| BE test #2 | `dotnet test SolutionErp.slnx` | `Passed! - Failed: 0, Passed: 45, Total: 45 - Domain.Tests` + `Passed! - Failed: 0, Passed: 569, Total: 569 - Infrastructure.Tests` ⇒ **614/0** |
| BE test #3 | `--filter ~ContractSigningPlan` | `Passed! - Failed: 0, Passed: 34, Total: 34` |
| npm fe-admin | `npm run build` | `✓ 1961 modules transformed` · `✓ built in 19.63s` · 0 TS error |
| npm fe-user | `npm run build` | `✓ built in 7.81s` · 0 TS error |

⇒ 614/0 = 45D+569I **khớp claim ở lượt #2/#3**; lượt #1 FAIL → F-4.

## §2 Code-vs-spec K3 (5 điểm)

1. **Seed 8 wf khớp ROSTER §0** — `DbInitializer.cs:526-688`. 3 Steps (`Phòng Cung Ứng` / `P. Kiểm soát Chi phí` / `Ban Giám đốc`), 11 email `Distinct` khớp từng dòng bảng §0 `sub-implbe-k3.md`; Cấp 1 OR-4 / OR-4 / OR-1, Cấp 2 `tra.bui` / `chuong.phan`; `AllowApproverFinalize=true` **chỉ** ở `(CCM, Cấp 2, chuong.phan)` đặt PER-ROW; idempotent `AnyAsync(w.Code == code)`; UNGATED demo-seed. ✅
2. **Finalize qua choke-point + 2-lệnh-grep LUẬT** — chạy CẢ HAI:
   - `Phase = ContractSigningPlanPhase.DaDuyet` → `:280`, `:322` (+ `:336` là dòng doc)
   - `ApplyApprovedValuesOnFinalize` → `:279`, `:321` (+ doc/def/comment)
   ⇒ **cùng khớp**: 2 site gán DaDuyet ↔ 2 site gọi helper, helper đứng NGAY TRƯỚC gán ở cả hai. ✅
3. **Rào (v)** — `ContractSigningPlanFeatures.cs:411-420`, regex `"^KHKK-N([1-8])$"` neo 2 đầu, khớp literal seeder `$"KHKK-N{n}"` n=1..8; so với `approvalGroup` (`:395` = `request.ApprovalGroup ?? Default`); code ngoài họ → cho qua. ✅
4. **8 key VÀO All + DELTA (đếm MÁY)** — `MenuKeys.All` = **64** (HEAD `2967c43` = **56**) ⇒ **+8**; policies DERIVED = **256** (HEAD **224**) ⇒ **+32**. 8 menu-row `DbInitializer.cs:1980-1989` order 3..10 dưới `ApprovalWorkflowsV2`. ✅ mã — ❌ khai (F-3).
5. **FE phép-âm `Khkk_` = 0-hit + control dương** — `git diff -U0 | grep -E "^\+.*Khkk_[A-Za-z]+: *'"` = **0**; control dương cùng regex trên cả `Layout.tsx` = **6** hit ⇒ mẫu CÓ RĂNG. Nhánh mới `Layout.tsx:181` `/^KhkkN[1-8]$/` nằm TRONG `awV2Match`, đặt SAU whitelist 3 code cũ. ✅

## §3 Phán 3 quyết-định lead

- **(i) GIỮ PUT-Conflict (spec vá-3 bảo gỡ) — LEAD ĐÚNG, spec vá-3 LIỆT NHẦM.** `UpdateAwDefinitionCommandHandler` (`:515-649`) **0 lần** tham chiếu `typeEnum`/`ContractSigningPlan`; grep 2 token toàn file cho hit ở `:97/:125/:126/:331/:341/:349/:387` — **tất cả** thuộc list/create, KHÔNG hit nào trong thân PUT. Guard `:598-603` (echo cờ) · `:612-617` (dòng mới) · `:627-632` (bỏ người giữ cờ) là **luật versioning CHUNG mọi ApplicableType**; gỡ = nới rào cho PE/Contract/Proposal = regression ngoài scope K3. Thêm: gỡ FE-lock (đúng) đã đủ để cờ seed `true` không đẻ 409 lúc PUT vì payload nay echo giá trị THẬT.
- **(ii) GIỮ ép-false `AllowApproverSkipToFinal` type-10 (O-1) — LEAD ĐÚNG ở BE, nhưng sweep BẤT ĐỐI XỨNG (F-2).** Tự verify: token `AllowApproverSkipToFinal` trong `ContractSigningPlanWorkflowService.cs` = **1 hit duy nhất `:47`, và là COMMENT** ⇒ service **0 lần đọc cờ**; control dương: cùng token trong `PurchaseEvaluationWorkflowService.cs` = **3 hit** (code thật) ⇒ grep có răng. Echo cờ cho type-10 = đẻ config-lie #78 mới ⇒ giữ ép-false ĐÚNG.
- **(iii) Seed cờ cùng-commit ≠ config-lie #78 — ĐÚNG, nhánh THẬT tồn tại.** `ContractSigningPlanWorkflowService.cs:276-289`: `if (actingLevel.AllowApproverFinalize && applyLevelFinalize)` → `EndedByLevelFinalize=true` → `ApplyApprovedValuesOnFinalize(plan)` → `Phase=DaDuyet` → xoá 2 con-trỏ → log → `return`. Cờ seed CÓ máy tiêu thụ trong cùng diff. ✅ *(nhưng đường ĐI NGƯỢC — bỏ tick để trình CEO — chết ở tầng UI: F-1)*

## §4 Ranh giới

- `ReturnOrRejectAsync` (`:354+`), `ApplyApprovedValuesOnFinalize` (`:345-349`), nhánh advance thường (`:293-329`): `git diff -U0` trên service chỉ **5 hunk** = `@@-37,10` (comment) · `@@-89,0` (thêm param) · `@@-124` (truyền param) · `@@-245` (chữ ký private) · `@@-266,0 +270,21` (khối finalize MỚI) ⇒ **0 dòng nội-dung đổi** trong 3 vùng trên. ✅
- Submit-guard K2 / file K1: không có trong `git diff --name-only`. ✅
- **0 migration mới**: `git diff --name-only | grep -i migration` = **0**; `git status --porcelain -- src tests | grep '^??'` = **rỗng**. ✅
- Route/menu-row NGOÀI 8 `AwV2_KhkkN*`: DbInitializer thêm đúng 8 row; Layout thêm đúng 1 nhánh regex họ `AwV2_`. ✅
- Rào (v) chỉ ở CREATE — `UpdateContractSigningPlanCommandHandler` không chạm `ApprovalWorkflowId`/`ApprovalGroup` (0 hit trong thân) ⇒ không có lỗ update-lách. ✅

## §5 Test lane (mở ruột 2 test)

- **T-a `ApproveV2_CapMangCoKetThuc_ChotPhieuNgay_KhongTrinhTiepBuocSau`** (`ContractSigningPlanApprovalTests.cs:944-994`): đo hành-vi THẬT qua service. Sanity 3 vế TRƯỚC (`ShouldBeAt(ChoDuyet,1,2)` · `EndedByLevelFinalize=false` · `StepCountAsync == 3` kèm lý-do *"Cấp mang cờ KHÔNG được là cấp cuối — nếu là cấp cuối thì phiếu kết thúc ở CẢ HAI thế giới và test này xanh mà không chứng minh gì"* = **anti-tautology guard**). Đo SAU 5 trục: phase + 2 con-trỏ · cờ runtime · `ApprovedAmount` 3 dòng gồm ca `??=` giữ 300 · opinion signer. Kèm **chứng-nhân song sinh** `WitnessAtFlagLevel` bất động (cả cờ lẫn giá). ✅
- **T-e `CreateAwDefinition_LoaiKhkk_GiuNguyenCoKetThucTaiCap_KhongConEpFalse`** (`:1079-1104`): 3 vế — (i) tick ⇒ `true`; (ii) không tick ⇒ `false` (chặn "bật bừa"); (iii) **đối-chứng type khác** `DuyetNcc` cũng `true` ⇒ phân biệt "gỡ ép-false" với "đổi thành ép-true". Đi qua `CreateAwDefinitionCommandHandler` THẬT. ✅
- **0 đè test cũ**: `git diff -- tests/ | grep -c '^-[^-]'` = **2**, cả 2 là cùng một sửa cơ-học `TransitionAsync(..., CancellationToken.None)` → `(..., ct: CancellationToken.None)` (named-arg vì thêm param) — **không** nới assert nào.

## §6 FINDING (8)

**MAJOR**

- **F-1** `fe-{user,admin}/src/pages/khkk/KhkkWorkflowPanel.tsx:136-142` — **opt-out `applyLevelFinalize` KHÔNG có tầng UI ⇒ trạm CEO chết ở 8/8 workflow seed.** Body POST là **literal đúng 2 field** `{action, comment}`; `grep applyLevelFinalize fe-*/src` = **0 hit** ở KHKK. Controller `ContractSigningPlansController.cs:213` `?? true` ⇒ mọi lượt Chương bấm "Duyệt" đều finalize. Seeder cắm cờ cho `chuong.phan` ở **cả 8** workflow ⇒ Step 3 «Ban Giám đốc» (`truong.nguyen`) **không đường nào tới**. **Đối-chứng chứng minh đây KHÔNG phải khuôn PE**: `fe-*/src/components/pe/PeWorkflowPanel.tsx:63` = `useState(false)` + `:259` LUÔN gửi tường minh ⇒ PE mặc-định **KHÔNG** kết-thúc-sớm (opt-in tại UI); KHKK mặc-định **LUÔN** kết-thúc-sớm. Lời owner OG-3/OG-9 (*"a Chương sẽ quyết định là cái đó cần CEO ko"*) **không thực thi được từ UI**. Kèm: comment `KhkkWorkflowPanel.tsx:15-17` khẳng định *"không có gì để panel render thêm"* — tiền-đề SAI, chính wave này vừa đẻ tham số để người duyệt quyết. Acceptance: thêm ô tick opt-out ở panel (mirror `PeWorkflowPanel.tsx:778`), HOẶC khai tường minh lên HANDOFF rằng "trình CEO" hiện chỉ làm được bằng admin bỏ cờ ở Designer.
- **F-2** `ApprovalWorkflowV2AdminFeatures.cs:387` ⟂ `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:263` + `:1300-1310` — **sweep ép-false bất-đối-xứng ⇒ tick bị NUỐT IM LẶNG.** FE gỡ khoá **cả 2** ô (gồm `allowApproverSkipToFinal`), BE **giữ** ép-false `SkipToFinal` cho type-10. Admin tick "Cho phép duyệt thẳng Cấp cuối" trên quy trình KHKK → CREATE lưu `false`, UI không báo gì. Đây đúng class mà comment vừa bị xoá đã cảnh báo (*"màn hình nói một đằng dữ liệu một nẻo"*), chỉ đảo chiều. **0 test phủ** (T-e chỉ đo `Finalize`). Acceptance: giữ `disabled` riêng ô SkipToFinal cho type-10, HOẶC bỏ ép-false BE + port nhánh.
- **F-3** `docs/STATUS.md:6` — **DELTA đo được nhưng chưa khai.** Máy: `All` 56→**64** (+8); policies 224→**256** (+32); STATUS vẫn ghi "Policies **224** · Menu keys **56**". Spec cụm-1 vá-1 bắt "mỗi commit kèm lệnh đếm lại + cập nhật 2 row STATUS". **Tái phát y hệt** finding ④ của gate K1 cùng phiên.

**MINOR / khai-thật**

- **F-4** Suite **flake 1/3 lượt**: `dotnet test SolutionErp.slnx` lượt #1 `Failed: 1, Passed: 568` — `ContractSigningPlanCrudTests.DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds` chết ở `KhkkFixture..ctor()` `:62` `EnsureCreated()` → `CallSiteRuntimeResolver`. Fixture có từ W2 ⇒ **không phải K3 đẻ**, nhưng câu "614/0 tươi" chỉ đúng 2/3 lượt — đừng khai trần.
- **F-5** `DbInitializer.cs:665` — seeder idempotent per-Code **thuần skip, 0 nhánh UPDATE**: roster đổi (người nghỉ / đổi phòng) ⇒ phải UPDATE tay trên prod. Cùng lớp seeder SP-002 ở K1.
- **F-6** `DbInitializer.cs:637-640` — `Description` khai *"Admin có thể chỉnh người/quyền tại chỗ hoặc tạo version mới"*: nửa sau đúng, nửa đầu **sai với cờ finalize** — `ApprovalWorkflowV2AdminFeatures.cs:598-600` chặn đổi cờ, `:627-629` chặn bỏ người giữ cờ khi sửa tại chỗ. Chuỗi này hiển thị ra UI admin.
- **F-7** `ContractSigningPlan.cs:51` + `fe-*/src/types/khkk.ts:260` — `EndedByLevelFinalize` **được GHI nhưng chưa expose DTO** (chính FE type tự khai). Test T-a lý-giải *"FE/GĐ3 đọc cột này"* — hiện **chưa** ai đọc; ghost-window đã-khai, không phải bug, nhưng câu biện-minh mạnh hơn sự thật.
- **F-8** `ContractSigningPlanWorkflowService.cs:37-42` + `:335-337` — 2 lệnh grep tự-kiểm in ra **gồm cả chính dòng `///` doc** (`:336`, `:337`) ⇒ người chạy sau đếm 3 vs 5 rồi tưởng lệch. Nên nêu rõ "bỏ dòng `///` khi đếm".

## §7 Verdict

**PASS-WITH-FLAGS** — build 0W/0E · suite 614/0 (lượt tươi) · npm ×2 sạch; 5/5 điểm code-vs-spec khớp; 3/3 quyết-định lead ĐÚNG về kỹ-thuật; ranh giới sạch (0 mig, 0 đổi hành-vi vùng cấm, 0 route ngoài họ). **0 blocker kỹ-thuật**, nhưng **F-1 + F-2 là nợ HÀNH-VI**: F-1 làm mất đúng cái quyền quyết mà owner vừa chốt (OG-3/OG-9); F-2 tái tạo class "UI nói một đằng DB một nẻo" ở ô còn lại. F-3 là acceptance chưa đóng. Owner/lead quyết ship-rồi-vá hay vá-trước-ship.

END sub-reviewer-gate-k3 — TOTAL: 8 finding (3 MAJOR / 5 MINOR) · 0 blocker build-test · verdict PASS-WITH-FLAGS
