# c1-l2 đủ-vs-spec

Lane 2/3 — review cụm-1 (`sub-invest-fable-b2-cum1.md`) đối chiếu spec-v2 (`spec-4gd-khkk-tong-quat-31-07-2026.md`).
Lens: **ĐỦ-vs-SPEC** (nghĩa vụ nào của spec-v2 KHÔNG được cụm-1 gánh) — mọi claim đo lại trên đĩa.

**VERDICT: FAIL (cổng tiền-implement) — 17 finding / 3 HIGH.** Nền vững (anchor ~95% chính xác, 0 trôi số canonical); thủng ở **acceptance mù**: cả 3 HIGH đều thuộc lớp *"impl SAI vẫn PASS 100% checkbox"*. Vá bằng chỉnh acceptance + bổ sweep, KHÔNG cần làm lại invest.

---

## A. Bảng phủ 9 quyết định §② (Q1)

| QĐ | Nghĩa vụ | Cụm-1 gánh ở | Kết |
|---|---|---|---|
| ②.1 OG-1 gộp | schema GỘP-capable | PRE `:79` ghi tên; việc THẬT = swap UNIQUE `:86` nhưng không dán nhãn OG-1; 0 acceptance riêng | ⚠️ 0-bit gate |
| ②.2 per-nhóm + line hạng mục | cột + UNIQUE + rào + Σ + 16-consumer | `:84-87` · `:111-112` · `:132` · `:136-153` (17 dòng) | ✅ (trừ M1, M6) |
| ②.3 8 Code + K2-chủ-cột | seed 8 + validator + "K3 chỉ đọc" | `:185-192` · `:210` · `:130` | ✅ |
| ②.4 vai 2 tầng | **"khai limitation trong doc K1"** | chỉ 2 chú-thích cột `:21`,`:40` — **0 deliverable doc** trong commit K1 `:240`, **0 acceptance** `:69-75` | ❌ **RƠI** |
| ②.5 NĐUQ tham khảo | cột text, seed null | `:23` | ✅ |
| ②.6 backfill=1 (+42-leaf→K4a) | default 1 + COUNT==Σ8 | `:83` + acceptance `:168` | ✅ |
| ②.7 admin ẩn | cụm-1 chỉ cần CHỪA subtree AwV2 | `:214` | ✅ (đúng phạm vi) |
| ②.8 nguồn + OG-6 | chặn seed-land | `:7` · `:241` · `:256` = **3 chỗ** | ✅ |
| ②.9 freeze | freeze tại finalize + né #81-EXT | K2.b `:90-107` | ⚠️ H2 + M4 |

**PRE-gate (2 câu hỏi đích danh):** OG-6 chặn seed-land K1 = **CÓ**, 3 chỗ (`:7`/`:241`/`:256`). OG-2 verify-LIVE trước seed K3 = **CÓ**, 3 chỗ (`:176`/`:229`/`:243`) — và **khả thi thật**: `ApprovalWorkflowsV2Controller.cs:17-21` `GET api/approval-workflows-v2` `[Authorize]` trần, DTO có `IsActive` (`ApprovalWorkflowV2AdminFeatures.cs:176`) ⇒ phân biệt được LIVE/archived.

**14 vá b1b thuộc cụm-1 (8 mục):** #2 ✅ · #3 ✅ · #4 ✅ · #5 ✅BE/❌UI (M1) · #6 ✅ · #7 ✅ · #11 ✅ · #13 ✅.

---

## Findings

### HIGH

**H1 — leaf `ContractCatalog` NGOÀI `All` + "grant qua SeedAdminPermissionsAsync loop" = tự mâu thuẫn ⇒ menu KHÔNG BAO GIỜ hiện.**
Cụm-1 `:57`. Đo: `DbInitializer.cs:2058` `foreach (var menuKey in MenuKeys.All)` ⇒ key ngoài `All` không sinh Permission row. `GetMyMenuTreeQuery.cs` build cây THUẦN từ `db.Permissions` + filter `HasAccess = CanRead || child` — **không có admin-bypass**; inherit-root chỉ **4** (Contracts/Workflows/PurchaseEvaluations/PeWorkflows), **`Catalogs` KHÔNG có**. ⇒ 0 row ⇒ leaf bị filter, kể cả Admin.
Tiền lệ dẫn SAI: `Pe_*` sống được CHÍNH VÌ `PurchaseEvaluations` là inherit-root; `HopDongCung` thì 0 menu-row. Khuôn THẬT của leaf catalog: `MenuKeys.cs:168` `Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems` — **cả 5 đều TRONG `All`**.
Lý do (iv) "0 policy/menu-key mới = 0 drift STATUS row Policies" (`:47`) = **Goodhart**: giữ số doc đứng yên bằng cách làm menu chết. Acceptance K1.f `:69-75` **không có mục nào đo "leaf hiện trong sidebar"** ⇒ lỗi ship với 7/7 xanh.
Đề nghị: chọn 1 trong 2 — (a) vào `All` và KHAI drift `Menu keys` 55→56 + `Policies` 220→224 (2 row cùng lúc); (b) grant tường minh kiểu `SeedKeHoachKyKetAccessAsync`. Thêm acceptance "login Admin thấy leaf ở nhóm Danh mục" + 1 test seed-grant.

