# sub-cicd-monitor-0 — ĐỢT-1 lane PRECHECK (W0.4 prod-state, ĐỌC-ONLY)

> Run: `2026-08-08-S182-khkk-awf-mirror-ncc` · Role: cicd-monitor · Spawn: đợt-1 (W0.4)
> SPEC: `spec-khkk-awf-mirror-ncc-08-08-2026.md` MỤC 2 W0.4 (`:24`) + R-2 (`:10`) + F11 audit.
> 🔴 R-2: PROD chỉ ĐỌC. 0 lệnh ghi (không `scp`, không tạo file remote, không INSERT/UPDATE/DELETE).
> ⚠️ Naming: TASK gọi artifact `sub-cicd-monitor-0.md`; boilerplate RUN-TRACE gọi `sub-cicd-monitor-3.md`.
> Ghi vào **`-0`** theo TASK (cụ thể hơn). Cả 2 đều trong run-folder ⇒ ISOLATION OK.

## Trạng thái ghi (chống-#53 — đổ ruột liên tục)

- [x] S0 connectivity + control dương
- [x] Q1 ApprovalWorkflows type-10 — **KHỚP**
- [x] Q2 ContractSigningPlans GROUP BY ApprovalGroup, Phase — 🔴 **LỆCH**
- [x] Q3 COUNT ContractSigningPlanLevelOpinions — **13** (dồn hết vào N4)
- [x] Q4 audit pin-lệch (F11) — **KHỚP 0 row** (control mạnh)
- [x] BONUS đo thêm: flag-state 88 level + pin/opinion per-Code (de-risk W1d + W2d)
- [x] VERDICT = **LỆCH ở Q2, hệ-quả-chặn-W1 = 0**

---

## S0 — Connectivity + CONTROL DƯƠNG

Lệnh (khuôn S177, 0 file remote — `-Q` inline, KHÔNG `scp`):
`ssh vietreport-vps 'sqlcmd -S .\SQLEXPRESS -d SolutionErp -E -W -s "|" -Q "..."'`

| Phép | Kết quả | Ý nghĩa |
|---|---|---|
| `DB_NAME()` / `SUSER_SNAME()` | `SolutionErp` / `WIN-8M58521HH5D\Administrator` | integrated auth OK, KHÔNG cần password |
| `GETDATE()` srv | `2026-08-08 01:32:45` | đo tươi |
| `COUNT(*) ApprovalWorkflows` | **17** | >0 ⇒ bảng đọc được |
| `COUNT(*) ContractSigningPlans` | **12** | >0 ⇒ bảng đọc được. 🔴 **memory-pack ghi 3 phiếu (001-003) — nay 12** (đã lớn từ S177) |
| `COUNT(*) MenuItems` | **200** | ✅ **khớp ĐÚNG mốc prod đã biết (MEMORY anchor 200)** |
| `COUNT(*) Permissions` | **2221** | ✅ **khớp ĐÚNG mốc prod đã biết (anchor 2221)** |

🔑 **Control dương MẠNH:** 2 mốc độc lập (`MenuItems`=200, `Permissions`=2221) trùng khít anchor đã ghi từ #438 ⇒ thước ĐÚNG DB, ĐÚNG instance, không phải DB rỗng/nhầm. Mọi kết quả `0` phía dưới do đó là **vắng thật**, không phải hỏng thước.

**Tên bảng verify tại chỗ** (`INFORMATION_SCHEMA.TABLES LIKE 'ContractSign%'`) — 7/7 có mặt:
`ContractSigningPlans` · `~Approvals` · `~Attachments` · `~Changelogs` · `~DossierItems` · `~LevelOpinions` · `~Lines`
⇒ `ContractSigningPlanLevelOpinions` là tên ĐÚNG (không đoán).

---

## Q1 — `ApprovalWorkflows` ApplicableType=10

Kỳ vọng: 8 row `KHKK-N1..8`, `IsActive=1`. → ✅ **KHỚP 8/8**

| Code | Version | IsActive | IsUserSelectable |
|---|---|---|---|
| KHKK-N1 | 1 | 1 | 1 |
| KHKK-N2 | 1 | 1 | 1 |
| KHKK-N3 | 1 | 1 | 1 |
| KHKK-N4 | 1 | 1 | 1 |
| KHKK-N5 | 1 | 1 | 1 |
| KHKK-N6 | 1 | 1 | 1 |
| KHKK-N7 | 1 | 1 | 1 |
| KHKK-N8 | 1 | 1 | 1 |

