CICD-K7: PARTIAL — K7 ĐÃ LIVE & verify sạch (gate 639=45+594 · Mig GIỮ 71 · bundle rotate ×2 marker 8/8 · seeder `0 added + 2 upgraded` + COUNT 1→3 · endpoint mới 400-không-403). 2 lệch: run `aaed699` bị **CANCELLED** (ship qua #442, đề-xuất gotcha #86) + restart×2 không quan-sát-được trong cửa-sổ.

# sub-cicd-verify-k7 — deploy verify K7 (bridge KHKK→HĐ), S167 2026-08-01

> Artifact chống-#53: ghi TỪNG MỤC ngay khi có bằng chứng. Return chỉ là tóm tắt.
> Baseline PRE đọc từ `sub-cicd-verify-k6.md` (post-#440): admin js `Ig1R5tAh` · eoffice js `DFOCQ2ks` · gate 622=45+577 · Mig 71 · sys.tables 97.

---

## M0 — Push + path-filter (ĐÃ ĐO)

- HEAD = `aaed699228f418fa31ecba07414709de465502fc`
  subject: `[CLAUDE] Contract: K7 SP-002 bridge KHKK->HD hoi-2-khoa + Line.ContractId + grant seeder (gate PASS-WITH-FLAGS 10, F-8 fixed)`
  committed: 2026-08-01 13:04:49 +0700
- `git log origin/main..HEAD` = **RỖNG** ⇒ đã push thật.
- Range `1c6d77a..aaed699` = 7 commit (6 × `wal: flush` + 1 code).
- **File code trong range (13/13 khớp đề bài):**
  - BE (4): `Api/Controllers/ContractSigningPlansController.cs` · `Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` · `Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs` · `Infrastructure/Persistence/DbInitializer.cs`
  - FE mirror ×2 app (6): `fe-{admin,user}/src/hooks/usePipelineStages.ts` · `.../pages/khkk/KhkkDetailPage.tsx` · `.../types/khkk.ts`
  - Test (3): `Api/AuthorizePolicyRegressionTests.cs` · `Api/KhkkBridgeAuthzTests.cs` · `Application/CreateContractFromSigningPlanTests.cs`
  - (còn lại 14 file đều `.claude/**` = paths-ignore, KHÔNG trigger)
- **M3-phần-1 (git):** `git diff --name-only 1c6d77a..aaed699 -- '*Migrations*'` = **0 file** ⇒ 0-mig chứng-minh-được từ git.
- Path-filter: có file `src/**` + `fe-*/**` ⇒ CI PHẢI chạy (không skip).

---

## M1 — Run Gitea 🔴 PHÁT HIỆN LỚN: run của `aaed699` bị **CANCELLED**, KHÔNG success

Đo lúc 13:09-13:10 qua `actions/tasks?limit=3` (khớp theo `head_sha`, KHÔNG theo run_number):

| run | head_sha | status | started | updated | ghi chú |
|---|---|---|---|---|---|
| **#441** (id 554) | **`aaed699`** | 🔴 **cancelled** | 13:04:57 | 13:07:59 | sống ~3m02s rồi bị **huỷ giữa chừng** |
| **#442** (id 555) | `628132c` | running → (đo tiếp) | 13:08:26 | … | run-2 lead báo |
| #440 (id 553) | `1c6d77a` | success | 11:54:38 | 12:01:07 | mốc PRE (K6) |

**Cơ chế:** push `628132c` lúc 13:08:26 ⇒ Gitea **cancel-in-progress** giết run #441 đang chạy.
⇒ **Đề bài mục 1 ("run `aaed699` success") KHÔNG THOẢ THEO NGHĨA ĐEN** — `aaed699` chưa bao giờ tự deploy xong.

**Nhưng K7 VẪN ship, chứng minh bằng ancestry (không phải suy đoán):**
- `git merge-base --is-ancestor aaed699 628132c` = **YES** ⇒ cây của run #442 CHỨA trọn 13 file K7.
- `git diff --name-only aaed699..628132c` = 6 file (`docs/STATUS.md` · `fe-{admin,user}/…/KhkkWorkflowPanel.tsx` · 3 `scripts/*.ps1`) — không đụng file K7 nào.
⇒ **run #442 = phương-tiện ship DUY NHẤT của K7.** Mọi phép đo POST vì thế quy về #442.

### 🔴 GOTCHA MỚI đề-xuất (#86) — "push-đè nuốt deploy im lặng"
Push code A rồi push B ngay sau (<run-time của A) ⇒ run A **cancelled**. Nếu B **rơi trọn vào `paths-ignore`**
(docs/memory-only) thì run B **skip** ⇒ **code A KHÔNG BAO GIỜ deploy**, mà bảng Actions trông vẫn "lành"
(1 cancelled + 0 run) và `git log` thì đã push. Lần này thoát **NHỜ MAY**: `628132c` có chạm `fe-*/src/**`
nên mới đẻ run. **Luật rút ra: sau 2 push liên tiếp, PHẢI xác minh run cuối cùng có head_sha là TIP và
tip ⊇ commit code, chứ không được đọc "có run xanh" là xong.**

**Hệ quả cho mục 5 (restart-proof):** giả định "run-1 + run-2 = restart ×2 miễn phí" của lead **KHÔNG còn
đúng** — run #441 bị giết TRƯỚC bước deploy nên **không hề restart pool**. Trong cửa sổ này chỉ có **1 lần
restart thật** (từ #442). Idempotent-qua-2-restart do đó phải hạ cấp thành "chưa chứng-minh-được trong
cửa-sổ" (xem M5) — tao KHÔNG tự restart pool prod (READ-only, cấm #6).

---

## M4-PRE — mốc bundle + tập marker (ĐO TRƯỚC khi deploy đè, 13:06-13:07)

**Bundle PRE (live, khớp đúng baseline K6 post-#440):**

| app | js | css | Last-Modified (GMT) | size js |
|---|---|---|---|---|
| admin | `index-Ig1R5tAh.js` | `index-DqXm5Hpn.css` | Sat 01 Aug 2026 **04:59:40** (=11:59:40 +07) | 1.758.853 B |
| eoffice | `index-DFOCQ2ks.js` | `index-DlN3yZ5L.css` | Sat 01 Aug 2026 **05:00:41** (=12:00:41 +07) | 1.676.752 B |

**Kiểm-tra-tiền-đề cho marker (bài K6 nâng cấp — kiểm ĐƠN-VỊ-MÃ-HOÁ + ĐỊA-CHỈ trước khi tin 0-hit):**
1. `file` báo bundle "ASCII text" ⇒ **nghi non-ASCII bị `\u`-escape**. Bác bỏ bằng đo: `grep -c -F 'Hợp đồng'`
   = **4**, `'Kế hoạch'` = **3** trên bundle PRE ⇒ tiếng Việt nằm **thô (UTF-8)**, `grep -F` đọc được. ✅
2. **Nghi code-split** (nếu `KhkkDetailPage` nằm chunk lazy riêng thì marker 0-hit là vô nghĩa): index.html chỉ
   trỏ **đúng 2 asset** (`index-*.js` + `index-*.css`), và bundle **0 ref** dạng `"./xxx-<hash>.js"` ⇒ **single-bundle**, không lazy-chunk. ✅
3. **CONTROL DƯƠNG** (chứng `KhkkDetailPage` THẬT nằm trong `index-*.js` + grep có răng) — 4 chuỗi bản **tiền-K7**
   của chính file đó, đo trên **cả 2** bundle PRE: `Phiếu nguồn chưa có NCC trúng thầu.`=1/1 ·
   `Chưa khai căn cứ nào.`=1/1 · `Giá tham chiếu (phiếu NCC)`=1/1 · `Tên TVGS xử lý`=1/1 ⇒ **4/4 hiện diện**. ✅

**CONTROL ÂM / tập marker K7 — 0-hit PRE trên CẢ 2 bundle (8/8, `grep -c -F`):**

| marker (chuỗi UI nút bridge) | adm PRE | eof PRE |
|---|---|---|
| `Đưa dòng kế hoạch vào Hợp đồng` | 0 | 0 |
| `Chọn dòng đưa vào hợp đồng` | 0 | 0 |
| `Loại hợp đồng` | 0 | 0 |
| `Quy trình duyệt hợp đồng` | 0 | 0 |
| `Phiếu chưa có dòng nhà cung cấp nào.` | 0 | 0 |
| `khkk-bridge-line` | 0 | 0 |
| `Tạo hợp đồng` | 0 | 0 |
| `Chưa có giá chốt` | 0 | 0 |

⇒ marker có **tín hiệu thật** (0→≥1 mới là bằng chứng ship). Diff FE đối xứng: `fe-admin` và `fe-user`
mỗi bên +308 dòng `KhkkDetailPage.tsx`, +61 `usePipelineStages.ts`, +49 `types/khkk.ts` (796 insert/40 delete tổng)
⇒ kỳ vọng **element-wise 8/8 ở CẢ 2 app**.

---

## M3 — Migration (phần git, ĐÃ CHỐT) + prod PRE

- git: `1c6d77a..aaed699` **0 file** dưới `*Migrations*` ⇒ **0-mig**. (Range mở rộng tới `628132c` cũng 0 — 6 file kia không có Migrations.)
- prod PRE (sqlcmd 13:06): `MIG_TOP` = `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` = **Mig 71** ✅ ·
  `sys.tables(is_ms_shipped=0)` = **97** ✅ · `MenuItems`=**200** · `Permissions`=**2221** (khớp anchor #438/#440).

---

## M5-PRE — trạng thái quyền `Contracts` TRƯỚC deploy (mấu chốt K7)

`SELECT COUNT(*) FROM Permissions WHERE MenuKey='Contracts' AND CanCreate=1` → **PRE = 1** ✅ (đúng dự-đăng-ký của gate-K7)
`SELECT COUNT(*) … WHERE MenuKey='Contracts'` (mọi cờ) → **PRE = 13 row** (13 role, mỗi role 1 row)

Bảng đầy đủ 13 role PRE — `CanRead=1` **toàn bộ**, `CanCreate=1` **duy nhất Admin**:

| role | create | read | | role | create | read |
|---|---|---|---|---|---|---|
| Accounting | 0 | 1 | | Equipment | 0 | 1 |
| **Admin** | **1** | 1 | | Finance | 0 | 1 |
| AuthorizedSigner | 0 | 1 | | HrAdmin | 0 | 1 |
| CatalogManager | 0 | 1 | | **Procurement** | **0** | 1 |
| CostControl | 0 | 1 | | ProjectManager | 0 | 1 |
| DeptManager | 0 | 1 | | | | |
| Director | 0 | 1 | | | | |
| **Drafter** | **0** | 1 | | | | |

⇒ **Drafter + Procurement ĐÃ CÓ row sẵn** (read=1, create=0) ⇒ seeder phải **UPGRADE 2 row**, **KHÔNG add row nào**
⇒ acceptance `0 added + 2 upgraded` **nhất-quán với trạng thái prod đo được**, và kỳ vọng POST:
**CanCreate=1 → 3** đồng thời **tổng row `Contracts` GIỮ NGUYÊN 13** (nếu thành 15 ⇒ seeder add trùng ⇒ FAIL).
Đọc code seeder xác nhận khuôn UPGRADE-ONLY: `SeedContractCreateAccessAsync` gọi **sau** revoker, chỉ nâng cờ, không hạ.

---

## M1-bis — run #442 (`628132c`) = phương-tiện ship K7: **SUCCESS**

- start **13:08:26** → settle **13:14:51** = **6m25s** (nằm dải bình thường 5m3x-6m3x, không escalate).
- status `success` (field `conclusion` = None như thường lệ với `tasks` — tin `status`, đã biết từ trước).
- URL: `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/442`

---

## M2 — Test gate: **639 = 45 Domain + 594 Infra** ✅ KHỚP TUYỆT ĐỐI

Đo LIVE ở web-UI log (`/actions/runs/442/jobs/0/logs`, 18.695 B) — **tách phần**, không chỉ khớp tổng:

```
Passed!  - Failed: 0, Passed:  45, Skipped: 0, Total:  45, Duration: 169 ms  - SolutionErp.Domain.Tests.dll (net10.0)
Passed!  - Failed: 0, Passed: 594, Skipped: 0, Total: 594, Duration: 1 m 47 s - SolutionErp.Infrastructure.Tests.dll (net10.0)
```

| | PRE (#440) | POST (#442) | Δ |
|---|---|---|---|
| Domain | 45 | **45** | 0 |
| Infrastructure | 577 | **594** | **+17** |
| **tổng** | 622 | **639** | **+17** |
| Failed / Skipped | 0 / 0 | **0 / 0** | — |

**Δ+17 phân rã khớp TỪNG ĐỒNG với nguồn** (đếm attr trong repo — an toàn vì **0 `[Theory]`/`[InlineData]`**, hết rủi ro undercount):
- `CreateContractFromSigningPlanTests.cs` = **12** `[Fact]`
- `KhkkBridgeAuthzTests.cs` = **5** `[Fact]`
- `AuthorizePolicyRegressionTests.cs` (file **sửa**, không thêm test) = **0** attr test mới trong diff
- ⇒ 12 + 5 + 0 = **17** = đúng Δ Infra đo được. ✅

**Flaky đã khai:** `DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds`
(`ContractSigningPlanCrudTests.cs:482`) — gate báo **Skipped 0 / Failed 0** ⇒ **XANH lần thứ 4** liên tiếp
(#439, #440, local, #442). Chưa có cớ hô hồi-quy.

---

## M3 — Migration GIỮ 71 + `sys.tables` 97 ✅ (2 nguồn ⟂)

| nguồn | kết quả |
|---|---|
| **git** `1c6d77a..aaed699 -- '*Migrations*'` | **0 file** ⇒ 0-mig |
| **git** mở rộng tới `628132c` | **0 file** (6 file của run-2 không có Migrations) |
| **sqlcmd** `__EFMigrationsHistory` TOP | `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` = **Mig 71** (PRE == POST) |
| **sqlcmd** `COUNT(*) __EFMigrationsHistory` | **71** (khớp tên-đỉnh — 2 cách đọc độc lập cùng chốt) |
| **sqlcmd** `sys.tables (is_ms_shipped=0)` | **97** (PRE == POST) |

⇒ NO-Mig proof đủ 2 chân: history KHÔNG tiến + range git rỗng file Migrations.
Anchor phụ giữ nguyên: `MenuItems` **200** · `Permissions` **2221** · control revoker `Hrm*/Off*` CanRead=1 = **29**.

---

## M4 — Bundle rotate ×2 app ✅ + marker 8/8 element-wise ✅

**Hash + LM + size (curl LIVE sau `status=success`):**

| app | PRE js | POST js | LM POST (GMT / +07) | size PRE→POST | Δ |
|---|---|---|---|---|---|
| admin | `Ig1R5tAh` | **`RgvHZkPH`** | 06:13:22 / **13:13:22** | 1.758.853 → **1.765.720** | **+6.867 B** |
| eoffice | `DFOCQ2ks` | **`D65FJPGd`** | 06:14:22 / **13:14:22** | 1.676.752 → **1.683.623** | **+6.871 B** |

- LM cả 2 **nằm trong cửa-sổ deploy** (13:08:26 → 13:14:51) ⇒ ship THẬT, không phải FROZEN-kẹt.
- Δ size 2 app **gần bằng nhau** (+6.867 / +6.871) — nhất quán với diff FE **mirror đối xứng** (+308/+61/+49 dòng mỗi app). Nhân chứng ⟂ grep.
- CSS **giữ nguyên** cả 2 (`DqXm5Hpn` / `DlN3yZ5L`) — hợp lý: class mới đều là utility Tailwind đã tồn tại, không sinh CSS mới. Không phải cờ đỏ.
- **NHÂN CHỨNG THỨ 4 — log CI in thẳng hash+size, khớp curl tới từng byte:**
  `dist/assets/index-RgvHZkPH.js  1,765.72 kB` ↔ curl `Content-Length: 1765720` ✅
  `dist/assets/index-D65FJPGd.js  1,683.62 kB` ↔ curl `Content-Length: 1683623` ✅
- **Chống SPA-fallback trap (#S72):** `Content-Type: application/javascript` cho cả 2 hash mới;
  hash **BỊA** `index-ZZZZfake9.js` → `text/html` 900 B ✅ discriminator còn răng;
  hash **CŨ** `Ig1R5tAh` → `text/html` 900 B ⇒ **bundle cũ đã biến khỏi đĩa = chứng ghi-đè** (control âm tầng-2) ✅

**Marker (đo bằng `grep -c -F` — KHÔNG regex-neo-quote, đúng recipe K6):**

| marker | ADM pre→post | EOF pre→post |
|---|---|---|
| `Đưa dòng kế hoạch vào Hợp đồng` | 0 → **1** | 0 → **1** |
| `Chọn dòng đưa vào hợp đồng` | 0 → **1** | 0 → **1** |
| `Loại hợp đồng` | 0 → **1** | 0 → **1** |
| `Quy trình duyệt hợp đồng` | 0 → **1** | 0 → **1** |
| `Phiếu chưa có dòng nhà cung cấp nào.` | 0 → **1** | 0 → **1** |
| `khkk-bridge-line` | 0 → **1** | 0 → **1** |
| `Tạo hợp đồng` | 0 → **1** | 0 → **1** |
| `Chưa có giá chốt` | 0 → **1** | 0 → **1** |
| **CONTROL DƯƠNG** `Phiếu nguồn chưa có NCC trúng thầu.` | 1 → **1** | 1 → **1** |
| **CONTROL DƯƠNG** `Chưa khai căn cứ nào.` | 1 → **1** | 1 → **1** |
| **CONTROL ÂM ĐỘC-QUYỀN** `…vào Hợp đồng XYZ` (bịa) | 0 → **0** | 0 → **0** |
| **CONTROL ÂM ĐỘC-QUYỀN** `khkk-bridge-line-nonexistent` | 0 → **0** | 0 → **0** |

⇒ **8/8 × 2 app**, control dương giữ, control âm câm ⇒ tín hiệu sạch cả 2 chiều.

**TRÍCH BYTE THẬT** (bắt buộc trước khi kết luận — bài K6):
```
ADMIN   idx=1667835 : …title:`Đưa dòng kế hoạch vào Hợp đồng`,footer:(0,N.jsxs)(N.Fragment,…
ADMIN   idx=1669865 : …jsx)(`input`,{type:`radio`,name:`khkk-bridge-line`,checked:a===e.id,…
EOFFICE idx=1501164 : …title:`Đưa dòng kế hoạch vào Hợp đồng`,footer:(0,F.jsxs)(F.Fragment,…
EOFFICE idx=1503194 : …jsx)(`input`,{type:`radio`,name:`khkk-bridge-line`,checked:a===e.id,…
```
🔑 **Xác nhận lại bài K6:** rolldown/Vite-8 minify literal thành **BACKTICK** (`` title:`…` ``) — regex neo `["']`
sẽ 0-hit GIẢ. `grep -F` là đúng công cụ. 2 app chỉ khác tên biến module (`N` vs `F`) = minifier, code y hệt.

**Kiểm-tra-tiền-đề đã làm TRƯỚC khi tin marker** (ghi lại vì suýt sập bẫy mới):
`file` báo bundle "ASCII text" ⇒ nghi `\u`-escape ⇒ bác bỏ bằng đo (`grep -c -F 'Hợp đồng'`=4 trên bundle PRE);
và nghi code-split ⇒ bác bỏ bằng đo (index.html chỉ 2 asset, 0 ref `"./x-<hash>.js"`). Cả 2 nghi ngờ đều SAI,
nhưng nếu không đo mà kết luận "0-hit = chưa ship" thì đã FAIL-OAN như K6.

---

## M5 — 🎯 K7-đặc-thù: seeder grant restart-proof (#75/#76)

### (a) Log API — dòng acceptance CHỐT bởi gate-K7: **CÓ, verbatim** ✅

```
2026-08-01 13:14:40.592 +07:00 [INF] Seeded Contracts.Create access: 0 added + 2 upgraded
   (K7 cầu KHKK→HĐ - Drafter/Procurement/Admin; câu III default chờ ratify)
```
(`C:\inetpub\solution-erp\api\logs\solution-erp-20260801.log`; ký tự Việt hiển thị lỗi do console codepage, không phải log hỏng.)

**Chứng restart THẬT (3 chân kiềng, không đọc số lúc `running`):**
1. Dòng **ĐẦU TIÊN** của log = `13:14:33.867 [INF] Applying migrations...` ⇒ tiến trình khởi động mới.
2. `w3wp` **PID 7008**, `StartTime = 13:14:27` — và **map PID→pool bằng `appcmd list wp`** xác nhận
   `WP "7008" (applicationPool:SolutionErp-Api)`. ⚠️ Map này KHÔNG thừa: PID 7680 cũng start 13:13:09 nhưng là
   `vietreport-admin` (**dự án khác**) — đọc vội sẽ tưởng pool ta restart 2 lần.
3. Cả 3 mốc (13:14:27 / :33 / :40) nằm gọn trong cửa-sổ run #442 (13:08:26 → 13:14:51). ✅

### (b) sqlcmd đếm row — PRE **1** → POST **3** ✅

| phép đo | PRE (13:06) | POST (13:16) | phán |
|---|---|---|---|
| `COUNT(*) Permissions WHERE MenuKey='Contracts' AND CanCreate=1` | **1** | **3** | ✅ đúng kỳ vọng |
| `COUNT(*) Permissions WHERE MenuKey='Contracts'` (mọi cờ) | 13 | **13** | ✅ **KHÔNG đẻ row** ⇒ chứng "0 added" |
| `COUNT(*) Permissions` (toàn hệ) | 2221 | **2221** | ✅ hệ quả tất yếu của 0-added |

3 role có `CanCreate=1` POST + **hình-dạng cờ** (không chỉ đếm):
```
Admin       | create=1 read=1 upd=1 del=1   ← vốn đã đủ từ trước (không bị đụng)
Drafter     | create=1 read=1 upd=0 del=0   ← NÂNG, và KHÔNG tràn sang Update/Delete
Procurement | create=1 read=1 upd=0 del=0   ← NÂNG, và KHÔNG tràn sang Update/Delete
```
⇒ vừa đúng số, vừa đúng **hình dạng** (không over-grant) — thứ mà đếm-tổng đơn thuần không thấy được.

### (c) Idempotent — ⚠️ **PARTIAL: chứng ở tầng-row (ĐO ĐƯỢC), chưa chứng ở tầng restart×2 (không quan-sát-được)**

**Đo được (mạnh):** `STARTUP_COUNT('Applying migrations...')` trong log = **1** ⇒ trong cửa-sổ này pool
`SolutionErp-Api` restart **đúng 1 lần**. Giả định của lead "run-1 + run-2 = restart ×2 miễn phí" **KHÔNG thành**
vì run #441 bị **giết trước bước deploy** (M1) ⇒ **không có lần restart thứ hai để đo**.
Tao **KHÔNG tự recycle pool prod** (READ-only, anti-pattern #6/#8).

**Nhưng nhánh no-op ĐÃ CHẠY THẬT và đo được — nhờ Admin đóng vai control sẵn có:**
code đếm (`DbInitializer.cs`, diff K7):
```csharp
if (existing.TryGetValue(role.Id, out var row)) {
    var changed = false;
    if (!row.CanRead)   { row.CanRead   = true; changed = true; }
    if (!row.CanCreate) { row.CanCreate = true; changed = true; }
    if (changed) upgraded++;      // ← CHỈ tăng khi cờ THỰC SỰ lật
    continue;                     // ← row có sẵn ⇒ KHÔNG BAO GIỜ Add
}
```
Seeder chạy trên **3** role nhưng báo **2 upgraded** (không phải 3) ⇒ **Admin bị bỏ qua vì đã đúng sẵn**
⇒ **đó chính là nhánh idempotent, đã thực thi trên prod và đo được**, chứ không phải suy từ code.

**Suy (chưa đo — khai rõ):** restart lần 2, cả 3 row đã ở trạng thái đích ⇒ `added=0, upgraded=0` ⇒ do guard
`if (added > 0 || upgraded > 0)` bọc câu log, **sẽ KHÔNG in gì cả (IM LẶNG)** — chứ không in `0 added + 0 upgraded`.
🔎 Ai verify lần sau: **đừng đọc "log không có dòng seeder" thành hồi-quy** — đó là hình-dạng ĐÚNG của lần 2.
Phép đo rẻ nhất cho (c) ở deploy kế: đếm lại COUNT (phải vẫn **3**) + `STARTUP_COUNT` (phải **≥1** ở log mới).

---

## M6 — Smoke ✅ (kể cả endpoint MỚI)

Bearer admin OK (`accessToken` len=468, roles=`['Admin']`).

| phép thử | kỳ vọng | thực đo | |
|---|---|---|---|
| `POST /api/auth/login` (admin) | 200 + token | 200, token 468 ký tự | ✅ |
| `GET /api/contracts` | 200 | **200** | ✅ |
| `GET /api/purchase-evaluations` | 200 | **200** | ✅ |
| `GET /api/menus` | 200 | **200** | ✅ |
| `GET /api/contract-signing-plans` | 200 | **200** | ✅ |
| **control âm** `GET /api/contracts` KHÔNG token | 401 | **401** | ✅ |
| **control âm** `GET /api/bogus-route-xyz` + admin | 404 | **404** | ✅ |
| 🎯 **MỚI** `POST /api/contract-signing-plans/{0-guid}/create-contract` + **admin** | 404/400, **KHÔNG 403** | **400** | ✅ |
| 🎯 **MỚI** cùng endpoint, **không token** | 401 | **401** | ✅ |

**Wire-proof mạnh hơn cả mã số** — thân 400 là **lỗi bind/validate của chính command K7**, tức request đã đi
xuyên authz vào tới FluentValidation:
```json
{"status":400,"errors":{"PlanId":["'Plan Id' must not be empty."],
                        "ApprovalWorkflowId":["'Approval …"]}}
```
⇒ endpoint **đã wire thật**, policy **không chặn nhầm** Admin. (403 mới là FAIL — không xảy ra.)

### ⚠️ Hạn chế trung thực — tầng-2 (#82) của Drafter CHƯA đo LIVE được
`nv.test@solutions.com.vn / TestUser@123456` → **401 "Email hoặc mật khẩu không đúng"** (tài khoản UAT không
đăng nhập được ở thời điểm đo). Vì vậy:
- **Đã đo:** row `Contracts` của Drafter/Procurement có `CanCreate=1` (tầng dữ-liệu/hiển-thị).
- **CHƯA đo:** một principal **Drafter thật** gọi endpoint và KHÔNG bị 403 (tầng API-authz).
- Bằng chứng thay thế (CI, không phải prod): `KhkkBridgeAuthzTests` **5/5 PASS** trong gate #442.
- ⚠️ Admin qua được **không** tách bạch được công của row vs admin-bypass (chính comment seeder cũng nói vậy)
  ⇒ **không** dùng kết quả Admin để tuyên bố grant Drafter đã ăn ở tầng API. Đề nghị: UAT đăng nhập 1 tài khoản
  Drafter thật bấm nút, hoặc cấp lại mật khẩu `nv.test` rồi đo lại 1 lệnh curl.

---

## 📌 UAT — NÓI TRƯỚC (ghi theo yêu cầu lead)

> **Nút "Đưa vào Hợp đồng" chỉ sáng cho Drafter / Procurement SAU KHI USER ĐĂNG XUẤT & ĐĂNG NHẬP LẠI.**
> Lý do: menu/permission được client cache sau lần login trước; quyền `Contracts` vừa được nâng ở phía server
> lúc 13:14:40 hôm nay nên phiên đang mở vẫn mang bộ quyền CŨ. Không phải lỗi deploy, không cần báo bug —
> chỉ cần **re-login**. (Admin không thấy khác biệt vì vốn đã có quyền.)

---

## Tổng kết 6 mục

| # | Mục | Phán | Bằng chứng gọn |
|---|---|---|---|
| 1 | Run `aaed699` success | 🔴 **LỆCH** | #441 **cancelled** 13:07:59 (bị push `628132c` giết); K7 ship qua **#442 success** 6m25s, ancestry `aaed699 ⊂ 628132c` đã verify |
| 2 | Gate **639 = 45+594** | ✅ **PASS** | log web-UI tách phần; Δ+17 = 12+5 `[Fact]` khớp từng đồng; 0 fail/0 skip; flaky xanh lần 4 |
| 3 | Mig **71** + tables **97** | ✅ **PASS** | git 0-file Migrations ⟂ sqlcmd TOP + COUNT=71 + sys.tables=97 |
| 4 | Bundle rotate ×2 + marker | ✅ **PASS** | `Ig1R5tAh→RgvHZkPH`, `DFOCQ2ks→D65FJPGd`; LM in-window; hash+size khớp log CI; marker **8/8 × 2 app** + 2 control dương + 2 control âm |
| 5 | Seeder restart-proof | 🟡 **PARTIAL** | (a) log verbatim `0 added + 2 upgraded` ✅ (b) COUNT 1→3, row 13→13 ✅ (c) restart×2 **không quan-sát-được** (chỉ 1 startup) — nhánh no-op vẫn chứng được qua Admin |
| 6 | Smoke + endpoint mới | ✅ **PASS** | 4 GET 200 · 401/404 control · endpoint mới **400 (không 403)** + 401 khi thiếu token; ⚠️ Drafter-live chưa đo (login UAT hỏng) |

**Việc cần lead xử:**
1. **Gotcha #86 (đề xuất mới)** — push-đè nuốt deploy: nếu commit thứ 2 rơi trọn `paths-ignore` thì code commit
   thứ 1 **im lặng không bao giờ lên prod**. Lần này thoát nhờ may. Luật: sau 2 push liên tiếp phải kiểm
   "run cuối có head_sha = TIP" **và** "TIP ⊇ commit code".
2. Mở lại tài khoản UAT `nv.test` (hoặc cấp 1 Drafter khác) để đóng nốt tầng-2 #82 cho K7.
3. Deploy kế: đếm lại `Contracts/CanCreate=1` (phải vẫn **3**) để khép mục 5(c).

---

END cicd-k7 — VERDICT=PARTIAL (K7 ĐÃ LIVE, 4/6 PASS sạch + 1 LỆCH-cơ-chế run bị huỷ + 1 PARTIAL restart×2 không quan-sát-được; 0 defect trong code K7)