**H2 — K3.⚠️ liệt 3 việc code, SÓT ≥3 site "ép false" ⇒ tính năng Chương-tick chết ở lần admin lưu lại quy trình đầu tiên, mà mọi acceptance K3 vẫn PASS.**
Cụm-1 `:181` liệt (a) nhánh service (b) cột runtime (c) gỡ khoá FE `:151-152`. Sót:
- `ApprovalWorkflowV2AdminFeatures.cs:380-381` — CREATE **ép false server-side**: `AllowApproverSkipToFinal/Finalize = typeEnum != ApprovalWorkflowApplicableType.ContractSigningPlan && l.…` (comment `:377-379` "ép false server-side (FE đã ép ở levelFlagsPayload — đây chặn curl bypass)").
- `:591-594` PUT sửa-tại-chỗ: echo lệch → `ConflictException` "không được bật/tắt cờ …".
- `:605-610` dòng MỚI mang cờ → `ConflictException`.
- FE `ApprovalWorkflowsV2Page.tsx:247-258` `levelFlagsPayload(e, lockEarlyFinalize)` ⇒ `allowApproverFinalize: lockEarlyFinalize ? false : …`, bật bởi `:638 earlyFinalizeLocked = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN`. Gỡ note `:151-152` mà không gỡ 2 chỗ này ⇒ UI mở nhưng payload vẫn false.
Vì sao acceptance mù: K3.d test 2 & 5 đo trên workflow **SEED** (ghi thẳng DbContext, không qua command) ⇒ xanh. Đây đúng lớp **#81-EXT** mà chính doc viện dẫn — doc quét kỹ `Phase = ` nhưng **không quét `AllowApproverFinalize`**.
Đề nghị: bổ sweep `grep -rn "AllowApproverFinalize\|AllowApproverSkipToFinal"` vào K3, liệt đủ site vào PR + 1 test "tạo version mới qua CreateCommand cho type-10 GIỮ được cờ".

**H3 — `CreateContractSigningPlanCommand` +`ApprovalGroup` required, nhưng `KhkkCreatePage` bị dispose "KHÔNG-ĐỔI" ⇒ tạo phiếu vỡ (hoặc feature vô hình); acceptance curl-only không bắt.**
Cụm-1 `:111` (+`ApprovalGroup int`, validator `InclusiveBetween(1,8)`) ⟂ `:151` K2.f row 16-17 `KhkkCreatePage.tsx:8` = "KHÔNG-ĐỔI đợt này" ⟂ `:112` lại đòi sửa `KhkkCreatePage` ×2 app.
Đo: `fe-admin/src/pages/khkk/KhkkCreatePage.tsx:72-77` body = `{ peId: purchaseEvaluationId, approvalWorkflowId, … }` → `api.post('/contract-signing-plans', body)` — **không có `approvalGroup`**. Header `:3` khai "File MIRROR SHA256 identical với fe-admin" ⇒ phải sửa cả 2 app. Không sửa: `approvalGroup=0` → 400 mọi lần tạo. Nếu impl né bằng default=1: mọi phiếu FE tạo ra đều nhóm 1 ⇒ feature vô hình.
K2.h `:169` đo bằng **curl** ⇒ bỏ qua FE hoàn toàn. Đề nghị: 1 dòng acceptance "tạo phiếu nhóm 2 **từ UI** ×2 app".

### MEDIUM

**M1 — nửa-UI của OG-7 có trong văn, 0 acceptance / 0 test / 0 dòng commit.** `:112` mô-tả nút "Tạo thêm cho nhóm khác" + `existingGroups: int[]`; K2.g `:157-163` không có test; K2.h `:167-172` không có mục; commit-K2 `:242` chỉ ghi "DTO + FE types". Impl SAI vẫn PASS: BE mở rào, UI giữ 409 cũ ⇒ đúng thứ owner nhìn thấy thì vắng.