🟢 **Bom per-type (R-1/`AdminFeatures.cs:339-343`) CHƯA NỔ** — 8/8 IsActive=1, 0 nhóm Archived-oan ⇒ **không cần recovery-tạo-version-mới sau W2**.
🟢 Cả 8 đều `IsUserSelectable=1` ⇒ **D6/W2g (FE lọc `isActive`) an toàn**: auto-pin không mất mục tiêu; nút Ghim chưa ai bỏ.
🟢 Cả 8 đều `Version=1` ⇒ chưa ai bấm "Tạo phiên bản mới" trên panel type-10 ở prod (khớp cảnh báo R-1 "bom armed nhưng chưa kích").

**Đối chứng per-type (context cho R-6):** `GROUP BY ApplicableType` toàn bảng —
type 1 = 3 row / **1** active · type 3,4,5,6,7,9 = 1 row / 1 active · **type 10 = 8 row / 8 active**.
⇒ Type-1 đang đúng-hành-vi per-type (3 version, 1 active). Type-10 là ca DUY NHẤT cần per-Code ⇒ khẳng định phạm vi hẹp của W1a là đúng, và **T2 (regression type-1 Code-mới) có dữ liệu thật để soi**.

---

## Q2 — `ContractSigningPlans` GROUP BY ApprovalGroup, Phase

Kỳ vọng (SPEC `:24` + memory-pack): **mọi phiếu Phase=3 (DaDuyet TERMINAL)**.
→ 🔴 **LỆCH: chỉ 3/12 là Phase=3.**

| ApprovalGroup | Phase | n |
|---|---|---|
| 1 | **1** | 5 |
| 1 | **98** | 1 |
| 2 | **1** | 1 |
| 3 | **1** | 1 |
| 4 | 3 | 3 |
| 7 | **1** | 1 |

Tổng = 12 ✅ (khớp `COUNT(*)`=12 ở S0 — 2 phép độc lập cộng khít).

**Enum đọc từ NGUỒN, không đoán** — `src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanPhase.cs:10-14`:
`DangSoanThao=1` (Nháp) · `ChoDuyet=2` · `DaDuyet=3` (Terminal) · `TraLai=98` · `TuChoi=99`.

⇒ Giải mã: **8 phiếu Nháp · 1 phiếu Trả-lại · 3 phiếu Đã-duyệt · `ChoDuyet` = 0.**

🔑 **Vì sao memory-pack sai mà KHÔNG phải memory hỏng:** memory ghi *"phiếu 001-003 đều test, Phase=3"* — **vẫn đúng nguyên văn tới hôm nay** (001/002/003 = đúng 3 phiếu Phase=3, nhóm 4). Cái đổi là **thế giới**: 9 phiếu MỚI (004-012) đẻ ra sau khi memory được ghi. ⇒ Không phải "ký ức sai", mà là **ký ức đóng băng thời điểm** — SPEC `:24` chép lại mốc cũ rồi phổ quát hoá thành "đều Phase=3", đó mới là chỗ gãy.

**Ý nghĩa cho W1b (2 vế TÁCH, cấm nén):**
- ❌ **Vế 1 — tiền đề SPEC SAI:** có **9 phiếu ở phase submit-được** (8 Nháp + 1 TraLai), không phải 0. Chúng SẼ đi qua rào `EnsureWorkflowGroupMatchAsync` mới ở `SubmitAsync`.
- ✅ **Vế 2 — hệ quả = 0:** rào chỉ chặn khi pin-lệch, mà pin-lệch **đo được = 0** (Q4, control mạnh). ⇒ **0 phiếu bị kẹt submit sau W1b.** Không cần data-fix, không cần thông báo user.
- Phụ: `ChoDuyet=0` ⇒ **0 phiếu đang treo giữa workflow** ⇒ W1c (changelog PUT, lọc `Phase != DaDuyet && != TuChoi`) trên prod hiện chạm **9 phiếu** (8 Nháp + 1 TraLai), **không** chạm 3 phiếu N4 đã duyệt. Test T8 cần phiếu `ChoDuyet` ⇒ **phải seed local, prod không có mẫu**.

---

## Q3 — `COUNT(*) ContractSigningPlanLevelOpinions`

