Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md
2026-08-01 10:37:08 +07:00

324 lines
24 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.

CICD-K4AB: PASS — 7/7 (run #438 success 6m23s · gate 621=45+576 · Mig GIỮ 71 · seed 49 menu+637 perm tam-giác-3-nguồn · A1 8+48 & A5 restart×2 đứng im · bundle JS ROTATE ×2 marker 3/3 · smoke 401/404/200 + bind-400)
# cicd-monitor — verify deploy K4a+K4b SP-002 (`88e7ecf`) + phủ luôn F-1 (`7a903cf`)
> Artifact chống #53: ghi TỪNG MỤC ngay khi có bằng chứng. Kết bằng `END ... VERDICT=`.
> Repo `D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP` · push `7a903cf..88e7ecf main` · phiên S166 01/08.
> Khuôn kế thừa k1/k2/k3 cùng folder: tam-giác 3-nguồn ⟂ control ÂM ⟂ recycle-proof ⟂ dự-đoán-TRƯỚC-khi-đo.
## §0 — Tiền đề (git, đo LIVE)
```
HEAD = 88e7ecf263f019f4b31d4defb9520704568042a9 2026-08-01 10:10:11 +0700
[CLAUDE] Contract: K4a+K4b SP-002 sidebar 8 nhom x 6 leaf + noi ?group= end-to-end (0-mig)
git log origin/main..HEAD -> (rỗng — đã push)
```
`git diff --name-only 7a903cf..88e7ecf` = 33 file. Phân loại vs `paths-ignore` 7-glob:
- **CODE (13, trigger CI):**
- BE 4: `ContractSigningPlansController.cs` · `ContractSigningPlanFeatures.cs` · `MenuKeys.cs` · `DbInitializer.cs`
- FE-admin 4: `components/Layout.tsx` · `pages/khkk/KhkkCreatePage.tsx` · `pages/khkk/KhkkListPage.tsx` · `types/khkk.ts`
- FE-user 4: y hệt 4 file trên (cặp song sinh)
- test 1: `tests/…/Application/KhkkGroupMenuSeedTests.cs`
- **IGNORED (20):** `docs/STATUS.md` (`**/*.md`) + `.claude/WAL.md` + `.claude/agent-memory/**` ×10 + `.claude/workflows/runs/**` ×8
- **0 file `*Migrations*`** ⇒ K4a+K4b = 0-mig (khẳng định này đo lại độc lập ở §3).
**F-1 `7a903cf` (run #437, chưa verify riêng — lượt này phủ luôn):**
`git diff --name-only 15349e8..7a903cf` = 41 file; CODE 5 (`fe-admin/khkk/KhkkWorkflowPanel.tsx` + `fe-admin/types/khkk.ts` + `fe-user/khkk/KhkkWorkflowPanel.tsx` + `fe-user/types/khkk.ts` + `ContractSigningPlanFeatures.cs`) — khớp mô tả "FE+DTO nhỏ"; phần còn lại ignored-path (`CLAUDE.md`, `docs/**`, `.claude/**`).
### §0.1 — Snapshot PRE đo LIVE lúc run #438 CÒN `running` (10:12-10:14 +07)
Đây là mốc PRE MẠNH NHẤT có thể có: đo trong lúc CI đang chạy ⇒ chắc chắn chưa bị K4a/K4b ghi đè.
**Bundle PRE (curl index.html, 10:12:09):**
```
admin /assets/index-1yiNV4VH.js + /assets/index-Cf3iCOOv.css
eoffice /assets/index-DoULfmdT.js + /assets/index-DX1FRk5n.css
```
✔ Khớp ĐÚNG mốc đề bài lead giao (`1yiNV4VH` / `DoULfmdT`) và khớp nguồn gốc của mốc đó
(`sub-reviewer-gate-k4a.md:131,133` — gate-K4a đo post-#437). **2 nguồn độc lập ⇒ mốc PRE hợp lệ.**
**Prod DB PRE (sqlcmd `localhost\SQLEXPRESS`):**
```
MENU_TOTAL = 151
MENU_KHKK_PCT = 7 (đúng 7 key nhóm-1 không-infix: Khkk_G1/WfView/List/Create/Pending/Approved/Deleted)
MENU_KHKK_G1_LABEL = "1. Kế hoạch ký kết HĐ (NCC-TP)" <- nhãn SKELETON S159, chưa backfill
PERM_TOTAL = 1584
PERM_KHKK_PCT = 91 (= 7 × 13, và CanRead=1 cả 91 ⇒ họ Khkk đang mở đủ)
ROLE_TOTAL = 13 (Accounting/Admin/AuthorizedSigner/CatalogManager/CostControl/DeptManager/
Director/Drafter/Equipment/Finance/HrAdmin/Procurement/ProjectManager)
MIGTOP = 20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime · MIGCOUNT = 71
CONTROL ÂM nền #84 : Hrm*/Off* rows=377 nhưng CanRead=1 chỉ **29** (revoker đang SỐNG, giữ 348 row tắt)
```
⚠️ Cột PK `MenuItems` = **`[Key]`**; bảng quyền `Permissions(Id,RoleId,**MenuKey**,CanRead,CanCreate,CanUpdate,CanDelete)`
— 1 ROW / cặp (role,key) với 4 cột cờ, **KHÔNG phải 4 row**. Bảng role = `Roles` (non-prefix).
🔴 **ĐÍNH CHÍNH MỐC PRE TRONG ĐỀ BÀI (đo, không đoán):** lead giao `MENU_TOTAL PRE=143 ⇒ kỳ vọng 192`.
Đo LIVE ra **151**. Truy PHẠM-VI-QUERY trước khi hô drift (bài #422) → giải thích TRỌN, element-wise:
K3 (#436) đẻ thêm **8 menu-row `AwV2_KhkkN1..N8`** (đo: `MENU_AwV2_ALL=10` = 2 cũ `AwV2_DuyetNcc`/
`AwV2_DuyetNccPhuongAn` + 8 mới, liệt kê đủ tên). **143 + 8 = 151 khít** ⇒ 143 là mốc PRE-K3 đã lão hoá,
không phải drift. **Kỳ vọng đúng cho lượt này = 200, KHÔNG phải 192.**
**w3wp PRE:** `appcmd list wp``WP "4876" (applicationPool:SolutionErp-Api)`, StartTime **01/08 09:11:51**
= đúng cửa-sổ recycle của #437 (09:05:59→09:12:21). ⇒ mốc-so-sánh recycle-proof cho #438.
(VPS có 6 w3wp chung VIETREPORT ⇒ map PID→pool bằng `appcmd`, CẤM đoán PID.)
### §0.2 — DỰ ĐOÁN TỪ SOURCE, ghi ĐĨA TRƯỚC KHI ĐO POST
Đọc `MenuKeys.cs:72-84` + `DbInitializer.cs` (vòng seed menu K4a + `labelBackfill` + grant site-2):
- `KhkkGroupNumbers=[2..8]` (7) × (`KhkkGroup(n)` 1 + `KhkkLeafActions` 6 = `WfView,List,Create,Pending,Approved,Deleted`)
`KhkkGroupMenuKeys` = **7 × 7 = 49 key MỚI** (7 group + 42 leaf).
- Vòng seed menu-row dùng CHÍNH bộ sinh đó ⇒ **MENU_TOTAL 151 → 200** · **MENU_KHKK_PCT 7 → 56**.
- `labelBackfill["Khkk_G1"]` ⇒ Label đổi → **"1. Thiết bị, máy móc, vật tư phụ (A1)"** (key GIỮ, 0 row mới).
- Grant: site-1 loop `MenuKeys.All` cấp **Admin**; site-2 `KhkkKeys().Concat(KhkkGroupMenuKeys)` cấp
**CanRead mọi role, skip-existing** ⇒ hợp lại = 49 key × **13 role** = **+637 ROW**.
**PERM_TOTAL 1584 → 2221** · **PERM_KHKK_PCT 91 → 728** (chéo: 728 = 56 × 13, và 72891 = 637 — hai lối tính khớp).
- ⚠️ Số **196** trong đề bài (`49×4`) là **POLICY** (|All|×4 action, tầng authz — `MenuKeys.cs:57-62` khai đúng vậy),
KHÔNG phải row DB. Hai đại lượng khác nhau, đừng cộng nhầm vào PERM_TOTAL.
- Mig: 0 file `*Migrations*` trong diff ⇒ **GIỮ 71 / top `20260731085624`**.
---
## §1 — Run Gitea Actions — ✅ PASS (2 run: #438 K4a+K4b · #437 F-1)
API `.../actions/tasks?limit=8` (anon, public repo). Match theo **`head_sha`**, KHÔNG theo run_number:
```
id=551 | run# 438 | 88e7ecf263 | status=success | concl=None | 10:10:19 -> 10:16:42 +07 <- K4a+K4b
id=550 | run# 437 | 7a903cf108 | status=success | concl=None | 09:05:59 -> 09:12:21 +07 <- F-1 (phủ lượt này)
id=549 | run# 436 | 15349e8ee0 | status=success | concl=None | 31/07 18:21:48 -> 18:27:59 <- K3 (neo thứ tự)
```
- **#438 duration 6m23s** · **#437 duration 6m22s** — in-band (#436 6m11s · #435 6m12s · #434 6m31s).
- **Control neo thứ tự 2 tầng:** run kề dưới #438 = `7a903cf` (F-1), kề dưới nữa = `15349e8` (K3, đã verify ở
`sub-cicd-verify-k3.md`) ⇒ chuỗi #436#437#438 liền mạch, **không có run lạ chen giữa**, không nhầm run.
- Tao **đo LIVE lúc #438 còn `running`** (poll 10:12→10:16:51, 3 nhịp `running` rồi `success`) ⇒ không dính bẫy
false-positive "verify khi còn running" (Run #242).
- `concl=None` là bình thường với endpoint `tasks` ⇒ tin `status` (gate test chạy TRƯỚC build/deploy).
- URL: `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/438` (và `/437`).
## §2 — Gate CI **621 = 45 + 576** — ✅ PASS (khớp TỪNG VẾ + đo luôn F-1)
Log web-UI `.../actions/runs/{438,437}/jobs/0/logs` (HTTP 200; 18.665 B và 18.689 B). Đọc bằng **bytes**
(text-mode crash cp1252); regex nới khoảng-trắng vì log dùng **2 space** sau `Passed!`:
```
#438 Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 — SolutionErp.Domain.Tests.dll
#438 Passed! - Failed: 0, Passed: 576, Skipped: 0, Total: 576 — SolutionErp.Infrastructure.Tests.dll
#437 Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 — SolutionErp.Domain.Tests.dll
#437 Passed! - Failed: 0, Passed: 569, Skipped: 0, Total: 569 — SolutionErp.Infrastructure.Tests.dll
```
- **#438 = 45 + 576 = 621** ✔ khớp kỳ vọng TÁCH-PHẦN (45D/576I), 0 fail / 0 skip.
- **Chuỗi delta khép kín 3 mốc** (đây là chỗ việc phủ-2-run trả giá trị): #436 K3 = 45+569 = 614 →
**#437 F-1 = 45+569 = 614 (Infra GIỮ 569, +0)** → **#438 = 45+576 = +7**.
⇒ F-1 đúng là "FE+DTO nhỏ, 0 test mới"; **toàn bộ +7 thuộc về K4a+K4b**, khớp `KhkkGroupMenuSeedTests.cs`
(file test DUY NHẤT trong diff #438). Domain **GIỮ 45** suốt 3 run.
- **Control ÂM:** `grep -c 'Failed!'` = **0** trên CẢ HAI log ⇒ 2 dòng `Passed!` là TOÀN BỘ project,
không có project thứ 3 bị nuốt (nếu fail, dotnet in `Failed! - Failed: N…`).
## §3 — Mig **GIỮ 71** — ✅ PASS (2 nguồn ĐỘC LẬP repo ⟂ prod)
```
prod: SELECT MAX(MigrationId) -> 20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime ; COUNT(*) = 71
repo: git diff --name-only 7a903cf..88e7ecf -- '*Migrations*' -> (rỗng)
```
- MIGCOUNT 71 (§0.1 PRE) == 71 (POST) ⇒ **không có mig lén chen vào**; top y nguyên mốc K2.
- ⚠️ Mục "GIỮ NGUYÊN" tự nó ít bit ⇒ răng lấy từ **control DƯƠNG cùng phiên**: §4 chứng minh DbInitializer
ĐÃ chạy (seed 49 row + backfill 1 label) ⇒ startup-hook SỐNG. "Mig không nhích" là vì **không có mig**,
chứ không phải vì app không khởi động lại.
## §4 — Seed 49 menu-row + 637 permission-row + backfill label — ✅ PASS (TAM-GIÁC 3 NGUỒN, khớp TỪNG ĐỒNG)
Mọi số dưới đo **SAU recycle** (recycle-proof ở §4.1). Đối chiếu với dự đoán §0.2 **đã ghi đĩa TRƯỚC khi đo**:
| Chỉ số | PRE (§0.1) | DỰ ĐOÁN §0.2 | ĐO POST | |
|---|---|---|---|---|
| `MENU_TOTAL` | 151 | **200** | **200** | ✔ |
| `MenuItems` LIKE `Khkk%` | 7 | **56** | **56** | ✔ |
| `Khkk_G1`.Label | "1. Kế hoạch ký kết HĐ (NCC-TP)" | "1. Thiết bị, máy móc, vật tư phụ (A1)" | **đúng chuỗi đó** | ✔ |
| `PERM_TOTAL` | 1584 | **2221** | **2221** | ✔ |
| `Permissions` LIKE `Khkk%` | 91 | **728** (=56×13) | **728** | ✔ |
| … trong đó `CanRead=1` | 91 | 728 | **728** (100%) | ✔ |
| `MIGCOUNT` | 71 | 71 | **71** | ✔ |
**3 NGUỒN ĐỘC LẬP cùng ra 49 / 637** (khuôn tam-giác K1):
- (a) **source-parse**: `KhkkGroupNumbers`[2..8] × (1 group + `KhkkLeafActions` 6) = 7×7 = 49; grant 49 × 13 role = 637.
- (b) **sqlcmd prod**: menu 151→200 (+49) · perm 1584→2221 (**+637**).
- (c) **Serilog prod** (nguồn thứ 3, độc lập hoàn toàn với 2 nguồn trên):
```
10:16:30.458 [INF] Seeded 49 menu items
10:16:30.531 [INF] Backfilled 1 menu labels
10:16:30.711 [INF] Seeded 49 admin permissions
10:16:31.298 [INF] Seeded all-roles review perms: 588 added + 0 upgraded (139 keys × 13 roles)
```
🔑 **Phân rã 637 khớp CHÍNH XÁC cơ chế 2-site**: site-1 (loop `MenuKeys.All`, chỉ Admin) = **49**;
site-2 (`KhkkKeys().Concat(KhkkGroupMenuKeys)`, mọi role, **skip-existing** nên Admin không đếm lại) =
**588 = 49 × 12 role còn lại**. **49 + 588 = 637** ✔. Con số 588 này KHÔNG suy ra được từ tổng —
nó chỉ khớp nếu đúng cơ chế skip-existing ⇒ đây là bit thông tin thật, không phải trùng khớp số học rỗng.
`0 upgraded` cũng đúng: 49 key MỚI toàn đi nhánh INSERT.
**SHAPE element-wise 8 nhóm** (không chỉ đếm — soi từng dòng, khớp bảng nhãn owner `danh-muc-sp002-transcribe.md`):
```
Khkk_G1|ord=1|kids=6| 1. Thiết bị, máy móc, vật tư phụ (A1) <- nhãn ĐÃ backfill
Khkk_G2|ord=2|kids=6| 2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)
Khkk_G3|ord=3|kids=6| 3. Hoạt động văn phòng (A4)
Khkk_G4|ord=4|kids=6| 4. Vật tư (B1)
Khkk_G5|ord=5|kids=6| 5. Thầu phụ xây dựng (B2)
Khkk_G6|ord=6|kids=6| 6. Thầu phụ MEP (B3)
Khkk_G7|ord=7|kids=6| 7. BCH công trường (C1-C3)
Khkk_G8|ord=8|kids=6| 8. HĐ chưa ủy quyền — CEO (5.2)
```
8 nhóm × đúng **6 kids** mỗi nhóm · Order 1..8 liền không trùng · 8/8 nhãn khớp bảng owner từng ký tự.
**Control ÂM (chứng "đúng NGƯỜI, đúng TẬP", không quét bừa):**
- `CTRL_OUTOFSET_G0_G9plus = 0` — không đẻ `Khkk_G0`/`Khkk_G9+` ngoài miền 1..8.
- **Revoker #84 còn SỐNG mà KHÔNG đụng Khkk**: `Hrm*`/`Off*` có 377 row nhưng `CanRead=1` chỉ **29**
**y hệt PRE (29)**. Nếu revoker chết thì số này bung lên 377; nếu revoker quét lố sang `Khkk_*` thì 728
đã tụt. Cả hai đều không xảy ra ⇒ checker còn răng VÀ phạm vi đúng (chân-kiềng iii).
## §4.1 — Recycle-proof (2 nguồn) — mọi số §4 đo SAU khởi động lại
- `appcmd list wp``WP "7020" (applicationPool:SolutionErp-Api)`; `Get-Process w3wp` → PID 7020
StartTime **01/08 10:16:22**. **KHÁC PRE (PID 4876 @ 09:11:51)** ⇒ pool ĐÃ recycle, đúng cửa-sổ deploy #438.
(VPS có 6 w3wp chung VIETREPORT ⇒ map PID→pool bằng `appcmd`, CẤM đoán PID.)
- Nguồn 2: dòng **ĐẦU TIÊN** của `solution-erp-20260801.log` = `10:16:23.961 [INF] Applying migrations...`
(deploy xoá `logs/` ⇒ dòng đầu = boot). Seed log 10:16:30 nằm sau đó ⇒ đúng lần boot này.
## §6 — Bundle ×2 — DỰ ĐOÁN TỪ SOURCE **trước khi curl** (ghi đĩa trước, đo sau)
🔴 **LOẠI marker lead gợi ý — nó KHÔNG CÓ RĂNG.** Đề bài đề xuất `Kết cấu, hạ tầng, phá dỡ` (từ
`KHKK_APPROVAL_GROUP_LABELS`) với kỳ vọng "0-hit PRE / 1-hit POST". Đo nguồn:
`git show 7a903cf:fe-user/src/types/khkk.ts | grep -c` = **1** (và fe-admin cũng **1**) ⇒ chuỗi này ĐÃ CÓ
từ trước K4 (nó vào repo ở wave trước, K4 chỉ thêm comment quanh nó) ⇒ **đã nằm sẵn trong bundle PRE**
⇒ 0 bit. Cùng lớp sai lầm với `earlyFinalizeLocked` (§5 k3). Thay bằng 3 marker sau, đều là **string
literal** (minifier giữ nguyên) và đều có **transition PRE→POST đo được**:
| Marker | Loại | Nguồn PRE `7a903cf` | Nguồn POST `88e7ecf` | Kỳ vọng bundle LIVE |
|---|---|---|---|---|
| `/khkk/list?group=1` | POS | admin 0 · user 0 | admin 4 · user 4 | **>0 CẢ HAI** |
| `/khkk/list?view=deleted` | NEG | admin 1 · user 1 | admin 0 · user 0 | **0 CẢ HAI** |
| `workflow-matrix?type=10&group=` | DIFFERENTIAL | admin 0 · user 0 | admin 1 (**trong `//` comment**) · user 2 (**code**) | **admin 0 · user 2** |
- NEG **ĐỘC QUYỀN** (bài "control ÂM phải độc quyền"): `grep -rn` toàn `fe-admin/src`+`fe-user/src` bản POST
= **0 hit** ⇒ vắng mặt trong bundle là vắng mặt THẬT, không phải chuỗi còn sống ở chỗ khác.
- DIFFERENTIAL là marker mạnh nhất lượt này, **2 mũi cùng lúc**: (1) chứng bất-đối-xứng CỐ Ý đã ship
(fe-admin cố ý KHÔNG nới regex `Khkk_G*` — lead chốt 27/07), (2) **tự chứng comment bị strip**
admin có đúng 1 lần xuất hiện và nó nằm trong `//` (dòng 39) nên phải ra 0; nếu admin ra 1 thì hoặc
comment lọt vào bundle hoặc tao grep nhầm file.
### §6.1 — ĐO BUNDLE (sau khi dự đoán trên đã ghi đĩa) — ✅ PASS, khớp 3/3 marker
| App | PRE (§0.1) | POST (đo 10:23) | CT | Size | Last-Modified |
|---|---|---|---|---|---|
| admin js | `index-1yiNV4VH.js` | **`index-qOcoitOr.js` ROTATE** | application/javascript | 1.756.651 | 01/08 03:15:14 GMT = **10:15:14 +07** |
| admin css | `index-Cf3iCOOv.css` | `index-Cf3iCOOv.css` FROZEN | text/css | 86.364 | **10:15:14 +07** |
| eoffice js | `index-DoULfmdT.js` | **`index-D9J3BbcA.js` ROTATE** | application/javascript | 1.673.668 | 01/08 03:16:16 GMT = **10:16:16 +07** |
| eoffice css | `index-DX1FRk5n.css` | `index-DX1FRk5n.css` FROZEN | text/css | 92.148 | **10:16:16 +07** |
- **JS ROTATE ×2 = ĐÚNG YÊU CẦU** (cả 2 app đổi code thật: Layout + ListPage + CreatePage + types).
- Cả 4 LM **trong cửa-sổ deploy** 10:10:19→10:16:42 ⇒ deploy CÓ chạm cả 4 file (#69: LM-in-window mới là
ship-proof, hash-delta thì không). **CSS FROZEN lành tính**: K4a/K4b không thêm class Tailwind mới
(chỉ đổi chuỗi route + logic), LM vẫn in-window ⇒ re-ship byte-identical (#69 mode-B), KHÔNG phải kẹt bundle cũ.
- **Control ÂM 2 tầng** — hash CŨ **và** hash BỊA cùng rơi `text/html` 900 B / 876 B:
`admin/index-1yiNV4VH.js`→text/html 900 · `eoffice/index-DoULfmdT.js`→text/html 876 · `admin/index-ZZfakeZZ.js`→text/html 900.
`text/html` thật sự nghĩa "không tồn tại" (SPA-fallback), và **bundle PRE đã biến mất khỏi đĩa = chứng ghi-đè**.
**Byte-marker đo trên file tải về (`adm.js` 1.738.624 ký tự · `eof.js` 1.658.019 ký tự):**
| Marker | Kỳ vọng §6 | admin | eoffice | |
|---|---|---|---|---|
| POS `/khkk/list?group=1` | >0 cả hai | **4** | **4** | ✔ (khớp ĐÚNG 4 = số lần trong source mỗi app — element-wise, không chỉ ">0") |
| NEG `/khkk/list?view=deleted` | 0 cả hai | **0** | **0** | ✔ (chuỗi độc quyền, PRE source =1 ⇒ transition đo được) |
| DIFF `workflow-matrix?type=10&group=` | admin 0 · user 2 | **0** | **2** | ✔ (bất-đối-xứng cố ý ship + comment bị strip đúng như dự đoán) |
| _(marker lead gợi ý)_ `Kết cấu, hạ tầng, phá dỡ` | — | **1** | **1** | 🔴 **=1 ở CẢ HAI ⇒ xác nhận bằng ĐO rằng nó 0-bit**: có mặt bất kể K4 ship hay không |
## §5 — Acceptance A1 (quyền user-thường) + A5 (idempotent qua restart ×2) — ✅ PASS
**A1 — SQL JOIN `Permissions × Roles × MenuItems`, role NON-admin:**
```
Procurement : GROUP = 8 LEAF = 48 (CanRead=1)
Drafter : GROUP = 8 LEAF = 48 (CanRead=1)
```
- Khớp ĐÚNG kỳ vọng đề bài "8 group + 48 leaf". Toán khép: group 8 = `Khkk_G1` + `Khkk_G2..G8`;
leaf 48 = 6 (nhóm-1 không-infix) + 42 (nhóm 2..8) ⇒ tổng 56 = `MENU_KHKK_PCT`.
- Toàn họ: `PERM_KHKK_CANREAD1 = 728 = 56 × 13`**KHÔNG role nào bị hụt** (nếu 1 role sót thì ≤ 715).
**A1-bis — chứng ĐI TRỌN STACK (nguồn thứ 3, mạnh nhất): `GET /api/menus/me` materialize qua EF**
(http 200, 44.656 B; parse cây theo `key`, không grep thô):
```
GROUP distinct = 8 LEAF distinct = 42 per-group = {G2..G8 : 6 mỗi nhóm}
OLD nhóm-1 leaf non-infix = 6 [Khkk_Approved, Khkk_Create, Khkk_Deleted, Khkk_List, Khkk_Pending, Khkk_WfView]
TOTAL Khkk* distinct = 56
Khkk_G1 -> 1. Thiết bị, máy móc, vật tư phụ (A1) Khkk_G5 -> 5. Thầu phụ xây dựng (B2)
Khkk_G2 -> 2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3) Khkk_G6 -> 6. Thầu phụ MEP (B3)
Khkk_G3 -> 3. Hoạt động văn phòng (A4) Khkk_G7 -> 7. BCH công trường (C1-C3)
Khkk_G4 -> 4. Vật tư (B1) Khkk_G8 -> 8. HĐ chưa ủy quyền — CEO (5.2)
```
⇒ 8 nhãn ra ĐÚNG tới dấu tiếng Việt qua đường Domain→Infra→Api (không phải chỉ có row trong bảng);
`per-group = 6 đều` chứng không nhóm nào thiếu leaf. **Đây là cái sqlcmd không nói được.**
**A5 — RESTART APP-POOL ×2 THẬT** (`appcmd recycle apppool /apppool.name:SolutionErp-Api`, KHÔNG gọi seeder lẻ
qua reflection — chuỗi `SeedAsync` đủ chỉ chạy lúc boot):
| Mốc | w3wp PID | StartTime | MENU_TOTAL | Khkk menu | PERM_TOTAL | Khkk perm | CanRead=1 | A1 Proc leaf | CTRL Hrm/Off CanRead=1 |
|---|---|---|---|---|---|---|---|---|---|
| sau deploy | 7020 | 10:16:22 | 200 | 56 | 2221 | 728 | 728 | 48 | **29** |
| **restart #1** | **220** | **10:23:55** | 200 | 56 | 2221 | 728 | 728 | 48 | **29** |
| **restart #2** | **6636** | **10:24:38** | 200 | 56 | 2221 | 728 | 728 | 48 | **29** |
- **Restart là THẬT, không no-op**: PID đổi 3 lần (7020→220→6636) + `appcmd` xác nhận đúng pool
`SolutionErp-Api` + Serilog có **3 dòng `Applying migrations...`** (10:16:23 · 10:23:57 · 10:24:39).
- **Idempotent**: sau boot 2 và 3, Serilog **KHÔNG có** `Seeded … menu items` / `Seeded … admin permissions` /
`Seeded all-roles review perms`. Đây là suy luận CÓ CƠ SỞ, không phải đoán từ im-lặng — đã soi điều kiện
log trong source: `DbInitializer.cs:2180-2183 if (added > 0)` · `:2247 if (updatedLabels > 0)` ·
`:2310 if (added > 0)` · `:2524 if (added > 0 || upgraded > 0)`. ⇒ vắng dòng ⟺ **0 added / 0 upgraded**.
Cộng với 8 con số DB đứng im hoàn toàn ⇒ seeder hội tụ, không nhân bản row.
- **Revoker-proof (#84)**: `Hrm*`/`Off*` giữ **29/377** CanRead=1 qua cả 3 mốc. Đây là control ÂM có RĂNG
2 chiều: revoker vẫn CHẠY mỗi boot (nếu chết → bung lên 377) và vẫn KHÔNG chạm `Khkk_*` (nếu quét lố →
728 tụt). Đúng như comment K4a khai ở `DbInitializer.cs` ("filter chỉ bắt prefix `Hrm*`/`Off*` + `Personal`").
- Nhãn `Khkk_G1` giữ nguyên bản backfill qua cả 2 restart ⇒ `labelBackfill` không lật ngược về skeleton.
## §7 — Smoke prod — ✅ PASS
```
GET /api/contract-signing-plans (no token) -> 401 authz đóng đúng
GET /api/contract-signing-plans/deleted?group=1 (no token) -> 401 route MỚI cũng đóng
GET /api/contract-signing-plans-khong-ton-tai-xyz -> 404 \ 2 route BỊA
GET /api/route-rac-k4ab-999 -> 404 / ⇒ 401 ở trên CÓ NGHĨA
GET /health/ready -> 200
GET /health/live -> 200
GET https://admin.solutions.com.vn/ -> 200
GET https://eoffice.solutions.com.vn/ -> 200
```
🔑 **401 chỉ có nghĩa vì đi kèm 404 của route bịa** — nếu server nuốt mọi path thành 401 thì phép đo rỗng.
**Giới hạn phép đo đã gỡ được (đề bài dự phòng đúng):** `?group=abc` **không token → 401**, tức authz chặn
TRƯỚC model-binding ⇒ không đọc được bind-error. Đã lấy bearer admin re-probe để đóng lỗ này:
```
(có token) ?group=abc -> 400 <- BIND-ERROR: param ĐƯỢC WIRE thật (nếu bị nuốt, ASP.NET trả 200)
(có token) ?group=1 -> 200 ?group=8 -> 200 (không group) -> 200
(có token) /deleted?group=1 -> 200
body ?group=1 = {"items":[],"total":0,"page":1,"pageSize":20,"totalPages":0,"hasNext":false,"hasPrev":false}
```
`?group=` **nối end-to-end THẬT** (chứng bằng 400 bind-error, không bằng lời khai).
⚠️ **2 quan sát KHÔNG hạ verdict, chuyển tiếp cho wave sau:**
1. **Chân-lý-rỗng còn nguyên (kế thừa K2):** `ContractSigningPlans` prod vẫn **0 dòng**`items:[]` với MỌI
group. Phép đo `?group=n` chứng được **đường ống + binding**, KHÔNG chứng được **lọc đúng nhóm**
backfill/lọc sai sẽ trông y hệt. Muốn có răng phải có ≥1 phiếu thật ở ≥2 nhóm khác nhau.
2. `?group=999`**200** (không phải 400/404): biên API vẫn không chặn ngoài-miền 1..8 — **y hệt K2 ghi
nhận**, chưa hồi quy nhưng cũng chưa đóng. Vô hại hôm nay vì bảng rỗng.
---
END sub-cicd-verify-k4ab — VERDICT=PASS 7/7
(1: run #438 success 6m23s match head_sha + #437 F-1 success 6m22s, chuỗi #436#437#438 liền mạch ·
2: gate 621 = 45D + 576I tách-phần, F-1 = 614 (+0) ⇒ trọn +7 thuộc K4, control ÂM Failed!=0 cả 2 log ·
3: Mig GIỮ 71 / top 20260731085624, 2 nguồn repo ⟂ prod ·
4: seed 49 menu + 637 perm + backfill 1 label — TAM-GIÁC 3 nguồn (source-parse ⟂ sqlcmd ⟂ Serilog
49+588=637), shape 8 nhóm × 6 kids element-wise, recycle-proof PID 4876→7020 + Serilog dòng đầu ·
5: A1 Procurement/Drafter 8-group + 48-leaf + menus/me materialize 8+42+6=56 qua EF; A5 restart ×2 THẬT
(PID 7020→220→6636) 8 chỉ số ĐỨNG IM + log-condition đã soi source ⇒ 0 added; revoker Hrm/Off giữ 29 ·
6: bundle JS ROTATE ×2 (1yiNV4VH→qOcoitOr · DoULfmdT→D9J3BbcA), 4 LM in-window, marker 3/3 khớp
(POS 4/4 · NEG 0/0 · DIFF admin 0 / user 2); marker lead gợi ý ĐO RA 1/1 ⇒ bác vì 0-bit ·
7: smoke 401/401 + 404 route bịa ×2 + 200 ×4; bind-error 400 chứng ?group= wire thật)