wal: flush 20260808T0118

This commit is contained in:
pqhuy1987
2026-08-08 01:18:20 +07:00
parent 11bb2b9d11
commit 6a2f5f02c9
2 changed files with 88 additions and 8 deletions

View File

@ -16,6 +16,35 @@
- [ ] 7. Mâu thuẫn 3 lens
- [ ] 8. Sót chung (notification / invalidate keys / optimistic)
## GHI NHẬN SAU KHI ĐỌC 4 PIN (trước khi mở code)
- Spec DRAFT 68 dòng, 3 invest đọc đủ. Nghi vấn dựng sẵn để đo:
- (i) W2b: BE `History` trong `AwTypeSummaryDto` chứa TẤT CẢ version hay LOẠI active? Nếu loại → `versions.find(v=>v.isActive)` chết → active=null giả. PHẢI đọc `:199-213`.
- (ii) W1a P1: scope `w.Code == request.Code` CÓ kèm `ApplicableType == typeEnum` không? UNIQUE(Code,Version) là GLOBAL — Code trùng xuyên-type khả thi về schema.
- (iii) FE-invest D1 đề xuất scope theo REGEX `^KHKK-N[1-8]$`, BE-invest P1 theo typeEnum==10 — 2 công thức KHÁC NHAU (code `QT-KHKK-V2-001` rơi vào nhánh nào?). Spec chọn P1 mà không khai lệch lens.
- (iv) W1b: rào (v) "code NGOÀI họ → CHO QUA" — extract giữ nguyên lỗ này? Phiếu ChoDuyet đã pin lệch TRƯỚC vá không bị đụng — cần SQL audit prod, spec chưa có.
- (v) W1d: module pin loose-Guid (không FK) → hard-delete THÀNH CÔNG vật lý → dangling pin. Danh sách FK-Restrict của DB-invest chỉ có 3 (PE/Contract/KHKK) — WorkflowApps pins = FK hay loose? PHẢI đếm ModelSnapshot.
- (vi) D3=C: ô #4 Trả-về-Drafter hành-vi = LUÔN-CHO dù cờ tắt → mờ kiểu "chưa áp dụng" là NÓI DỐI CHIỀU NGƯỢC; phải hiển thị forced-true.
- (vii) Acceptance #3 (tạo v02 N1) chạy Ở ĐÂU? Prod = mutate config thật, local = workflow rỗng người. Không có activate-endpoint để rollback IsActive.
## ĐO TRỰC TIẾP (đợt 1 — AdminFeatures:80-420 + FE page:150-364)
- **HELD (i):** `AwTypeSummaryDto.History` = FULL list mọi version (BE `:206-211``versions` dùng cho CẢ `Active` lẫn `History`) ⇒ synthetic slice W2b `history.filter(code)` + `.find(isActive)` ĐÚNG shape. Ordering: query `OrderByDescending(Version)` ⇒ slice giữ Version DESC per code — đủ cho render.
- **HELD:** deactivate `:340-343` đúng như invest tả: `Where(ApplicableType == typeEnum && IsActive)`. Comment dòng 339 tự khai "only ONE active per type".
- **Đo thêm:** `nextVersion` `:334-337` đã tính theo **Code** (per-Code sẵn) — chỉ deactivate là per-type ⇒ vá P1 là nhất quán hoá scope với chính hàm.
- **Đo FE:** queryKey `['approval-workflow-v2-overview', selectedTypeInt]` (`:292`) — 8 kind nhóm sẽ share key với panel ContractSigningPlan (cùng 10) = cache chung, invalidate `:348` prefix-match toàn key ⇒ mutation từ panel nhóm tự làm tươi panel type-10 và ngược lại. KHÔNG cần key mới.
- **Đo FE:** `DEFAULT_CODE_BY_TYPE[10]='QT-KHKK-V2-001'` (`:179`) — nút "Tạo quy trình mới" từ panel ContractSigningPlan (không lockedCode) vẫn đề xuất code NGOÀI-họ type-10. Sau P1 (scope theo typeEnum==10 ⇒ per-Code) code lạ này KHÔNG tắt 8 nhóm. Nếu executor lỡ implement theo công-thức FE-invest D1 (regex `^KHKK-N[1-8]$`) thì code lạ rơi nhánh per-type ⇒ NUKE cả 8 nhóm — 2 công thức KHÁC HÀNH VI, spec phải khoá công thức.
## ĐO TRỰC TIẾP (đợt 2 — guards/delete/FK/middleware/scope/fe-user)
- **HELD:** PUT T2 Code-echo 409 (`:539-541`) · CEO-echo (`:547-550`) · F5/F2-echo (`:598-603`) · ô Mã FE `disabled={isEdit}` (`:927`) · CEO box `disabled={isEdit}` only (`:954` — CREATE-mode mở cho MỌI type kể cả 10).
- **HELD:** DELETE handler `:1196-1204` = load+Remove+Save, 0 guard; comment `:1187-1189` tự khai nợ. Middleware chỉ map 5 app-exception (`GlobalExceptionMiddleware.cs:32-40`), `_` → 500.
- 🔴 **MỚI — pin FK thật:** grep `HasForeignKey("ApprovalWorkflowId")` snapshot = **4 hit** (Steps-Cascade + KHKK/Contract/PE-Restrict). Domain có **8 cột pin** (thêm Proposal/Leave/Ot/Vehicle/Travel — ItTicket KHÔNG có) ⇒ **5 module Office pin = LOOSE 0-FK** ⇒ DELETE hôm nay với office-pin THÀNH CÔNG vật lý → dangling pin. DB-invest M3 "data KHÔNG mất (DB chặn được)" = SAI cho 5 module này. W1d usage-check là guard DUY NHẤT cho office.
- 🔴 **MỚI — query filter:** PE (`:84`) + Contract (`:44`) + KHKK (`:55`) đều `HasQueryFilter(!IsDeleted)` ⇒ đếm-pin naive sẽ BỎ SÓT phiếu xoá-mềm (FK vẫn chặn 547) ⇒ W1d PHẢI `IgnoreQueryFilters()` cho 3 module này, không thì đúng ca "đã xoá phiếu rồi mà vẫn 500" tồn tại tiếp.
- **HELD:** 8 bảng `*LevelOpinion` (8 FK `ApprovalWorkflowLevelId`) khớp claim "CẢ 8".
- 🔴 **MỚI — W1b unbuildable như viết:** `ContractSigningPlanScope` = `internal static` (`ContractSigningPlanFeatures.cs:263`); `InternalsVisibleTo` CHỈ có `SolutionErp.Infrastructure.Tests` (csproj:25). Submit KHÔNG có Application-handler (comment `:19` — controller gọi thẳng service Infrastructure) ⇒ helper internal KHÔNG gọi được từ `SubmitAsync`. Executor phải chọn: public-guard-class mới (Application) / inline-duplicate / InternalsVisibleTo-Infrastructure. Spec chưa khai.
- **HELD:** `ContractSigningPlanEntityType.Workflow = 4` tồn tại + changelog entity đủ cột ⇒ W1c 0-enum-extend. Phase filter {DaDuyet=3, TuChoi=99} đúng tên enum.
- **HELD:** rào (v) `:479-486` cho-qua code ngoài-họ LÀ CHỦ ĐÍCH (comment :477-478). UpdateDraft `:603-607` chỉ EnsureWorkflowTypeAsync. Submit `:163-213` chỉ check Lines↔group.
- 🔴 **MỚI — IsActive server-side cho KHKK-create = KHÔNG CÓ:** bridge KHKK→HĐ đã vá đòi `IsActive ∧ IsUserSelectable` (vá-3, `CreateContractFromSigningPlanFeatures.cs:141-147` — chính comment nêu "khuôn ContractFeatures thiếu 2 vế ⇒ forge POST pin được quy trình retired"); Create/UpdateDraft phiếu KHKK thì CHỈ check type ⇒ pin bản archived = 200. fe-user KhkkCreatePage `:104` filter `isUserSelectable` KHÔNG lọc `isActive`, `:114` `find(code)` ăn theo ordering Version-DESC. Hệ quả SAU W1a: (a) tạo v02 xong, v01 vẫn selectable=true (POST không đụng cờ này) — auto-pin trúng v02 chỉ NHỜ ordering; (b) "Bỏ ghim v02" ⇒ rơi về v01 archived im lặng, KHÔNG chặn tạo như spec mô tả răng nút Ghim.
- **Grep IsActive consumer (W1a regression):** Office features 0 lọc IsActive trên ApprovalWorkflows (mọi hit IsActive = Users/MeetingRooms/OtPolicies/Catalog) · V1 `WorkflowDefinitions.IsActive` không liên quan · consumer THẬT của IsActive-V2: Overview `Active` (display) + bridge type-3 vá-3 + FE dropdowns (PE/Contract create lọc client-side). KHÔNG AI resolve "active-per-type-10" server-side ⇒ P1 đổi scope type-10 an toàn, P2 không cần.
## FINDINGS
- (đang đổ)