**13** (control dương cùng lượt: `~Approvals`=18 · `~Changelogs`=71 · `~Lines`=20 — 3 bảng anh em đều >0 ⇒ thước sống, 13 là số thật).

Phân bổ theo Code (đo riêng, cross-check khớp 13):

| Code | levels | opinions |
|---|---|---|
| KHKK-N1 | 11 | 0 |
| KHKK-N2 | 11 | 0 |
| KHKK-N3 | 11 | 0 |
| **KHKK-N4** | 11 | **13** |
| KHKK-N5 | 11 | 0 |
| KHKK-N6 | 11 | 0 |
| KHKK-N7 | 11 | 0 |
| KHKK-N8 | 11 | 0 |

⇒ 8 workflow × **11 level** = 88 level, đều nhau. Toàn bộ 13 chữ ký nằm ở N4 (đúng nhóm có 3 phiếu DaDuyet).

---

## Q4 — Audit pin-lệch (F11)

Kỳ vọng: 0 row → ✅ **KHỚP: 0 row.**

🔑 **CONTROL DƯƠNG cho một kết-quả-RỖNG** (bài học `#435` chân-lý-rỗng: `0==0` xanh mà 0 bit): chạy **CHÍNH câu đó bỏ vế `<>`** ⇒ trả **12 row**, liệt kê được từng dòng:

| MaKeHoach | Group | Phase | IsDeleted | pinned_code | khớp? |
|---|---|---|---|---|---|
| KHKK/2026/005 | 1 | 98 | 1 | KHKK-N1 | ✅ |
| KHKK/2026/006 | 1 | 1 | 1 | KHKK-N1 | ✅ |
| KHKK/2026/007 | 1 | 1 | 1 | KHKK-N1 | ✅ |
| KHKK/2026/008 | 1 | 1 | 1 | KHKK-N1 | ✅ |
| KHKK/2026/009 | 1 | 1 | 1 | KHKK-N1 | ✅ |
| KHKK/2026/011 | 1 | 1 | 0 | KHKK-N1 | ✅ |
| KHKK/2026/012 | 2 | 1 | 0 | KHKK-N2 | ✅ |
| KHKK/2026/010 | 3 | 1 | 0 | KHKK-N3 | ✅ |
| KHKK/2026/001 | 4 | 3 | 0 | KHKK-N4 | ✅ |
| KHKK/2026/002 | 4 | 3 | 0 | KHKK-N4 | ✅ |
| KHKK/2026/003 | 4 | 3 | 0 | KHKK-N4 | ✅ |
| KHKK/2026/004 | 7 | 1 | 0 | KHKK-N7 | ✅ |

⇒ 0-row của audit là **VẮNG THẬT**, không phải phép-trích-rỗng. Xác nhận **element-wise 12/12** bằng mắt, không chỉ tin `COUNT=0`.

🔑 **Bịt điểm-mù CẤU TRÚC của chính câu audit F11:** câu spec lọc `w.Code LIKE 'KHKK-N%'` ⇒ **mù** với phiếu pin vào workflow NGOÀI họ (vd type-10 code lạ `QT-KHKK-V2-001`, hoặc workflow type khác). Bịt bằng **đẳng thức đếm**: `ApprovalWorkflowId IS NOT NULL` = **12** và JOIN-in-family = **12** ⇒ 12=12 ⇒ **0 phiếu pin ngoài họ, 0 phiếu chưa pin.** (Nếu 2 số lệch nhau thì audit F11 sẽ báo "sạch" một cách sai.)

---

## BONUS — 2 phép đo thêm (đọc-only, de-risk W1d + W2d)

### B1. Pin per-Code + soft-delete ⇒ đúng con số 409 mà W1d sẽ in ra

| Code | pins_total | pins_softdeleted | pins_submittable (Phase 1/98) | opinions |
|---|---|---|---|---|
| KHKK-N1 | **6** | **5** | 6 | 0 |
| KHKK-N2 | 1 | 0 | 1 | 0 |
| KHKK-N3 | 1 | 0 | 1 | 0 |
| KHKK-N4 | 3 | 0 | 0 | **13** |
| KHKK-N7 | 1 | 0 | 1 | 0 |
| KHKK-N5 / N6 / N8 | **0** | 0 | 0 | 0 |

