# 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**