# cicd-verify `ea962b1` — S159 ĐỢT-5 "menu 6-leaf y-chang-Duyệt-NCC + GET /contracts/deleted + bỏ hero"

> Ghi-TRỌN-RA-ĐĨA (chống #53). Lead /pause — **file này là sản phẩm chính, đọc ở phiên sau qua `/tiep`**.
> Đợt-1 `b5799fc` PASS 9/9 · đợt-2 `1a47a61` PARTIAL 8/9 · đợt-3 `2a72695` PASS 9/9 · đợt-4 `a2bbcb9` PASS 9/9 — 4 file cũ KHÔNG chạm.
> Bắt đầu: 2026-07-29 ~14:42+07 · cicd-monitor (READ-only)

---

## M1 — Push + phạm vi · ✅ PASS

```
git log -1 → ea962b1e59432bd6b003836cf0c93bed73732abf
             [CLAUDE] Contract+FE-User: dot-5 menu 6-leaf y-chang-Duyet-NCC
             + GET /contracts/deleted + bo hero + "Tong quan quy trinh"
             2026-07-29 14:40:10 +0700
git log origin/main..HEAD → EMPTY (đã push)
```
Range `ceeebf3..ea962b1` = **10 file, 173+/371−** (net âm vì gỡ hero + gỡ code cũ). Khớp brief.

| File | vai trò |
|---|---|
| `src/Backend/…/Api/Controllers/ContractsController.cs` | **+`GET deleted`** |
| `src/Backend/…/Application/Contracts/ContractFeatures.cs` | `ListDeletedContractsQuery` |
| `src/Backend/…/Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs` | comment |
| `src/Backend/…/Persistence/DbInitializer.cs` | 🔴 **seeder**: Ct +3 leaf ×7 · Khkk 6 · labelBackfill · reviewKeys |
| `fe-user/src/pages/UserDashboardPage.tsx` | **BỎ HERO** + đổi title |
| `fe-user/src/pages/contracts/MyContractsPage.tsx` | phase/deleted + guard |
| `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` | matrix + `type=3` |
| `fe-user/src/App.tsx` · `fe-user/src/components/Layout.tsx` | route alias · Ct-regex + staticMap + MenuLeaf-caps |
| `fe-admin/src/components/Layout.tsx` | staticMap + 2 |

**Cả 2 app FE đều sửa ⇒ bundle 2 app đều phải rotate.**

## M2 — Migration VẮNG là ĐÚNG · ✅ PASS
```
git diff --name-only ceeebf3..ea962b1 -- '*Migrations*'  →  0
```
BE thêm **query + endpoint + seeder**, không schema. Mig giữ **68** / 89 bảng.
⇒ Toàn bộ thay đổi menu đi qua **SEEDER lúc app khởi động** → rủi ro thật nằm ở M6 (seeder-run).

## 📌 BASELINE TRƯỚC DEPLOY (đo 14:41–14:42+07, run chưa land — pool CHƯA recycle)

Đây là "vế TRƯỚC" để chứng minh seeder thật sự chạy (delta 0→7 rất sắc):

| Phép đo | TRƯỚC |
|---|---|
| `Ct_%_WfView` (MenuItems) | **0** |
| `Ct_%_Approved` | **0** |
| `Ct_%_Deleted` | **0** |
| `Khkk_%` | **5** |
| `Ct_%_Pending` có `Label LIKE N'Đang%'` | **0** (trên tổng 7 key Pending) |
| `Dashboard` có `Label LIKE N'%quy tr%'` | **0** |
| **MenuItems TỔNG** | **119** |
| 38-key set | **min=13 max=13 keys=38** |

Bundle trước deploy: admin js `CC1pGE1F` css `DWDbm5As` · user js `CLfAftby` css `B6bUSFBt`
— khớp baseline hậu-đợt-4 ⇒ hợp lệ.

## 🔎 M3-pre — Marker kỳ vọng (dự đoán TRƯỚC khi đo, khuôn đợt-4)

`UserDashboardPage.tsx` (50.142 B — đợt-4 là 63.124 B, giảm đúng với "bỏ hero"):

| Chuỗi | trong UserDashboardPage | vai trò |
|---|---|---|
| `Tổng quan quy trình` | **1** | dương |
| `Toàn trình 4 giai đoạn` | **0** | (hero h2 đã gỡ) |
| `Quy trình mua sắm` | **0** | (eyebrow hero đã gỡ) |

### ⚠️⚠️ CẢNH BÁO: 1 trong 2 control ÂM của brief **KHÔNG HỢP LỆ** — đã soi TRƯỚC khi đo

`grep -rn` toàn `fe-user/src` + `fe-admin/src`:
```
fe-user/src/components/pe/PePipelineStrip.tsx:24:   aria-label="Toàn trình 4 giai đoạn"
fe-admin/src/components/pe/PePipelineStrip.tsx:24:  aria-label="Toàn trình 4 giai đoạn"
```
⇒ Chuỗi `Toàn trình 4 giai đoạn` **VẪN SỐNG** trong `PePipelineStrip` (aria-label, thêm từ đợt-2).
Nó **KHÔNG độc quyền** cho hero ⇒ **dự đoán bundle USER = 1, KHÔNG phải 0**.
**Nếu máy móc áp "phải VẮNG" sẽ báo FAIL OAN** — trong khi hero thật sự đã gỡ.
→ Xử lý: coi đây là control **KHÔNG dùng được**, thay bằng phép đúng ở dưới.

`Quy trình mua sắm`: `grep -rc` toàn 2 app = **0 hit ở mọi file** ⇒ **control ÂM HỢP LỆ**,
dự đoán bundle = **0**. Đây là phép chứng "hero đã gỡ" đáng tin của lượt này.

(Đúng bài đợt-4: marker phổ-thông/dùng-chung chỉ kết luận "≥1", muốn phép ÂM phải chọn chuỗi ĐỘC QUYỀN.)

## 🔎 M7-pre — Endpoint mới (đọc code, chốt kỳ vọng)

`ContractsController.cs`:
```csharp
[ApiController]
[Route("api/contracts")]      // L12
[Authorize]                   // L13  (class-level)
…
[HttpGet("deleted")]                        // L28
[Authorize(Policy = "Contracts.Read")]      // L29  (chặt hơn [Authorize] trần)
public async Task<ActionResult<PagedResult<ContractListItemDto>>> ListDeleted(…)
```
⇒ URL thật = **`GET /api/contracts/deleted`** (brief ghi `/contracts/deleted`, thiếu prefix `api`).
Kỳ vọng **401** khi không token (route tồn tại) · **404 = FAIL** (route không tồn tại).
👍 Ghi nhận: có policy method-level chứ không dựa `[Authorize]` trần — đúng bài 2-tầng authz (#82).

## M4 — Poll terminal · ✅ PASS
- **id=539 · run_number=426 · head_sha `ea962b1e`** · created 14:40:15 → **`status=success` 14:45:53**
- **Duration 5m38s** (đợt 1-4: 5m59s / 5m44s / 5m56s / 5m40s) — ổn định.

### M4b — Test gate · ✅ 562 EXACT
```
Passed! - Failed: 0, Passed:  45, Skipped: 0, Total:  45, 151 ms - SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 517, Skipped: 0, Total: 517, 1m23s  - SolutionErp.Infrastructure.Tests.dll
```
`Failed!` ×0 ⇒ **562**. Guard đợt-3 (`StillHiddenKeys`/`ReopenedKeys`) nằm trong 517 này, vẫn xanh.

## M5 — Bundle · ✅ PASS (đo 14:46:14, sau status=success)

| App | asset | TRƯỚC | SAU | KQ |
|---|---|---|---|---|
| **admin** | js | `CC1pGE1F` | **`D0sXA0fe`** | ✅ ROTATE |
| admin | css | `DWDbm5As` | `DWDbm5As` | FROZEN — lành tính (chứng dưới) |
| **user** | js | `CLfAftby` | **`Y6dW_5CM`** | ✅ ROTATE |
| **user** | css | `B6bUSFBt` | **`6YIAufJR`** | ✅ ROTATE |

"Bundle ×2 rotate (cả 2 app đổi)" — **đạt qua JS cả 2 app** ✅.
```
USER  index-Y6dW_5CM.js  ct=application/javascript  size=1.595.577  LM=14:45:25+07 (in-window)
ADMIN index-D0sXA0fe.js  ct=application/javascript  size=1.678.823  LM=14:44:29+07 (in-window)
ADMIN index-DWDbm5As.css ct=text/css                size=   83.705  LM=14:44:29+07 (in-window)  ← FROZEN
```
🔑 **admin-css FROZEN nhưng `Last-Modified` 14:44:29 NẰM TRONG cửa sổ deploy** (14:40:15→14:45:53)
⇒ file **đã được ghi lại**, chỉ là thay đổi `fe-admin` lần này là **staticMap trong Layout.tsx (JS thuần,
không thêm class Tailwind mới)** nên CSS ra byte y hệt. Đúng khuôn #69 mode-B đã dùng ở đợt-4:
**frozen + LM-in-window = re-ship giống hệt** (khác "bundle cũ kẹt lại" — cái đó LM sẽ cũ).

Control âm 2 tầng: hash CŨ `CLfAftby` / `CC1pGE1F` đều → `ct=text/html` 876/900 B ⇒ bundle đợt-4 đã
biến mất khỏi đĩa = chứng ghi-đè thật.

## M3 — Byte-marker · ✅ PASS (dự đoán TRƯỚC khớp ĐO 100%)

| Chuỗi | USER | ADMIN | Kỳ vọng đã chốt trước | KQ |
|---|---|---|---|---|
| `Tổng quan quy trình` | **1** | 0 | dương ở user (chỉ fe-user có) | ✅ |
| `Quy trình mua sắm` | **0** | **0** | **ÂM (control HỢP LỆ)** | ✅ |
| `Toàn trình 4 giai đoạn` | **1** | **1** | **dự đoán 1, KHÔNG phải 0** | ✅ đúng dự đoán |

🔑 **CỨU MỘT FALSE-FAIL — điểm quan trọng nhất lượt này:**
Brief yêu cầu control âm `Toàn trình 4 giai đoạn` phải **VẮNG**. Nếu áp máy móc thì lượt này **FAIL OAN**,
vì đo ra **1**. Nhưng tao đã soi TRƯỚC KHI ĐO và biết lý do:
```
fe-user/src/components/pe/PePipelineStrip.tsx:24:  aria-label="Toàn trình 4 giai đoạn"
fe-admin/src/components/pe/PePipelineStrip.tsx:24: aria-label="Toàn trình 4 giai đoạn"
```
Chuỗi này **không độc quyền cho hero** — nó còn sống trong `PePipelineStrip` (thêm từ đợt-2) dưới dạng
`aria-label`. Hero **thật sự đã gỡ** (`UserDashboardPage` chứa 0 lần; file giảm 63.124 → **50.142 B**).
⟹ Kết luận: control này **KHÔNG DÙNG ĐƯỢC**, thay bằng `Quy trình mua sắm` (eyebrow hero) —
`grep -rc` toàn 2 app = **0 hit ở mọi file** ⇒ độc quyền ⇒ bundle = **0** ⇒ **chứng hero đã gỡ**.

**Luật rút ra (nối tiếp đợt-4):** trước khi nhận một chuỗi làm **control ÂM**, phải grep repo-wide
xác nhận nó **ĐỘC QUYỀN** cho thứ mình muốn chứng vắng. Chuỗi dùng-chung chỉ làm marker DƯƠNG.

## M6 — 🔴 SEEDER-RUN sau restart · ✅ PASS (delta khớp từng đơn vị)

**Bước 0 — chứng minh restart THẬT (khuôn 3 chân kiềng, đợt-3):**

| Bằng chứng | Giá trị |
|---|---|
| Dòng ĐẦU log `solution-erp-20260729.log` | **`14:45:37.283 +07:00 [INF] Applying migrations...`** |
| `w3wp` StartTime mới nhất | **14:45:32** (và 14:45:31) |

Deploy xoá `logs/` ⇒ log mở lại 14:45:37 = **boot mới**; DbInitializer đã chạy. Đo SAU mốc này.

| Phép đo | TRƯỚC (14:41) | **SAU (14:46)** | Δ | KQ |
|---|---|---|---|---|
| `Ct_%_WfView` | 0 | **7** | +7 | ✅ |
| `Ct_%_Approved` | 0 | **7** | +7 | ✅ |
| `Ct_%_Deleted` | 0 | **7** | +7 | ✅ |
| `Khkk_%` | 5 | **7** | +2 | ✅ (6 leaf + G1) |
| `Ct_%_Pending` nhãn `N'Đang%'` | 0/7 | **7**/7 | +7 | ✅ labelBackfill ăn |
| `Dashboard` nhãn `N'%quy tr%'` | 0 | **1** | +1 | ✅ |
| **MenuItems TỔNG** | **119** | **142** | **+23** | ✅ |
| root | — | **12** | 0 | ✅ không double-insert |
| `DUPKEYS` | — | **0** | — | ✅ |

🔑 **Toán khớp từng đồng:** +23 = 7 (`WfView`) + 7 (`Approved`) + 7 (`Deleted`) + 2 (`Khkk`) = **23** ✅.
Không có row thừa nào ⇒ seeder insert ĐÚNG tập, không nhân đôi.

**Permission cho key MỚI (nhánh insert của reviewKeys — key chưa có row nên insert ăn):**
```
F_NEWPERM  min=13  max=13  keys=21        (21 = 7 WfView + 7 Approved + 7 Deleted)
```
✅ Mỗi key mới đủ **13/13 role canread**. Đây đúng như ghi chú code đợt-3: nhánh grant là
**skip-existing** nên chỉ ăn với key MỚI — lần này key hoàn toàn mới ⇒ grant thành công (khác
tình huống đợt-2/3 nơi row `false` sẵn có không được nâng).

## M7 — Endpoint mới `GET /api/contracts/deleted` · ✅ PASS (401, KHÔNG 404)

| Probe | HTTP | Đọc |
|---|---|---|
| **`GET /api/contracts/deleted`** (no token) | **401** | ✅ **route TỒN TẠI**, auth chặn đúng |
| `GET /api/contracts/zzznope` (route bịa) | **404** | ✅ **control** — 404 vẫn hoạt động ⇒ 401 ở trên CÓ NGHĨA |
| `GET /api/contracts` (route cũ đã biết) | 401 | ✅ nhất quán |

🔑 Có **control 404** nên 401 không thể là "server trả 401 cho mọi thứ" — discriminator sạch.
(Nếu chỉ đo mỗi 401 thì không loại trừ được khả năng catch-all.)

## M8 — Restart-durability lần 3 · ✅ PASS

| Phép đo | Kết quả SAU restart | KQ |
|---|---|---|
| **38-key set** | **min=13 max=13 keys=38 · totcr=494/494** | ✅ GIỮ (lần 3) |
| Control ÂM `Hrm%`/`Off%`/`Personal` non-Admin canread | **0** | ✅ revoker CÒN SỐNG |
| `Pe%` | **197 / 185** | ✅ |
| `Hdc_*` nhãn `- Bảng cứng` (`%-%`) | **7** | ✅ nguyên |
| 3 root ẩn — `SUM(IsVisible)` | **0** | ✅ cả 3 vẫn ẩn |

⚠️ **Lưu ý hình-dạng-truy-vấn (quan trọng cho lần sau):** đợt này `Ct_*` đẻ thêm 21 key mới
(`_WfView`/`_Approved`/`_Deleted`). Nếu vẫn dùng `LEFT(MenuKey,3)='Ct_'` như các đợt trước thì
"38-key set" sẽ thành **59 key** và **không so sánh được với 4 đợt trước**. Nên tao **cố ý loại
3 hậu tố mới** khỏi mệnh đề để giữ đúng tập 38 gốc:
```sql
WHERE MenuKey IN (10 key Master-set + 'Contracts')
   OR (LEFT(MenuKey,3)='Ct_'
       AND MenuKey NOT LIKE 'Ct[_]%[_]WfView'
       AND MenuKey NOT LIKE 'Ct[_]%[_]Approved'
       AND MenuKey NOT LIKE 'Ct[_]%[_]Deleted')
```
⇒ ra đúng **38 key**, so được apples-to-apples với đợt 2/3/4. (Bài học query-shape từ đợt-1: khi tập
dữ liệu nở ra, phải neo lại phạm vi truy vấn, nếu không con số "tăng" sẽ bị đọc nhầm thành drift.)

## M9 — Smoke · ✅ PASS 4/4
```
/health/live 200 Healthy · /health/ready 200 Healthy · admin 200 · eoffice 200
```

---

## VERDICT: **PASS — 9/9**

| # | Mục | KQ |
|---|---|---|
| 1 | Push + range 10 file (173+/371−, net âm do bỏ hero) | ✅ |
| 2 | Migration VẮNG là ĐÚNG | ✅ |
| 3 | Run #426 success 5m38s | ✅ |
| 4 | Test gate **562** (45+517, 0 fail) | ✅ |
| 5 | Bundle: JS rotate **cả 2 app**; admin-css frozen chứng minh lành tính bằng LM-in-window | ✅ |
| 6 | Marker `Tổng quan quy trình`=1 · control ÂM HỢP LỆ `Quy trình mua sắm`=0 | ✅ |
| 7 | **SEEDER-RUN**: 0→7 ×3 · Khkk 5→7 · Pending-label 7 · Dashboard-label 1 · tổng 119→142 (+23 khớp) · root 12 · DUPKEYS 0 · 21 key mới 13/13 | ✅ |
| 8 | **Endpoint `/api/contracts/deleted` = 401** (route sống) + control bịa **404** | ✅ |
| 9 | **Durability lần 3**: 38-key 494/494 · control âm 0 · Pe 197/185 · Hdc nhãn 7 · 3 root ẩn | ✅ |

**Không có cờ đỏ. Không khuyến nghị rollback.**

### ⚠️ 1 điểm cần owner biết (KHÔNG phải lỗi deploy)
Control âm `Toàn trình 4 giai đoạn` trong brief **không dùng được** — chuỗi vẫn sống hợp lệ ở
`PePipelineStrip.tsx` (`aria-label`, cả 2 app). Hero đã gỡ đúng (chứng bằng `Quy trình mua sắm`=0 +
`UserDashboardPage` 63.124→50.142 B). Lần sau nếu muốn phép ÂM cho hero, dùng `Quy trình mua sắm`.

Baseline mới: **admin js `D0sXA0fe` / css `DWDbm5As`(frozen hợp lệ) · user js `Y6dW_5CM` / css `6YIAufJR`** ·
gate 562 · Mig 68 / 89 bảng · MenuItems **142** · root 12 · 38-key 13/13 · `Ct_` leaf mới 21 key × 13.

### Arc S159 — 5 đợt
đợt-1 PASS 9/9 → đợt-2 **PARTIAL 8/9** (bắt regression revoker ăn 447/494 row) → đợt-3 PASS 9/9
(đóng regression, 3 chân kiềng) → đợt-4 PASS 9/9 (durability lần 2) → **đợt-5 PASS 9/9
(durability lần 3 + seeder-insert delta khớp toán + endpoint mới sống)**.

<!-- END cicd-verify-ea962b1 · VERDICT=PASS 9/9 -->