View File

@ -9,31 +9,82 @@ _(pending)_
## 1. Thứ tự wave + phụ thuộc
_(pending)_
**Topology W0→W5 về logic là ĐÚNG** (prep → BE → FE → [W3 điều kiện] → test → ship), nhưng spec **THIẾU câu ràng buộc ghép-deploy** — đây là finding nặng nhất của review này:
- **[C-1] Spec không cấm ship W2 (FE) trước W1a (BE).** Executor là hmw fan-out per-wave; nếu wave FE commit + CI chạy TRƯỚC wave BE, thì 8 nút "Tạo phiên bản mới" trên 8 panel mới = 8 cò súng nối thẳng vào bom deactivate-per-TYPE còn nguyên (`ApprovalWorkflowV2AdminFeatures.cs:339-343` — đã verify tận mắt: `Where(w.ApplicableType == typeEnum && w.IsActive)` tắt CẢ 8). Chú ý thêm 2 sự thật làm C-1 nặng hơn spec tưởng:
1. **Bom đã armed TỪ HÔM NAY** — panel `/system/approval-workflows-v2/ContractSigningPlan` (TYPE_CODE_TO_INT có key này, `:171`) đã cho bấm "Tạo quy trình mới" vào type-10 từ K3. W1a vì thế không chỉ là "điều kiện cho W2" mà là **vá nợ prod đang hở** — càng sớm càng tốt, độc lập với FE.
2. Chiều ngược (W1 trước, W2 sau) **an toàn tuyệt đối**: 4 fix BE đều backward-compatible với FE cũ (deactivate hẹp hơn, guard thêm 409, changelog thêm row, DELETE 409 thay 500).
**Cách vá spec:** thêm vào MỤC 2 dòng ràng buộc: *"W1+W2 = MỘT commit code duy nhất (mặc định); nếu buộc tách thì thứ tự BẮT BUỘC W1→CI xanh→W2; CẤM tồn tại commit nào chứa W2 mà thiếu W1a."* — điều-kiện-chặn đã đưa vào WAVE-PLAN FINAL §8.
- W1 nội bộ: a/b/c/d độc lập nhau (2 file + 1 helper), làm chung 1 wave là đúng. W1b phụ thuộc helper extract từ rào (v) `ContractSigningPlanFeatures.cs:479-486` (đã verify site) — Create + UpdateDraft (`:603-607` chỉ có `EnsureWorkflowTypeAsync`, đã verify thiếu group-match) + Submit (`ContractSigningPlanWorkflowService.cs` sau `:167 EnsureCanSubmitAsync`, cạnh rào hạng-mục `:169-187` — điểm chèn tự nhiên, đã verify).
- W2 phụ thuộc W0 (local có 8 row để nhìn panel) — đúng như spec.
- W4 (test-specialist đóng suite) phải chạy SAU khi D3 chốt (số test W3 thay đổi theo phương án) — spec đã ngầm đúng, wave-plan final ghi tường minh.
- W5 phụ thuộc TẤT CẢ — đúng.
## 2. Deploy-safety
_(pending)_
- **1 commit hay tách?** → **1 commit BE+FE** (khuyến nghị chốt trong spec, hiện spec để trống). Lý do: 0 migration, diff nhỏ (~2 file BE + 1 file FE + tests), revert nguyên tử, và triệt tiêu C-1. Nếu lead muốn tách theo wave để dễ bisect: BẮT BUỘC W1 trước (điều kiện chặn ở §8).
- **cicd path-filter (#41):** đã verify `.gitea/workflows/deploy.yml``paths-ignore` gồm `docs/**`, `**/*.md`, `.claude/workflows/runs/**`. Diff này chạm `src/Backend` + `fe-admin` + `tests`**trigger đủ**. Artifact run-folder (`sub-*.md`) không trigger CI — commit spec/artifact riêng = 0s, đúng thiết kế.
- **Bundle rotate:** deploy.yml build CẢ 2 FE mỗi run (`:111-125`) + copy đè cả 2 dist (`:163-169`), nhưng Vite hash theo content ⇒ fe-user không đổi source thì hash **giữ nguyên**. Chỉ W2a-d ⇒ **admin rotate, eoffice GIỮ** (đúng spec); làm W2e (chạm `fe-user/src/lib/menuKeys.ts`) ⇒ eoffice rotate theo. Khớp spec W5.
- **Phiếu đang treo trên prod:** đã đối chiếu enum `ContractSigningPlanPhase` (`Domain/ContractSigningPlans/ContractSigningPlanPhase.cs:10-14`): 3 phiếu prod "phase=3" (đo S168) = **DaDuyet — TERMINAL**, không phải phiếu treo. ⇒ W1b Submit-guard và W1c changelog không đụng phiếu sống nào tại thời điểm đo. Deploy giữa giờ = app-pool restart vài giây (mỗi deploy đều vậy) + seeder idempotent skip (§3). Rủi ro THẤP. **Điều kiện:** chạy lại query 2 (GROUP BY ApprovalGroup, Phase) TRƯỚC deploy để re-confirm 0 phiếu ChoDuyet/TraLai — nếu có phiếu treo pin lệch nhóm thì nó sẽ kẹt submit sau vá W1b (by design, message chỉ việc); phải biết trước con số.
- **[m-4] Pre-deploy prod-state check thiếu trong spec:** chạy query 1 (Code, Version, IsActive) TRƯỚC deploy — nếu ai đó đã lỡ bấm tạo version trên panel ContractSigningPlan từ K3 tới nay thì có nhóm đang Archived-oan; đường chữa KHÔNG cần SQL tay: sau khi W1a live, tạo version mới cho đúng nhóm đó qua UI (POST tự re-activate per-Code).
- **Seeder chạy mỗi restart:** verify §3 — skip per-Code, không re-activate, an toàn.
## 3. Seeder tương tác (wipe-durability #75/#76)
## 3. Seeder tương tác (wipe-durability #75/#76) — VERIFIED-BY-CODE
_(pending)_
Mở `DbInitializer.cs:549-687` đọc tận mắt (không tin invest):
- **Idempotency = `AnyAsync(w => w.Code == code)` (:610)** — match BẤT KỲ row nào cùng Code, không phân biệt Version/IsActive. ⇒ admin tạo v02 cho N1 (v01 archived): restart → `AnyAsync("KHKK-N1")` = true → **SKIP. KHÔNG re-seed, KHÔNG re-activate.** An toàn lớp #75/#76. ✅
- **Nhưng chiều ngược:** admin XOÁ **hết** version của 1 Code (v01 lẫn v02, khi chưa pin) → boot kế `AnyAsync` = false → seeder **TÁI SINH v01** `IsActive=true, IsUserSelectable=true` với **roster snapshot email cứng** (:554-585). Đây là hành vi lớp #75/#76 CÓ THẬT nhưng chiều "resurrection-as-safety-net" — nhóm 1..8 là cấu trúc (ApprovalGroup int trên phiếu + 8 menu leaf) nên tái sinh có lợi, SONG spec **không khai** ⇒ admin xoá nhóm rồi thấy nó sống lại sau restart = ticket "xoá không được". → finding F-3.
- Version tái sinh = `Version=1` — nếu v01 cũ đã xoá thì UNIQUE (Code,1) trống → insert sạch, không 2601. ✅
- Roster local: seeder resolve email qua `userManager.FindByEmailAsync` (:593) — local CÓ 14 user Solutions thật (CLAUDE.md "30 demo user"), nên **local KHÔNG chắc rỗng người** như W0 spec khẳng định; rỗng hay đủ đều không phải bug → finding F-9 (chỉnh chữ W0).
## 4. Acceptance prod — đủ đo chưa
_(pending)_
- **[M-4] Acceptance #3 ("Tạo phiên bản mới nhóm N1 ⇒ v02") chạy Ở ĐÂU — spec không nói, và chạy trên PROD là mutation MỘT CHIỀU.** Đã verify controller (`ApprovalWorkflowsV2Controller.cs`): đúng 5 action, **KHÔNG có endpoint re-activate**. Tạo v02 ⇒ v01 archived vĩnh viễn; xoá v02 KHÔNG tự re-activate v01 ⇒ nhóm rơi vào trạng thái 0-active (panel hiện empty-state "Chưa có quy trình..." `:405-409`; KhkkCreatePage vẫn pin được v01 vì lọc `isUserSelectable` không lọc `isActive``fe-admin/src/pages/khkk/KhkkCreatePage.tsx:104`, đã verify — nhưng badge/slice sai). **Vá:** acceptance #3 chạy **LOCAL**; trên prod chỉ đo ĐỌC (UI render 8 panel + 3 query SQL). Nếu anh muốn demo #3 trên prod thật: chấp nhận v02 (bản sao y v01) tồn tại vĩnh viễn — khai rõ trước khi bấm.
- **Account đo:** spec không nêu. Chốt: `admin@solutions.com.vn` (role Admin: bypass mọi policy + menu auto-grant `SeedAdminPermissionsAsync`). 8 leaf sidebar hiện cho role nào khác = tầng menu-grant per-role (display) — ĐỘC LẬP tầng API (`Workflows.Create` cho verb ghi, authenticated-only cho GET); acceptance thêm 1 dòng: *"role thường không có grant menu ⇒ không thấy leaf — đúng thiết kế, không phải bug"*.
- **3 query SQL — chạy bằng đường nào:** spec DB §4 giao query nhưng không giao ĐƯỜNG. Khuôn đã chứng S177: `ssh vietreport-vps` + `sqlcmd -E` (integrated auth, KHÔNG cần credential; env password TRỐNG là bình thường). Ghi vào wave-plan để executor không đi tìm password.
- **Thiếu 1 phép đo UI-ghi trên local:** acceptance #2 chỉ MỞ dialog. Thêm: sửa-tại-chỗ 1 nhóm (đổi tên 1 NV slot / tick 1 cờ Return) → 204 → panel refresh giữ `v01` + không đổi Version — chứng PUT đường nhóm sống thật (máy PUT là generic by-Id nên gần chắc chắn PASS, nhưng "gần chắc" ≠ đo).
- Acceptance #1/#2/#4 (8 slice + dialog khuôn + regression 4 route cũ): ĐỦ và đo được. Bổ sung điều kiện #4: "645 test" → **số đo tươi tại W0** (xem M-3 §5).
## 5. Test coverage W1
_(pending)_
Spec W1 liệt 4 test — **đúng nhưng THIẾU 3 ca** (tính chốt-cuối + nhánh phủ):
- **[M-5a] Submit chốt-cuối group-mismatch KHÔNG có test.** W1b vá 2-site (UpdateDraft + Submit) mà test list chỉ có ca UpdateDraft. Ca thiếu: phiếu ĐÃ pin lệch nhóm từ trước vá (seed data trực tiếp qua DbContext, bypass Create-guard) → `SubmitAsync` ⇒ 409 message chỉ việc. Đây chính là lý do tồn tại của chốt-cuối — không test thì không biết chốt có đóng.
- **[M-5b] DELETE 409 nhánh ĐỦ 2 chân:** (i) phiếu pin (đã có trong spec) + (ii) **workflow hết pin nhưng còn `ContractSigningPlanLevelOpinions` trỏ Levels** (FK Restrict `:6297-6301` — cascade Steps→Levels sẽ vấp) ⇒ 409 kể tên. Chân (ii) chưa có trong test list.
- **[M-5c] 2 ca NEGATIVE giữ hành-vi-cũ:** (i) changelog W1c: phiếu DaDuyet/TuChoi pin workflow bị PUT ⇒ **0 row** mới (mirror filter PE `:867-870`); (ii) workflow type-10 code NGOÀI họ `KHKK-N*` (vd `QT-KHKK-V2-001`) ⇒ Create/UpdateDraft/Submit **CHO QUA** như cũ (rào (v) comment `:477-478` khai chủ-đích) — helper extract xong mà siết nhầm cả code ngoài họ là regression lặng.
- **Concurrent create 2 admin cùng nhóm (câu hỏi lead):** UNIQUE (Code,Version) đỡ được — kẻ thua 2601 → 500 thô. ĐỒNG Ý với DB-invest: chấp nhận cho màn quản trị, KHÔNG mở scope map DbUpdateException đợt này (đổi GlobalExceptionMiddleware = blast radius mọi module) — nhưng phải ghi thành **accepted-risk** trong spec final, đừng im lặng.
- **Bỏ-ghim cả 8 nhóm = CHẶN TẠO PHIẾU toàn cục** (KhkkCreatePage lọc `isUserSelectable` — đã verify `:104`): hành vi CŨ có sẵn, không phải regression của wave này; acceptance đã nêu răng nút Ghim per-nhóm — thêm nửa câu "bỏ ghim cả 8 = đóng cửa tạo phiếu KHKK toàn hệ thống (không có rào BE chặn bỏ-ghim-cái-cuối)" để anh biết nút này sắc tới đâu.
- **[M-3] Baseline "645 test" trong spec ⟂ STATUS.md canonical "644 test PASS (45D+599I, CI #444)"** — lệch 1 và spec đang CHÉP SỐ (vi phạm B1 + measured-label gate: con số mang nhãn đo mà không kèm phép đo làm nó trượt). Vá: spec đổi thành *"baseline = `dotnet test` đo TƯƠI tại W0, ghi số vào run.md; mọi so sánh về sau so với số W0"*. (Nếu 645 là đo tươi thật thì STATUS stale — cũng chỉ có phép đo W0 phân xử được.)
- **Nơi đặt test:** homes có sẵn — `tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs` (PUT/create-deactivate/delete-guard) + `ContractSigningPlanApprovalTests.cs` (submit chốt-cuối) — đã verify 2 file tồn tại.
## 6. D3/D4 — chấm phương án
_(pending)_
**D3 (7 ô chưa-wire) — chấm từng phương án theo hệ quả VẬN HÀNH:**
- **A (giữ 9 ô tick được):** rẻ nhất hôm nay, đắt nhất về sau. 2 hệ quả spec CHƯA nêu đủ: (i) ticket-support loại "tôi tick Trả-về-1-Cấp mà phiếu vẫn về Drafter" — lỗi class #78 đúng nghĩa, khó debug vì DB nói một đằng máy chạy một nẻo; (ii) **mìn-ngủ khi port W3 sau này**: cờ đã tick từ nhiều tháng trước đột nhiên BẬT hành vi ngày port — surprise nguy hiểm hơn cả config-lie, và lúc đó phải audit toàn bộ tick tồn kho trước khi bật máy. A = tôi KHÔNG khuyến nghị.
- **B (port cả 7):** đổi hành vi duyệt (4 return-mode chạm state-machine TraLai-toàn-phần + con-trỏ-đôi reset; F3/F4 mở đường ghi lúc duyệt; F6 thêm rào 3-lớp) — mỗi cờ 1 quyết định nghiệp vụ thuộc owner + test RED→GREEN riêng; scope ước gấp 3-5 lần toàn bộ W1+W2 cộng lại. Chỉ đáng khi anh THẬT SỰ cần semantics đó ngay. Ghi chú: kể cả B thì `CeoApprovalThreshold` vẫn không port (OG-3 NĐUQ human-gate) ⇒ B cũng không đạt "9/9 sống" tuyệt đối.
- **C (mờ ô chưa-wire + tooltip):** ĐÚNG tiền lệ đã sống ngay trong trang này (`SkipToFinal :1292-1302` — checked-ép-false + disabled + title, đã verify) và trung thực #78. Khuôn giữ nguyên (9 ô cùng vị trí — "không lệch" ở nghĩa cấu trúc; trạng thái disabled là sự thật per-type, prod ĐANG hiển thị đúng kiểu này cho SkipToFinal). **→ KHUYẾN NGHỊ C**, port dần per-cờ theo lệnh owner sau (mỗi lần port = gỡ 1 ô mờ, đúng khuôn K3-F5: port + gỡ khoá CÙNG wave).
**[M-2] BẪY THI CÔNG của C mà spec chưa ghi** (đã verify cơ chế payload): pattern SkipToFinal ép cả `checked={type10 ? false : state}`. Ép-checked AN TOÀN với F2 **chỉ vì** DB type-10 F2 luôn false (BE ép-false từ W1 S161 trước khi 8 nhóm seed). Áp NGUYÊN pattern cho 6 ô còn lại là SAI 2 chỗ:
1. `AllowReturnToDrafter` seed **default TRUE** trên mọi slot — ép checked=false ⇒ dialog hiển thị "không trả về Drafter" trong khi runtime LUÔN trả về Drafter khi Trả lại (`ContractSigningPlanWorkflowService.cs` ReturnOrRejectAsync không đọc cờ) — display nói dối theo chiều NGƯỢC.
2. Ô #4 nói riêng phải hiển thị **tick-thật + disabled** + tooltip *"KHKK hiện luôn trả về Drafter khi Trả lại — cờ chưa phân nhánh"*.
**Luật thi công C:** 6 ô (4 Return + EditDetails + EditBudget) chỉ thêm `disabled` + `title`, **GIỮ `checked={entry.<flag>}` state thật, KHÔNG ép false**; payload `levelFlagsPayload` (`:255-267`) không đổi (echo state — đã verify PUT gửi state không gửi display). SkipToFinal giữ nguyên như đang có. **+ ô "Ngưỡng CEO" (`:944-961`)**: POST-mode disable khi type-10 + helper-text "KHKK không áp ngưỡng CEO (NĐUQ)" (PUT-mode đã khoá sẵn `disabled={isEdit}`).
Điều kiện disable dùng `applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` (không phải chỉ khkkGroup) ⇒ panel `ContractSigningPlan` cũ đồng bộ luôn — nhất quán, đúng.
**D4 (title panel):** ĐỒNG Ý phương án lead: title = menu-label "Duyệt KHKK — Nhóm {g}" (khớp chữ user vừa bấm ở sidebar — nguyên tắc least-surprise) + phụ đề nghiệp vụ `khkkGroupMenuLabel(g)` (helper 2-app identical có sẵn `types/khkk.ts:159-168`). 1 dây nhỏ executor hay quên: khuôn hiện chỉ render `applicableTypeLabel` (PageHeader `:309-317`) — phụ đề cần thêm ~1 prop/1 dòng ở panel; ghi vào wave-plan.
## 7. Rollback + monitoring
_(pending)_
- **Revert code = sạch phần MÁY, không sạch phần DATA — và đó là CHẤP NHẬN ĐƯỢC, nhưng phải khai:**
1. `ContractSigningPlanChangelogs` rows do W1c ghi (nếu có PUT sau deploy) — bảng có sẵn từ Mig 69, row là audit-history hợp lệ, revert KHÔNG cần xoá. Vô hại.
2. Version v02+ do admin tạo sau deploy — tồn tại vĩnh viễn (ratchet §4). Revert FE chỉ làm nó "tàng hình" khỏi panel nhóm (quay về landing), KHÔNG hỏng runtime (phiếu pin by-Id).
3. 🔴 Revert W1a ⇒ **bom deactivate-per-TYPE TÁI VŨ TRANG** — lần POST type-10 kế tiếp tắt cả 8 nhóm. Rollback-note bắt buộc: nếu revert, coi panel ContractSigningPlan + 8 panel nhóm là NO-GO cho "Tạo phiên bản mới" tới khi re-apply.
- **cicd-monitor 3-chân — spec nói khuôn nhưng thiếu MARKER cụ thể [m-3]:**
1. **Chân CI:** Run #N success + test-gate xanh (đối chiếu số W0).
2. **Chân bundle admin (byte-level, khuôn #77):** curl `admin.solutions.com.vn` lấy tên bundle mới ≠ bundle cũ (STATUS `:479` canonical) + grep trong js mới chuỗi chỉ-có-sau-W2: `KhkkN1` (regex parse) — control-âm: bundle cũ 0-hit. eoffice: KHÔNG đổi hash nếu bỏ W2e (đo để chứng, không suy).
3. **Chân BE DLL (khuôn S177 2-encoding):** ssh + grep `SolutionErp.Application.dll` token mới W1b (tên helper, vd `EnsureWorkflowGroupMatch`) — UTF-8 lẫn UTF-16, control-dương bằng token cũ đã biết có.
4. **Chân data (post-deploy):** chạy lại query 1 — kỳ vọng 8 row `IsActive=1` y trước deploy (deploy KHÔNG tự đổi data; lệch = có người bấm gì đó giữa chừng).
- **Post-deploy smoke KHÔNG-GHI trên prod:** login admin → 3/8 panel bất kỳ render đúng slice + mở dialog Sửa-tại-chỗ rồi Hủy. KHÔNG bấm "Tạo phiên bản mới" trên prod trong smoke (M-4).
## 8. WAVE-PLAN FINAL