CICD-K2: PARTIAL — 5/6 mục (mục 4 = CHÂN-LÝ-RỖNG, không phải sạch)

# CI/CD verify K2 SP-002 — commit `0779f2d`

> Artifact chống #53 — ghi TỪNG MỤC NGAY KHI ĐO. Vai: cicd-monitor (READ-only). Ngày: 2026-07-31.
> Deploy KHỎE, KHÔNG khuyến nghị rollback. Điểm trừ duy nhất = **phép đo mục 4 không có răng**
> (bảng rỗng), không phải lỗi deploy.

---

## Mục 0 — Push + path-filter (tiền đề)

```
$ git log -1 --format='%H|%s|%ci'
0779f2d45a932ec7c279c191d9ae3fe3505442af|[CLAUDE] Contract: K2 SP-002 phieu KHKK mang hang-muc + nhom duyet N1-8 (Mig 71) + FE x2 app + 11 test|2026-07-31 17:05:27 +0700

$ git log origin/main..HEAD --oneline
(RỖNG — đã push trọn)
```

`git diff --name-only 0779f2d^..0779f2d` = **23 file**: BE 9 · Mig 71 ×3 file (3-file rule đủ) ·
FE 8 (admin ×4 + user ×4 đối xứng) · test 3 · paths-ignore 2 (`docs/STATUS.md`,
`.claude/skills/ef-core-migration/SKILL.md` — KHÔNG chặn vì ≥1 file code).
Neo bằng SHA tuyệt đối `0779f2d^..0779f2d` (LUẬT S162: cấm `HEAD~1`, Stop-hook WAL auto-commit).

## Mục 0b — SNAPSHOT PRE-MIGRATE (đo lúc run #435 còn `running`, 17:07)

```
MIGTOP=20260731072441_AddContractCatalogEntries      <- Mig 70 (K1), Mig 71 CHƯA áp
PLANS=0 · LINES=0 · SUMPE=NULL|SUMPROP=NULL · SYSTABLES=97
admin   /assets/index-CKgq0YlH.js + /assets/index-DD3xQhNt.css
eoffice /assets/index-CHMCU35C.js + /assets/index-CdXRNT7E.css   (khớp mốc K1 #434)
```

🔴 **KHAI TRƯỚC — MỤC 4 SẼ RỖNG, KHÔNG PHẢI SẠCH.** Xem mục 4.

---

## Mục 1 — Gitea Actions run ✅ PASS

| | |
|---|---|
| Run | **#435** (id=548), `0779f2d4` |
| Status | **success** |
| Cửa sổ | created `17:05:33` → updated `17:11:45` = **6m12s** |
| URL | https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/435 |

Poll 6 vòng (17:08:31 → 17:12:26), running×5 → success. 6m12s ≈ K1 #434 (6m31s) — hợp lý cho
+Mig +11 test, chưa tới ngưỡng escalate.

## Mục 2 — Test gate 609 ✅ PASS (khớp TÁCH-PHẦN, không chỉ khớp tổng)

Log web-UI `.../actions/runs/435/jobs/0/logs` (18.677 B), grep nới `Passed![^\r\n]*`:
```
Passed!  - Failed:     0, Passed:    45, Skipped:     0, Total:    45, Du
Passed!  - Failed:     0, Passed:   564, Skipped:     0, Total:   564, Du
```
| | Domain | Infra | Tổng |
|---|---|---|---|
| K1 #434 | 45 | 553 | 598 |
| K2 #435 | **45** | **564** | **609** |
| delta | **0** | **+11** | +11 |

Kỳ vọng 609 = 598 + 9 lane + 2 gate-fix ⇒ khớp **từng vế**: Domain đứng yên, +11 rơi TRỌN vào
Infra — đúng nơi 3 file test mới nằm (`ContractSigningPlanApprovalTests` /
`~GroupAndCatalogTests` / `~SchemaTests`). 0 fail, 0 skipped.
Control ÂM: grep chuỗi bịa `ZZZ_KHONG_TON_TAI_9x7` = **0 hit**.
⚠️ Dùng regex nới ngay từ đầu vì bài K1: log dùng **2 khoảng trắng** sau `Passed!`.

## Mục 3 — EF migration prod + chứng-restart ✅ PASS

```
MIGTOP=20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime   <- TOP, đúng Mig 71
MIGCOUNT=71
SYSTABLES=97      (GIỮ NGUYÊN — Mig 71 có 0 CreateTable)
```
`sys.tables` 97 == 97 khớp phép đo nguồn `grep -c "CreateTable"` = **0** (control dương:
`grep -c "AddColumn"` = **4** ⇒ grep có tìm được thứ có thật).