- 🔴 **F2/T11 có DỮ LIỆU THẬT trên prod:** `KHKK-N1` bị pin bởi **6 phiếu, trong đó 5 XOÁ-MỀM**. Thiếu `IgnoreQueryFilters()` thì guard W1d(i) sẽ đếm **1** thay vì **6** ⇒ báo sai và (nếu qua được các rào khác) để lọt dangling FK. Đây không còn là ca giả định.
- **W1d(ii)** trên prod: xoá `KHKK-N4` ⇒ *"3 phiếu đang gắn + 13 chữ ký"*. Xoá `KHKK-N1` ⇒ *"6 phiếu đang gắn + 0 chữ ký"*.
- 🔴 **D5 có răng THẬT:** `N5/N6/N8` hiện **0 pin + 0 opinion** ⇒ **không có D5 thì 3 nhóm này XOÁ ĐƯỢC từ UI** (rào (i)/(ii) đều cho qua) → đúng ca seeder-tái-sinh M-1. Và vì cả 8 nhóm đều **Version=1 = version CUỐI**, sau D5 **không nhóm nào xoá được** — đúng chủ đích.

### B2. Flag-state THẬT của 88 level type-10 ⇒ nghiệm thu trước cho W2d (D3=C)

| # | Cờ | TRUE/88 |
|---|---|---|
| 1 | `AllowReturnOneLevel` | 0 |
| 2 | `AllowReturnOneStep` | 0 |
| 3 | `AllowReturnToAssignee` | 0 |
| **4** | **`AllowReturnToDrafter`** | **88 (100%)** |
| 5 | `AllowApproverEditDetails` | 0 |
| 6 | `AllowApproverEditBudget` | 0 |
| 7 | `AllowApproverSkipToFinal` | 0 |
| **8** | **`AllowApproverFinalize`** | **8** (= đúng 1/workflow) |
| 9 | `AllowApproverDelete` | 0 |

*(cột DB xác nhận tại chỗ qua `INFORMATION_SCHEMA.COLUMNS` — 9 cờ trên `ApprovalWorkflowLevels` khớp ĐÚNG 9 ô spec W2d liệt.)*

