# sub-cicd-monitor-4 — verify deploy `3119cb9` (W7 KHKK GĐ4 "Hợp đồng cứng")

> Ghi-từng-mục TRONG LÚC LÀM (chống #53). File này là SẢN PHẨM CHÍNH; return chỉ là tóm tắt.
> Bắt đầu: 2026-07-30 00:18 (+07)

## CHÂN-0 — Push + path-filter (ground truth từ đĩa)

- Commit: `3119cb91041bb08192d354da86fc68b305dde48b` (`3119cb9`)
- Subject: `[CLAUDE] Contract: W7 KHKK GD4 "Hop dong cung" 1-moc — HardCopiesPage x2 + ?type filter + hasSealedCopy badge + 3 test PIN (suite 590/0)`
- Author date: `2026-07-30 00:17:52 +0700`
- `git log origin/main..HEAD` → **EMPTY** ⇒ đã push, không còn commit treo local. ✅
- `git diff --shortstat HEAD~1 HEAD` → **12 files changed, 1358 insertions(+), 18 deletions(-)**
- 12 file (KHÔNG file nào khớp `paths-ignore` 7-glob ⇒ CI PHẢI chạy — đúng, run #431 đã trigger):
  - `fe-admin/src/App.tsx` · `fe-admin/src/components/Layout.tsx` · `fe-admin/src/pages/contracts/HardCopiesPage.tsx` · `fe-admin/src/types/contracts.ts`
  - `fe-user/src/App.tsx` · `fe-user/src/components/Layout.tsx` · `fe-user/src/pages/contracts/HardCopiesPage.tsx` · `fe-user/src/types/contracts.ts`
  - `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs`
  - `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs`
  - `src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs`
  - `tests/SolutionErp.Infrastructure.Tests/Application/ContractHardCopyTests.cs`
- **NO-Mig proof (vế 1/2):** `git diff --name-only HEAD~1 HEAD -- '*Migrations*'` → **0 file**. ⇒ kỳ vọng `__EFMigrationsHistory` = **69 GIỮ** (vế 2 = đo prod, CHÂN-2).
- FE đổi thật ở CẢ 2 app (4 file/app) ⇒ kỳ vọng bundle rotate ×2 (mode-A #69). Nếu FROZEN → phải chứng LM-in-window (mode-B).

## CHÂN-1 — CI (Gitea Actions)

- Run **#431** (id=**544**), head_sha `3119cb91`, created `2026-07-30T00:17:57+07:00`
- Poll status: (đang cập nhật bên dưới)

### Pre-deploy bundle baseline (chụp LÚC 00:18:48, TRƯỚC khi deploy job chạy — của run #430 `761d818`)

| App | js | css |
|---|---|---|
| admin.solutions.com.vn | `index-BPWQQe4q.js` | `index-KS0kL7bG.css` |
| eoffice.solutions.com.vn | `index-DV3fEpOw.js` | `index-Cpp9Qc7C.css` |

## DỰ ĐOÁN marker — CHỐT TRƯỚC KHI ĐO (chống Goodhart / false-fail)

Đọc source TRƯỚC, dự đoán con số, rồi mới curl. Lệch ⇒ phải truy nguyên nhân, không được hợp-lý-hoá sau.

**Tiền đề then chốt:** `HardCopiesPage` import **TĨNH/EAGER** ở cả 2 App.tsx (`import { HardCopiesPage } from '@/pages/contracts/HardCopiesPage'`, KHÔNG `React.lazy`) ⇒ code nằm trong **index-*.js CHÍNH**, không rơi vào lazy-chunk ⇒ curl index bundle là đủ, không phải săn chunk.

| Marker | Source occurrences/app | Trừ đi (comment-strip + type-erase) | **DỰ ĐOÁN bundle/app** |
|---|---|---|---|
| `hasSealedCopy` | 3 | −1 comment (`HardCopiesPage.tsx:194`) −1 TS-interface `types/contracts.ts:96` (erase lúc compile) | **đúng 1** (`c.hasSealedCopy` L195; property-name của object JSON KHÔNG bị mangle) |
| `hard-copies` | 12 | −2 comment (`HardCopiesPage.tsx:12`, `Layout.tsx:42/64`) | **10** = 1 route App.tsx + 2 queryKey (L74, L278) + 7 staticMap |

**⚠️ Control ÂM phải ĐỘC-QUYỀN (bài học #426 — suýt FAIL OAN):** brief bảo kiểm `coming-soon` 0 hit. Grep repo-wide TRƯỚC:
`coming-soon` VẪN SỐNG HỢP LỆ ở `App.tsx:64/57` (route `/coming-soon`), `pages/ComingSoonPage.tsx`, và fe-user `UserDashboardPage.tsx:1116` (`/coming-soon?stage=`) — đó là trang đích cho **menu skeleton GĐ2 khác**, KHÔNG phải Hdc_.
⇒ Đo `coming-soon` trần = **0 là SAI kỳ vọng**, áp máy móc sẽ FAIL OAN.
⇒ Control ÂM ĐÚNG = **cặp co-occurrence** (độc quyền cho đúng thứ cần chứng vắng):
- ÂM: `Hdc_<X>:"/coming-soon…"` → **DỰ ĐOÁN 0** (7 leaf đã re-point, không còn trỏ skeleton)
- DƯƠNG: `Hdc_<X>:"/hard-copies?type=` → **DỰ ĐOÁN đúng 7** (7 leaf × 1)

## CHÂN-1 — CI (Gitea Actions) — ✅ PASS

- Run **#431** (id=**544**) · head_sha `3119cb91` · `status=success`
- created `00:17:57` → updated `00:23:47` ⇒ **~5m50s** (khớp dải bình thường 5m38–5m59 của arc S159/S161)
- `conclusion=None` — bình thường cho endpoint `tasks` (không populate); tin `status` (đã ghi memory).
- Chờ tới `success` mới đo CHÂN-2/3 (deploy job là step CUỐI; đo lúc `running` = trạng-thái CŨ — bẫy S159 đợt-2).

### Test gate — ✅ **590/590**, đúng con số brief

Nguồn: web-UI job log `…/actions/runs/431/jobs/0/logs` (17.913 B).

| Project | Failed | Passed | Skipped | Total |
|---|---|---|---|---|
| `SolutionErp.Domain.Tests.dll` | 0 | **45** | 0 | 45 |
| `SolutionErp.Infrastructure.Tests.dll` | 0 | **545** | 0 | 545 |
| **GATE** | **0** | **590** | **0** | **590** |

Đối chiếu **TÁCH-PHẦN** (không chỉ khớp tổng): 45 + 545 = 590 ✅

**Delta chứng bằng run liền trước** (không tin số tổng trần): run **#430** (`761d818`, W6) = 45 + **542** = 587
⇒ `545 − 542 = ` **+3 Infra, +0 Domain** — khớp đúng "3 test PIN" trong subject commit. ✅

## CHÂN-3a — Bundle ×2 — ✅ PASS (rotate 4/4)

| App | Asset | PRE (#430) | POST (#431) | KQ |
|---|---|---|---|---|
| admin | js | `BPWQQe4q` | **`BBQKvUMa`** | ✅ rotate |
| admin | css | `KS0kL7bG` | **`BLUPWD4D`** | ✅ rotate |
| eoffice | js | `DV3fEpOw` | **`BV5--mPf`** | ✅ rotate |
| eoffice | css | `Cpp9Qc7C` | **`HIRTruGh`** | ✅ rotate |

Rotate 4/4 (mode-A #69) — nhất quán với diff: FE đổi thật 4 file/app × 2 app.
`Content-Type: application/javascript` · admin **1.734.048 B** · eoffice **1.650.214 B** (real-size, không phải SPA-shell).

### Ruột-marker — dự đoán khớp TỪNG ĐỒNG

| Marker | Dự đoán/app | admin | eoffice | KQ |
|---|---|---|---|---|
| `hasSealedCopy` | **đúng 1** | 1 | 1 | ✅ |
| `hard-copies` | **10** | 10 | 10 | ✅ |
| DƯƠNG `` Hdc_<X>:`/hard-copies?type=N `` | **7** | 7 | 7 | ✅ |
| ÂM `` Hdc_<X>:`/coming-soon `` | **0** | 0 | 0 | ✅ |

`hasSealedCopy=1` (KHÔNG phải 3) chứng dự đoán comment-strip + **type-erase** đúng: `types/contracts.ts:96` là TS-interface ⇒ biến mất lúc compile; `L194` là comment ⇒ bị strip; chỉ `c.hasSealedCopy` (L195) sống.
`hard-copies=10` phân rã ĐÚNG như dự đoán: **7** staticMap + **2** queryKey (L74 `[hard-copies,{type…}]` + L278 `invalidateQueries`) + **1** route `path:/hard-copies`.

7 leaf DƯƠNG liệt-kê-toàn-bộ, **distinct type 1..7 không trùng không thiếu**, giống hệt ở CẢ 2 app:
`Hdc_ThauPhu:1 · Hdc_GiaoKhoan:2 · Hdc_NhaCungCap:3 · Hdc_DichVu:4 · Hdc_MuaBan:5 · Hdc_NguyenTacNcc:6 · Hdc_NguyenTacDv:7`

### 🔑 SUÝT FALSE-CLEAN — control ÂM rỗng đọc nhầm thành sạch (tự bắt, đã vá)

Lần đo ĐẦU tao dùng regex `` Hdc_[A-Za-z]+:"/… `` (**dấu nháy kép**) → ÂM ra **0**… nhưng **DƯƠNG cũng ra 0**.
Cùng một hình-dạng regex mà vế DƯƠNG (phải có 7) cũng 0 ⇒ **regex chết, không phải map sạch**. Nếu chỉ nhìn vế ÂM sẽ báo "✅ 0 hit" = **FALSE-CLEAN**.
Nguyên nhân: minifier (rolldown) xuất **backtick** `` ` `` chứ không phải `"`. Vá regex → DƯƠNG **7** / ÂM **0** ⇒ control ÂM giờ MỚI có nghĩa (đã chứng checker còn răng trên chính bundle đó).
⇒ **LUẬT:** control ÂM chỉ đáng tin khi vế DƯƠNG **dùng ĐÚNG hình-dạng regex đó** trả về số > 0 mong đợi.

### ⚠️ `coming-soon` TRẦN KHÔNG dùng làm control (sẽ FAIL OAN)

Brief ghi "`Hdc_` không còn đi kèm `coming-soon` (0 hit)". Grep repo-wide TRƯỚC khi đo:
`coming-soon` **vẫn sống HỢP LỆ**: `App.tsx` route `/coming-soon`, `pages/ComingSoonPage.tsx`, và fe-user `UserDashboardPage.tsx:1116` (`/coming-soon?stage=`) — đích của **menu skeleton GĐ2 khác**, không liên quan Hdc_.
Đo thật: bare `coming-soon` = **1 (admin) / 2 (eoffice)** — ĐÚNG như kỳ vọng, không phải lỗi.
⇒ Chuỗi dùng-chung chỉ làm marker DƯƠNG; muốn chứng VẮNG phải dùng **cặp co-occurrence độc quyền** `Hdc_<X>:<route>` như trên.

### Control ÂM 2 tầng (chứng bundle CŨ đã bị ghi đè)

| Probe | http | Content-Type | size |
|---|---|---|---|
| admin hash **CŨ** `index-BPWQQe4q.js` | 200 | `text/html` | 900 |
| admin hash **BỊA** `index-ZZfakeZZ.js` | 200 | `text/html` | 900 |
| user hash **CŨ** `index-DV3fEpOw.js` | 200 | `text/html` | 876 |
| user hash **BỊA** `index-ZZfakeZZ.js` | 200 | `text/html` | 876 |

Hash CŨ trả về **trùng khít** hash BỊA ⇒ file cũ **không còn trên đĩa** = đã bị ghi đè thật.
Đồng thời chứng **200 trần VÔ NGHĨA** (SPA-fallback rewrite `/*`→index.html) — discriminator sạch là `Content-Type`, không phải mã 200.
Đối chứng từ phía đĩa (ssh): `index-BPWQQe4q.js` → **OLD-adminjs-GONE**. Hai nguồn độc lập khớp.

## CHÂN-2 — sqlcmd prod — ✅ PASS

`ssh vietreport-vps` + `sqlcmd -E -S .\\SQLEXPRESS` (4-backslash qua ssh), đo **SAU** khi run #431 `success` (không đo lúc `running` — bẫy S159 đợt-2).

| Chỉ số | Kỳ vọng | Đo được | KQ |
|---|---|---|---|
| `COUNT(*) MenuItems` | **142 GIỮ** | **142** | ✅ |
| `COUNT(*) __EFMigrationsHistory` | **69 GIỮ** | **69** | ✅ |
| `MAX(MigrationId)` | không đổi | `20260729122015_AddContractSigningPlans` (Mig 69, từ W1 #427) | ✅ |
| `sys.tables` (is_ms_shipped=0) | 96 GIỮ | **96** | ✅ |

**NO-Mig proof đủ 2 vế:** vế 1 = `git diff … -- '*Migrations*'` **0 file** (CHÂN-0) · vế 2 = history **69 KHÔNG nhích** + MIGTOP y nguyên. ✅
**MenuItems 142 GIỮ** đúng bản chất W7: chỉ **re-point `staticMap` phía FE**, không đụng seeder menu (7 leaf `Hdc_*` đã tồn tại sẵn từ đợt trước, W7 chỉ đổi ĐÍCH của chúng).

### Chứng minh app pool ĐÃ recycle (điều kiện để 3 số trên có nghĩa)

`appcmd list wp` → map pid→pool (không đoán "một w3wp nào đó"):
`WP "5576" (applicationPool:SolutionErp-Api)` ← đúng pool cần

| Mốc | Giờ | Trong cửa-sổ deploy 00:17:57→00:23:47? |
|---|---|---|
| API DLL `SolutionErp.Api.dll` mtime | **00:21:02** | ✅ |
| **w3wp pid=5576 (SolutionErp-Api) StartTime** | **00:23:20** | ✅ (và **SAU** mtime DLL 00:21:02 ⇒ nạp DLL MỚI) |
| admin `index-BBQKvUMa.js` LM | **00:22:18** size 1.734.048 | ✅ (size khớp CHÍNH XÁC bản curl về) |
| user `index-BV5--mPf.js` LM | **00:23:14** size 1.650.214 | ✅ (khớp chính xác) |

## CHÂN-3b — Smoke API prod

Login `admin@solutions.com.vn` → `accessToken` (len 468) ✅ 200.

| # | Call | Kỳ vọng | Đo | KQ |
|---|---|---|---|---|
| a | `GET /api/contracts?phase=9&pageSize=5` | 200 | **200** (`total=0, items=0`) | ✅ mã · ⚠️ field xem dưới |
| b | `GET /api/contracts?phase=9&type=1` | 200 | **200** | ✅ |
| c | `GET /api/contracts?type=999` | **400** KHÔNG 500 | **400** | ✅ |
| d | control route BỊA `/api/contracts-khong-ton-tai` | 404 | **404** | ✅ |

**(c) body 400 là ValidationProblemDetails THẬT, không phải lỗi chung chung:**
`{"title":"One or more validation errors occurred.","status":400,"errors":{"type":["The value '999' is invalid."]}}`
→ tên field `type` đích danh ⇒ chặn đúng ở tầng model-binding enum, không phải 500 rơi từ handler. ✅

**(d)** route bịa trả 404 ⇒ 200 ở (a)/(b) **CÓ NGHĨA**, không phải catch-all nuốt mọi thứ.

### ⚠️ `hasSealedCopy` per-item = **KHÔNG KIỂM CHỨNG ĐƯỢC LIVE** (không phải PASS, không phải FAIL)

Phép `all('hasSealedCopy' in i for i in items)` trả **True** — nhưng **N=0** ⇒ `all()` trên tập RỖNG luôn True. **Đây là phép trích RỖNG đọc nhầm thành sạch**, y hệt bẫy đã ghi trong memory. Không được tính là ✅.

Truy nguyên nhân bằng ĐO, không suy đoán — sqlcmd prod:
`CONTRACTS_TOTAL=0` · `CONTRACTS_NOTDELETED=0` · `PHASE9=0`
⇒ **Prod chưa có hợp đồng nào.** `total=0` là API trả ĐÚNG trên bảng rỗng — **không phải** IDOR-scope lọc mất, không phải regression của W7. Đã loại trừ khả năng "bug nuốt data" (đó mới là điều đáng sợ nếu bỏ qua).

⇒ Field `hasSealedCopy` hiện được bảo chứng bằng 3 nguồn KHÁC (không phải bằng payload live):
1. **Bundle prod** có `hasSealedCopy` (đo được, CHÂN-3a) — FE đã ship code đọc field.
2. **Gate CI 590/590** gồm 3 test PIN mới `ContractHardCopyTests` — có ca **LẬT-1-BIẾN** (thêm đúng 1 đính kèm `purpose=SealedCopy` ⇒ cờ false→true) nên không phải test "xanh vì cờ chết cứng".
3. Đây là **giới hạn của môi trường**, sẽ tự đóng khi prod có HĐ đầu tiên ở phase 9.

**Khuyến nghị (không tự làm — READ-only):** khi UAT tạo HĐ thật đầu tiên, re-curl `?phase=9` 1 lần để đóng nốt mục này.

## VERDICT

| Chân | Nội dung | Kết quả |
|---|---|---|
| **CHÂN-0** | push + path-filter + NO-Mig vế 1 | ✅ PASS |
| **CHÂN-1** | CI run #431 success · gate **590** (45+545, 0 fail/skip) · delta **+3** vs #430 | ✅ PASS |
| **CHÂN-2** | MenuItems **142** · MigHistory **69** · MIGTOP giữ · tables 96 · pool recycle chứng bằng pid đúng | ✅ PASS |
| **CHÂN-3a** | bundle rotate **4/4** · marker khớp dự đoán từng đồng (1 / 10 / 7 / 0) · control ÂM 2 tầng | ✅ PASS |
| **CHÂN-3b** | smoke 200/200/**400**/404 | ✅ PASS |
| — | `hasSealedCopy` per-item live | ⚠️ **UNVERIFIED-LIVE** (prod 0 HĐ — đã đo, không phải lỗi deploy) |

### 🟢 **VERDICT = PASS** (5/5 chân; 1 mục treo do môi trường thiếu dữ liệu, KHÔNG phải lỗi)

### Ghi nhận 2 lần tao SUÝT SAI (giữ lại để lần sau không lặp)

1. **Control ÂM rỗng** — regex nháy-kép cho ra ÂM=0 *và* DƯƠNG=0; nếu chỉ nhìn vế ÂM đã báo "sạch". Vá: minifier dùng **backtick**. ⇒ **Luật: control ÂM chỉ có giá trị khi vế DƯƠNG dùng ĐÚNG hình-dạng regex đó trả về số > 0 mong đợi.**
2. **Dự đoán `type=999` của tao SAI** — tao suy "không có validator cho `ListContractsQuery` (`IsInEnum` ở dòng 43 thuộc `CreateContractCommandValidator`, khác lệnh) ⇒ enum bind nhận số bừa ⇒ 200". Thực đo **400**: ASP.NET Core có `EnumTypeModelBinder` tự chặn giá trị enum không-định-nghĩa + `[ApiController]` → 400 tự động. **Brief đúng, tao sai.** ⇒ Nhờ ĐO thay vì khẳng định theo suy luận nên không báo sai. Giữ nguyên bài học: suy-luận-từ-code là GIẢ THUYẾT, prod mới là phán quyết.

---
END sub-cicd-monitor-4 — **VERDICT=PASS** (CHÂN-0 ✅ · CHÂN-1 ✅ 590 · CHÂN-2 ✅ 142/69 · CHÂN-3a ✅ 4/4 rotate · CHÂN-3b ✅ 200/200/400/404 · 1 mục UNVERIFIED-LIVE do prod 0 HĐ)