**3-CHÂN-KIỀNG (né bẫy #423 đo-trước-recycle):**
1. *Restart thật:* `appcmd list wp` → `WP "5364" (applicationPool:SolutionErp-Api)`;
   `Get-Process w3wp` → PID **5364 StartTime 17:11:25** ∈ cửa sổ deploy (17:05:33–17:11:45).
   (5 w3wp còn lại là pool VIETREPORT/Default — đã map PID→pool để không nhận nhầm.)
2. *Dòng ĐẦU Serilog:* `2026-07-31 17:11:27.310 +07:00 [INF] Applying migrations...`
3. *Mọi phép đo DB dưới đây chạy từ 17:12+* ⇒ SAU mốc recycle.

## Mục 4 — Data prod sau migrate ⚠️ PARTIAL — **CHÂN-LÝ-RỖNG**

Số đo nguyên văn (POST-migrate, sau recycle):
```
TOTAL=0 · GROUP1=0 · OUTOFRANGE=0 · LINES=0 · SUMPE=NULL|SUMPROP=NULL
CTRL_SUPPLIERS=60 · CTRL_CATALOG=86      <- control dương: bộ đếm CÓ đếm được
```
| Acceptance yêu cầu | Kết quả | Giá trị bằng chứng |
|---|---|---|
| `COUNT(ApprovalGroup=1) == COUNT(*)` | `0 == 0` ✅ | **RỖNG — 0 bit thông tin** |
| `COUNT(ApprovalGroup NOT BETWEEN 1 AND 8) = 0` | `0` ✅ | **RỖNG** |
| Σ `PeReferenceAmount`/`ProposedAmount` | `NULL\|NULL` (PRE cũng `NULL\|NULL`) | **RỖNG hai phía** |

🔴 `ContractSigningPlans` prod có **0 dòng cả TRƯỚC lẫn SAU**. Ba acceptance "xanh" nhưng
**không mang thông tin nào** về việc backfill có chạy đúng — backfill hụt sẽ trông y hệt.
Chính tác giả Mig 71 đã khai bẫy này ("đo local Σ = tập RỖNG hai phía … phép đo prod mới có
răng"); nay prod cũng rỗng ⇒ **phép đo prod cũng KHÔNG có răng**. Không hạ thành FAIL (không có
gì sai), nhưng CẤM đọc thành PASS.

**Bằng chứng THAY THẾ — không phụ thuộc số dòng (CÓ RĂNG):**

*(a) 4 cột, đúng kiểu/nullability/default (`INFORMATION_SCHEMA.COLUMNS`):*
```
ContractSigningPlans     |ApprovalGroup       |int             |null=NO |def=((0))
ContractSigningPlans     |EndedByLevelFinalize|bit             |null=NO |def=(CONVERT([bit],(0)))
ContractSigningPlanLines |CatalogEntryId      |uniqueidentifier|null=YES|def=-
ContractSigningPlanLines |TenHangMuc          |nvarchar(500)   |null=YES|def=-
```

*(b) HOÁN-ĐỔI INDEX — chứng bằng **liệt kê TRỌN HỌ** (14 index cả 2 bảng), không bằng grep-rỗng:*
```
✅ CÓ : IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId_CatalogEntryId
        unique=1 | filter=([IsDeleted]=(0)) | cols=ContractSigningPlanId,SupplierId,CatalogEntryId
✅ CÓ : IX_ContractSigningPlanLines_CatalogEntryId          (cols=CatalogEntryId)
✅ CÓ : IX_ContractSigningPlans_ApprovalGroup_Phase         (cols=ApprovalGroup,Phase)
❌ MẤT: IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId  (unique 2-cột cũ)
```
Vắng-mặt của index cũ suy ra từ **danh sách đầy đủ 14 index có thật**, nên đây là vắng-mặt ĐO
ĐƯỢC chứ không phải "truy vấn trả rỗng đọc thành sạch". Đây là phép đo mạnh nhất của mục 4.

*(c) Bất-biến Σ tiền — chứng theo CẤU TRÚC bằng phép ĐO nguồn:*
`grep -nE "PeReferenceAmount|ProposedAmount|ApprovedAmount"` trên Mig 71 → chỉ **2 hit, cả 2 ở
dòng 23-24 và đều là comment `//`** ⇒ **0 câu lệnh thực thi** chạm cột tiền. `migrationBuilder.Sql`
= đúng **1** câu (dòng 61, backfill `ApprovalGroup=1`). Σ bất biến đúng kể cả khi bảng có dữ liệu.

*(d) Materialize qua EF:* xem mục 6 — `?group=N` trả **200** (không 500) ⇒ cột `ApprovalGroup`
tồn tại & query được xuyên Domain→Infra→Api trên DB thật.

## Mục 5 — Bundle ×2 app (#77) ✅ PASS — 4/4 ROTATE

| app | asset | TRƯỚC (K1 #434) | SAU (K2 #435) |
|---|---|---|---|
| admin | js | `CKgq0YlH` | **`DVQEwGwf`** |
| admin | css | `DD3xQhNt` | **`D050EVYG`** |
| eoffice | js | `CHMCU35C` | **`CZAYiWWa`** |
| eoffice | css | `CdXRNT7E` | **`DX1FRk5n`** |

*Hash CŨ chết (control âm tầng-2 — bundle cũ biến mất khỏi đĩa):*
```
admin  /assets/index-CKgq0YlH.js -> 200|text/html|900   (SPA fallback, không phải JS)
eoffice/assets/index-CHMCU35C.js -> 200|text/html|876
admin  /assets/index-ZZBIADAT.js -> 200|text/html|900   <- hash BỊA, cùng dấu hiệu
```
Hash bịa cho ra ĐÚNG dấu hiệu của hash cũ ⇒ `text/html` thật sự nghĩa là "file không tồn tại"
(né bẫy SPA-fallback-200 S72; dùng `Content-Type` chứ không neo byte).

*Last-Modified in-window:* admin `10:10:20 GMT` = 17:10:20 +07 · eoffice `10:11:20 GMT` =
17:11:20 +07 — cả hai ∈ (17:05:33, 17:11:45). `Cache-Control: no-cache` cả 2 (hardening S88).

## Mục 6 — Smoke ✅ PASS

| Phép đo | Kỳ vọng | Thực tế |
|---|---|---|
| `/health/ready` | 200 | **200** |
| `/health/live` | 200 | **200** |
| `/health` trần | (control) | **404** ⇒ 200 ở trên có nghĩa |
| `GET /api/contract-signing-plans?group=2` KHÔNG token | 401 | **401** |
| route BỊA `/api/contract-signing-plans-KHONG-TON-TAI?group=2` | 404 | **404** |
| route BỊA 2 `/api/zzz-fake-9x7` | 404 | **404** |
| admin.solutions.com.vn | 200 | **200** |
| eoffice.solutions.com.vn | 200 | **200** |

401 (route có thật, có gác) tương phản 404 ×2 (route không có) ⇒ **401 CÓ NGHĨA**.

*Có token admin* (login `/api/auth/login`, `accessToken` len=468):
```
?group=1 -> 200  {"items":[],"total":0,"page":1,"pageSize":20,"totalPages":0,...}
?group=2 -> 200  (thân y hệt)
?group=8 -> 200  (thân y hệt)
inbox    -> 200      deleted -> 200
```
"mọi item `approvalGroup==1`" ⇒ **VACUOUS (0 item)** — không tính là bằng chứng.
⚠️ Đã soi **thân RAW** (không tin bộ parse) đúng bài K1 "0-hit là dương-giả tới khi soi format":
thân thật là `{"items":[],"total":0}` ⇒ rỗng THẬT, không phải lỗi đọc format.

**Chứng `group` ĐƯỢC WIRE (không bị nuốt im lặng):**
```
?group=abc -> 400   <- bind int THẬT; nếu param không tồn tại, ASP.NET bỏ qua query lạ và trả 200
```
Khớp nguồn: `ContractSigningPlansController.cs:42` → `[FromQuery(Name = "group")] int? group = null`
và `:49` truyền vào `ListContractSigningPlansQuery(..., group)`.

**Control DƯƠNG cho đường ống list:** `GET /api/catalogs/contract-catalog` → **86 item**
(seed K1 còn nguyên) ⇒ token + serializer + đường ống list CHẠY, nên `items:[]` của KHKK là
bảng rỗng thật, không phải ống vỡ.

---

## Lệch / quan sát (không chặn deploy)

1. **`?group=999` → 200** (không 400). Miền nghiệp vụ là 1..8 nhưng biên API không chặn
   ngoài-miền; hiện vô hại (trả rỗng) và mục 4 đã xác nhận 0 dòng ngoài 1..8. Ghi cho K4b cân
   nhắc cùng lớp với validator `CatalogEntryId` bắt buộc mà Mig 71 đã hẹn.
2. **Ghost-window `EndedByLevelFinalize`** — tác giả khai trước: K2 đẻ cột, K3 tiêu thụ; tới khi
   K3 land cột luôn = 0. Đã xác minh cột tồn tại `bit NOT NULL default 0`; **không** coi
   "toàn 0" là dữ liệu hỏng.
3. **Mục 4 rỗng** — xem trên. Khi có phiếu KHKK thật trên prod, cần đo LẠI 3 acceptance đó thì
   mới có răng; hiện chưa thể kết luận backfill đúng/sai từ dữ liệu.

## Bảng tổng

| # | Mục | Verdict |
|---|---|---|
| 1 | Gitea run `0779f2d` success | ✅ PASS |
| 2 | Test gate 609 = 45D + 564I | ✅ PASS |
| 3 | Mig 71 TOP + chứng-restart | ✅ PASS |
| 4 | Data prod sau migrate | ⚠️ **PARTIAL — vacuous** (schema/index CÓ răng ✅) |
| 5 | Bundle ×2 app rotate + hash cũ chết | ✅ PASS |
| 6 | Smoke 401/404/200 + group wired | ✅ PASS |

**Không ghi gì lên prod ngoài đọc** (toàn bộ là SELECT / curl GET / login POST lấy token).

END — VERDICT=PARTIAL 5/6 (mục 4 vacuous, deploy khỏe, không rollback)
