# cicd-monitor — verify deploy `a8027c0` (W1 KHKK, Mig 69 ContractSigningPlans)

> Agent: cicd-monitor (READ-only) · S161 · 2026-07-29
> Spec acceptance: `.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w1-schema-designer-29-07-2026.md` §③-B
> **Ghi-đĩa-TRONG-lúc-làm** (chống #53). Mỗi mục ghi NGAY khi có bằng chứng. Kết bằng dòng `END … VERDICT=`.

---

## CHÂN 0 — Push + path-filter (pre-condition)

| Mục | Giá trị |
|---|---|
| Commit | `a8027c04fe929750998f5c20e23854f9d0e7a765` |
| Subject | `[CLAUDE] Domain+App+Infra+Admin: W1 KHKK — Mig 69 ContractSigningPlans 7 bang + policy KeHoachKyKet + seeder UPGRADE-if-exists + CodeGen KHKK + Designer type-10` |
| Commit time | 2026-07-29 20:04:39 +0700 |
| `git log origin/main..HEAD` | **rỗng** ⇒ đã push |
| Files changed | **30 file, +8987 / −22** |
| Path-filter (#41) | 29/30 file NON-ignored (`src/Backend/**`, `fe-admin/src/**`, `tests/**`); chỉ `docs/STATUS.md` bị ignore ⇒ **CI PHẢI trigger** ✅ |

**Mig 69 on-disk (3-file rule OK):**
- `20260729122015_AddContractSigningPlans.cs` (372 dòng) + `.Designer.cs` (6960) + `ApplicationDbContextModelSnapshot.cs` (+550)
- `grep -c migrationBuilder.CreateTable` = **7** — đúng 7 bảng, 0 ALTER bảng cũ:
  `ContractSigningPlans` · `…Lines` · `…DossierItems` · `…LevelOpinions` · `…Attachments` · `…Changelogs` · `…Approvals`

**Byte-marker (chân 4) — kiểm ĐỘC QUYỀN TRƯỚC khi đo (luật S159):**
- `rg QT-KHKK-V2-001` toàn repo: **1 hit code** = `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:170`
  (`[TYPE_INT_CONTRACT_SIGNING_PLAN]: 'QT-KHKK-V2-001',` trong `DEFAULT_CODE_BY_TYPE`)
- `fe-user/src` = **0 hit** ⇒ chuỗi ĐỘC QUYỀN cho fe-admin, sinh mới @S161
- ⟹ **Dự đoán TRƯỚC khi đo:** admin bundle mới = **1** occurrence · user bundle = **0** · admin bundle CŨ (`D0sXA0fe`) = 0.

---

## BASELINE PRE-DEPLOY (đo 20:08:31 +0700, TRƯỚC khi run #427 xong)

| Chỉ số | Giá trị pre-deploy |
|---|---|
| `sys.tables WHERE is_ms_shipped=0` | **89** |
| `MAX(MigrationId)` | **`20260727033522_AddPeAllowApproverDelete`** (Mig 68) |
| `COUNT(*) MenuItems` | **142** |
| bảng `ContractSigningPlan%` | **0** |
| admin bundle | js `index-D0sXA0fe.js` · css `index-DWDbm5As.css` |
| user bundle | js `index-Y6dW_5CM.js` · css `index-6YIAufJR.css` |

Kỳ vọng post-deploy: tables **89 → 96** (+7) · MIGTOP → `20260729122015_AddContractSigningPlans` · MenuItems **142 GIỮ** · CSP tables **0 → 7**.

---

### Baseline CỜ policy (chân 5) — đo 20:09, TRƯỚC deploy

13 role prod. 8 key họ KHKK, **tất cả** = `rows=13 · R=13 · C=0 · U=0 · D=0`:
`KeHoachKyKet` · `Khkk_G1` · `Khkk_WfView` · `Khkk_List` · `Khkk_Create` · `Khkk_Pending` · `Khkk_Approved` · `Khkk_Deleted`
⟹ xác nhận tiền-đề F-S1: **row ĐÃ có sẵn CanRead-only** ⇒ đếm ROW = 0-bit, chỉ CỜ mới chứng UPGRADE ăn.

### DỰ ĐOÁN TRƯỚC KHI ĐO (đọc seeder `DbInitializer.SeedKeHoachKyKetAccessAsync` trong diff, KHÔNG đọc kết quả)

Tập key seeder đụng = **7** (`:2405+`): `KeHoachKyKet` + `Khkk_{WfView,List,Create,Pending,Approved,Deleted}`.
🔑 **`Khkk_G1` CỐ Ý NGOÀI tập** (comment: "group điều hướng — KHÔNG cần cờ ghi") ⟹ **control ÂM nội-họ**: nếu `Khkk_G1` cũng bật C/U thì phép đo của tao vô nghĩa (chứng tỏ có thứ khác lật bừa cả họ).

| Đại lượng | Dự đoán |
|---|---|
| 7 key đích: `SUM(CanCreate)` | **13** mỗi key (13/13 role) |
| 7 key đích: `SUM(CanUpdate)` | **13** mỗi key |
| 7 key đích: `SUM(CanDelete)` | **2** mỗi key (CHỈ Admin + Drafter) — chiều ÂM: 11 role còn lại phải =0 |
| `Khkk_G1` (control ÂM) | **C=0 · U=0 · D=0 GIỮ NGUYÊN** |
| Log seeder | `0 added + 91 upgraded` (91 = 7 key × 13 role, mọi row tồn tại sẵn) |
| MenuItems | **142 GIỮ** (seeder không thêm menu-row; `KeHoachKyKet` menu-row đã có từ S159) |

---

## CHÂN 1 — CI run

| Mục | Giá trị |
|---|---|
| Run | **#427** (task id **540**) |
| head_sha | `a8027c04` ✅ khớp (key theo sha, KHÔNG theo run_number) |
| created_at | 2026-07-29T20:04:48+07:00 |
| **Kết luận** | **`status=success`** @2026-07-29T20:10:28+07:00 |
| Duration | **5m40s** (20:04:48 → 20:10:28) — trong dải ~5m30-6m gần đây |
| Run URL | https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/427 |

**Test gate (log web-UI `…/actions/runs/427/jobs/0/logs`, 17.936 B):** 2 dòng `Passed!`, đối chiếu TÁCH-PHẦN:

```
Passed!  - Failed:     0, Passed:    45, Skipped:     0, Total:    45     <- SolutionErp.Domain.Tests
Passed!  - Failed:     0, Passed:   521, Skipped:     0, Total:   521     <- SolutionErp.Infrastructure.Tests
```

⟹ **566 PASS (45 D + 521 I), 0 fail, 0 skip** = ĐÚNG con số spec chờ (562 → 566, +4 test mới W1). ✅

**Dòng deploy (cuối log, giờ UTC trong log = +07 −7h):**
```
2026-07-29T13:10:05.8595528Z Deploy done. App pool started.
2026-07-29T13:10:19.7741482Z API /health/live -> 200
```

### ✅ CHÂN 1 = **PASS**

---

## CHỨNG MINH RESTART THẬT (điều kiện tiên quyết của chân 2/3/5 — luật 3-chân-kiềng)

| Bằng chứng | Giá trị |
|---|---|
| `w3wp` process mới | **pid 4980 · StartTime = 2026-07-29 20:10:05** — TRÙNG KHỚP dòng CI `App pool started` 20:10:05 |
| `SolutionErp.Api.dll` mtime | **2026-07-29 20:07:50** — trong cửa-sổ deploy (20:04:48–20:10:28) ⇒ file BE thật sự bị ghi đè |

⟹ Mọi số đo DB dưới đây lấy lúc **20:11+**, tức **SAU** mốc recycle 20:10:05 ⇒ hợp lệ (tránh bẫy đo-trước-recycle S159 đợt-2).

---

## CHÂN 2 — Mig 69 áp prod

| Chỉ số | Pre | Post | Kỳ vọng | Kết quả |
|---|---|---|---|---|
| `MAX(MigrationId)` | `20260727033522_AddPeAllowApproverDelete` | **`20260729122015_AddContractSigningPlans`** | = Mig 69 | ✅ |
| `COUNT(*) __EFMigrationsHistory` | 68 | **69** | 69 | ✅ |
| `sys.tables WHERE is_ms_shipped=0` | 89 | **96** | 89→96 (+7) | ✅ khớp TOÁN |
| bảng `ContractSigningPlan%` | 0 | **7** | 7 | ✅ |

7 bảng đếm đích danh (khớp 1:1 với 7 `CreateTable` trong file mig):
`ContractSigningPlans` · `…Lines` · `…DossierItems` · `…LevelOpinions` · `…Attachments` · `…Changelogs` · `…Approvals`

### ✅ CHÂN 2 = **PASS** (bằng chứng sqlcmd prod trực tiếp, không phải suy từ CI-log)

---

## CHÂN 3 — Menu KHÔNG đổi

`COUNT(*) MenuItems`: pre **142** → post **142** ⇒ **GIỮ NGUYÊN, 0 row thêm** ✅
(W1 chỉ đụng policy-key `KeHoachKyKet` trong `MenuKeys.All`, menu-row `Khkk_*` đã seed từ S159 — đúng Δ-1 của spec.)

### ✅ CHÂN 3 = **PASS**

---

## CHÂN 6 (phần 1) — 3 domain sống

| URL | HTTP |
|---|---|
| `https://api.solutions.com.vn/health/live` | **200** ✅ |
| `https://admin.solutions.com.vn/` | **200** ✅ |
| `https://eoffice.solutions.com.vn/` | **200** ✅ |

_(đo 20:11:30; lưu ý `/health` trần = 404 UNMAPPED — đường thật là `/health/live` + `/health/ready`)_

---

## CHÂN 4 — Bundle byte-marker (#77) + 3 tầng control ÂM

| App | Pre-deploy | Post-deploy | Trạng thái |
|---|---|---|---|
| admin js | `index-D0sXA0fe.js` | **`index-M8SDA7Ps.js`** | 🔄 ROTATE |
| admin css | `index-DWDbm5As.css` | **`index-C3xzT0H9.css`** | 🔄 ROTATE |
| user js | `index-Y6dW_5CM.js` | `index-Y6dW_5CM.js` | ⏸️ FROZEN — **lành tính** (xem dưới) |
| user css | `index-6YIAufJR.css` | `index-6YIAufJR.css` | ⏸️ FROZEN — lành tính |

**Ship-proof THẬT (không dựa hash-delta — #69 TWO-MODE):**

| Bằng chứng | admin js MỚI | user js (frozen) |
|---|---|---|
| `Content-Type` | `application/javascript` | `application/javascript` |
| Size thật | **1.679.452 B** | 1.595.577 B |
| `Last-Modified` | **Wed, 29 Jul 2026 13:09:05 GMT** = 20:09:05 +07 | **13:10:00 GMT = 20:10:00 +07** |
| Trong cửa-sổ deploy (20:04:48–20:10:28)? | ✅ | ✅ |

⟹ user FROZEN + **LM TRONG cửa-sổ** = re-ship byte-identical (#69 mode-B), KHÔNG phải bundle-cũ-kẹt. Đúng kỳ vọng: commit chỉ đụng `fe-admin/src`, `fe-user` 0 file.

**Byte-marker `QT-KHKK-V2-001` — đo SAU khi đã chứng ĐỘC QUYỀN:**

| Phép | Dự đoán (ghi trước) | Đo được | KL |
|---|---|---|---|
| admin bundle MỚI | **1** | **1** | ✅ khớp SỐ FILE nguồn (1 site `ApprovalWorkflowsV2Page.tsx:170`) |
| user bundle (control ÂM cross-app) | **0** | **0** | ✅ chứng marker không "khớp bừa" |
| admin bundle **CŨ** `D0sXA0fe` (control ÂM tầng-2) | biến mất | `http=200` **`ct=text/html` 900 B** | ✅ bundle cũ đã bị GHI ĐÈ khỏi đĩa |
| hash BỊA `ZZZZZZZZ` (mốc so trap SPA-fallback) | text/html | `http=200` **`ct=text/html` 900 B** | ✅ trùng khít mốc ⇒ 200 của hash-cũ là fallback, KHÔNG phải file thật |

### ✅ CHÂN 4 = **PASS** (3 tầng control ÂM đều nói cùng một chuyện)

---

## CHÂN 5 — ĐO CỜ policy-grant (F-04 / F-1 — vế acceptance đang treo)

**Đo SAU recycle 20:10:05.** Đối chiếu PRE → POST, mọi key 13 role:

| MenuKey | PRE (R/C/U/D) | POST (R/C/U/D) | Dự đoán | KL |
|---|---|---|---|---|
| `KeHoachKyKet` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| `Khkk_WfView` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| `Khkk_List` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| `Khkk_Create` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| `Khkk_Pending` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| `Khkk_Approved` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| `Khkk_Deleted` | 13/0/0/0 | **13/13/13/2** | 13/13/2 | ✅ |
| 🔑 `Khkk_G1` **(control ÂM nội-họ)** | 13/0/0/0 | **13/0/0/0** | **KHÔNG đổi** | ✅ |

**Vế DƯƠNG (role non-Admin):** `Drafter` trên cả 7 key đích = **C=1 · U=1 · D=1** ⇒ thoả acceptance "≥1 role non-Admin có CanCreate=1 ∧ CanUpdate=1".

**Vế ÂM (CanDelete chỉ Drafter/Admin):** liệt kê TOÀN BỘ row có `CanDelete=1` trong họ KHKK = **đúng 14 row = {Admin, Drafter} × 7 key**, không thừa 1 role nào ⇒ 11 role còn lại D=0. ✅

🔑 **Vì sao `Khkk_G1` là control ÂM có RĂNG:** seeder liệt kê tay 7 key và **cố ý bỏ `Khkk_G1`** ("group điều hướng — KHÔNG cần cờ ghi"). Nếu `Khkk_G1` cũng bật C/U thì phép đo vô nghĩa (chứng tỏ có thứ khác lật bừa cả họ). Nó đứng yên ⇒ đúng NGƯỜI làm, đúng TẬP.

**Chứng độc lập từ log API prod** (`C:\inetpub\solution-erp\api\logs\solution-erp-20260729.log`), khớp con số tao ghi TRƯỚC khi đo:

```
2026-07-29 20:10:18.902 +07:00 [INF] Seeded KeHoachKyKet access: 0 added + 91 upgraded
  (W1 GD2 golive - 13/13 role C+U, Delete=Drafter/Admin; UPGRADE-if-exists F-S1)
```

`0 added + 91 upgraded` — **91 = 7 key × 13 role**, khớp từng đồng dự đoán. `added=0` ⇒ đúng đường UPGRADE-if-exists, KHÔNG insert (F-S1 đóng: nhánh insert-NO-OP đã bị tránh). Timestamp 20:10:18 nằm SAU restart 20:10:05 ⇒ seeder chạy trong lần khởi động này, và chạy SAU revoke (đúng thứ tự `SeedAsync`).

### ✅ CHÂN 5 = **PASS** — đo CỜ (không đếm ROW), đủ 2 chiều dương/âm + control ÂM

---

## CHÂN 6 (phần 2) — Bearer + label type-10

Login `admin@solutions.com.vn` → `accessToken` dài 468 ⇒ OK.

| Endpoint (admin bearer) | HTTP |
|---|---|
| `GET /api/contracts` | 200 ✅ |
| `GET /api/purchase-evaluations` | 200 ✅ |
| `GET /api/menus` | 200 ✅ |
| `GET /api/approval-workflows-v2` | 200 ✅ |
| **control: `GET /api/khong-ton-tai-abc`** | **404** ⇒ 200 ở trên CÓ NGHĨA, không phải catch-all |

**F-7 label BE ăn prod** — `GET /api/approval-workflows-v2?applicableType=10` trả nguyên văn:

```json
{"types":[{"applicableType":10,"applicableTypeLabel":"Kế hoạch ký kết HĐ","active":null,"history":[]}]}
```

⟹ type **10** SỐNG trên prod với nhãn VN **"Kế hoạch ký kết HĐ"** (không rơi fallback `ToString()` tiếng Anh). `active:null` + `history:[]` = **chưa có quy trình type-10 nào** — ĐÚNG kỳ vọng W1 (admin sẽ dựng tay), xác nhận chéo bằng sqlcmd `COUNT(*) ApprovalWorkflows WHERE ApplicableType=10` = **0**.

### ✅ CHÂN 6 = **PASS**

---

## Kiểm tra phụ (không nằm trong 6 chân — chống hồi-quy)

| Phép | Kết quả | Nhận định |
|---|---|---|
| Họ `Ct_` (S159 durability) | **637/637 row CanRead**, 49 distinct key | ✅ 100%, revoker KHÔNG lật gì |
| MenuItems root (`ParentKey IS NULL`) | **12** | ✅ khớp mốc S159 |
| `MenuItems` trùng key | **0** | ✅ |
| Menu-row họ KHKK | **8** (`KeHoachKyKet` + 7 `Khkk_*`) | ✅ không đổi |
| `ContractSigningPlans` rows | **0** | ✅ đúng — W1 schema-only, chưa có dữ liệu |

⚠️ **Đính chính trí-nhớ của tao (không phải lỗi deploy):** ghi chú S159 nói họ `Ct_` có "59 key ⇒ tập gốc 38". Đo thật hôm nay: **49 distinct key**, trong đó 21 key mang 3 hậu tố mới ⇒ tập lọc còn **28 key × 13 role = 364**, nên `364/364` chứ không phải `494/494`. Denominator lệch là do **PHẠM-VI QUERY + số cũ ghi sai**, KHÔNG phải mất quyền — bằng chứng mạnh hơn: đo TOÀN họ `Ct_` được **637/637**.

---

## ⚠️ 2 phát hiện phụ cần báo (KHÔNG do commit này gây ra, KHÔNG chặn W1)

1. **Tài khoản UAT `nv.test@solutions.com.vn` KHÔNG TỒN TẠI trên prod.**
   `POST /api/auth/login` → **401** `"Email hoặc mật khẩu không đúng."`; sqlcmd `SELECT COUNT(*) FROM Users WHERE Email LIKE 'nv.test%'` = **0** (tổng 18 user).
   ⇒ Phép thử 2-bearer (gotcha #44 — soi 403 im lặng cho role non-Admin) **hiện KHÔNG chạy được trên prod**. Commit `a8027c0` không đụng Users ⇒ không phải hồi-quy của lượt này (nhiều khả năng demo-seed bị gate — gotcha #51). **Đề xuất em-main:** chọn 1 tài khoản non-Admin CÓ THẬT làm mốc UAT mới, hoặc bỏ vế 2-bearer khỏi checklist cho tới khi có tài khoản.
2. **Đường log API trong memory của tao đã STALE.** `C:\inetpub\solution-erppi\logs` = **không tồn tại**; đường THẬT = **`C:\inetpub\solution-erp\api\logs\solution-erp-YYYYMMDD.log`**. Đã sửa trong MEMORY.

---

## TỔNG KẾT

| Chân | Nội dung | Verdict |
|---|---|---|
| 1 | CI run #427 PASS + test gate **566 (45D+521I)** | ✅ **PASS** |
| 2 | Mig 69 áp prod (MIGTOP + 96 bảng + 7 bảng CSP) | ✅ **PASS** |
| 3 | MenuItems **142 GIỮ NGUYÊN** | ✅ **PASS** |
| 4 | Byte-marker `QT-KHKK-V2-001` + 3 tầng control ÂM | ✅ **PASS** |
| 5 | ĐO CỜ policy-grant 2 chiều + control ÂM `Khkk_G1` | ✅ **PASS** |
| 6 | Smoke 3 domain + bearer + label type-10 | ✅ **PASS** |

**Không có mục nào FAIL. Không đề xuất rollback.**

END cicd-verify a8027c0 — **VERDICT=PASS 6/6**


