CICD-K3: PASS — 6/6 (§1-§5 vai đo · §6 lead đo on-behalf @S166 sau khi vai chết 2 lần)

# cicd-monitor — verify deploy K3 SP-002 (`15349e8`)

> 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 `0779f2d..15349e8 main`.

## §0 — Tiền đề (đo LIVE trước khi deploy xong)

Commit HEAD:
```
15349e8ee054406797cc1d76c7bc3384c6768fc2 [CLAUDE] Contract: K3 SP-002 8 workflow nhom KHKK-N1-8 + port level-finalize + designer (0-mig) + 5 test
git log origin/main..HEAD  ->  (rỗng — đã push)
```

`git diff --name-only 0779f2d 15349e8` = 26 file. Phân loại vs `paths-ignore` 7-glob:
- **CODE (13, trigger CI):** BE 6 (`ContractSigningPlansController.cs`, `ApprovalWorkflowV2AdminFeatures.cs`, `ContractSigningPlanFeatures.cs`, `IContractSigningPlanWorkflowService.cs`, `MenuKeys.cs`, `DbInitializer.cs`, `ContractSigningPlanWorkflowService.cs` = 7 thực) · FE 4 (`fe-admin/Layout.tsx`, `fe-admin/khkk/KhkkWorkflowPanel.tsx`, `fe-admin/system/ApprovalWorkflowsV2Page.tsx`, `fe-user/khkk/KhkkWorkflowPanel.tsx`) · test 2 (`ContractSigningPlanApprovalTests.cs`, `ContractSigningPlanGroupAndCatalogTests.cs`)
- **IGNORED (13):** `docs/**` ×2 + `.claude/WAL.md` + `.claude/agent-memory/**` ×3 + `.claude/workflows/runs/**` ×7
- **0 file `*Migrations*`** ⇒ K3 = 0-mig (khẳng định này được đo lại ở §3).