**M2 — nhãn đo-lường SAI: "hit `IgnoreQueryFilters` đầu tiên trong src/Backend — S155 đo 0-hit"** (`:38`). Đo lại: **29 hit** trong `src/Backend` (trừ Migrations) — 18 `ApprovalWorkflowV2AdminFeatures.cs` · 4 `PeSoftDeleteFeatures.cs` · **3 `ContractSigningPlanFeatures.cs`** (chính module đang spec) · 3 `ContractFeatures.cs` · 1 `PurchaseEvaluationsController.cs`. Số ĐÚNG & chịu lực = **0 hit trong `DbInitializer.cs`** (đo: 0). Đơn thuốc vẫn ĐÚNG (chứng `DbInitializer.cs:2970` `db.Projects.Select(p => p.Code)` thiếu `IgnoreQueryFilters` — verified). Sửa nhãn hoặc bỏ nhãn.

**M3 — K3.⚠️(a) mở lại quyết định owner đã ĐÓNG bằng chữ "PHÁ VỠ" @S155, doc không nhắc một chữ.** 5 nơi ghi: `ContractSigningPlanWorkflowService.cs:44-46` · `ApprovalWorkflowV2AdminFeatures.cs:377` · `fe-{admin,user}/src/pages/khkk/KhkkWorkflowPanel.tsx:8` · `docs/HANDOFF.md:77` (vẫn nằm ở mục "🔸 Không chặn W1-W2" = CHƯA chốt). Spec v2 §④ OG-3 có câu owner @S164 nghe như tái-cho-phép và lead đã dán "🔄 SỬA @B2c1", nhưng câu đó nói về **NĐUQ/ngưỡng CEO**; đảo một phán quyết mang chữ "PHÁ VỠ" phải hỏi owner TƯỜNG MINH. → trả lời Q4.

**M4 — freeze tuyên "xong" ở cụm-1 nhưng lỗ duy nhất bịt bằng việc cụm-2.** `:107` tự khai: "xoá mềm line sau DaDuyet làm Σ `:711` trôi ÂM THẦM — chặn bằng phase-guard", mà phase-guard nằm ở **K4b (cụm-2)**. Đo: `Features.cs:709-711` Σ tính LIVE từ `ContractSigningPlanLines` ⇒ soft-delete 1 line đổi tổng phiếu ĐÃ DUYỆT. K2.g test 5 chỉ đo `ApprovedAmount != null`.

**M5 — neo chéo gãy: "Mig 71 item 6" không tồn tại.** `:181` ("gắn vào Mig 71 **item 6 (K2.a)**") + `:242` ("Mig 71 gồm cả item 6") ⟂ K2.a `:83-88` chỉ có **item 1-5**. Impl đọc K2.a rời → Mig 71 thiếu `EndedByLevelFinalize` → K3 kẹt hoặc phải đẻ Mig 72 trái QUYẾT `:242`.

**M6 — `CatalogEntryId` bắt-buộc hay không: 3 câu không thể cùng đúng.** `:87` "line MỚI/SỬA **bắt buộc** có `CatalogEntryId`" ⟂ `:113` auto-gen create "mồi `CatalogEntryId = null`" (mọi phiếu tạo sau Mig 71 đều sinh line null) ⟂ `:126` "null → CHO QUA". Cách đoán quyết định bẫy NULL-unique (`:87`) có nổ hay không.

### LOW