- ✅ **6 ô disabled (#1,#2,#3,#5,#6,#9) = 0/88** ⇒ lệnh *"GIỮ `checked` state THẬT, CẤM ép false"* render ra 6 ô **bỏ-tick + mờ**. Không có ca "mờ mà đang tick" gây rối mắt. Ép-false hay giữ-thật **hôm nay nhìn y hệt nhau** ⇒ 🔴 acceptance A2 **KHÔNG phân biệt được 2 cách làm trên dữ liệu prod**; muốn có răng phải **seed 1 level cờ=true ở LOCAL** rồi soi. (Ca kinh điển "chân-lý-rỗng" — báo trước để W2 không tự nghiệm-thu hớ.)
- ✅ **#4 = 88/88** ⇒ spec nói *"thường TRUE"* là **nói nhẹ**: thực tế **100%**. Chốt `forced-true-disabled + tooltip riêng` là đúng, và ô này sẽ hiện **tick** ở mọi level.
- ✅ **#8 = 8/88, đúng 1 mỗi workflow** ⇒ *"Finalize SỐNG"* không phải phòng xa: đây là **cờ chịu lực** (kết-thúc-tại-cấp) của cả 8 nhóm. Nếu W2 lỡ disable/ép-false #8 ⇒ **gãy hành vi finalize cả 8 nhóm prod**. Đây là ô nguy hiểm nhất trong 9 ô.
- ✅ **#7 = 0/88** ⇒ "GIỮ NGUYÊN" `:1292` là no-op trên dữ liệu hiện tại.

---

## VERDICT

### `LỆCH` — lệch ĐÚNG 1 chỗ (Q2), và chỗ đó **KHÔNG chặn W1**

| Q | Kỳ vọng | Đo được | Kết |
|---|---|---|---|
| Q1 | 8 row KHKK-N1..8 IsActive=1 | 8/8 IsActive=1, IsUserSelectable=1, Version=1 | ✅ **KHỚP** |
| Q2 | mọi phiếu Phase=3 | 3 DaDuyet · 8 Nháp · 1 TraLai · **0 ChoDuyet** | 🔴 **LỆCH** |
| Q3 | (không nêu) | **13**, dồn hết vào KHKK-N4 | ℹ️ ghi nhận |
| Q4 | 0 row pin-lệch | **0 row** + control 12/12 khớp element-wise | ✅ **KHỚP** |

**Kết luận cho lead — 3 câu:**
1. 🟢 **Bom per-type CHƯA NỔ.** 8/8 nhóm `IsActive=1`, `Version=1` ⇒ chưa ai bấm "Tạo phiên bản mới" trên panel type-10 prod. **Không cần recovery sau W2.** Nhưng bom vẫn **armed** ⇒ R-1 (cấm commit W2 thiếu W1a) giữ nguyên hiệu lực.
2. 🔴 **Tiền đề "phiếu đều Phase=3" của SPEC `:24` SAI** — thực tế 9/12 phiếu đang ở phase submit-được. **Nhưng** vì pin-lệch = 0 (đo, có control), **0 phiếu sẽ kẹt submit sau W1b**. Đề nghị: sửa câu `:24` cho khớp thực tế, **không** đổi thiết kế W1b.
3. 🟢 **W1 đi tiếp được.** Không phát hiện gì cần migration (R-4 an toàn), không cần data-fix prod.

**Cảnh báo mang sang wave sau:**
- W1d **BẮT BUỘC** `IgnoreQueryFilters()` — prod có **5 phiếu xoá-mềm** pin `KHKK-N1`; thiếu nó guard đếm 1 thay vì 6 (F2 = ca THẬT, không giả định).
- W2 acceptance A2 **không có răng trên dữ liệu prod** (6 ô đều false sẵn ⇒ ép-false và giữ-thật nhìn giống hệt). Muốn chứng thì **seed local 1 level cờ=true**.
- **#8 `AllowApproverFinalize` = ô chịu lực** (8/8 workflow đang dùng) — tuyệt đối không đụng.
- T8 (changelog phiếu `ChoDuyet`) **không có mẫu trên prod** (ChoDuyet=0) ⇒ phải seed local.

**R-2 tuân thủ:** 8 lệnh `ssh`, **100% `SELECT`**. 0 `INSERT/UPDATE/DELETE`, 0 `scp`, 0 file tạo trên VPS (dùng `-Q` inline, không `-i`). 0 lệnh chạm CI/deploy.

---

## PHỤ LỤC — ảnh chụp GIỮA CHỪNG lane W1 (⚠️ KHÔNG phải review)

⚠️ **Caveat mạnh:** lane `implementer-backend` đang chạy SONG SONG với tao (`git status` lúc 01:37 cho ` M ApprovalWorkflowV2AdminFeatures.cs`). Đây là **snapshot giữa chừng**, KHÔNG phải phán quyết — theo bài học *stale-diagnostic-in-background-agent* (#68), chỉ tin bản đo chạy SAU khi lane đóng. Ghi lại vì phát hiện B1 là **đầu vào trực tiếp** cho đúng đoạn code đang được viết ngay lúc này.

- ✅ Lane **đã ý thức** `IgnoreQueryFilters()`: **18 lần** trong file, có cả comment 🔴 `:991` tuyên "LÀ BẮT BUỘC, không phải trang trí", và phủ **8/8 bảng `*LevelOpinions`** (`:1000-1050`) + 8 nhánh orphan-check (`:1089-1118`), gồm `ContractSigningPlans` (`:1118`).
- ⚠️ **Nhánh W1d(i) — đếm PIN — chưa thấy trong ảnh chụp này.** `grep "ApprovalWorkflowId == def.Id"` chỉ ra **1 hit `:885`** (đó là khối changelog W1c), và `:1206` vẫn còn comment CŨ *"Sau UAT khi link với PE/Contract thật cần check usage trước khi delete."* ⇒ tại thời điểm chụp, rào đếm-pin có vẻ **chưa land** (hoặc đang gõ dở).
- ⇒ **B1 vẫn còn hiệu lực và cần tới tay người viết W1d(i):** `KHKK-N1` bị pin bởi **6 phiếu, 5 trong đó XOÁ-MỀM**. Đếm-pin mà thiếu `IgnoreQueryFilters()` ⇒ ra **1** thay vì **6**. (Nhánh opinions đã đúng rồi — chỗ cần soi là nhánh PIN, không phải nhánh opinions.)
- Đề nghị lead: chuyển B1 sang lane BE, và để reviewer đo lại SAU khi lane đóng — **không** lấy phụ lục này thay cho review.

**END sub-cicd-monitor-0 — VERDICT = LỆCH (Q2 premise) · W1 GO · 0 blocker**