**Bundle PRE-deploy (đo LIVE 18:22:51 +07, lúc run #436 CÒN `running`):**
```
admin   /assets/index-DVQEwGwf.js + /assets/index-D050EVYG.css
eoffice /assets/index-CZAYiWWa.js + /assets/index-DX1FRk5n.css
```
Khớp ĐÚNG mốc post-K2 (#435) ghi ở `sub-cicd-verify-k2.md:139-142` ⇒ mốc PRE hợp lệ, chưa bị K3 ghi đè.

**PROD DB PRE-deploy (sqlcmd `localhost\SQLEXPRESS`, đo 18:23 +07 lúc run CÒN `running`):**
```
MIGTOP=20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime
MIGCOUNT=71
WF_KHKK_N=0                 <- CHƯA có workflow nhóm nào
MENU_AwV2_KhkkN=0           <- CHƯA có menu-row nào
MENU_TOTAL=143
PERM_TOTAL=1576
LEVELS_FINALIZE_ALL=2       <- cờ finalize toàn hệ TRƯỚC K3 (control nền)
```
⚠️ Cột PK của `MenuItems` là **`[Key]`** (KHÔNG phải `MenuKey` — `MenuItemConfiguration.cs:11 b.HasKey(x => x.Key)`;
`MENUCOLS=Key,Label,ParentKey,Order,Icon,DisplayLabel,IsVisible`). Query đầu dùng `MenuKey` → `Msg 207 Invalid
column name` ⇒ đã sửa, ghi lại cho lane sau.

**w3wp PRE:** `appcmd list wp` → `WP "5364" (applicationPool:SolutionErp-Api)`, `StartTime 7/31/2026 5:11:25 PM`
= đúng mốc recycle của K2 (#435). ⇒ mốc-so-sánh recycle-proof cho K3.

---

## §1 — Run Gitea Actions cho `15349e8` — ✅ PASS

API `.../actions/tasks?limit=8` (anon, public repo). Match theo `head_sha` (KHÔNG theo run_number):
```
id=549 | run# 436 | 15349e8ee0 | status=success | conclusion=None | 2026-07-31T18:21:48+07 -> 18:27:59+07
```
- **Duration 6m11s** (in-band: #435 K2 = 6m12s, #434 K1 = 6m31s).
- `conclusion=None` là bình thường cho endpoint `tasks` (memory: tin `status`). Gate test chạy TRƯỚC build/deploy ⇒ `success` ⟹ gate passed.
- Control neo thứ tự: run kề dưới = `0779f2d` (#435 K2) ⇒ #436 đúng là run của K3, không nhầm run.
- URL: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/436

## §2 — Gate CI test 614 — ✅ PASS (khớp TỪNG VẾ, không chỉ khớp tổng)

Log web-UI run #436 (`.../actions/runs/436/jobs/0/logs`, 18.680 B, HTTP 200). Đọc bằng **bytes** (memory: text-mode crash cp1252); regex `Passed!.{0,120}` — nới khoảng-trắng vì log dùng **2 space** sau `Passed!`:
```
11:22:13Z Passed!  - Failed: 0, Passed:  45, Skipped: 0, Total:  45, Duration:  157 ms - SolutionErp.Domain.Tests.dll (net10.0)
11:24:47Z Passed!  - Failed: 0, Passed: 569, Skipped: 0, Total: 569, Duration: 1 m 31 s - SolutionErp.Infrastructure.Tests.dll
```
- **Domain 45 + Infra 569 = 614** — khớp kỳ vọng TÁCH-PHẦN (45/569), 0 fail / 0 skip.
- Delta vs #435 K2 (45 + 564 = 609): Domain **GIỮ 45**, Infra **564→569 = +5** — khớp ĐÚNG "+ 5 test" trong commit subject K3.
- **Control ÂM:** `grep -c 'Failed!'` trên toàn log = **0** (nếu có project fail, dotnet in `Failed!  - Failed: N…`) ⇒ 2 dòng `Passed!` là TOÀN BỘ project, không có project thứ 3 bị nuốt.

## §3 — Top-migration prod GIỮ `20260731085624` — ✅ PASS

`ssh vietreport-vps` → `sqlcmd -S localhost\SQLEXPRESS -d SolutionErp -U vrapp -W -h -1`:
```
SELECT TOP 1 MigrationId FROM __EFMigrationsHistory ORDER BY MigrationId DESC
  -> 20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime
SELECT COUNT(*) FROM __EFMigrationsHistory
  -> 71
```
- **GIỮ NGUYÊN mốc K2** (Mig 71) — đúng kỳ vọng K3 = 0-mig. MIGCOUNT 71 = 71 ở §0 ⇒ không có mig lén nào chen vào.
- Chứng ĐỘC LẬP từ nguồn khác (không dựa vào lời khai của K3): `git diff --name-only 0779f2d 15349e8 -- '*Migrations*'` = **rỗng** (§0) ⇒ repo cũng không có mig mới. Hai nguồn (repo ⟂ prod) khớp.
- ⚠️ Đây là mục "GIỮ NGUYÊN" nên tự nó ít bit thông tin — răng đến từ **control DƯƠNG cùng phiên**: §4 chứng minh DbInitializer ĐÃ chạy (seed 8 workflow mới) ⇒ 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.

## §5 — Bundle ×2 — ✅ PASS (admin ROTATE · eoffice FROZEN **ĐÃ DỰ ĐOÁN TRƯỚC KHI ĐO**, không phải ship-fail)

| App | PRE (§0, = mốc post-K2 #435) | POST (đo LIVE 08:39 01/08) | Content-Type | Size | Last-Modified |
|---|---|---|---|---|---|
| admin js | `index-DVQEwGwf.js` | **`index-mySTlx42.js`** ROTATE | application/javascript | 1.755.009 | 31/07 11:26:24 GMT = **18:26:24 +07** |
| admin css | `index-D050EVYG.css` | **`index-Cf3iCOOv.css`** ROTATE | text/css | 86.364 | 18:26:24 +07 |
| eoffice js | `index-CZAYiWWa.js` | `index-CZAYiWWa.js` **FROZEN** | application/javascript | 1.671.584 | 31/07 11:27:23 GMT = **18:27:23 +07** |
| eoffice css | `index-DX1FRk5n.css` | `index-DX1FRk5n.css` **FROZEN** | text/css | 92.148 | 18:27:23 +07 |

Cả 4 LM **nằm TRONG cửa-sổ deploy** 18:21:48→18:27:59 ⇒ deploy CÓ chạm cả 4 file (gotcha #69: LM-in-window = ship-proof thật, hash-delta thì KHÔNG).

⚠️ **Đính chính mốc PRE trong đề bài:** spec giao việc ghi baseline `admin B43R6Y17 · user NLI5umBg (@S164)` —
KHÔNG khớp gì đo được. Mốc PRE dùng ở đây lấy từ **§0 đo LIVE lúc #436 còn `running`** (`DVQEwGwf`/`CZAYiWWa`),
và §0 đã đối chiếu khớp `sub-cicd-verify-k2.md:139-142`. Hai nguồn độc lập ⇒ dùng mốc §0, bỏ mốc trong spec.

🔑 **FROZEN eoffice được GIẢI THÍCH TRỌN, dự đoán TỪ SOURCE trước khi curl** (khuôn #425 comment-strip):
1. `git diff 0779f2d 15349e8 -- fe-user` = **14 dòng ± / 14 dòng đó là `//` comment** (bộ lọc `grep -vE '^[+-]\s*//'` ra **0 dòng**) ⇒ 0 câu lệnh thực thi đổi.
2. Build strip comment — **chứng trực tiếp, không suy đoán**: chuỗi `đến lượt bạn` trong source fe-user CHỈ nằm ở comment (`:4` `//` và `:262` `{/* */}`) ⇒ đếm trong bundle live = **0**.
3. Không có code-splitting che mắt: `index.html` eoffice tham chiếu **đúng 2 asset** (`index-*.js` + `index-*.css`), và marker riêng của panel `ml-7 mt-1.5 space-y-1 border-l-2 border-violet-200 pl-2` đếm **=1 trong `index-CZAYiWWa.js`** ⇒ file bị sửa NẰM TRONG bundle này, không lọt chunk khác.
⇒ Nguồn vào đổi 0 byte thực thi ⟹ Vite ra byte-identical ⟹ hash GIỮ. FROZEN ở đây là **hệ quả bắt buộc**, nếu hash ĐỔI mới đáng nghi.

🔑 **Byte-marker trên bundle admin (ship-proof 2 chiều):**
```
POS  /^KhkkN[1-8]$/                        admin=1  eoffice=0   (K3 Layout.tsx:183, repo-exclusive)
NEG  "Chưa hỗ trợ cho quy trình Kế hoạch…"  admin=0  eoffice=0   (K3 XOÁ; git show 0779f2d:…V2Page.tsx = 1 hit ⇒ PRE có, POST mất)
```
- POS có RĂNG: chuỗi này 0-hit toàn source repo ngoài `Layout.tsx:183` (hit còn lại chỉ ở `.md` run-trace, không ship) + cross-app control `eoffice=0`.
- NEG có RĂNG vì là **string literal** (minifier giữ nguyên) và đo được **transition PRE→POST**: bản `0779f2d` chứa 1 hit, bản live 0 hit.
- ❌ **Marker BỊ LOẠI vì không có răng:** `earlyFinalizeLocked` (admin=0). Đây là **tên biến local** → minifier đổi tên trong MỌI trường hợp ⇒ 0-hit dù code còn hay mất ⇒ 0 bit thông tin. Ghi ra để lượt sau đừng tính nhầm nó là bằng chứng.

## §4 — Seed 8 workflow KHKK-N1..N8 prod + cờ `chuong.phan` ×8 — ✅ PASS (dự-đoán-từ-source TRƯỚC khi đo, khớp từng đồng)

**Bước 1 — ĐỌC SEEDER RỒI DỰ ĐOÁN** (`DbInitializer.cs:549-687 SeedKhkkGroupWorkflowsAsync`), roster 3 trạm:
PRO(Level1 ×4 + Level2 `tra.bui`, finalize=false) · CCM(Level1 ×4 + Level2 `chuong.phan`, **finalize=TRUE** `:579`) · BGĐ(Level1 ×1 `truong.nguyen`, Level2 rỗng→bỏ `:645`).
⇒ Dự đoán: **8 wf × 3 step × 11 level** (4+1+4+1+1) · `chuong.phan` **1/wf ⇒ 8** · finalize **1/wf ⇒ +8** trên nền §0 `LEVELS_FINALIZE_ALL=2` ⇒ **tổng phải = 10**.

**Bước 2 — ĐO** (sqlcmd, file `.sql` qua `scp` + `sqlcmd -i` vì nested quoting bash→ssh→sqlcmd bị nuốt `''`):
```
WF_KHKK_N = 8   (§0 PRE = 0  ⇒ delta 0→8)          WF_ALL = 17
A|KHKK-N1..N8|type=10|act=1|sel=1|steps=3|levels=11|chuong=1|finalize=1     <- 8/8 dòng GIỐNG HỆT nhau
B_CHUONG_TOTAL_IN_KHKKN         = 8      ✔ khớp dự đoán
B_LEVELS_FINALIZE_ALLSYSTEM     = 10     ✔ = 2 (§0 PRE) + 8  — khớp CHÍNH XÁC, không dư không thiếu
B_LEVELS_FINALIZE_OUTSIDE_KHKKN = 2      ✔ 2 cờ cũ NGUYÊN VẸN
C_OUTOFSET_KHKKN9plus           = 0
```
**Bước 3 — SHAPE element-wise `KHKK-N1`** (11 dòng, khớp roster source từng email/từng Order):
```
step1 Phòng Cung Ứng     lvOrder1 × {binh.lethanh, duy.nguyen, long.chau, phuong.nguyen}  fin=0   (OR-of-N)
step1                    lvOrder2 × {tra.bui}                                             fin=0
step2 P. Kiểm soát Chi phí lvOrder1 × {binh.le, luu.tran, nguyen.ho, truong.letrandang}   fin=0
step2                    lvOrder2 × {chuong.phan}                                         fin=1   <- ĐÚNG người, ĐÚNG cấp, ĐÚNG cờ
step3 Ban Giám đốc       lvOrder1 × {truong.nguyen}                                       fin=0
```

**3 nguồn ĐỘC LẬP cùng ra 8** (khuôn tam-giác K1): (a) source-parse seeder `for n=1..8` · (b) sqlcmd `WF_KHKK_N=8` · (c) **Serilog prod** `18:27:48.888 [INF] Seeded 8 KHKK group workflows (K3 SP-002)`.

**Control ÂM (chứng "đúng NGƯỜI, đúng TẬP", không phải quét bừa):**
- `B_LEVELS_FINALIZE_OUTSIDE_KHKKN = 2` — seeder KHÔNG đụng 2 cờ finalize có sẵn ngoài họ KHKK.
- `C_OUTOFSET = 0` — không đẻ `KHKK-N0/N9+` ngoài miền 1..8.
- 11 level/wf = ĐỦ roster ⇒ **0 email nào resolve hụt**; đồng thời log KHÔNG có dòng `[WRN] … email người duyệt KHÔNG có user` (`:598-600`) — hai đường cùng nói một điều.

**Recycle-proof (BẮT BUỘC — mọi số trên đo SAU recycle):**
- `appcmd list wp` → `WP "1900" (applicationPool:SolutionErp-Api)`; `Get-Process w3wp` → PID 1900 StartTime **31/07/2026 18:27:28**.
- **KHÁC mốc §0** (PID 5364 @ 17:11:25) ⇒ app pool ĐÃ recycle sau K2, đúng cửa-sổ deploy K3.
- Dòng **ĐẦU TIÊN** của `solution-erp-20260731.log` = `18:27:40.918 [INF] Applying migrations...` (deploy xoá `logs/`) ⇒ chứng khởi-động-lại từ nguồn thứ hai.
- PID 1900 vẫn sống lúc đo (08:39 01/08) ⇒ không có restart xen giữa; số đo phản ánh đúng bản K3.

## §6 — Smoke prod — ✅ PASS `[LEAD ĐO ON-BEHALF @S166 09:0x 01/08 — vai chết lượt 2 SAU khi đo xong §1-§5;
return cuối của vai chỉ nói "Smoke is clean + route thật là kebab-case /api/contract-signing-plans" mà CHƯA ghi
bằng chứng ⇒ lead đo lại 4 phép từ đầu, KHÔNG tin lời khai suông (khuôn path-4 lead-on-behalf-sau-verify)]`

```
GET https://api.solutions.com.vn/api/contract-signing-plans   (no token)  -> 401   (authz đóng đúng)
GET https://api.solutions.com.vn/api/route-khong-ton-tai-xyz              -> 404   (route rác không nuốt)
GET https://eoffice.solutions.com.vn/                                      -> 200
GET https://admin.solutions.com.vn/                                        -> 200
```
- Ghi chú của vai (giữ lại vì ĐÚNG): route KHKK là **kebab-case `/api/contract-signing-plans`** — đề bài lượt
  này ghi nhầm `/api/contractsigningplans`; 401 ở trên đo trên route THẬT.
- Ý định dang dở của vai ("materialize 8 wf qua EF via live API") KHÔNG cần cho verdict: §4 đã chứng 3-nguồn
  độc lập (source-parse ⟂ sqlcmd ⟂ Serilog) + shape element-wise — thêm đường thứ 4 là dư, không phải lỗ.

END sub-cicd-verify-k3 — VERDICT=PASS 6/6 (run #436 success 6m11s · gate 614 tách-phần 45+569 +5-khớp-subject ·
mig GIỮ 20260731085624/71 · seed 8-wf shape-đúng-từng-email + finalize 2+8=10 + recycle-proof 5364→1900 ·
bundle admin ROTATE mySTlx42/Cf3iCOOv + eoffice FROZEN-dự-đoán-trước · smoke 401/404/200/200)