- **L1** — `:105` gắn cờ 🔴 "PHẢI XỬ" cho `WorkflowService.cs:342` do đọc nhầm chú thích `:39`. Đo: `ContractSigningPlanWorkflowService.cs:36-42` đã KHAI RÕ service có **ĐÚNG MỘT** nhánh set DaDuyet, và `:39` là trích **nguồn-copy** `ContractWorkflowService.cs:190-193` như thứ CỐ Ý không chép. `:342` nằm trong `ReturnOrRejectAsync` (`:310-318`, `targetPhase ∈ {TraLai,TuChoi}`; guard `:320-323` đòi `Phase == ChoDuyet`). Phương án (i) "gọi `ApplyApprovedAmounts` ngay trước `:342`" = thuốc SAI. Phương án (ii) test-khoá-hành-vi hợp lệ, bổ cho `ContractSigningPlanApprovalTests.cs:523-533` (T3 hiện chỉ khoá write-path Admin-approve).
- **L2** — `:11` đặt file `Domain/Master/ContractCatalogEntry.cs` "(cạnh WorkItem)"; `WorkItem.cs` thật ở `src/Backend/SolutionErp.Domain/Master/Catalogs/WorkItem.cs`. 2 vế không cùng đúng.
- **L3** — GroupCode dòng CEO: `:19` liệt giá trị `CEO` ⟂ `:40` bảo lấy heading "(A1..C3, **5.2**)" ⇒ ra `"5.2"`. Heading nguồn: `danh-muc-sp002-transcribe.md:131` "## 5.2 — CEO".
- **L4** — `ContractSigningPlanFeatures.cs:32` tự khai luật "positional record, field append-CUỐI (đừng chèn giữa — vỡ FE mapping)"; K2.e `:131` liệt 3 DTO cần +field nhưng không nói vị trí.
- **L5** — `AddColumn … defaultValue: 1` (`:83`) để lại DEFAULT constraint vĩnh viễn ⇒ insert quên set `ApprovalGroup` IM LẶNG thành nhóm 1 thay vì fail (cùng gốc M6/H3).
- **L6** — đếm QUYẾT: lead hỏi "3 QUYẾT của lane", đo được **5** (`:44`+`:47` policy · `:84` loose-Guid · `:90`+`:96` freeze-reuse · `:210` convention-parse · `:242` gộp-Mig-71).
- **L7** — `:86` trỏ "vùng `:24-26`" để lấy tên index EF-sinh; index thật ở `ContractSigningPlanLineConfiguration.cs:22-24`.
- **L8** — acceptance `:167` (`SUM(PeReferenceAmount)` trước == sau migrate) gần 0-bit: Mig 71 không chạm cột tiền ⇒ không impl-sai nào làm nó TRƯỢT. Răng thật ở test `:163` (mồi 0 khi tách dòng) — nên nêu vế đó.

---

## Kháng được soi (đo lại, ĐÚNG)

- **P1** — bảng #81-EXT `:100-105`: `grep -rnE "Phase\s*=[^=]"` → đúng 4 site (`Features.cs:373` · `WorkflowService.cs:174/:278/:342`); "DbInitializer 0 site KHKK" đo được **0**. Cảnh báo "đừng grep `\.Phase`" xác nhận đúng.
- **P2** — 13/13 email+tên K3.a khớp `DbInitializer.cs:1613-1632` từng dòng; bẫy `long.nguyen@` (HRA `:1629`) có thật; ô ⚠️ Trưởng/Trình có thật (`:1625` Lê Tự Đăng Trình ⟂ `:1626` Lê Trần Đăng Trường).
- **P3** — "KHKK không đọc bảng NS live": đo lại chỉ **1 hit source** (`Features.cs:162` nhãn DTO) ⇒ lý do bỏ 11 cột PE Mig 67 đứng vững.
- **P4** — khuôn authz catalog: `CatalogsController.cs:13-14` + đúng **12** `[Authorize(Roles = "Admin")]` + policy-per-action **0 hit**. Số khớp tuyệt đối.
- **P5** — 86 & phân bố 5/10/5/23/14/15/13/1 tự-kiểm từ transcribe: A1=5 · A2 1+A3 9=10 · A4=5 · B1=23 · B2=14 · B3=15 · C1 6+C2 3+C3 4=13 · CEO=1 ⇒ Σ=86 ✓; EN có mặt **86/86**; 8 cột map đủ (NduqNote seed null, khớp ⚠️3).
- **P6** — 0 trôi số canonical: STATUS `Tests 590` (`:472`) · `SQL tables 96` (`:466`) · `Migrations 69` (`:465`) ⇒ "Mig 70/71" · "96→97" · "baseline ~590 +17" đều đúng.
- **P7** — OG-2 verify-LIVE khả thi đúng như mô tả (route + `IsActive` đều có).
- **P8** — anchor `Features.cs` :34 · :342-347 · :350 · :389 · :458-462 · :570-574 · :622-625 · :711 · :1201-1202 và `Line.cs:15/:17-20/:23/:25` · `LineConfig:22-26/:29` · `Layout.tsx:166-172` — **đo lại khớp hết**.

## Verdict

**FAIL** (cổng tiền-implement) — 17 finding: **3 HIGH · 6 MEDIUM · 8 LOW**, 8 điểm kháng-soi.
Chặn: H1 (menu chết), H2 (feature owner-yêu-cầu chết im lặng), H3 (tạo phiếu vỡ). Cả 3 cùng một lớp: **acceptance không có phép đo nào TRƯỢT khi impl sai**.
Cần owner gật: **M3** (đảo phán quyết "PHÁ VỠ" @S155).
