Compare commits
22 Commits
936d1aed6c
...
706134daa9
| Author | SHA1 | Date | |
|---|---|---|---|
| 706134daa9 | |||
| 18f8f76bac | |||
| 1642c89912 | |||
| affaf66c61 | |||
| 27d5cd1356 | |||
| c688c1bba4 | |||
| 13b0038f78 | |||
| 220ddc8fc8 | |||
| 8d6ab2cd8e | |||
| 6a2f5f02c9 | |||
| 11bb2b9d11 | |||
| b7140ea9c7 | |||
| d6975e2dfd | |||
| ba95a68e3d | |||
| 99df7788ed | |||
| a035f75a30 | |||
| cf25ea53c3 | |||
| 1ff2fea3b9 | |||
| de62c3634a | |||
| ca9e7e1ad6 | |||
| 42fd3d51f9 | |||
| 79281d16a7 |
@ -1,11 +1,25 @@
|
||||
# WAL — auto-generated, không sửa tay
|
||||
updated: — | session: — | branch: —
|
||||
updated: 2026-08-08T01:08+07:00 | session: S182/L13 | branch: main
|
||||
|
||||
goal: (trống — không có mạch dở)
|
||||
goal: YC-013 — 8 workflow "Duyệt KHKK — Nhóm 1..8" mirror ĐỦ chức năng trang Duyệt NCC (UI không lệch); pipeline anh chỉ định 3-invest-Fable → wave-plan → 2-review-Fable → hmw fan-out Opus 5 MAX
|
||||
|
||||
chain:
|
||||
(rỗng)
|
||||
[x] bookend @open 5 vòng + ctx-audit — runs/2026-08-07-S182-bookend-open/
|
||||
[x] YC-013 ghi sổ (so-yeu-cau:46) + run-folder
|
||||
[x] B1 3× fable-real invest XONG-TRỌN-ĐĨA: BE 21KB (generic ~85%, 4 site sửa) · DB 18KB (VERDICT 0-MIG, 3 mìn M1/M2/M3) · FE 14KB (1 file chính ~60-90 dòng, SHA-mirror KHÔNG áp dụng)
|
||||
[x] B2 spec DRAFT ghi (spec-khkk-awf-mirror-ncc-DRAFT.md — W0..W5)
|
||||
[x] D3 anh chốt = C "Mờ ô chưa nghe" (YC-014, sổ + owner-decisions-08-08-2026.md) ⇒ W3 HỦY
|
||||
[x] B3 2× fable-real reviewer — R1 PASS_WITH_FIXES 2C/5M (F1-F12) · R2 PASS_WITH_FIXES 1C/5M+7m (§8 FINAL)
|
||||
[x] B4 spec FINAL land (spec-khkk-awf-mirror-ncc-08-08-2026.md — merge 2 review, R-1..R-6 + D5/D6 bậc-2 lead-chọn-CÓ)
|
||||
[x] B5 ĐỢT-1 wf_a07b53c8-47e — PRECHECK trả trọn (LỆCH-Q2-nhưng-GO: pin-lệch=0, bom chưa nổ 8/8; T11/D5 có răng THẬT trên prod; #8 Finalize=8/88 CỜ CHỊU LỰC cấm đụng; A2 cần seed local mới có răng) · W0 XONG (baseline 645, mig×2, seed 8 row) · 3 lane code ĐỨT GIỮA (BE ~40% AdminFeatures-only · FE ~85% chưa compile · TEST skeleton)
|
||||
[x] B5 ĐỢT-1bis XONG 3/3 — BE 5-fix land (Guards.cs mới + 3 site + D5/D6, Application build 0-err) · FE compile sạch (tsc 0-err ×2, SHA-pair 2/2, FLAG-B spec-tự-mâu-thuẫn đã RATIFY state-thật) · TEST 14 ca land → 🎯 FULL SUITE 659 PASS / 0 FAIL (=645+14 đúng từng số)
|
||||
[!] B5 ĐỢT-2: npm build ×2 (nền) ‖ reviewer pre-commit (chạy) → lead COMMIT MỘT PHÁT (R-1) → push → cicd 4-chân → acceptance prod đọc-only
|
||||
|
||||
next: (trống)
|
||||
wf: run-id=wf_a07b53c8-47e (đợt-1 đóng) + 3 Agent-lẻ đợt-1bis | trạng-thái=1bis-đang-chạy | artifact=sub-implementer-backend-0.md (Edit tiếp) · sub-implementer-frontend-1.md (Edit tiếp) · sub-test-specialist-2.md (Edit tiếp) | relaunch=đọc 3 artifact + git diff → spawn tiếp phần thiếu; KHÔNG resumeFromRunId (cache-hit vô dụng với lane empty)
|
||||
|
||||
verify: (trống)
|
||||
next: 3 lane 1bis xong → dotnet build + tsc + npm build ×2 + FULL suite (kỳ vọng 659 = 645+14) → reviewer pre-commit → commit 1 phát → push → cicd 4-chân
|
||||
lưu ý đợt-2: T8 cần seed phiếu ChoDuyet local (prod 0 mẫu) · A2 chỉ có răng khi seed 1 level cờ=true local · eoffice bundle SẼ rotate (W2e menuKeys fe-user đã chạm)
|
||||
|
||||
verify: dotnet build exit-0 ; npx tsc --noEmit ×2 exit-0 ; dotnet test = 659/0fail ; git status codefiles đổi CHƯA commit (đúng — chờ đợt-2)
|
||||
|
||||
treo-chờ-anh (từ bookend @open, KHÔNG chặn YC-013): slot(67) upload-khi-duyệt · VK-5 nhánh A/B · (72) mfe-eval · ring5-audit slot(41)
|
||||
|
||||
@ -26,11 +26,13 @@
|
||||
13. **Muốn ĐẾM cái UI hiển thị thì DỰNG LẠI dữ liệu thật rồi chạy chính bộ lọc đã ship** — đừng đếm bằng mắt/suy luận. Và cho phép đo **tự kiểm**: cây menu dựng lại được đối chứng bằng invariant T6 (mọi key `MenuKeys.All` phải có menu-row) — dựng sai thì chỉ số đó vỡ trước.
|
||||
14. **Route-resolver có nhánh ≠ thứ đó tồn tại.** `resolvePath` nhận code `'Contract'` không có nghĩa menu-row `AwV2_Contract` tồn tại (0 hit repo). Cùng họ với "bịa field": đọc nhánh code rồi suy ra dữ liệu là suy ngược.
|
||||
15. 🔴 **Khuôn chỉ port được khi VỎ TRANG cùng hình dạng — đo BỀ NGANG bằng SỐ trước khi mirror.** Khuôn cột-trái `19rem` sinh ra cho trang GIẤY-CUỘN; trang 3-PANEL cao-cố-định (`h-[calc(100vh-4rem)]` + `grid-cols-[320px_1fr_360px]`, grid KHÔNG `gap`) không có 304px dư: sidebar `xl:w-80` ⇒ @1366 panel chi tiết 346px→**42px**. Mirror-y-hệt ở đây = hỏng CÂM (build xanh). Cộng bề ngang ra số TRƯỚC, và nhớ cơ chế thu gọn của component có thể là `lg:hidden` = chỉ sống ở dải mà panel kia đã ẩn ⇒ vô dụng ở desktop.
|
||||
16. 🔴 **`title` trên control `disabled` = CÂM ở Chromium** (Chrome/Edge nuốt sự kiện chuột của form-control disabled ⇒ tooltip không bao giờ bật; `<label>` bao ngoài KHÔNG tự thừa kế). Chỗ cắm đúng = `<label>`/wrapper **không disabled**. Hệ quả nặng hơn: **thông tin PHÂN BIỆT không được nằm sau hover** — 1 ô khoá "vì lý do KHÁC" mà chỉ nói bằng tooltip thì trông y hệt N ô khoá "chưa áp dụng" = **ngược nghĩa**, phải có chữ nhìn-thấy-được. Thêm chữ vào ô con của `grid grid-cols-2`: sibling auto-place rơi sang **cột 1** (đọc nhầm sang ô bên trái + đẩy lệch các ô `col-span-2`) ⇒ ghim `col-start-2`.
|
||||
|
||||
---
|
||||
|
||||
## 📌 Nhật ký (móc 1 dòng — verbatim → `archive/2026-H2.md`, cũ hơn → `archive/2026-H1.md` + `_INDEX.md`)
|
||||
|
||||
- **S182 (08-08) vá MAJOR-1** (reviewer pre-commit bắt) — 8 `title` dời `<input disabled>` → `<label>` ở `ApprovalWorkflowsV2Page` (admin-only, 0 cặp mirror). Reviewer liệt 7 site, grep cùng-lớp ra **site thứ 8 `:1443` (K3, có sẵn)** — vá kèm. Ô #4 thêm **chữ hiện** `col-start-2` vì tooltip là thông-tin-phân-biệt. Vùng cấm đo lại sau khi vá: 9/9 `checked=` byte-identical · #8 `Finalize` 0 `disabled`/0 `title`. Control-âm: tiêm lại 1 title vào input ⇒ checker nổ đúng 1. **FLAG-C treo:** nút "+ Thêm NV" `:1226` cùng bệnh, ngoài scope ⇒ khai không chạm. `[s182, title-tren-disabled-la-cam, grep-cung-lop-ra-site-thu-8]`
|
||||
- **S167 (08-01) GĐ3** cây 4-folder lên cụm Duyệt-HĐ → **BLOCKED @§0, 0 file chạm**. 7 loại HĐ = 1 component trang (`?type=`), KHÔNG 7 file. 3 lệch tiền-đề: vỏ 3-panel không nhận khuôn cột-19rem (@1366 còn 42px) · fe-user có 3 ứng viên trang (`MyContracts`/`Inbox`/`WfMatrix`) vs admin 1 · cặp trang KHÁC FILE (`ContractsListPage`⟂`MyContractsPage`) ⇒ SHA-pair identical bất khả thi (đối chứng dương: 4 cặp component/hook/HardCopies BẰNG nhau). +K6 đã ẩn `Contracts` khỏi sidebar admin ⇒ nửa admin là trang deep-link-only. `[s167, khuon-can-cung-vo-trang, sub-implfe-gd3.md]`
|
||||
- **S166 (08-01) K6** admin thu gọn về vai NƠI-CONFIG (fe-admin ONLY 1 file, fe-user 0-diff). `filterForAdmin` lọc ĐỆ QUY ⇒ chỉ liệt 12 ROOT; **dựng lại cây seed 200 node rồi chạy bộ lọc đã ship ⇒ 166 lá → 15 lá liệt đủ tên**, tự-kiểm bằng invariant T6 (113 key `All` → 0 thiếu row). Comment vá lineage **2 lần đảo**. `[s166, k6-admin-thu-gon, route-resolver-khac-menu-row]`
|
||||
- **S166 (08-01) K4c** WfView lọc nhóm (chốt cụm-2, fe-user ONLY 1 file, fe-admin 0-diff). Lọc `wf.code === khkkGroupWorkflowCode(n)` client-side ⇒ **không đổi queryKey** (khai rõ); rỗng-vì-lọc tách câu riêng. 21/21 proof + 2 control-âm. `[s166, khkk-k4c-wfview-loc-nhom]`
|
||||
|
||||
@ -8,6 +8,7 @@ Read-only INTERNAL audit SOLUTION_ERP. Tools: Read/Grep/Glob/Bash + 5 RAG MCP. O
|
||||
- ❌ NOT: external docs/CVE/lib → investigator-api · write → implementer · test → test-specialist · architecture decision → em main
|
||||
|
||||
## 📌 Recent invest (1-line digest; verbatim → archive + runs)
|
||||
- **S182 (08-08) ×Fable KHKK 8-nhóm AWF mirror DuyetNcc [`runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-invest-be-fable.md`]:** BE generic ~85% — 8 workflow seed `KHKK-N{n}` CÙNG ApplicableType=10, phân biệt bằng CODE (`DbInitializer.cs:549-687`, idempotent per-Code CỐ Ý né khuôn per-type) ⇒ bất biến only-1-active-per-type VỠ TỪ SEED + POST version mới deactivate CẢ 8 (`ApprovalWorkflowV2AdminFeatures.cs:339-343`); 9-checkbox = 9 cột per-Level dùng chung mọi type nhưng KHKK runtime CHỈ đọc F5 (`ContractSigningPlanWorkflowService.cs:276`) ⇒ 6/9 tick cho type-10 = #78-class cờ-nằm-im; bypass rào-(v) 2-bước (UpdateDraft re-pin `ContractSigningPlanFeatures.cs:603-607` chỉ check type, không re-check group-match); DELETE version 0 usage-check + `DbUpdateException` KHÔNG được middleware map ⇒ FK Restrict nổ 500 không phải 409 (`GlobalExceptionMiddleware.cs:39`); 32 policy `AwV2_KhkkN*` 0 consumer BE (0-hit Api + control-dương `KeHoachKyKet` 29-hit cùng path) — mirror y 2 key `AwV2_DuyetNcc*` cũ; FE 8 route KhkkN đổ landing chung (`TYPE_CODE_TO_INT` thiếu key). #53 cắt return giữa phiên — artifact đổ-ruột-liên-tục cứu trọn, 0 mất. Tag `[s182, seed-code-family-cung-type, khkk-runtime-chi-F5, policy-menu-only, dbupdate-500]`
|
||||
- **S180-W1a (08-07) kiểm-kê MÁY-ĐỌC-TÊN [`runs/2026-08-07-S180-adap-upgrade-pack-phased/sub-w1a-mayd-octen.md` 44.9KB]:** 14 đoạn máy (11 vỡ-CÂM · 3 ồn · **0 tự-thích-nghi**) · BÁC 2 (`detectors:1716`+`:1679` = CHÚ THÍCH không phải mã; `applied-eval-nokey.ps1` = 0 file-IO, control-dương mfe-eval=14). **Luật rút ra: nhóm tên sạch CHÍNH XÁC tới độ sâu máy KIỂM, không sâu hơn 1 nấc** — STOP-HARD KHÔNG cần (`detectors:1639` chỉ INFORM mà `session-<N>` sạch 12/12); cần **máy TỪ CHỐI**, vì *đọc-rồi-liệt* (`$unmapped:1109`) + *chấp-2-tên* (`-or :1660`) + *[skip] not found* đều để tên trôi. 🔴 `memory-selfimprove-audit.ps1:298` còn dùng `-Filter 'sub-*.md'` = ĐÚNG bug C11(b) đã vá @S159 ở `detectors:1102` — **vá tệp A quên tệp B**. Bất biến ngầm 0-máy-kiểm: `agents/<role>.md`.BaseName ≡ `agent-memory/<role>/`. Miễn-trừ 5 vùng đóng băng nuốt **73,2% kho .md (1131/1545) + 90,7% lệch đã biết (225/248)** ⇒ máy soi quét .md trả 0-hit vô nghĩa; đường thoát = quét **231 tên vật-bộ-máy, 0 miễn trừ**. `CLAUDE.md:151` khớp 64/159=40,3% đĩa, 0 máy chặn, sống nhiều tháng. Tag `[s180, sach-toi-do-sau-may-kiem, va-tep-A-quen-tep-B, mau-so-sau-mien-tru]`
|
||||
- **S180 (08-07) ×Fable fit-map 4-thư upgrade-pack 7GĐ [`runs/2026-08-07-S180-adap-upgrade-pack-phased/sub-invest-fitmap-S180.md`]:** PHỦ-ĐỦ 7GĐ/12TC/9LỖI. Nghề: hash mesh CHỈ tin máy canonical `scripts/stamp_verify.py` (tự chế MISMATCH cả 4 = suýt phán tamper sai) · TÔI = vai 7/7 dính #53 + bị bắt LỚP-7 tự-giẫm (202K đọc/7KB đĩa) → "đổ ruột TRƯỚC đo SAU" cứu; tỉ-lệ byte-đĩa/token-đọc = tín hiệu giám sát · xung đột duy nhất gói: SE exit-0-advisory (`governance-detectors.ps1:1963`) ⟂ hub cổng-chặn-mã-lỗi → VƯỢT-KHUNG · GĐ-5 gần xong nhất / GĐ-4 hổng nhất (0 sổ-yêu-cầu-anh, 0 chấm-điểm) · bác-một-phần lead: `pull_warn_days` trống = CỐ Ý owner có khai (`check-email.md:48`) ≠ ghost-wire. Tag `[s180, stamp-verify-canonical-only, lop7-tu-giam, exit0-vs-gate]`
|
||||
- **S175 (08-05) ×Fable cột DỰ ÁN ma-trận NS [seed on-behalf; `runs/2026-08-05-S175-duan-budget-column/`]:** BÁC 2 tiền-đề lead — cột = CHỖ-CHỪA-SẴN (`e33481e` cùng-commit comment `PeWorkItemBudget.cs:19`, KHÔNG phải di-chứng Mig50) · `—` = 11 ô/app không phải 3; phả-hệ đo bằng commit-sinh-ra + comment cùng commit, đừng suy từ khoảng-thời-gian-gần. `Project.BudgetTotal` sống từ Mig-2 nhưng Dev 61/69 NULL = "cơ-chế đúng dữ-liệu không có" lần 7. Spawn TASK-RỖNG cứu bằng run.md trên đĩa. Tag `[s175, pha-he-commit, artifact-dia-cuu]`
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Reviewer Agent — Persistent Memory
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S182 — verbatim 2 lượt, probe 7/7 + control-âm 0] S182 (08-08) YC-013 KHKK 8-panel — lượt SPEC `2C/5M`, lượt DIFF pre-commit `0C/3M`+6m:** 🟢 **3/3 finding 🔴 lượt-1 được đóng ĐÚNG BẢN CHẤT** (assembly-ranh → file `public static class` riêng · `IgnoreQueryFilters` **2 CHIỀU NGƯỢC NHAU trong cùng 1 diff**: DELETE bỏ filter vì FK còn thật ⟂ changelog GIỮ filter vì ghi vết phiếu-đã-xoá là rác · 8 module pin verified bằng grep Domain, loại +1 hit FK nội bộ) — dữ liệu hiếm, ghi để đừng phá. 🔴 **tooltip đặt trên `<input disabled>` ⇒ Chromium KHÔNG hiện** (label bao ngoài không mang `title`) — quyết-định-owner nằm trong tooltip mà tooltip không có cửa hiện = **máy dựng không ai gọi**; ô "hành-vi-LUÔN-BẬT" câm ⇒ đọc thành NGƯỢC NGHĨA. 🔴 **3 thành phần bất đồng HOA/thường** (regex CultureInvariant **case-SENSITIVE** ⟂ `Code ==` theo collation SQL Server **CI** ⟂ FE `.toUpperCase()`) và **test MÙ THEO CẤU TRÚC vì SQLite = BINARY case-sensitive ⇒ tầng DB trong test có ngữ nghĩa NGƯỢC prod** — không assertion nào bịt được. 🔴 **guard mới ở write-path CHÍNH (Create) 0 test** — lỗ của SPEC, implementer đúng spec, **KHÔNG hạ thước theo spec**. **m:** assert `Contain("N5")∧Contain("N3")` **mù THỨ TỰ toán hạng** (3 call-site 2 tham số kề — phải đối chiếu TAY) · **BOM `ef bb bf`+CRLF lẫn vào commit code** ⇒ `git add` từng file. **FLAG-A đo được:** `npx tsc --noEmit` trần biên dịch **0 file** (`files: []`+references) = xanh THEO CẤU TRÚC; `--listFiles|wc -l` 0 ⟂ 225/214 ⇒ **acceptance "lệnh X xanh" phải kèm phép chứng thước KHÔNG RỖNG**.
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S180 — verbatim, probe 10/10 + control-âm 0] S180 (08-07) LANE A/6 GĐ-0+GĐ-1 (đĩa `runs/…-S180-adap-upgrade-pack-phased/sub-n3-lane-a-gd01.md` 57 KB):** 🔴 **"0 hit" ĐÚNG THEO LUẬT vẫn là LỖ THẬT** — `_index` thiếu **24/69** thư (lead giao "4"); `to=se` **23/23 sạch** ⟂ `to=all-fit` 22 có/20 thiếu; luật `:7` trỏ `COMMS-LEDGER` mà `find "*COMMS*"`=**0 trong SE** (control `*ledger*`=4) ⇒ **sổ mà luật trỏ tới KHÔNG TỒN TẠI**; S153 phán dương-giả = đúng cho câu-hỏi CŨ. ⇒ gặp 0-hit phải hỏi thêm **"sổ đó có tồn tại không"** · **phân-họ regex 160/159, lệch 1 vì `S57bis`** ⇒ luôn cộng lại đối-chiếu tổng · **`CLAUDE.md` quy-ước nhật-ký khớp ≤64/159 đĩa** · **"single source" tự-xưng ≠ chuẩn** (bảng-dịch; 3 phép thử tiên-đoán/phủ-sóng/hướng) · **số-trong-chú-thích trôi** (`hmw.js` "11 class" ⟂ đĩa **12**).
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S179 — verbatim] S179 (08-07) C7 re-review sau vá — `PASS-WITH-8`, 3 MAJOR:** **2/3 MAJOR là lỗi TRONG CHÍNH BẢN VÁ vừa pass 12/12 acceptance** — vì acceptance đo **HÌNH-DẠNG chuỗi, không đo HÀNH-VI**. `cut -c4-` đẻ path RÁC (rename 1 dòng `R old -> new`) · ba-chấm→hai-chấm = **regression có điều-kiện** (thủ phạm thật = `git diff` mù untracked, lead gán sai tội) · mỏ-neo `:112` **tự-vô-hiệu ngay lúc viết** ⇒ neo TÊN SELECTOR không neo số dòng · census "8/9" thiếu 3 ý đóng im lặng.
|
||||
@ -79,20 +81,16 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
- Hook-cap **>17.1KB** (24.4KB read-limit) → move recent → L2 `archive/<period>.md` (additive) + `_INDEX.md` pointer. Stale >3mo → remove.
|
||||
- **Trình tự BẮT BUỘC (S102 recovery, cut-not-moved ×5 `f229b07`):** append archive TRƯỚC → probe moved-not-cut **bằng token lấy từ THÂN BÀI, không lấy từ digest** (digest là paraphrase ⇒ 0-hit GIẢ; @S168 suýt giữ 938 B vô ích) + control-âm → mới cắt.
|
||||
- **3 bài đo:** số trong sổ phải là số **ĐO** không ước (`wc -c`, baseline thừa-kế cũng stale) · script sinh chữ **đẻ chữ hỏng câm** (byte-count không bắt, chỉ ĐỌC LẠI mới thấy) · hook đếm **KÝ TỰ** còn `wc -c` đếm **BYTE** — tiếng Việt đa-byte ⇒ 2 số lệch nhau là bình thường, đừng truy "ai sai".
|
||||
- **Prev:** S180 (probe 10/10 + control-âm) · S168 (22.2→17.1) · S166-K5 (3/3) · S162 (2/2) · S145 · S140 · S134 · S128 · S102 · S116 · S113 · S109 · S80 · S71 · S70 · S40.
|
||||
- ⚠️ **NỢ CÒN LẠI (đo, không ước):** curate S182 đưa **22.518 → 20.292 B** (−2.226, đồng thời THÊM entry S182) — **vẫn trên trần mềm 17.1KB**, còn dưới trần đọc 24.4KB nên chưa truncate. Lượt sau cắt tiếp: 2 bullet dài nhất là **S182 (dòng 3)** và **digest gộp S167-K6… (dòng ~17)**, cả hai đã có verbatim trong `archive/`. 🔴 **2 bài THƯỚC phiên này:** probe archive bằng token lấy từ **DIGEST L1** cho **3/7 = 0-hit GIẢ** (archive dùng chữ khác) ⇒ luôn lấy token từ **THÂN BÀI** · **`grep -icF` trong MSYS trả CHUỖI RỖNG thay vì `0`** ⇒ đếm bằng `python -c "s.count(...)"`, đừng tin `$(grep -c)`.
|
||||
- **Prev:** S182 (22.5→20.3, probe python 7/7 + control-âm 0) · S180 (probe 10/10 + control-âm) · S168 (22.2→17.1) · S166-K5 (3/3) · S162 (2/2) · S145 · S140 · S134 · S128 · S102 · S116 · S113 · S109 · S80 · S71 · S70 · S40.
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S152-R1 — verbatim] S152 R1 ranh NEVER:** verdict-only; NEVER áp cho ĐÍCH (code-tree·commit·push — chưa từng vỡ); Write/Edit runtime hợp lệ cho sub-file run-folder + diary (AS-10); containment thật = git-diff lead.
|
||||
|
||||
- **[→ archive/2026-07.md @S165-curate] S159 đợt-1 (07-29) diff pipeline — 7 FLAG:** rút TOÀN BỘ chuỗi hiển-thị MỚI từ diff rồi soi từng chuỗi = phép đo 100%-tiếng-Việt · 2 queryKey cùng resource = 2 ảnh-chụp khác thời-điểm · a11y nút TRÙNG tên đọc-được · bác-claim cũng phải có chứng.
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S176 — verbatim] S176 (08-06) SETUP-REVIEW đội STYLE 3 vai — `SỬA-6`:** chấm **THIẾT-KẾ một đội sub-agent**, không phải diff code (trục mới của vai). 🔴 **thước dispatch MÙ WORKING-TREE** (`git diff origin/main...HEAD` ba-chấm trả RỖNG ⇒ đội gate pre-commit **chết câm** đúng use-case chính) · **đo TREE ≠ đo HISTORY** (thước tree tự lành ca revert ⇒ lo-nghi "revert phá thước" = lo hão) · **miễn-trừ chỉ-con-trỏ = dấu cao-su** ⇒ đòi TRÍCH NGUYÊN VĂN ≤1 dòng, khép vòng bằng owner-visibility · **thách-CLEAN bẻ được nếu không bắt neo `file:line` cả 2 bờ** · **3/5 lo wiring của lead là LO HÃO** (script enumerate ĐỘNG — đừng giả-định hardcode, mở ra đo) · **chép-luật-vào-persona vi phạm chính B1 viết trong CÙNG file** ⇒ viết luật cấm-chép xong phải grep chính file mình. *(🔄 @S180: khoản "3 vai thiếu dir agent-memory" ĐÃ ĐÓNG — 26⟂26 khớp.)*
|
||||
- **[→ `archive/2026-08.md` §@S176 — verbatim] S176 (08-06) SETUP-REVIEW đội STYLE 3 vai — `SỬA-6`:** chấm **THIẾT-KẾ một đội sub-agent**, không phải diff code. 🔴 **thước dispatch MÙ WORKING-TREE** (`git diff origin/main...HEAD` ba-chấm trả RỖNG ⇒ gate pre-commit chết câm đúng use-case chính) · **đo TREE ≠ đo HISTORY** · **miễn-trừ chỉ-con-trỏ = dấu cao-su** ⇒ đòi TRÍCH NGUYÊN VĂN ≤1 dòng · **thách-CLEAN bẻ được nếu không bắt neo `file:line` cả 2 bờ** · **3/5 lo wiring của lead là LO HÃO** (script enumerate ĐỘNG — mở ra đo) · **chép-luật-vào-persona vi phạm chính B1 viết trong CÙNG file**.
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S180-LANE-B — verbatim, probe 3/3 + control-âm 0] S180 (08-07) N3 LANE-B GĐ-2 (digest):** NGHI-THỨC ≠ MÁY — đọc CỘT KIỂU error-ledger (AS-17 `procedural`, AS-19 tự khai chưa-cơ-khí-hoá) · guard-byte MÙ skeleton ⇒ vị-từ nhãn-treo-heading (use⟂mention, 0 dương-giả) · sổ RETIRED-có-khai vẫn làm điểm-danh-2-nguồn MẤT VẾ PHẢI (73/88); nguồn-2 = taskList snapshot run.md, KHÔNG phải `run-chua-gom` (đọc đĩa = nguồn-1) · secrets_sweep chỉ rglob sessions/ + runs/ excluded ⇒ vật chứng đắt nhất ngoài 2 lưới cùng lúc · tỉ lệ không mẫu số = vô nghĩa (42%/72%/25% cùng 1 sự thật) · lead-stale-auditor 0-hit runs/ (control FLAG=4).
|
||||
- **[→ `archive/2026-08.md` §@S180-LANE-B — verbatim, probe 3/3 + control-âm 0] S180 (08-07) N3 LANE-B GĐ-2 (digest):** NGHI-THỨC ≠ MÁY (đọc CỘT KIỂU error-ledger) · guard-byte MÙ skeleton ⇒ vị-từ nhãn-treo-heading (use⟂mention) · sổ RETIRED-có-khai vẫn làm điểm-danh-2-nguồn MẤT VẾ PHẢI · **secrets_sweep bỏ sót đúng thư-mục chứa vật chứng đắt nhất** (2 lưới hụt cùng lúc) · **tỉ lệ không mẫu số = vô nghĩa** (3 số cùng 1 sự thật) · auditor 0-hit vì không quét `runs/` (control FLAG=4).
|
||||
|
||||
|
||||
- **[→ `archive/2026-08.md` §@S180-ba-khối — verbatim, probe 6/6 + control-âm 0] S180 (08-07) N4 wave-plan · W0 lane-a fitmap · W0 lane-b `_index` (digest gộp):** **ô-đòi-MÃ không nhận CÂU-VĂN** (wave không trỏ mã TC = thoát ràng buộc nghiệm thu) · tách trục **CHỮA ⟂ NGHIỆM-THU** (thiếu THƯỚC ≠ thiếu THUỐC) · 2 hệ đánh số trong 1 artifact phải khác TIỀN-TỐ · **ghi-đĩa-4-nhát qua Edit-append anchor-đuôi** (13 vai dính #53, 4 nhát land sạch).
|
||||
- 🔴 **acceptance ĐO HÌNH-DẠNG-DÒNG, KHÔNG ĐO GIÁ-TRỊ-Ô** — `grep -c '^| GĐ-[0-6]'` vẫn ra 7 dù ô điền sai từ-vựng (từ-vựng Phần C nhét vào ô Phần A ⇒ đẻ giá-trị-thứ-tư, đúng lỗi file sinh ra để đóng, nghiệm thu vẫn xanh) ⇒ thêm phép đo GIÁ-TRỊ · **2 tệp anh em cùng bảng 7 dòng = dương-giả thật** ⇒ chạy chéo 2 chiều + control-dương · **`grep -c` exit 1 khi 0-hit CẮT ĐỨT chuỗi `&&`** · **1 phép đo 3 nguồn 3 số** ⇒ chép PHẦN LIỆT KÊ, đừng chép số tổng.
|
||||
- 🔴 **W0-b, 3 bài đắt nhất:** (a) **lệnh lead sai theo chiều PHÁ chính mục-đích lệnh** — *"backfill `se` cho 22 dòng cũ"* nhưng sổ có **45 dòng** (23 `se` + 22 `all`) ⇒ gán hàng loạt dán nhãn sai lên đúng 22 dòng fan-out mà P6 sinh ra để hợp-thức-hoá; gốc = **hai số 22 khác nghĩa va nhau trong CÙNG bản giao việc** ⇒ trị bằng + nghĩa ngược = con số thôi làm chứng, dẫn giá-trị **từng dòng từ đĩa**. (b) **`grep -c <substring>` có TRẦN CỨNG** — lead đọc `=4` thành *"mới bù 4"* trong khi **17 dòng đã land** ⇒ **biết tên bẫy KHÔNG đủ, phải khai mẫu-số NGAY CẠNH con số**. (c) **glob `*/` bỏ trọn thư-mục GỐC** ⇒ mù đúng nơi thư *chưa xử lý* hạ cánh; 69/24 khớp chỉ vì gốc chỉ có `README.md` = trùng hợp, không phải tương đương.
|
||||
- 🟢 **Khuôn tái dùng:** thêm cột vào sổ bằng-chứng thì đặt **CUỐI hàng** ⇒ *"không sửa dòng cũ"* thành **phép kiểm chạy được** (dòng cũ = **tiền-tố nghiêm ngặt** của dòng mới; 45/45, khối kia byte-identical). 🔸 `status` **định-nghĩa-bằng-thư-mục** ⇒ gộp 2 kênh làm cột chỉ đo đúng NỬA số dòng (fan-out `/adap-apply` không đụng thư mục ⇒ `processed` = *đã move*, KHÔNG phải *đã áp*). 🔸 **mốc nước nói về VẮNG-KHỎI-HỘP**, không miễn cho thư ĐÃ NẰM TRONG hộp mà thiếu dòng — **lead nhận lập luận, phán bù nốt 7** ⇒ đợt-2: 24/24 dòng bù, `stamp_verify.py` **24/24 exit 0 · 0 tamper**, vắng-mặt **24→0**, 5/5 bất-biến đo LẠI vẫn đạt. `status: DRAFT` ⟂ đòi-dấu-đã-duyệt = **lệch trạng-thái, KHÔNG tamper**. 🔸 2 số 69 (đĩa ⟂ dòng) **KHÔNG phải phép đo thứ hai** — là hệ quả của (0 vắng ∧ 0 mồ-côi ∧ 0 trùng).
|
||||
- **2 thước hỏng:** **heredoc `<<'EOF'` gãy** với tiếng Việt + backtick (lead dính y hệt) ⇒ tệp dài dùng **Write/Edit** · **probe `grep -F` sai HOA/thường ⇒ 0-hit GIẢ** (`ô-` ⟂ `Ô-`) ⇒ 0-hit phải thử lại **đúng ca chữ TRƯỚC** khi kết luận mất nội dung.
|
||||
- Tag `[s180, w0-lane-b, colliding-22, grep-substring-ceiling, glob-star-slash-blind, prefix-preserving-column, status-defined-by-folder, shape-vs-value-acceptance, heredoc-broken, probe-case-trap]`
|
||||
- **[→ `archive/2026-08.md` §@S180-ba-khối — verbatim, probe 6/6 + control-âm 0] S180 (08-07) N4 wave-plan · W0 lane-a/b (digest gộp):** **ô-đòi-MÃ không nhận CÂU-VĂN** · tách trục **CHỮA ⟂ NGHIỆM-THU** · 2 hệ đánh số trong 1 artifact phải khác TIỀN-TỐ · 🔴 **acceptance ĐO HÌNH-DẠNG-DÒNG chứ không ĐO GIÁ-TRỊ-Ô** (`grep -c '^| GĐ-…'` vẫn xanh dù ô điền sai từ-vựng) · **2 tệp anh em cùng hình-dạng = dương-giả thật** ⇒ chạy chéo 2 chiều + control-dương · **`grep -c` exit 1 khi 0-hit CẮT ĐỨT chuỗi `&&`** · **lệnh lead có thể sai theo chiều PHÁ chính mục-đích lệnh** (2 số cùng trị khác nghĩa va nhau trong CÙNG bản giao việc) ⇒ dẫn giá-trị **từng dòng từ đĩa** · **`grep -c <substring>` có TRẦN CỨNG** ⇒ khai mẫu-số NGAY CẠNH con số · **glob `*/` bỏ trọn thư-mục GỐC** · 🟢 thêm cột thì đặt **CUỐI hàng** ⇒ "không sửa dòng cũ" thành phép kiểm chạy được (tiền-tố nghiêm ngặt) · `status` **định-nghĩa-bằng-thư-mục** đo đúng NỬA số dòng · **2 số bằng nhau từ 2 nguồn KHÔNG phải phép đo thứ hai** nếu số sau là hệ-quả số trước · thước hỏng: **heredoc `<<'EOF'` gãy** với tiếng Việt+backtick ⇒ dùng Write/Edit · **probe `grep -F` sai HOA/thường ⇒ 0-hit GIẢ**.
|
||||
|
||||
@ -93,3 +93,40 @@
|
||||
- `stamp_verify.py` **17/17 OK, exit 0, 0 tamper**; 4 thư gói khai `content_sha256` **8-hex** (script so tiền-tố). 4 thư mang `status: DRAFT`+`reviewer_gate: PENDING` ⟂ `thu-chinh` đòi dấu đã-duyệt ⇒ **lệch trạng-thái, KHÔNG tamper**.
|
||||
- Thước hỏng phiên này: **heredoc `<<'EOF'` gãy với nội dung tiếng Việt + backtick** (lead dính y hệt) ⇒ ghi tệp dài dùng **Write/Edit**, đừng `cat >>`.
|
||||
- Tag `[s180, w0-lane-b, broadcasts-index, p6-additive, colliding-22, grep-substring-ceiling, glob-star-slash-blind, prefix-preserving-column, status-defined-by-folder, watermark-scope, heredoc-broken]`
|
||||
|
||||
---
|
||||
|
||||
## @S182 — YC-013 KHKK 8 panel nhóm mirror DuyetNcc (2 lượt review: SPEC rồi DIFF)
|
||||
|
||||
**Đĩa:** `runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-review-1-fable.md` (lượt SPEC) · `.../sub-reviewer-precommit.md` (lượt DIFF).
|
||||
|
||||
### Lượt 1 — review SPEC (`PASS_WITH_FIXES 2C/5M`)
|
||||
|
||||
Chấm SPEC = đo lại claim của spec trên code. Đa số HELD, nhưng:
|
||||
|
||||
- 🔴 **spec-unbuildable vì ASSEMBLY-RANH.** Spec bảo "extract helper rồi gọi 3 site" — chết ở site-3 (`SubmitAsync`) vì site đó nằm ở **Infrastructure** còn helper định để `internal` trong **Application**, và `InternalsVisibleTo` chỉ mở cho Tests. ⇒ **Luật rút ra:** mọi đề-xuất extract-helper đa-assembly phải kiểm ACCESSIBILITY trước khi phán "vá rẻ".
|
||||
- 🔴 **usage-count trên bảng có `HasQueryFilter` mà thiếu `IgnoreQueryFilters()`** = đếm sót phiếu xoá-mềm trong khi FK vẫn chặn ⇒ test hẹp xanh, ca phổ biến vẫn 500. Claim "hết 500" MẠNH HƠN bản vá.
|
||||
- 🔴 **"FK chặn nên data không mất" phải grep FK THẬT.** 8 cột pin ⟂ chỉ 4 `HasForeignKey` hit ⇒ 5 pin loose-Guid hard-delete êm + dangling. Suy từ 3 module đã biết là suy sai.
|
||||
- **2 invest cùng dùng chữ "per-Code" nhưng KHÔNG cùng công-thức** (`typeEnum==10` ⟂ regex-họ-mã) — khác hành vi ở code ngoài-họ. Spec khai "hội tụ" ⇒ che mất chỗ lệch. Phải khoá công thức + test ca phân-kỳ.
|
||||
- **Cờ ignored-nhưng-hành-vi-luôn-bật**: gray-out kiểu "chưa áp dụng" = nói dối theo CHIỀU NGƯỢC.
|
||||
- Baseline "645" trong spec ⟂ STATUS 644 — đo tươi, đừng chép.
|
||||
|
||||
### Lượt 2 — review DIFF pre-commit (`PASS_WITH_FIXES 0C/3M` + 6 minor)
|
||||
|
||||
**Đo độc lập (không nhận số của lane):** `dotnet test` = 45 + 614 = **659 P / 0 F** (baseline 645 = 45+600 ⇒ +14 đúng ở Infrastructure) · `npm run build` ×2 exit 0 · tsc `-p tsconfig.app.json` 0-err (225 / 214 file).
|
||||
|
||||
**3/3 finding 🔴 của lượt 1 được đóng ĐÚNG BẢN CHẤT, không đóng bằng chữ** — đây là dữ liệu hiếm: (a) assembly-ranh → file `public static class` riêng ở Application, 3 call-site, đối số đúng thứ tự cả 3; (b) `IgnoreQueryFilters` đúng 3 bảng có filter, và **phân biệt được 2 CHIỀU** (DELETE cần bỏ filter vì FK còn thật; changelog CỐ Ý giữ filter vì ghi vết cho phiếu đã xoá là rác) — cùng một hàm-gọi, hai ngữ nghĩa ngược nhau, có khai tại chỗ; (c) 8 module pin verified bằng `grep -E "public (Guid|Guid\?) ApprovalWorkflowId"` trên Domain (đúng 8, +1 hit FK nội bộ của Step phải loại), 5 module loose-0-FK được khai là "rào DUY NHẤT" và T12 chứng minh dangling THẬT trong DB.
|
||||
|
||||
**MAJOR-1 — tooltip gắn trên `<input disabled>` ⇒ Chrome/Edge không hiện.** 7 `title=` đặt trên chính checkbox bị `disabled`; Chromium chặn sự kiện chuột trên form-control disabled nên native tooltip thường không bật (Firefox thì có), và `<label>` bao ngoài KHÔNG mang `title` nên rê vào chữ cũng không ra. Nặng nhất ở ô #4: câu riêng của nó là thông tin DUY NHẤT phân biệt "cờ chưa phân nhánh" với 6 ô "chưa áp dụng" ⇒ tooltip câm = ô #4 đọc thành NGƯỢC NGHĨA, đúng class lỗi mà ERRATA vừa sửa xong ở tầng `checked`. **Luật rút ra: quyết-định-của-owner nằm trong tooltip thì phải kiểm tooltip có CỬA HIỆN không — máy dựng mà không ai gọi.** Tiền lệ cùng bệnh đã tồn tại từ đợt trước ⇒ vá phải vá cùng LỚP.
|
||||
|
||||
**MAJOR-2 — 3 thành phần bất đồng HOA/thường, và test MÙ THEO CẤU TRÚC.** Cùng câu hỏi "code này có phải nhóm N{n}", ba luật khác nhau: regex `^KHKK-N([1-8])$` **CultureInvariant, KHÔNG IgnoreCase** (case-sensitive) ⟂ `w.Code == request.Code` chạy theo **collation SQL Server `_CI_AS`** (case-INsensitive) ⟂ FE `.toUpperCase()` (case-insensitive). Hệ quả: mã `khkk-n1` gõ tay ở panel gộp sẽ **hạ IsActive bản thật**, **vẫn được FE auto-pin**, mà **rào nhóm + rào version-cuối TẮT im lặng**. 🔴 **Test không thể thấy vì fixture chạy SQLite (BINARY = case-SENSITIVE) ⇒ tầng DB trong test có ngữ nghĩa NGƯỢC với prod.** Đây không phải test ẩu — mù theo cấu trúc, không assertion nào bịt được. **Luật rút ra: mọi bất-biến dựa trên so-sánh CHUỖI ở tầng DB đều không được test-SQLite bảo chứng; chuẩn hoá ở ĐƯỜNG GHI (`Trim().ToUpperInvariant()`) hoặc khai phụ-thuộc-collation ngay tại chỗ.**
|
||||
|
||||
**MAJOR-3 — guard mới ở write-path CHÍNH có 0 test.** Rào D6 cắm 2 site (Create + UpdateDraft) nhưng 4 chân của ca test đều đi qua UpdateDraft ⇒ nhánh Create (đường mọi phiếu prod sinh ra) không có răng nào. Gỡ nó đi thì 659 test vẫn xanh. Đây là **lỗ của SPEC** (spec liệt 14 ca và thiếu ca này), implementer làm đúng spec — nhưng **không hạ thước theo spec**.
|
||||
|
||||
**MINOR đáng nhớ:** (1) `Contain("N5").And.Contain("N3")` **mù THỨ TỰ toán hạng** — hàm mới nhận 2 tham số kề nhau ở 3 call-site, đảo là vẫn xanh (tôi phải đối chiếu TAY mới chốt được cả 3 đúng); pin nguyên câu có thứ tự mới giữ được bất-biến. (2) File governance bị thêm **BOM `ef bb bf` + CRLF** lẫn vào commit code (HEAD `7b 0a` → worktree `ef bb bf 7b 0d 0a`) ⇒ `git add` từng file, đừng `-A`. (3) rào version-cuối đếm anh-em **KHÔNG lọc `ApplicableType`**. (4) "state THẬT" của ô disabled chỉ đúng cho đường CLONE/EDIT — đường TẠO MỚI thì 6 ô khoá bị đóng băng ở DEFAULT của FE, admin không sửa được.
|
||||
|
||||
**FLAG-A của FE lane — đo được và ĐÚNG:** `npx tsc --noEmit` trần ở repo này **biên dịch 0 file** (`tsconfig.json` = `{"files": [], "references": [...]}`) ⇒ luôn xanh **theo cấu trúc**. Chứng bằng `--listFiles | wc -l`: **0** dòng cho dạng trần ⟂ **225 / 214** dòng cho `-p tsconfig.app.json`. 🔸 Bổ sung: `npm run build` = `tsc -b && vite build`, mà `tsc -b` CÓ đi theo project references ⇒ acceptance `npm run build` là thước THẬT. **Luật rút ra: acceptance kiểu "lệnh X xanh" phải kèm MỘT phép đo chứng thước không rỗng (`--listFiles`, `-c`, số file/ca) — exit 0 của một lệnh không đọc file nào trông y hệt exit 0 của một lệnh đọc 225 file.**
|
||||
|
||||
**Khuôn chống-nhiễu đáng tái dùng (từ chính bộ test này):** ca Submit có SẴN 3 cửa 409 khác ⇒ bắt được `ConflictException` là CHƯA nói lên gì; bộ test xử bằng cách **kéo dữ liệu fixture đi theo biến đang đo** (hạng-mục lấy `ApprovalGroup` của phiếu) để 2 cửa nhiễu im, rồi thả **một phiếu chứng-nhân khác đúng một biến** trong CÙNG lượt.
|
||||
|
||||
- Tag `[s182, yc-013, khkk, assembly-boundary-extract, ignorequeryfilters-two-directions, tooltip-on-disabled-input, collation-vs-sqlite-blind, guard-untested-write-path, argument-order-blind-assert, vacuous-tsc-noemit, bom-in-code-commit]`
|
||||
|
||||
57
.claude/sessions/session-13/_context-s-13.md
Normal file
57
.claude/sessions/session-13/_context-s-13.md
Normal file
@ -0,0 +1,57 @@
|
||||
# _context-s-13 — SESSION-MODEL narrative durable
|
||||
|
||||
<!-- SCAFFOLD-META: sID=13 | ts-moc=2026-08-07T23:37:36+07:00 | nguon-ts=git log -1 --format=%cI @ HEAD 936d1aed | generator=scripts/session-scaffold.ps1 -->
|
||||
|
||||
> **sID (LOGIC-session) = 13.** 1 phiên-logic (bootstrap → work → sweep) trải nhiều window vật-lý nối bằng `/pause`+`/tiep`. Window = 0-ID governance; **sID = đơn-vị LOGIC**. Folder pin: `.claude/sessions/session-13/`.
|
||||
> ts-mốc khởi-tạo = **2026-08-07T23:37:36+07:00** (nguồn: `git log -1 --format=%cI @ HEAD 936d1aed`) — mốc git-committer, KHÔNG phải wall-clock tuỳ-tiện.
|
||||
>
|
||||
> 🔴 **CẤM rewrite / tự-tóm block FLOW** (chống self-summary lossy). FLOW = append-only @`/pause`-only; vượt cap `session_ctx_kb` → distill SECTION CŨ thành pointer/gist, TUYỆT ĐỐI KHÔNG sửa entry verbatim đã ghi.
|
||||
|
||||
---
|
||||
|
||||
## (a) STOCK-map — reference-not-copy
|
||||
|
||||
> Bảng pointer tới nguồn durable. **KHÔNG copy nội-dung** (chống drift). Điền @bootstrap; row session-specific append dưới các row stable.
|
||||
|
||||
| Tên | Path | 1-dòng |
|
||||
|---|---|---|
|
||||
| WAL | `.claude/WAL.md` | mạch-việc-dở máy-state (H-22) — nối bằng `/tiep` |
|
||||
| AI-context | `CLAUDE.md` | AI agent context SOLUTION_ERP (scope + quick-rules) |
|
||||
| Trạng-thái | `docs/STATUS.md` | canonical mọi con-số (mig · bảng · test · roster) |
|
||||
| Bàn-giao | `docs/HANDOFF.md` | brief 5 phút + bảng số chờ-anh + carry |
|
||||
| Sổ đếm H24 | `.claude/governance/.session-counter.json` | counter + nhịp lead-self-audit |
|
||||
| Thư cross-project | `broadcasts/_index.md` | INBOUND directed (`outbox/se`) — fan-out `outbox/all` KHÔNG vào đây |
|
||||
| _(session-specific)_ | _(path)_ | _(1-dòng — append @bootstrap phiên này)_ |
|
||||
|
||||
---
|
||||
|
||||
## (b) FLOW — append-only @pause-only
|
||||
|
||||
> 🔴 **CẤM rewrite FLOW.** Mỗi `/pause` APPEND đúng 1 entry theo SCHEMA dưới (đặt DƯỚI dòng `FLOW-START`). Entry cũ = immutable. FLOW = SOURCE durable của "đang-đến-đâu"; bảng-đầu-việc = VIEW derive từ đây (single-source).
|
||||
|
||||
<!-- ENTRY-SCHEMA (guidance, KHONG phai entry that — /pause command dien; parser real-entry match `^### PAUSE-\d+`, placeholder `<k>` khong false-match):
|
||||
|
||||
### PAUSE-<k> <ts ISO-8601>
|
||||
|
||||
> anh: <tin-nhan anh VERBATIM — copy nguyen-van, KHONG paraphrase>
|
||||
|
||||
**(1) quyet-dinh da CHOT** / **(2) delta con SONG** / **(3) con-tro**
|
||||
|
||||
```json
|
||||
{ "machine_block": "python scripts/session_ctx.py machine-block --session <N> --json" }
|
||||
```
|
||||
-->
|
||||
|
||||
<!-- FLOW-START — entries append bên dưới dòng này, mới nhất ở CUỐI -->
|
||||
|
||||
_(chưa có PAUSE — entry đầu append @`/pause` đầu tiên của phiên)_
|
||||
|
||||
---
|
||||
|
||||
## (c) STOCK-touched — máy-derive
|
||||
|
||||
> Bảng file-đã-đụng trong phiên, **máy-derive** từ `git diff --name-only <anchor>..HEAD` @pause/snapshot. KHÔNG điền tay.
|
||||
|
||||
| File | Δ | Ghi-chú |
|
||||
|---|---|---|
|
||||
| _(máy-derive @pause/snapshot)_ | | |
|
||||
116
.claude/sessions/session-13/_mind-s-13.md
Normal file
116
.claude/sessions/session-13/_mind-s-13.md
Normal file
@ -0,0 +1,116 @@
|
||||
# _mind-s-13 — SESSION-MODEL lớp MỀM (trí-nhớ suy-nghĩ 4 tầng)
|
||||
|
||||
<!-- SCAFFOLD-META: sID=13 | ts-moc=2026-08-07T23:37:36+07:00 | nguon-ts=git log -1 --format=%cI @ HEAD 936d1aed | render=lead Write @session-start Phase 3.5 (session-scaffold.ps1 KHONG render tep nay) -->
|
||||
|
||||
<!-- MIND-RULES-START -->
|
||||
|
||||
> **sID (LOGIC-session) = 13.** Tệp này giữ 4 thứ KHÔNG nằm ở WAL / HANDOFF / `_context`: **(A)** gói-turn · **(B)** hướng-tiếp · **(C)** kế-hoạch delta-suy-nghĩ · **(D)** đang-thảo-luận (+ **(E)** dòng-sống spawn). Nhà: `.claude/sessions/session-13/_mind-s-13.md`.
|
||||
> ts-mốc khởi-tạo = **2026-08-07T23:37:36+07:00** (nguồn: `git log -1 --format=%cI @ HEAD 936d1aed`) — mốc git-committer, KHÔNG phải wall-clock tuỳ-tiện.
|
||||
>
|
||||
> **GHI = lead (single-writer)** @4 nhịp: block-0 cuối bootstrap · refresh @`/pause` · đọc + đối-chiếu @`/tiep` · mục E gom MỌI spawn/engine-run. **KIỂM = máy + vai** (tách khỏi ghi): `python scripts/session_ctx.py mind-check --session 13` mỗi cửa (~$0) + vai `ctx-audit` @2 bookend (INFORM-only, quyền phán TRƯỢT).
|
||||
> **Trần tệp** = khoá `mind_ctx_kb` trong `.claude/agent-memory/memory-budget.json` — **B1: trỏ khoá, CẤM chép số vào tệp này.**
|
||||
|
||||
### 🔴 CHIỀU CHÈN — NGƯỢC với `_context` (đọc trước khi chèn block đầu tiên)
|
||||
|
||||
- **`_mind` = MỚI-NHẤT-Ở-TRÊN.** Block mới chèn NGAY DƯỚI **dòng marker điểm-chèn** (dòng comment nằm ngay trên block top, dưới khối luật này).
|
||||
- **3 dòng comment marker của tệp** (mở khối luật · đóng khối luật · điểm-chèn) xuất hiện **ĐÚNG 1 LẦN** mỗi cái. Vì vậy phần chữ ở dưới CHỈ gọi chúng bằng **mô-tả**, KHÔNG viết lại tên marker — máy/người tìm-chuỗi sẽ không bắt nhầm anchor rồi chèn vào giữa khối luật. Máy khớp anchor bằng **token** trên dòng bắt đầu `<!--` (KHÔNG kèm `-->`, vì marker có chú-thích viết cùng dòng — mirror `FLOW-START` của `session-context-template.md`).
|
||||
- **`_context` FLOW = MỚI-NHẤT-Ở-CUỐI** (`.claude/templates/session-context-template.md`, dòng `FLOW-START`). **HAI TỆP NGƯỢC CHIỀU NHAU** — CẤM "adopt verbatim" khuôn bên kia sang đây. Lý-do: `_mind` đọc để **nạp lại trạng-thái** ⇒ thứ mới nhất phải ở màn hình đầu; `_context` là **biên-niên-sử durable** ⇒ đọc xuôi thời-gian.
|
||||
- **Số hiệu = ĐẾM số block hiện có TRƯỚC KHI chèn** (block-0 chiếm số 0 ⇒ đang có n block thì block mới là `MIND-n`). Máy chỉ đòi *số duy-nhất* + *block top ≥ mọi số dưới* — KHÔNG neo công-thức đếm, nhà đổi công-thức vẫn kiểm được.
|
||||
- **Block cũ BẤT BIẾN** (append-only, không rewrite / không tự-tóm). **Ngoại lệ DUY NHẤT**: nén khi vượt trần — nén block CŨ NHẤT **trừ block-0 và block top**, verbatim vẫn còn trong git; **<3 block ⇒ TỪ-CHỐI nén**, giương cờ vượt trần (nợ hiển thị, không xoá chứng-nhân).
|
||||
- **Việc ĐÃ-CHỐT ⇒ chỉ con-trỏ** (`.claude/WAL.md` · `docs/HANDOFF.md` · `.claude/workflows/runs/<id>/…`) — **CẤM chép nội-dung** (2-sự-thật + phình trần).
|
||||
- **Mục trống ⇒ ghi đúng chuỗi `(trống — khai)`** trên 1 dòng riêng, KHÔNG thêm chữ, KHÔNG bọc dấu nhấn (máy so chuỗi). CẤM bỏ heading — vắng-mặt trông giống ổn.
|
||||
- **5 heading trong mỗi block = `### A.` … `### E.`** (thứ tự A→E cố định) — đây là hợp-đồng hình-dạng cho máy `mind-check` và vai `ctx-audit`.
|
||||
- **Mục D: mỗi ý 1 dòng + BẮT BUỘC nhãn**, viết **đúng chuỗi CÓ DẤU**: `{mới-nêu | đang-cãi | gần-chốt | treo-chờ-anh}`. Máy so chuỗi ⇒ viết không dấu = **trượt-cấu-trúc**. (Phần chữ hướng-dẫn trong khối luật này theo khuôn nhà có thể không dấu, nhưng **token máy đọc thì giữ dấu** — đừng chép ngược.)
|
||||
|
||||
### 3 RÀO NỘI-DUNG (máy `mind-check` phép 1-3 — mỗi rào kèm LÝ-DO ĐO ĐƯỢC)
|
||||
|
||||
1. **Rào-1 — CẤM verbatim lời anh.** Dạng bị bắt = dòng blockquote mở đầu bằng `anh` + hai chấm, ví-dụ:
|
||||
> anh: <lời anh nguyên-văn — dòng NÀY hợp-lệ vì nằm TRONG khối luật, xem mục enclosure>
|
||||
|
||||
*Lý-do đo được:* verbatim lời anh có **nguồn-duy-nhất** = `_context` FLOW (`### PAUSE-<k>` → dòng quote). `_mind` là lớp mềm bị nén/diễn-giải mỗi cửa ⇒ chép verbatim vào đây tạo bản-thứ-2, và khi 2 bản lệch thì bản mềm (đọc trước, giàu ngữ-cảnh hơn) sẽ thắng bản durable. Trong `_mind` chỉ được TRỎ: `xem PAUSE-<k>`.
|
||||
2. **Rào-2 — CẤM khoá `ts:` ở ĐẦU DÒNG** (mọi biến thể: thụt space/tab, hoa/thường, có khoảng trắng trước dấu hai chấm). Mốc thời-gian sống **trong dòng heading, sau em-dash**: `## MIND-<k> — <ISO-8601> @ <HEAD>`.
|
||||
*Lý-do đo được:* marker nhà (`_pause-*` · `_tiep-*` · `_snapshot-*`) mang khoá đó ở đầu dòng **by-design**; reader-mốc TƯƠNG-LAI (gap C10-LATENESS của `scripts/governance-detectors.ps1` — C10 tự khai "no reliable source" cho ts-compare) sẽ quét cả `session-<N>/_*` mà **không lọc theo tên tệp** ⇒ một dòng khoá trong `_mind` đủ để nó lấy sai "mốc gần nhất". Regex canonical = `_TS_KEY_RE` trong `scripts/session_ctx.py`; **CẤM cài bản thứ 2** ở bất kỳ máy nào (2-sự-thật chính là thứ rào này chặn).
|
||||
3. **Rào-3 — CẤM secret, KỂ CẢ dưới dạng ví-dụ.** Tập tiền-tố cấm = `SECRET_PATTERNS` trong `scripts/session_ctx.py` — **trỏ nguồn, KHÔNG chép mẫu vào tệp này**. Ngoại lệ DUY NHẤT: nhãn đã che `[đã che <8-hex>]`. Bắt buộc quét NHÁP **trước** khi chèn: `mind-check --session 13 --draft <tệp-nháp-scratchpad>`.
|
||||
*Lý-do đo được, 2 tầng:* (i) block BẤT BIẾN ⇒ secret lọt là **vĩnh viễn** (còn trong git dù xoá sau); (ii) ⚠️ **enclosure KHÔNG đối-xứng giữa 2 lưới**: `mind-check` chỉ soi DƯỚI marker đóng khối luật, còn `secrets-sweep` (cùng `session_ctx.py`, chạy ở auto-snapshot mỗi `/pause`) **rglob TRỌN `session-<N>/` và không biết khối luật này** ⇒ một mẫu secret viết làm ví-dụ ngay tại đây sẽ **chặn commit ở MỌI cửa pause**. Vì thế rào-2 và rào-3 ở trên **cố ý KHÔNG có ví-dụ literal**; chỉ rào-1 có, vì rào-1 chỉ `mind-check` đọc và nó enclosure-aware.
|
||||
|
||||
### Enclosure use ⟂ mention
|
||||
|
||||
Máy chỉ quét phần **DƯỚI** marker đóng khối luật (dòng comment cuối cùng của phần luật này). Khối luật được phép chứa chuỗi bị cấm để **giải thích** (citation-trap: doc định-nghĩa anti-pattern tất-yếu chứa chính mẫu đó). Hệ quả 2 chiều: **đừng** dịch ví-dụ xuống dưới marker đóng, và **đừng** viết nội-dung thật lên trên nó.
|
||||
|
||||
<!-- BLOCK-SCHEMA (guidance, KHONG phai block that — /pause dien; parser real-block match `^## MIND-\d+`, placeholder `<k>` khong false-match).
|
||||
Prose huong-dan duoi day khong dau theo khuon nha; TOKEN may so chuoi thi GIU DAU: nhan muc D + chuoi muc trong `(trống — khai)`.
|
||||
|
||||
## MIND-<k> — <ISO-8601> @ <HEAD ngan> (window <w>)
|
||||
|
||||
### A. Goi-turn
|
||||
<1-3 gach dau dong: turn vua roi lam gi, ket qua gi — GOI lai, khong ke lai>
|
||||
|
||||
### B. Huong-tiep + nhanh-da-loai
|
||||
<di dau tiep VA nhanh nao da LOAI + vi sao loai (chong dao lai vong cu)>
|
||||
|
||||
### C. Ke-hoach (delta suy-nghi)
|
||||
<phan SUY-NGHI doi so lan truoc — CAM chep checklist/todo (cho o WAL/HANDOFF)>
|
||||
|
||||
### D. Dang-thao-luan
|
||||
<moi y 1 dong + nhan CO DAU: {mới-nêu | đang-cãi | gần-chốt | treo-chờ-anh}>
|
||||
|
||||
### E. Dong-song spawn/engine-run
|
||||
<1 dong / spawn hoac engine-run: <vai> — <verdict> — <con-tro path (run-folder / sub-file / commit)>>
|
||||
<muc trong ⇒ dung chuoi: (trống — khai)>
|
||||
-->
|
||||
|
||||
<!-- MIND-RULES-END -->
|
||||
|
||||
<!-- diem-chen: block MOI chen NGAY DUOI dong marker duoi day (moi-nhat-o-TREN -- NGUOC chieu _context FLOW).
|
||||
Marker duoi day phai giu NGUYEN-VAN, KHONG viet chu-thich cung dong: may khop chuoi DAY-DU
|
||||
(`_MIND_TOP_MARKER` trong scripts/session_ctx.py) -- khac khuon `FLOW-START` cua _context (khong may nao doc). -->
|
||||
<!-- MIND-TOP -->
|
||||
|
||||
## MIND-0 — 2026-08-07T23:37:36+07:00 @ 936d1aed (window 1)
|
||||
|
||||
### A. Gói-turn
|
||||
|
||||
- Bootstrap `/session-start` trọn: Sàn-3 5/5 im · `session-13` mở · counter 55→56 · test 645 PASS · **spawn-probe 5/5 ALIVE ⇒ slot (73) đóng bằng vật, không bằng static-check**.
|
||||
- Bookend @open V1+V2 đủ 6 vai. Verdict → xem `.claude/workflows/runs/2026-08-07-S182-bookend-open/bookend-open-synthesis.md`.
|
||||
- Anh hỏi giữa chừng *"ring-3 và ring-4?"* — đo đĩa ra: ring3 **cố ý không tồn tại** (V3 dùng `harness-audit`), ring4 **NO-OP đúng luật**, **ring5 là GAP thật**.
|
||||
|
||||
### B. Hướng-tiếp + nhánh-đã-loại
|
||||
|
||||
**Hướng:** chờ anh chốt **slot (67) · VK-5 · deep-sweep · (72) · `ring5-audit` slot (41)** *(🔴 viết thẳng tên slot thay vì đếm "4 câu" — `ctx-audit` FLAG-5: cụm "4 câu số" khớp nhầm vào `HANDOFF:6`, nơi 4 câu **đã trả lời + thi hành @S181** mà bảng chưa lật ⇒ cửa-sổ sau sẽ hỏi lại anh 4 câu đã chết)*; rồi vá lớp tự-quy-chiếu (`session-start.md:161/:162` + `HANDOFF:58` + `ring2-audit/MEMORY.md:28`) vì cả 3 đang **lây vào brief của chính lead**.
|
||||
**Nhánh đã LOẠI:**
|
||||
- (a) **Lật slot 71 theo đề-xuất H2** — LOẠI. `ring1` chứng `HANDOFF:60` slot đã gạch ✅ ĐÓNG @S179, số nằm trong `🧊 nguyên-văn slot cũ:` ⇒ lật = **sơn lại ảnh chụp**, đúng lớp R-1 bắt @S181.
|
||||
- (b) **Sweep `"roster 23"` ở `STATUS:478` theo dòng** — LOẠI. Dòng 15.804 ký tự chứa **2** chuỗi; (b) là ảnh-chụp S149 ĐÚNG và tự khép kín. Neo theo char/đoạn.
|
||||
- (c) **Ghi "16/17 đích gist" làm baseline** — LOẠI. `ring1` chạy cùng lệnh ra **19**; 2 engine cãi nhau chưa tìm ra engine hỏng ⇒ **không chọn số nào**.
|
||||
- (d) **Hạ số "12 root" ở `permission-matrix:3`** — LOẠI, đây là bẫy: 12 là **ĐÚNG SỐ**; chỗ sai là **cây `:44-77`** vẽ 8. Sửa = bồi cây, không hạ số.
|
||||
|
||||
### C. Kế-hoạch (delta suy-nghĩ)
|
||||
|
||||
Delta lớn nhất phiên này: **lỗi bề-mặt không nằm yên — nó lây vào brief mà lead viết cho vai đo.** Ba ca độc lập cùng chiều trong một phiên (enum ma ở file LỆNH → brief `lead-stale` · `HANDOFF:58` → brief `ring2` · `MEMORY.md:28` auto-inject → chính vai KIỂM). ⇒ **vá bề-mặt điều-khiển phải ưu tiên trên vá bề-mặt kể-chuyện**, vì chỉ loại đầu mới nhân bản. Và hệ quả kỷ luật: **brief lead viết ra là một artifact CẦN ĐƯỢC KIỂM**, không phải kênh miễn nhiễm — lượt này `ring2` BROKE đúng tiền-đề tao giao nó.
|
||||
|
||||
Delta thứ hai: **17/17 ĐẠT đọc dễ thành "sạch"**, nhưng đó là **độ chính xác ⟂ độ phủ**. `ring2` mở đúng một chỗ ngoài vùng 2 vai đã chạm là ra ngay 1 FLAG sót cùng class ⇒ mọi TOTAL lượt này phải mang nhãn **SÀN**.
|
||||
|
||||
### D. Đang-thảo-luận
|
||||
|
||||
- `{treo-chờ-anh}` slot (67) người SOẠN có được upload file khi duyệt KHKK — policy rộng hơn ý định, im 9 nhãn phiên, sắp bị archive.
|
||||
- `{đang-cãi}` VK-5 — **3 bề mặt lệch nhau** (`ctx-audit` FLAG-3): `_mind` treo ⟂ `owner-decisions-…-dot-2.md:49` "✅ CHỐT GIỮ PHỤ LỤC @S181" ⟂ `sub-lead-gap-auditor.md:95` "ĐÃ CHỐT". Không tự phán bên nào: nếu lead đúng thì ledger **ngân một chữ OK quá tay**; nếu ledger đúng thì §D đang giữ câu đã trả lời ⇒ **hỏi anh lần 2 = vi phạm A5**. Fail-safe: hạ `owner-decisions:49` xuống nấc *"lead đọc là chốt, chưa có chữ chỉ nhánh"*.
|
||||
- `{gần-chốt}` deep-sweep H24 — 🔴 **`ctx-audit` FLAG-4: đổi nhãn KHÔNG đổi kết quả.** @S181 lead bị bắt vì **hạ** xuống NEXT-em; @S182 lead **nâng** lên `treo-chờ-anh` — hai nước ngược chiều, **cùng kết quả: không chạy**, 15/15 → **16/15**. `treo-chờ-anh` còn tệ hơn: khoá món sau cánh cửa chỉ anh mở được ⇒ nợ trôi tiếp **với lý do trông chính đáng**. Nhãn hạ về `gần-chốt` = việc của em, không đẩy sang anh.
|
||||
- `{mới-nêu}` **V-5 vị-từ mã-hoá chưa cắm** — `ctx-audit` FLAG-2: RƠI THẬT, và `lead-gap` FLAG-6 **SÓT** nó ⇒ số món rơi ≥ **3**. Nhà duy nhất còn sống = `bao-cao-cuoi.md:79` = artifact **đã đóng + đã gửi hub**, không phải hàng đợi. Control dương cùng dòng `:79`: `W3b` đi tiếp tới `HANDOFF:7`, V-5 cùng câu **không đi đâu**.
|
||||
- `{treo-chờ-anh}` (72) bật `mfe-eval` vào nhịp closeout thường xuyên không.
|
||||
- `{mới-nêu}` `ring5-audit` build SE-form ngay hay chờ nhịp adopt chung VIPIX — slot (41), V5 đang chạy 64 FLAG mà **0 vai chấm lại**.
|
||||
- ~~`{đang-cãi}` 7 baseline `-Expect`~~ 🔴 **ERRATA @open, `ctx-audit` FLAG-1 bắt — dòng này SAI cả 2 vế, giữ gạch làm vết.** Anh **ĐÃ CHỐT** phương án (b) tách-file-dữ-liệu (`so-yeu-cau-chu-du-an.md:40` `YC-007` `"6 -> b"`, trạng thái **đóng**) **VÀ đã SHIP**: `scripts/naming-baseline.json` 1.354 B + `naming-scan.ps1:63,71,90,121,177` đọc thật, land `a3776eaa`. Đường lây 3 tầng: `lead-gap` FLAG-6(a) grep **TÊN CŨ** `-Expect` → 0 hit → kết "rơi" (quyết định land dưới tên **"baseline"** trong `.json`+`.ps1`, ngoài tập grep) → `ring2` chấm `lead-gap` 6/6 ĐẠT nhưng G-6 chỉ kiểm vế tự-rút → lead promote vào §D. **Bài: 0-hit theo TÊN đọc thành "đã rơi".**
|
||||
- `{mới-nêu}` đề-nghị `ring2` *"đòi phép đo trước khi vào tally"* — luật về chính bộ đếm H24; **carry từ `_mind-s-12:84`, cũng đã rơi**.
|
||||
- `{gần-chốt}` W3b cắm mã thoát — phân loại 64/64 xong, chỉ chờ soak (tự tan).
|
||||
- `{mới-nêu}` `ring1-audit` persona ghi *"CẤM tự chạy ở open"* mà lead giao @open 5 phiên liền — sửa persona hay sửa vận hành.
|
||||
|
||||
### E. Dòng-sống spawn/engine-run
|
||||
|
||||
- `front-end-reviewer-style` · `back-end-reviewer-style` · `database-reviewer-style` · `score-counter` · `score-count-auditor` → **5/5 `ALIVE`** → `.claude/workflows/runs/2026-08-07-S182-bookend-open/spawn-probe-registry.md`
|
||||
- `tooling-auditor` → `PASS_WITH_FLAGS — 4` → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-tooling-auditor.md`
|
||||
- `harvest-curator` → `GATE-FAIL — 7` → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-harvest-curator.md`
|
||||
- `lead-stale-auditor` → `11 FLAG` → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-lead-stale-auditor.md`
|
||||
- `lead-gap-auditor` → `6 FLAG` → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-lead-gap-auditor.md`
|
||||
- `ring1-audit` → `41 ĐẠT / 7 TRƯỢT / 8 KC` → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-ring1-audit.md`
|
||||
- `ring2-audit` → `17 ĐẠT / 0 TRƯỢT` + 2 errata + 1 FLAG SÓT → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-ring2-audit.md`
|
||||
- `ring4-audit` → **NO-OP có khai** (SHARD-PROBE `pending=3 / ngưỡng=15 ⇒ IM`, pin missing) → không spawn
|
||||
- `ctx-audit` → `TRUOT — 7 FLAG` → `.claude/workflows/runs/2026-08-07-S182-bookend-open/sub-ctx-audit-open.md` *(🔴 dòng này thêm SAU khi vai trả — `ctx-audit` FLAG-7: block-0 ghi ở Phase 3.5 còn `ctx-audit` spawn ở 3.5(iii) ⇒ §E luôn ghi **một nhịp trước** lượt cuối nó phải kể. **Cùng HÌNH với FLAG-6** `mind-blocks`. Tiền lệ vá @S173: §E có dòng cho vai ĐANG chạy)*
|
||||
- 🔴 **6/6 vai bookend garble first-return, recover 6/6 bằng resume, mất 0 byte.** Hai câu garble **chứa finding thật** ⇒ đừng vứt câu garble.
|
||||
@ -0,0 +1,67 @@
|
||||
# bookend-open-synthesis — S182 (phiên-LOGIC L13, window 1)
|
||||
|
||||
**Ngày:** 2026-08-07 → 08 · **HEAD @open:** `936d1aed` *(🔴 đọc là "cây làm việc tại `936d1aed`" — xem ERRATA-2)* · **counter:** 56 · **session-logic:** `session-13`
|
||||
|
||||
## Đã chạy
|
||||
|
||||
| vòng | vai | verdict |
|
||||
|---|---|---|
|
||||
| **V1** tooling/harvest | `tooling-auditor` | `PASS_WITH_FLAGS — 4 finding` |
|
||||
| | `harvest-curator` | `GATE-FAIL — 7 finding` |
|
||||
| | → `ring1-audit` | **41 ĐẠT / 7 TRƯỢT / 8 KC** |
|
||||
| **V2** soi-lead H24 | `lead-stale-auditor` | **11 FLAG** |
|
||||
| | `lead-gap-auditor` | **6 FLAG** |
|
||||
| | → `ring2-audit` | **17 ĐẠT / 0 TRƯỢT** + 2 errata + **1 FLAG SÓT** |
|
||||
| **V3** trio-memory | — | ⚪ chưa chạy (vòng AUTO giữa 2 bookend) |
|
||||
| **V4** nén-ngủ | `ring4-audit` | 🔇 **NO-OP** — `SHARD-PROBE pending=3 / ngưỡng=15 ⇒ IM` |
|
||||
| **V5** floor/detector | *(không có vai KIỂM)* | 🔴 **GAP** — slot (41) `[carry:ring5-audit-gap]` |
|
||||
| **0.6b** registry-probe | 5 vai mới | **5/5 ALIVE** ⇒ slot (73) ĐÓNG |
|
||||
|
||||
## Máy đã đo (0 hardcode)
|
||||
|
||||
Sàn-3 **5/5 im** · test **645 PASS** (45D+600I) · `governance-detectors` **64 FLAG**, canonical mig=72 test=645 gotcha=87 table=97 menu=113 policy=452, disk cross-check khớp · H17 **2 GAP:MED** · `nhip-no-probe` `light 3/6 · deep 16/15 OVERDUE · 3 over-cap · run-chưa-gom 0 · pull-cách 2 ngày · so-yc treo 2 / nhắc2+ 1` · RAG **2449 chunk**, index stale 2026-05-29 · hot-load **726.660 B ≈ [181.665..242.220] tok**, headroom **137.780** · `h24-signal-write` **9 class FIRE**.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Kết quả CHỐT — số nào tin được, số nào không
|
||||
|
||||
| đại lượng | số ĐÚNG | ai chốt | ghi chú |
|
||||
|---|---|---|---|
|
||||
| root menu (`DbInitializer`) | **12** (cây skill vẽ 8, **thiếu 4**) | ring1 parser depth-aware, cross-check `MenuKeys` khớp | H1 tự khai thước hỏng ⇒ **đúng, không né** |
|
||||
| `permission-matrix/SKILL.md:3` "12 root" | **ĐÚNG SỐ** | ring1 | lỗi chỉ còn là **vi-phạm luật-không-chép**, KHÔNG sai sự-thật |
|
||||
| HANDOFF "thiếu 5 root" | **SAI** (thật 4) | ring1 | H1 **từ chối chép** ⇒ quyết định đúng |
|
||||
| nợ harvest S169 | **264.049 B** | ring1 | H2 ghi 264.451 — **lỗi CỘNG +402 B**, 9/9 thành phần khớp byte |
|
||||
| **TỔNG nợ harvest** | **576.255 B / 18 lượt-vai** | ring1 | H2 ghi 576.657 (kế thừa lỗi cộng) |
|
||||
| "17 đích gist" | 🔴 **KHÔNG TÁI-LẬP ĐƯỢC** | ring1 ra **19** cùng lệnh | **CẤM ghi 16/17 làm baseline** |
|
||||
| slot 71 "nợ MA S171" | 🔴 **BÁC** | ring1 `HANDOFF:60` | slot **đã gạch ✅ ĐÓNG @S179**; số nằm trong `🧊 nguyên-văn slot cũ:` ⇒ đề-xuất "lật slot" = **sơn lại ảnh chụp** |
|
||||
| `gap-carry-aged = 0` | 🔴 **số 0 CHẾ TẠO** | lead-gap, ring2 verify **7/7 byte-exact** | vùng câm `L1–178` = **42.606/186.729 = 22,82%** |
|
||||
| counter | **56** (cây) / **55** (blob) | ring2 ERRATA-2 | tick trong working tree, chưa vào blob |
|
||||
| tập over-cap | {`ring2-audit` 29.582 · `tooling-auditor` 29.429 · `lead-stale-auditor` 27.658} | ring2 `stat` 28/28 | `HANDOFF:40` sai 2/3; `reviewer` đã tụt **21.058** |
|
||||
|
||||
## 🔴 Ba lỗi tự-quy-chiếu trong CÙNG một phiên
|
||||
|
||||
1. **`session-start.md:161/:162` liệt 5 class, enum có 12** → lead đọc file lệnh, viết brief *"5 nhánh view-*"*, giao cho `lead-stale-auditor` → **chính vai đó bắt được brief sai vì nguồn sai**. Đợt vá ghost-enum @S180 sửa `hmw.js:33` + persona nhưng **bỏ file LỆNH**.
|
||||
2. **`HANDOFF:58` là NGUỒN LÂY** → lead chép số S172 vào brief giao `ring2-audit` như thể là S173 → ring2 mở artifact gốc, **BROKE tiền-đề của lead** (`lead-gap` trích ĐÚNG, **brief lead trích SAI**). Và `:58` bị **đóng ké dưới `~~(70) ✅ ĐÓNG @S179~~`** ⇒ quét "slot còn mở" không thấy.
|
||||
3. **`agent-memory/ring2-audit/MEMORY.md:28`** ghi *"(5 class `view-*`)"* trong `## 🎯 Role baseline` — **auto-inject ~200 dòng đầu mỗi lượt spawn** ⇒ vai bị nạp **thước thiếu một vạch TRƯỚC khi chấm**. Vai **tự bắt @S173**, dòng trôi 25→28, **số sai vẫn nguyên 9 phiên**.
|
||||
|
||||
## 🔴 Cảnh báo vá — 2 bẫy
|
||||
|
||||
- **`STATUS.md:478` là BẪY NEO** (ring1 R-1): dòng **15.804 ký tự** chứa **HAI** chuỗi "roster 23" — (a) char ~2050 **SAI** (sinh ở `d2918790`, đúng là 28) · (b) char ~3475 **ĐÚNG** (ảnh-chụp S149, 23−20=3 tự khép kín). Sweep theo DÒNG sẽ **giết luôn (b)** ⇒ tái diễn R-1 S181. **Neo theo char/đoạn, không theo số dòng.**
|
||||
- **`permission-matrix/SKILL.md` tự mâu thuẫn nội-tại** (ring1 R-2): `:3` đúng (12), cây `:44-77` sai (8). Sửa đúng = **bồi cây lên 12**, KHÔNG hạ số 12.
|
||||
|
||||
## 🔸 Nợ mới phát sinh trong phiên
|
||||
|
||||
- **`contract-workflow/SKILL.md` trễ 2 migration** (Mig 71 nhóm-duyệt N1-N8 · Mig 72 MaHangMuc): 0 hit `ApprovalGroup`/`N1`/`nhóm duyệt`/`MaHangMuc`, control dương `phase`=25 hit.
|
||||
- **`ring4-audit` sổ đóng băng 12 ngày** — chạy thật @S170 (`sub-ring4-audit-sleep-S170.md` 20.122 B) mà `MEMORY.md` mtime 2026-07-26, `grep -c "S170"`=0.
|
||||
- **`ring1-audit` persona thối so với vận hành 5 phiên** (R-3): ghi *"KIỂM end-only… CẤM tự chạy ở open"* trong khi lead giao @open ở S152·S159·S173·S180·S182.
|
||||
- **`runs/…S180…/lane-memory-proposals/MEMORY.md` 2.670 B** — có thể là delta đã soạn chưa APPEND ⇒ làm F-01 **mạnh hơn** bản H2 nộp. Cần disposition.
|
||||
- **`cicd-monitor/archive/2026-08.gist.md` VẮNG** (control dương: 10 cặp anh em đủ trong cùng dir).
|
||||
|
||||
## 🔴 Nhắc đọc đúng nấc
|
||||
|
||||
**17/17 ĐẠT của ring2 = độ CHÍNH XÁC, KHÔNG phải độ PHỦ.** `lead-stale` tự khai chỉ chạm **~7% `STATUS`** (30/853 dòng) và **6/29** persona; ring2 mở đúng MỘT chỗ trong phần chưa chạm (`agent-memory/**`) là ra ngay **1 FLAG sót cùng class**. ⇒ **11 và 6 là SÀN. Cấm làm tròn sàn thành tổng.**
|
||||
**`gap-carry-aged` 0→1 là do vai TỰ TÍNH TAY**, máy sinh số đó **vẫn đang mù 22,82% đầu file**. **Đừng đọc thành "detector đã khỏi".**
|
||||
|
||||
## #53 datum
|
||||
|
||||
**6/6 vai garble first-return** (tooling · harvest · lead-stale · lead-gap · ring1 · ring2). **Recover 6/6 bằng SendMessage-resume, mất 0 byte.** Khớp chuỗi S180 (6/6) và S181 (9/9). 🔸 Hai câu garble **chứa finding thật** (`"Tree shows 8 roots…"` → F-4 · `"Measured: 12 roots…"` → số load-bearing của cả phiên) ⇒ **câu garble không phải rác, đừng vứt**.
|
||||
31
.claude/workflows/runs/2026-08-07-S182-bookend-open/run.md
Normal file
31
.claude/workflows/runs/2026-08-07-S182-bookend-open/run.md
Normal file
@ -0,0 +1,31 @@
|
||||
# run — S182 bookend @open (phiên-LOGIC L13, window 1)
|
||||
|
||||
- ts-moc: 2026-08-07T23:37:36+07:00 → 2026-08-08T00:35
|
||||
- HEAD @open: `936d1aed` 🔴 đọc là **"cây làm việc tại `936d1aed`"** (ring2 ERRATA-2: counter blob=55, cây=56)
|
||||
- session-logic: `session-13`
|
||||
- counter: **56** (tick S181 → S182)
|
||||
- nhịp: light 3/6 (hình-B: vô-điều-kiện mỗi bookend) | deep **16/15 OVERDUE**
|
||||
|
||||
## Stages
|
||||
|
||||
- [x] **V1** tooling/harvest → `tooling-auditor` PASS_WITH_FLAGS 4 · `harvest-curator` GATE-FAIL 7 → **`ring1-audit` 41 ĐẠT / 7 TRƯỢT / 8 KC**
|
||||
- [x] **V2** soi-lead H24 → `lead-stale-auditor` 11 FLAG · `lead-gap-auditor` 6 FLAG → **`ring2-audit` 17 ĐẠT / 0 TRƯỢT** (+2 errata, +1 FLAG SÓT trong file của chính nó)
|
||||
- [ ] **V3** trio-memory — chưa chạy (vòng AUTO giữa 2 bookend, `session-start §2.1.9(a)`)
|
||||
- [x] **V4** nén-ngủ — `SHARD-PROBE pending=3 / ngưỡng=15 ⇒ IM` ⇒ không chạy, `ring4-audit` **NO-OP có khai**
|
||||
- [x] **V5** floor/detector — `governance-detectors` 64 FLAG · H17 2 GAP:MED · 🔴 **0 vai KIỂM** (slot 41 `ring5-audit` GAP)
|
||||
- [x] **BƯỚC 0.6b** registry-probe — **5/5 ALIVE** ⇒ slot (73) ĐÓNG
|
||||
- [x] **Phase 3.5** ctx soft-memory — `_mind-s-13` block-0 + `mind-check` + **`ctx-audit` TRUOT 7 FLAG**
|
||||
|
||||
## Artifact
|
||||
|
||||
`spawn-probe-registry.md` · `sub-tooling-auditor.md` · `sub-harvest-curator.md` · `sub-lead-stale-auditor.md` · `sub-lead-gap-auditor.md` · `sub-ring1-audit.md` · `sub-ring2-audit.md` · `sub-ctx-audit-open.md` · **`bookend-open-synthesis.md`**
|
||||
|
||||
## Assertion
|
||||
|
||||
- **residual-write = 0** — soát TRONG CÙNG TURN với wave (Stop-hook fire ở turn-boundary; soát turn sau = soát sổ đã bị hook nuốt). Chỉ `.session-counter.json` đổi = bản ghi của lead qua `h24-signal-write.ps1`. `agent-memory/` **không bị chạm** bởi 8 vai propose-only.
|
||||
- `mind-check --session 13` → `dat=10 TRUOT=0 co=1 exit 0`. **`co=1` GIỮ CÓ CHỦ ĐÍCH** = máy nêu đúng `ctx-audit` FLAG-7 (*"chèn 1 block refresh SAU khi vòng cuối báo xong"*) — vá ở `/pause`, không vá bây giờ.
|
||||
|
||||
## #53
|
||||
|
||||
**7/7 vai garble first-return** (tooling · harvest · lead-stale · lead-gap · ring1 · ring2 · ctx-audit). **Recover 7/7 bằng SendMessage-resume, mất 0 byte.**
|
||||
🔸 **3 câu garble CHỨA finding thật** — `"Tree shows 8 roots…"` (→F-4) · `"Measured: 12 roots…"` (→số load-bearing cả phiên) · `"mind-blocks=1 vs disk=2…"` (→FLAG-6). **Câu garble không phải rác.**
|
||||
@ -0,0 +1,25 @@
|
||||
# spawn-probe registry — BƯỚC 0.6b @S182 (đóng slot 73)
|
||||
|
||||
> 🔴 Đây là **REGISTRY-probe**, KHÁC model-probe (0.6). Vai có trên đĩa + có tên trong `VALID_ROLES` = điều-kiện **CẦN, KHÔNG ĐỦ**.
|
||||
> Bằng-chứng falsifiable DUY-NHẤT = spawn THẬT + nhận return. Static-check (`ls` + grep) không đủ.
|
||||
|
||||
**Bối cảnh:** roster đổi 26→28 @S181 (+2 vai score W4b) và 3 vai STYLE land @S176 nhưng **chưa probe lần nào** — slot (73) HANDOFF:32 nói anh đã "OK" @S179 mà chưa restart. Phiên S182 = phiên đầu sau restart ⇒ probe chạy được.
|
||||
|
||||
**Task probe:** tối-thiểu, trả đúng `ALIVE|<role>`, **cấm gọi tool**.
|
||||
|
||||
| # | vai | land | kết quả | tok | tool_uses | ms |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | `front-end-reviewer-style` | S176 | ✅ `ALIVE\|front-end-reviewer-style` | 39.964 | 0 | 2.070 |
|
||||
| 2 | `back-end-reviewer-style` | S176 | ✅ `ALIVE\|back-end-reviewer-style` | 53.192 | 0 | 2.956 |
|
||||
| 3 | `database-reviewer-style` | S176 | ✅ `ALIVE\|database-reviewer-style` | 54.604 | 0 | 2.094 |
|
||||
| 4 | `score-counter` | S181 | ✅ `ALIVE\|score-counter` | 29.113 | 0 | 1.996 |
|
||||
| 5 | `score-count-auditor` | S181 | ✅ `ALIVE\|score-count-auditor` | 29.214 | 0 | 2.140 |
|
||||
|
||||
**Kết: 5/5 ALIVE, 0 error, 0 garble.** Slot (73) ĐÓNG bằng bằng-chứng spawn thật (không phải static-check).
|
||||
|
||||
## 🔸 KHAI CHẶT PHẠM VI (đừng đọc thành "đã phủ hết")
|
||||
|
||||
1. Probe này phủ đường **Agent-tool registry**. Đường **`hmw.js VALID_ROLES` → `agentType`** là **BỀ-MẶT KHÁC**, chỉ chứng được khi có workflow THẬT gọi vai đó. Roster đổi = đổi **CẢ HAI** bề-mặt; 1 probe KHÔNG phủ cả 2.
|
||||
2. `tool_uses=0` ở đây = **ĐÚNG THEO LỆNH** (lệnh cấm gọi tool), **KHÔNG** phải chữ-ký #53 (= `tool_uses=0` **CỘNG VỚI** bịa đã-làm-việc). Đừng nhìn cột đó rồi tưởng garble.
|
||||
3. Probe chứng **spawn được**, **KHÔNG** chứng **soi được**. Cùng caveat mark `RC-pqhuy1987-15-07-2026-15-32-23` đã ghi cho 2 vai H24 khi mới land.
|
||||
4. Con-số bảng này là **ảnh chụp S182**, không phải roster hiện-hành — 23 vai còn lại không probe lượt này (roster ổn-định phần đó).
|
||||
@ -0,0 +1,103 @@
|
||||
# sub-ctx-audit-open — SOI-CHUỖI lớp trí-nhớ MỀM @open S182 (L13)
|
||||
|
||||
**VERDICT: `CTX-AUDIT: TRUOT — 7 FLAG`**
|
||||
Ca **FALLBACK HỒI-TỐ ĐẦY-ĐỦ** (0 pause / 0 tiep ⇒ `ctx-curator` + `ctx-verifier` chưa chạy lần nào; 5-khoản + 4-khoản là **TỰ SOI**, không phải kiểm-vết).
|
||||
|
||||
> ⚠️ **#53:** first-return garble — câu garble chứa finding thật (`mind-blocks=1` vs đĩa 2). Recover bằng resume. **7/7 vai garble lượt này.**
|
||||
|
||||
---
|
||||
|
||||
## FLAG-1 [khoản 2+5] **HIGH** — `_mind-s-13:99` hồi sinh một quyết-định ĐÃ CHỐT + ĐÃ SHIP thành `{đang-cãi}`, kèm khẳng-định SAI
|
||||
|
||||
Dòng viết: `{đang-cãi}` 7 baseline `-Expect` … *"đã rơi khỏi mọi sổ bền"*.
|
||||
**Sự thật đo được:** `owner-decisions-07-08-2026-dot-2.md:20` anh chốt câu **6 = "b — tách ra file dữ-liệu"** · `so-yeu-cau-chu-du-an.md:40` **`YC-007`** trạng thái **đóng** · **đã thi hành**: `scripts/naming-baseline.json` **1.354 B**, header *"Owner chot phuong an (b) tach-file-du-lieu @S181 (cau 6)"*; `naming-scan.ps1:129-131` đọc baseline thay hardcode; land `a3776eaa`.
|
||||
|
||||
**Cơ-chế:** `lead-gap` FLAG-6(a) đo bằng **một chuỗi TÊN CŨ** `-Expect` trên 5 tệp doc → 0 hit → kết "rơi". Quyết-định land dưới **tên khác** ("baseline"/"khoản 6") và trong `.json`+`.ps1` — **ngoài tập grep**; vai còn tự ghi control-dương *"`baseline` có hit ở STATUS"* mà **không lần theo sợi đó**.
|
||||
🔴 **Bài: 0-hit theo TÊN đọc thành "đã rơi".**
|
||||
**Rủi ro:** §D là bề mặt cửa-sổ mới đọc TRƯỚC TIÊN ⇒ **mời revert đúng cái single-source anh chọn ngược khuyến-nghị lead**.
|
||||
|
||||
## FLAG-2 [khoản 2] **HIGH** — món `{gần-chốt}` **V-5 RƠI THẬT**; đây là món `lead-gap` FLAG-6 **SÓT**
|
||||
|
||||
`_mind-s-12:114` `{gần-chốt}` **V-5 vị-từ mã-hoá** (*"chỉ chờ chỗ cắm"*). Vắng khỏi block-0 `_mind-s-13`; `HANDOFF` 0 · `WAL` 0 · `STATUS` 0 (hit duy nhất = chữ "0-mojibake" entry cũ G-009, **khác vật**, tách bằng `grep -o` ngữ cảnh). Nhà duy nhất còn sống = `bao-cao-cuoi.md:79` = **artifact ĐÓNG + đã gửi hub**, không phải hàng đợi việc.
|
||||
|
||||
🔴 **Control-dương nằm TRONG CÙNG MỘT DÒNG `:79`:** *"W3b cắm mã thoát (soak, tự tan) · **V-5 vị-từ mã-hoá chưa cắm** · …"* — `W3b` đi tiếp tới `HANDOFF:7` **và** `_mind-s-13:101`; V-5 **cùng câu, cùng người ghi, không đi đâu**.
|
||||
⇒ **Số món rơi thật ≥ 3, không phải 2.**
|
||||
|
||||
## FLAG-3 [khoản 5] **MED** — VK-5: ba bề mặt, `_mind` ngược hai cái kia
|
||||
|
||||
`_mind-s-13:95` `{treo-chờ-anh}` ⟂ `owner-decisions-…-dot-2.md:49` *"✅ VK-5 — CHỐT GIỮ PHỤ LỤC @S181"* ⟂ `sub-lead-gap-auditor.md:95` *"4 câu `HANDOFF:6` — ĐÃ CHỐT … (4) VK-5 giữ phụ lục"*.
|
||||
Vai **không phán bên nào đúng**: nếu lead đúng (chữ "OK" không chỉ nhánh) thì ledger đang **ngân một chữ OK quá tay** — đúng ca S179 mà `HANDOFF:64` giữ làm bài học; nếu ledger đúng thì §D đang giữ câu đã trả lời và sẽ **hỏi anh lần 2 (vi phạm A5)**.
|
||||
**Hướng fail-safe:** hạ `owner-decisions:49` xuống nấc *"lead đọc là chốt, chưa có chữ chỉ nhánh"*.
|
||||
|
||||
## FLAG-4 [khoản 5] **MED** — deep-sweep H24: **đổi nhãn, không đổi kết quả**
|
||||
|
||||
`_mind-s-13:96` gán `{treo-chờ-anh}`; `HANDOFF:7` gán *"NEXT-em (tự chạy, không cần anh)"*; `HANDOFF:11` ghi `lead-gap` @S181 đã FLAG `gap-decision-sunk` vì lead **hạ** nó xuống ngang việc vặt, kèm *"Cổng kêu mà không đổi hành-vi = cổng không có răng."*
|
||||
⇒ @S181 bị bắt vì **HẠ**, @S182 **NÂNG** lên treo-chờ-anh — **hai nước đi ngược chiều, cùng kết quả: không chạy**, 15/15 → **16/15**.
|
||||
🔴 `treo-chờ-anh` **tệ hơn** ở một điểm: khoá món sau cánh cửa **chỉ anh mở được** ⇒ nợ trôi tiếp **với lý do trông chính đáng**.
|
||||
**Máy mù trọn:** phụ-9 chỉ hỏi *"có nhãn chưa"*, không hỏi *"nhãn có chuyển chủ-thể trách-nhiệm không"*.
|
||||
|
||||
## FLAG-5 [khoản 5] **MED** — §B trỏ "4 câu số" = referent mơ hồ, bề mặt gần nhất của nó đã THỐI
|
||||
|
||||
`_mind-s-13:79` §B: *"chờ anh chốt **4 câu số**"*. §D phiên này có đúng 4 ý `treo-chờ-anh` — một cách đọc. Nhưng `HANDOFF:6` **cũng có đúng 4** câu dưới *"🔢 CHỜ ANH"*, và **cả 4 đã trả lời + thi hành @S181** (`936d1aed`), **bảng chưa lật**.
|
||||
⇒ Cửa-sổ mới đọc §B rồi mở HANDOFF sẽ **khớp nhầm vào danh sách đã chết** và hỏi lại anh 4 câu đã trả lời. §B là ô dựng ra để **chống đảo lại vòng cũ**, ở đây nó **tự trỏ vào vòng cũ**.
|
||||
**Sửa rẻ:** viết thẳng **số slot** thay vì đếm "4 câu".
|
||||
*(`HANDOFF:6` thối là lane `lead-stale`; 11 FLAG lượt này rơi vào `:38 :40 :7 :11 :8 :9`+STATUS+persona, **không có** `:5`/`:6` — nêu vì `_mind` §B tựa vào đó, **không chấm vai kia**.)*
|
||||
|
||||
## FLAG-6 [cân-sổ ở chốt-kết] **MED** — `session-12/_end:7` khai `mind-blocks=1`, đĩa có **2**; lỗi **CẤU-TRÚC**, không phải gõ nhầm. **MÁY CÓ BẮT, mức `co-INFO`**
|
||||
|
||||
Đĩa: `grep -c '^## MIND-[0-9]' _mind-s-12.md` = **2**. `_end:7`: `markers: _pause=1 · _tiep=1 · _snapshot=0 · mind-blocks=1`.
|
||||
**Cơ-chế chứng bằng mtime:** `_end` = **23:28:49**, `_mind-s-12.md` = **23:30:11** ⇒ block refresh-@closeout ghi **sau `_end` 82 giây** ⇒ dòng `mind-blocks=` **về nguyên tắc KHÔNG BAO GIỜ đếm được** block refresh-bookend.
|
||||
|
||||
🔴 **Hệ quả nặng:** bản vá S162 dựng vế fail-LOUD *"`|block|` không tăng thì `_end` BẮT BUỘC mang `mind-refresh: BỎ QUA — <lý do>`"* — nhưng từ `_end`, ca **đã refresh** và ca **bỏ qua refresh** in ra **CÙNG MỘT con số**. **Vế fail-LOUD mất răng ngay tại sổ nó phải kêu.**
|
||||
|
||||
**Phép quyết định (ĐÃ CHẠY):** `mind-check --session 12` → `(6) bat-bien: co` / `co-INFO lech tu-khai: '_end' mind-blocks=1 vs DIA 2 (DIA THANG)` / `verdict: dat=9 TRUOT=0 co=2 exit 0`.
|
||||
⇒ Máy **có răng ở mức cờ-INFO, KHÔNG chặn** (exit 0), và cờ đó **chỉ sống ở phiên 12 — không ai đọc lại sau khi phiên đóng**. Đúng hợp-đồng §(iv): **đĩa thắng**, bất-biến chuỗi vẫn ĐẠT.
|
||||
**Sửa rẻ:** đếm `mind-blocks=` **SAU** bước refresh.
|
||||
|
||||
## FLAG-7 [khoản 3] **LOW-MED** — §E thiếu đúng lượt spawn cuối của chính cửa-sổ mình, và thiếu CÓ HỆ-THỐNG
|
||||
|
||||
§E phủ **12/12** vật đã có verdict lúc ghi: 5 probe (1 dòng, trỏ registry) + 6 vai bookend + `ring4-audit` **NO-OP có khai** ⇒ ✅ **ca NO-OP KHÔNG bị bỏ im — ĐẠT, ghi công lead.** Verdict đối chiếu **4/4 khớp** sub-file + synthesis.
|
||||
**Thiếu:** `ctx-audit` — **0 hit** dưới marker (2 hit duy nhất `:10`, `:22` đều **trong khối luật**; control-dương `ring2-audit`=2, `ring4-audit`=1).
|
||||
**Cơ-chế:** block-0 ghi ở Phase 3.5, `ctx-audit` spawn ở Phase 3.5**(iii)** ⇒ block-0 **luôn ghi một nhịp TRƯỚC lượt cuối nó phải kể** — 🔴 **CÙNG HÌNH với FLAG-6**. Tiền lệ vá được: @S173 §E có dòng cho vai **đang chạy**.
|
||||
|
||||
---
|
||||
|
||||
## Khoản 1 — block-0 có NỘI-DUNG THẬT: **ĐẠT**
|
||||
|
||||
Không dựa metadata. Mảnh khuôn/máy không tự sinh được: tên ý §D (*"`ring1-audit` persona ghi «CẤM tự chạy ở open»"* `:102`) + E-verdict cụ thể (`ring1-audit` → `41 ĐẠT / 7 TRƯỢT / 8 KC` `:111`) + §B nhánh-đã-loại (d) (*"12 là ĐÚNG SỐ, chỗ sai là cây `:44-77`"* `:84`).
|
||||
🟢 **Block-0 GIÀU NỘI-DUNG NHẤT trong 5 phiên vai chấm** — khác hẳn S173 (4/4 phiên block-0 = bản sao SCAFFOLD-META).
|
||||
|
||||
## Khoản 4 — việc ĐÃ-CHỐT chỉ dùng con-trỏ: **ĐẠT**
|
||||
|
||||
§A/§E dùng path đầy đủ tới run-folder/sub-file; **0 ca chép lại nội-dung verdict đã chốt**. §A `:74` trỏ `bookend-open-synthesis.md` thay vì chép bảng.
|
||||
|
||||
## Khoản 6 — 🎣 MỒI-NGẦM gieo cho @close
|
||||
|
||||
> **MỒI = ý `_mind-s-13:102` — `{mới-nêu}` "`ring1-audit` persona ghi «CẤM tự chạy ở open» mà lead giao @open 5 phiên liền — sửa persona hay sửa vận hành".**
|
||||
|
||||
**Lý do chọn:** ý chưa-chốt **DUY NHẤT trong §D không có bất kỳ nhà nào**. Đo: `carry:ring1*` trong HANDOFF = **0** · `"CẤM tự chạy"` trên HANDOFF/STATUS/WAL = **0/0/0** · `"R-3 / persona thối"` trong HANDOFF = **0**; **control-dương** `carry:ring5-audit-gap` = **6 hit** (ý anh em CÓ nhà). Không slug, không slot, không sổ ⇒ **rủi ro rơi cao nhất**, đúng khuôn vừa giết V-5 (FLAG-2) và suýt giết baseline (FLAG-1).
|
||||
**Cách chấm @close:** **grep NỘI DUNG**, không neo số dòng (bài S158) — tìm cụm persona/vận-hành `ring1` ở block `_mind` mới · `_tiep-*` · WAL · HANDOFF. **ĐẠT** nếu được mang tiếp HOẶC đóng-CÓ-KHAI; **TRƯỢT** nếu biến mất im lặng.
|
||||
🔴 **Khai kẽ (không tô thành tính năng):** single-lead ⇒ *"bên bị chấm không biết mồi"* chỉ đúng **xuyên cửa-sổ**; trong cùng cửa-sổ này lead đọc được return ⇒ mồi **không mù**.
|
||||
|
||||
## checklistEvidence — số TỰ tái dựng (không tin lời khai)
|
||||
|
||||
- Đĩa s13: `_pause-*|pause-*` = **0** ⇒ p=0; `^## MIND-[0-9]` = **1**; kỳ vọng `[p, p+3]`=[0,3] ⇒ **ĐẠT**. Máy in y hệt ⇒ **0 lệch**.
|
||||
- Đĩa s12: p=**1**, |block|=**2**, [1,4] ⇒ **ĐẠT** (tự-khai lệch ⇒ FLAG-6).
|
||||
- 5 mục A-E **5/5** đúng thứ tự, 0 mục trống. Nhãn D **9/9** đúng chuỗi có dấu ⇒ **trượt-cấu-trúc = 0** (sai là **NỘI DUNG** nhãn — FLAG-1/3/4 — **máy không thấy**).
|
||||
- 3 rào tự soi (dòng >68): rào-1 = 0 · rào-2 = 0 · rào-3 = 0; **control-dương** `^- ` = **25** ⇒ thước sống, 0-hit là thật. **Máy KHÔNG hở.**
|
||||
- §B vs `WAL next:` — WAL rỗng 182 B, `next: (trống)` ⇒ không có lệch để "tin sổ máy"; hệ quả gộp vào FLAG-5.
|
||||
- **Tự re-chạy thay vì tin PIN:** `mind-check --session 13` → `dat=11 TRUOT=0 co=0 exit 0`, **khớp PIN từng số**. 🟢 **Lần đầu trong 4 lượt PIN của lead còn tươi** (S162 · S169×3 · S172 đều hết hạn). Kỷ luật re-chạy giữ, nhưng lần này nó **xác nhận lead, không bắt lead**.
|
||||
|
||||
## Thách-CLEAN
|
||||
|
||||
Máy 0 trượt 0 cờ ⇒ nghĩa vụ kích hoạt. Soi 2 chỗ máy mù: (a) §E vs `runs/`+registry → FLAG-7 + xác nhận NO-OP ĐẠT; (b) ý D vs WAL/owner-decisions/sổ REQUEST → FLAG-1, 3, 4. **7/7 FLAG đều NGOÀI tầm máy** (ngữ-nghĩa + liên-phiên).
|
||||
|
||||
## Khai lane
|
||||
|
||||
FLAG-1 lộ lỗi đi qua **3 tầng** rồi vào `_mind`: `lead-gap` FLAG-6 phát → `ring2` chấm `lead-gap` **6/6 ĐẠT** (G-6 chỉ kiểm vế **tự-rút P5/P7/P8**, không đo lại 2 món còn khai "rơi") → lead promote vào §D. Vai **KHÔNG chấm** `lead-gap`/`ring2` (lane khác); chỉ nêu **đường lây** vì đích đến là §D — lane của mình. **Escalate lead phán.**
|
||||
|
||||
## memoryDelta đề xuất (propose-only — vai PHÁN, lead GHI)
|
||||
|
||||
- `mind-carry-restore-stale-form` — vớt carry mà **không re-đo trạng-thái** ⇒ hồi sinh việc đã chốt.
|
||||
- `mind-selfcount-before-refresh` — FLAG-6 + FLAG-7 **CÙNG MỘT HÌNH**: sổ cân được ghi **một nhịp trước** sự kiện cuối nó phải kể.
|
||||
- `carry:v5-vi-tu-ma-hoa` — món rơi cần nhà thật.
|
||||
@ -0,0 +1,91 @@
|
||||
# sub-harvest-curator — V1 @open S182 (H2 harvest-integrity 5 trục)
|
||||
|
||||
**VERDICT: `HARVEST-CURATOR: GATE-FAIL — 7 finding`**
|
||||
Scope: HEAD `936d1aed`, @open S182 (L13-w1). Read-only, propose-only — 0 Write/Edit.
|
||||
|
||||
> ⚠️ **#53 datum:** first-return = garble (*"Now the S169/S170 block. Let me verify the sleep-batch claim fresh since it swings ~237 KB."*). Recover bằng SendMessage-resume, path-2. **Mất 0 byte.**
|
||||
|
||||
---
|
||||
|
||||
## F-01 [Coverage] `runs/2026-08-07-S180-adap-upgrade-pack-phased/` — nợ **MỚI 229.823 B / 5 lượt-vai `reviewer`**, KHÔNG có trong HANDOFF slot 71
|
||||
|
||||
Vai `reviewer` đẻ 9 sub-file @S180; memory chỉ có 5 mục. Mồ côi: `sub-n3-lane-c-gd34.md` 72.156 + `sub-n3-lane-d-gd56.md` 43.795 + `sub-n3-lane-e-benh.md` 38.531 + `sub-n3-lane-f-maytay.md` 52.923 + `sub-review-n2-S180.md` 22.418 = **229.823 B**.
|
||||
|
||||
**Chứng bằng NỘI DUNG (không đo tên):** `grep -ric` trên `agent-memory/reviewer/` → `"bẫy A1"`=0 · `"sổ-3-khối"`=0 · `"gần xong nhất"`=0 · `"13,0/20"`=0 · `"3,0/17"`=0 · `"KHÔNG ÁP DỤNG có khai"`=0 · `"đóng-hụt"`=0 · `"máy ⟂ tay"`=0 · `"triết lý thứ hai"`=0 · `"F-10"`=0. **Control dương cùng lệnh:** `"S180"`=22, `"reviewer"`=108 ⇒ thước sống, 0-hit là 0 THẬT.
|
||||
|
||||
🔴 **Bẫy đã tránh:** `"lane C"`=7 / `"lane D"`=3 / `"lane E"`=2 hit — nhưng đọc ra thì đó là **trích dẫn hạng-hai bên trong mục N4** (`:71` · `:73`), không phải bài học của chính lane. **Đo TÊN ⇒ tưởng 3/5 đã về; đo RUỘT ⇒ 0/5.**
|
||||
|
||||
## F-02 [Coverage] `runs/2026-08-01-S168-bookend-close/` — nợ **82.383 B / 3 vai**, ĐỨNG YÊN từ S173
|
||||
|
||||
`sub-harvest-curator-close-S168.md` 29.299 + `sub-lead-gap-close-S168.md` 29.820 + `sub-lead-stale-close-S168.md` 23.264 = **82.383 B** (`stat -c%s`).
|
||||
0 vai nào có mục mở đầu S168 (`grep -n "^[-*#]{1,4}\s*\**S168\b"` = **0 dòng**), trong khi cùng lệnh trả 1 dòng cho mỗi vai `ctx-audit` `ring1-audit` `ring2-audit` `tooling-auditor` `harness-audit` `harness-eval` `harness-refine` `cicd-monitor` (**control dương 8 vai**). 7/10 vai đã đóng, đúng 3 vai này thủng.
|
||||
|
||||
🔴 Con số **82.383 khớp TỪNG BYTE** với số S173 ghi 2 phiên trước ⇒ **hai phiên có tuyên bố uỷ-quyền-gom mà 0 byte được gom.**
|
||||
|
||||
## F-03 [Coverage] `runs/2026-08-01-S169-bookend-open/` — nợ **264.451 B / 10 lượt-vai** — khối LỚN NHẤT, HANDOFF slot 71 **HOÀN TOÀN VẮNG**
|
||||
|
||||
`harness-audit-return.md` 36.119 + `harness-eval-return.md` 24.015 + `harness-refine-return.md` 26.596 + `sub-harvest-curator-open-S169.md` 26.137 + `sub-lead-gap-open-S169.md` 18.318 + `sub-lead-stale-open-S169.md` 22.503 + `sub-ring1-audit-open-S169.md` 34.806 + `sub-ring2-audit-open-S169.md` 32.197 + `sub-tooling-auditor-open-S169.md` 23.236 = 243.927 **+ `sub-ring4-audit-sleep-S170.md` 20.122** = **264.451 B**.
|
||||
|
||||
**Đã về nhà, KHÔNG tính nợ:** `sub-ctx-audit-open-S169.md` 34.793 · `sub-reviewer-sleep-fidelity-S170.md` 38.167.
|
||||
|
||||
## F-04 [Fidelity] 🔴 TỰ BÁC con số của CHÍNH VAI NÀY — sleep-batch **237.251 B KHÔNG phải nợ**; nhưng "14/16" trong sổ cũng SAI, đo tươi ra **16/17**
|
||||
|
||||
`sub-harvest-curator-sleep-batchA-S170.md` 113.797 + `batchB` 123.454 = 237.251 B là **bản nháp gia công**, không phải delta cần gom.
|
||||
Đếm đích thật: `grep -h "^## "` → **17 đích gist**. Kiểm tồn tại: **16 LANDED** (vd `cicd-monitor/archive/2026-05-runs.gist.md` 31.779 · `test-specialist/archive/baseline-history.gist.md` 28.437 · `implementer-frontend/archive/2026-H2.gist.md` 30.144); mtime đích **08-03 10:51/11:03** > mtime nháp **09:51/09:52** ⇒ nháp đã thi hành.
|
||||
|
||||
**1 MISSING:** `.claude/agent-memory/cicd-monitor/archive/2026-08.gist.md` **không tồn tại** (thư mục có `2026-08.md` 5.912 B mà **không có** `.gist.md` anh em) ⇒ nợ THẬT còn lại của khối S170, đúng 1 đích.
|
||||
|
||||
🔴 **Khai giới hạn thước:** vai đo **tồn-tại + thứ-tự mtime**, KHÔNG đo **byte-exact**. Sổ ghi "14/16 byte-exact"; vai ra "16/17 tồn-tại". Hai phép đo **KHÁC CÂU HỎI** ⇒ **không chọn bừa một số**. Mẫu-số 16 trong sổ **SAI** (đích thật 17, đếm được). Tử-số 14 vs 16 thì thước **không đủ mạnh để bác** ⇒ **`CHƯA ĐO`** cho vế byte-exact, cần md5.
|
||||
|
||||
## F-05 [Coverage] `S171-khkk-ui-mirror-pe` = **ĐÃ ĐÓNG** — HANDOFF slot 71 đang mang **NỢ MA 143.682 B**
|
||||
|
||||
4 vai / 6 tệp đều có mục thật: `implementer-backend/MEMORY.md:9` · `implementer-frontend/MEMORY.md:3` · `investigator-codebase/MEMORY.md:14` + `archive/2026-08-curate-s180.md:15` · `reviewer/archive/2026-08.md:36` — mục sau ghi thẳng *"cả 3 CHẾT hết-giới-hạn nhưng ĐĨA CỨU 74.070 B"*, và 26.461+23.177+24.432 = **74.070 khớp từng byte**. Cộng 11.650+13.132+44.830 ⇒ **143.682 B** đúng bằng số HANDOFF đang ghi là nợ.
|
||||
|
||||
**Cùng lớp, cũng đã đóng:** S172 10/10 · S173 10/10 · S174 · S176 · S181 (9 vai). `S159-bookend-open` KHÔNG tính nợ (theo lệnh).
|
||||
|
||||
## F-06 [Placement] HANDOFF nói **8** dir rỗng mis-land class-S54 — đo tươi còn **0**
|
||||
|
||||
`find . -type d -empty -not -path "./.git/*"` → **5 dòng, 0 dòng class-S54**: `.vs/sd` · 2× `node_modules/.vite-temp` · 2× `obj/{Debug,Release}/net10.0/staticwebassets` — toàn rác IDE/build.
|
||||
7 dir dưới `runs/2026-08-01-S169-bookend-open/` và `fe-user/.claude` mà HANDOFF liệt: **không còn**.
|
||||
**Kiểm chéo lệnh khác chiều:** `find . -type d -name ".claude" | grep -v "^./.claude$"` → **rỗng** ⇒ 0 stray lồng.
|
||||
⇒ Trục Placement **PASS**, nhưng số trong HANDOFF là **neo thối cần lật**.
|
||||
|
||||
## F-07 [Completeness] **5 vai** có thư mục memory mà chưa từng có delta của chính mình
|
||||
|
||||
- 🔴 **`ring4-audit` THỦNG CÓ BẰNG CHỨNG:** `MEMORY.md` 2.315 B, mtime **2026-07-26**, `grep -c "S170"`=0 **dù** `sub-ring4-audit-sleep-S170.md` 20.122 B nằm trên đĩa ⇒ **vai chạy thật, sổ đóng băng 12 ngày**.
|
||||
- **LOW (không kết luận thủng):** `database-agent` 9.038 B mtime 07-29 · `investigator-api` 9.021 B mtime **07-25** · `office-document` 3.323 B mtime **07-10** — 0 hit S168→S181, **nhưng có thể đơn giản là không được spawn**. Control dương: cùng lệnh trả 8 hit S168 cho `harvest-curator`.
|
||||
- ✅ **`score-counter` 2.075 B / `score-count-auditor` 2.033 B nay ĐÃ CÓ mục S181** (mtime 08-07 23:10) ⇒ **FLAG F-02 của S181 coi như ĐÓNG** — cần lead xác nhận dòng seed cũ *"CHƯA CHẠY LẦN NÀO"* đã bị **thay** chứ không phải **nằm cạnh**.
|
||||
|
||||
---
|
||||
|
||||
## Mục bắt buộc — chốt số
|
||||
|
||||
**1. Orphan run-folder (DUAL-ACCEPT `tiep.md §0` verbatim):** quét **89 thư mục**, **86 có `run.md`** (mẫu số thật). Kết quả **1 dòng ORPHAN duy nhất = `runs/2026-08-07-S182-bookend-open/`** — chính folder phiên này, mở 23:43, mới có `run.md` 652 B ⇒ **ĐANG CHẠY, không phải orphan thật**.
|
||||
🔴 **Cảnh báo:** S182 đóng mà không đẻ `*-synthesis.md` ⇒ thành orphan thật y hệt cách 4 món S169-S172 sinh ra.
|
||||
3 thư mục không có `run.md` ⇒ ngoài định nghĩa: `S117-pe-approve-minmax-range` · `S117-pe-luyke-finalize` (spec-only) · `S144-bootstrap` (có `bootstrap-synthesis.md`).
|
||||
|
||||
**2. Nợ harvest đo TƯƠI — tổng 576.657 B / 18 lượt-vai:**
|
||||
|
||||
| khối | byte | vai | so với HANDOFF slot 71 |
|
||||
|---|---|---|---|
|
||||
| S168-bookend-close | **82.383** | 3 | slot ghi 162.916 / 9 vai ⇒ **quá cao** (6 vai đã gom @S172) |
|
||||
| S169-bookend-open (+ring4 S170) | **264.451** | 10 | 🔴 **HOÀN TOÀN VẮNG** |
|
||||
| S180-adap 5 lượt `reviewer` | **229.823** | 5 | 🔴 **MỚI, chưa ai ghi** |
|
||||
| `cicd-monitor/archive/2026-08.gist.md` | 1 đích | — | dư từ khối S170 |
|
||||
| **TỔNG** | **576.657 B** | **18** | slot ghi ~306 KB ⇒ **thiếu gần nửa** |
|
||||
| S171-khkk-ui-mirror-pe | **0** | 0 | slot ghi 143.682 = **NỢ MA** (F-05) |
|
||||
| sleep-batch A+B 237.251 B | **0** | — | **KHÔNG phải nợ** (F-04) |
|
||||
|
||||
`a2bbcb9` / `b5799fc` mở từ S162: **CHƯA ĐO** (chưa chạm `git log -S`, không lấp bằng trí nhớ).
|
||||
|
||||
**3. Stray / empty-dir:** **5 dir rỗng, 0 class-S54; 0 stray `.claude` lồng.** Số 8 trong HANDOFF đã chết.
|
||||
|
||||
**4. Diary 0-delta:** `ring4-audit` thủng có bằng chứng · 3 vai LOW chưa kết luận · nhóm vai mới còn lại **đã có delta** (`ctx-curator` S175/S177 · `ctx-verifier` S171-S181 · `ctx-audit` S168-S173 · 3 vai style S179 ×2 · 2 vai score S181).
|
||||
|
||||
**5. Corruption: `CHƯA ĐO`.** Không chạy census U+FFFD ⇒ **KHÔNG tuyên "0 corruption"**. Baseline gần nhất = 24 hit / 11 file (S172). Bẫy đơn-vị S180 F-05 (22 **dòng** ⟂ 24 **lần**) — kỳ sau phải khai đơn-vị + control dương trước khi đọc số.
|
||||
|
||||
## Đề xuất cho lead (propose-only, idempotent)
|
||||
|
||||
1. **Lật slot 71 HANDOFF:** bỏ S171 (nợ ma) · sửa S168 xuống 82.383/3 vai · **thêm S169 264.451/10 vai + S180-reviewer 229.823/5 vai** · sửa "8 dir rỗng" → 0.
|
||||
2. **Khi gom:** lấy danh sách từ **`ls runs/<id>/` (GỐC)**, tuyệt đối không từ bảng dẫn xuất — đúng cơ chế đã làm hụt 2 sổ H24 @S172 (53.084 B khớp từng byte).
|
||||
3. **Trước khi đóng S182:** đẻ `*-synthesis.md` khác 0-byte trong run-folder này, nếu không nó thành **orphan thứ 5 cùng lớp**.
|
||||
@ -0,0 +1,101 @@
|
||||
# sub-lead-gap-auditor — V2 @open S182 (H24 soi CÁI BỊ THIẾU)
|
||||
|
||||
**VERDICT: `LEAD-GAP: 6 FLAG`**
|
||||
HEAD `936d1aed` · counter 56 · propose-only, 0 Write/Edit/commit, 0 RAG-write.
|
||||
|
||||
> ⚠️ **#53 datum:** first-return garble (*"Now let me verify which dropped slugs are genuinely STILL OPEN, with disk measurement."*). Recover bằng SendMessage-resume. **Mất 0 byte.**
|
||||
|
||||
---
|
||||
|
||||
## FLAG-1 `[gap-carry-dropped]` — **HIGH** — khối re-stamp @S181 (`HANDOFF:9`, 23 slug) tự tuyên là tập-mở đầy đủ, nhưng ≥**2 slug CÒN SỐNG bị bỏ hẳn**
|
||||
|
||||
**`[carry:hook-vs-budget-cap]`** — TỪNG tồn tại: `HANDOFF:217` khối *"Carry MỞ re-stamp @S151 — **đủ MỌI slug còn mở**"* · `:218` máy H24-2 đo tươi @S152 **streak = 12** (cao nhất mọi slug, `ring2` re-implement CONFIRMED) · `:213` slot **(29)** *"3 carry-age 12/9/7 — xử hay gia-hạn?"*.
|
||||
**Đo tươi NAY:** **13/28** tệp `agent-memory/*/MEMORY.md` vượt hook-cap 17.100 B; **3** vượt cả autoinject-cap 25.600 (`ring2-audit` 29.582 · `tooling-auditor` 29.429 · `lead-stale-auditor` 27.658) ⇒ câu hỏi thiết-kế đang treo, và nay **TỆ HƠN** lúc nêu chứ không tự khỏi. Probe nền cũng in `tran-bo-nho 3 over-cap`. **0 hit** trên `WAL.md` (rỗng 182 B) · `STATUS.md` · `error-ledger.md` · `migration-todos.md` · `so-yeu-cau-chu-du-an.md`.
|
||||
|
||||
**`[carry:carry-age-vacuous]`** — `HANDOFF:297` *"carry-age structural — detector 0-fire vì tiering<M=6: hạ M hay non-tiered-ledger?"*, tái-stamp qua 8 khối (`:217 :222 :239 :243 :247 :252 :256 :260 :264 :277`, n=11 dòng). Chưa ai trả lời, và **FLAG-2 dưới chứng minh nó vẫn đúng 100%**.
|
||||
|
||||
🔴 **Loại được bào-chữa bằng chính DIFF, không bằng lý lẽ:** đợt vá @S179 (`HANDOFF:78-79`) đã với tay qua segment cũ vớt lại **7 slug**, gồm **2/3** slug của slot (29) (`uat-s133-budget-freeze`, `uat-s134-luyke`) — nhưng **bỏ đúng cái thứ 3**, `hook-vs-budget-cap`, cái có streak **CAO NHẤT (12)**. Không phải "bỏ cả khối vì gấp"; là bỏ **có chọn lọc**, trúng món nặng nhất.
|
||||
|
||||
**Tự BÁC 4 nghi-vấn của chính vai** (kỷ-luật S179 *"sổ nói dối theo hướng BI QUAN"*): `[carry:mind-tran-nen-moi-cua]` + `[carry:adap-apply-2-thu]` vắng là **ĐÚNG** (`:84` đóng có bằng-chứng đĩa) · `[carry:ctx-verifier-no-self-append]` vắng **ĐÚNG** (đổi tên @S170) · `[carry:hmw-width-vs-roster]` vắng **ĐÚNG** (đóng @S181). Cũng KHÔNG flag `retier-megaline` (dòng dài nhất nay `STATUS:478` = 17.902 ch < cap Read 25K; tiền-đề "55.447 ch" hết đúng), `session-model-adopt`, `restart-s124`, `h24-2vai-chua-chay`.
|
||||
|
||||
**Hệ-quả:** khối re-stamp là nghi-thức DUY NHẤT chống carry-rơi, mà nó đang **ban phước cho chính chỗ rơi** — lần thứ **5** liên tiếp. `class_repeat.gap-carry-dropped = 12` = **4× ngưỡng jump**, và ngưỡng đó **không máy nào đọc** ⇒ đếm tới bao nhiêu cũng không đổi hành-vi.
|
||||
|
||||
## FLAG-2 `[gap-carry-aged]` — **HIGH** — 🔴 số `gap-carry-aged = 0` là **số 0 CHẾ TẠO**, không phải honest-zero
|
||||
|
||||
Port **NGUYÊN VĂN** regex `scripts/governance-detectors.ps1:892` `(?m)^\*\*(?:[^\x00-\x7F]+\s*)?NEXT\s+(?:anh|em)\b` sang Python trên `HANDOFF.md` @`936d1aed` ⇒ `marks.Count = 25`, **`marks[0]` = HANDOFF:179**.
|
||||
|
||||
**Vùng câm L1–178 = 42.606 B / 186.729 B = 22,8%** (S173 đo 31.698 B/L1-129; S180 đo 21,3% — **đơn-điệu TĂNG mỗi closeout**, vì closeout mới prepend segment kiểu **heading** `### 🔵 NEXT em @S179` mà regex neo `^\*\*` không bắt).
|
||||
|
||||
Vùng câm nuốt **91 occurrence / 28 slug**, **gồm TRỌN khối re-stamp `:9` (23 slug)**. `carryLines[0]` — tập DUY NHẤT giữ được streak sống — là **hoá-thạch kỷ S159–S160**, đúng **5 slug**: `ctx-t9-dogfood` (2) · `ring5-audit-gap` (1) · `hmw-width-vs-roster` (1) · `hmw-subfile-index-collision` (1) · `adap-apply-2-thu` (3). **2/5 đã ĐÓNG**. Mọi streak < M=6 ⇒ máy in `[ok]` toàn bộ ⇒ **0 fire, vĩnh viễn**.
|
||||
|
||||
`governance-detectors.ps1` commit cuối `febe6b16` **2026-07-29** — **9 ngày không ai chạm** kể từ khi lỗi được chứng lần đầu.
|
||||
|
||||
🔴 **Cái THIẾU (đúng trục vai này):** lỗi máy này **không có `[carry:*]` slug nào**, và bị **đóng ké** vào slot đã gạch — `HANDOFF:58` nằm dưới `~~(70)~~ ✅ ĐÓNG @S179` ⇒ mọi lượt quét "slot còn mở" **không thấy nó**. Trên 6 sổ bền: `H24-2` chỉ 4 hit ở HANDOFF (văn kể chuyện), **0 hit** ở `WAL`/`error-ledger`/`migration-todos`/`so-yeu-cau`; control-dương: `S181` ra 4/3/8/16 hit ⇒ grep sống.
|
||||
|
||||
🔸 `HANDOFF:58` còn ghi *"HANDOFF:90 ⇒ L1-89 · 12 slug"* — thực đo nay **L1-178 · 28 slug**, số cũ đọc thành **nhẹ gấp đôi** (trục `view-*`, vai không lấn).
|
||||
|
||||
**Hệ-quả — mù KÉP:** FLAG-1 làm carry rơi khỏi sổ, FLAG-2 làm máy không thấy carry nào già. Ô `gap-carry-aged = 0` sẽ được đọc thành "lành", và **càng lâu càng câm hơn** vì vùng mù tự nới mỗi phiên.
|
||||
**Resolve:** neo regex bắt **CẢ 3 dạng** `**` / `##` / `###` (bài `ring2` @S173: gốc là **anchor**, không phải chỉ-số `[0]`), rồi đo lại `marks[0]` phải = `HANDOFF:6-9`.
|
||||
|
||||
## FLAG-3 `[gap-decision-sunk]` — **MED-HIGH** — nợ `OVERDUE:deep` trôi thêm 1 nhịp mà bề mặt vẫn in số cũ
|
||||
|
||||
`HANDOFF:11` đã viết bằng chữ mạnh nhất cả file: *"Cổng ĐÃ kêu, máy ĐÃ in, lead ĐÃ chép 2 sổ — rồi vẫn hoãn. **Cổng kêu mà không đổi hành-vi = cổng không có răng.**"*
|
||||
**Đo tươi:** `.session-counter.json` → `counter = 56`, `deep_at_counter = 40` ⇒ **56 − 40 = 16/15**; `light_at = 53` ⇒ 3/6. Nhưng `HANDOFF:7` (dòng hành-động phiên này) vẫn viết **15/15**.
|
||||
|
||||
**Cái THIẾU:** chẩn-đoán đúng bệnh rồi **không sinh ra thay đổi hành-vi nào** — slug vẫn ở hàng NEXT-em cạnh "3 tệp over-cap", **không** lên `CHỜ ANH:6`, **không** mốc, **không** điều-kiện hết-hạn. Trên 6 sổ bền: `h24-deep-overdue` chỉ có ở `HANDOFF` (2 hit) + `_end` session-12; **0 hit** ở `STATUS`/`error-ledger`/`migration-todos`/`ACTIVE-MARKS`.
|
||||
|
||||
**Hệ-quả:** deep-sweep là cổng DUY NHẤT còn răng theo hình-B (`light_every` nay stats-only). 16/15 = quá hạn hơn **một chu-kỳ light trọn vẹn**. **Ca tự-chứng:** dòng `:11` chẩn-đoán đúng bệnh của chính nó rồi vẫn tái phát ngay nhịp kế.
|
||||
|
||||
## FLAG-4 `[gap-owner-specifics]` — **MED** — bảng VERBATIM lời anh bị chốt ở phụ lục; **8 câu P1-P8 chưa vào sổ dưới bất kỳ dạng nào**
|
||||
|
||||
`HANDOFF:16` tự khai lý-do sinh ra bảng: *"vá `lead-gap-auditor` FLAG-4; trước đó specifics chỉ sống trong WAL, mà WAL reset trắng ở closeout ⇒ suýt mất"*. Câu (4) `HANDOFF:6` = **VK-5**: *nhập bảng vào khối chính sổ, hay giữ phụ lục?*
|
||||
|
||||
⚠️ Anh trả **"1,2,3,4 → OK"**; commit `936d1aed` tự khai đọc thành *"lấy nhánh AN TOÀN theo bậc-2: giữ phụ lục"*. Nhưng (4) là câu **chọn A-hay-B**, không phải yes/no — **"OK" trên câu chọn không chỉ ra nhánh nào**. Lead có tự khai điều này trong commit message (*"Anh muốn nhánh kia thì 1 chữ là đổi"*) — kỷ-luật đúng. Nhưng **hệ-quả đo được**: `so-yeu-cau-chu-du-an.md` giữ **0 dòng nguyên-văn** của bảng `:16-27`.
|
||||
|
||||
🔴 **Nặng hơn:** **8 câu P1-P8 @S181** — trong đó **P4 là lệnh đổi số THẬT** (`hmw_width.cap` 20→30, `memory-budget.json:_cap_raised_S181` ghi *"chu du an noi dut khoat 'nang cap'"*) — nguyên-văn P1-P8 **không có nhà nào** ngoài HANDOFF/commit-message. Trong khi chính `_cap_raised_S181` ghi bài học ngược: @S179 anh nói "OK" mà lead tự thu-hẹp thành *"CHƯA phải lệnh đổi số"*.
|
||||
|
||||
**Hệ-quả:** đúng cơ-chế mà bảng `:16-27` được dựng để chặn — chữ của anh sống ở nơi bị nén/xoay vòng, sổ REQUEST thì trống. Lần thứ **2** cùng lớp trong 3 phiên; `class_repeat.gap-owner-specifics = 5`.
|
||||
|
||||
## FLAG-5 `[gap-decision-sunk]` — **MED** — slot **(67)** *"CHỈ ANH PHÁN"* chìm dần theo tiering, 0 lần surface
|
||||
|
||||
`HANDOFF:48-51`: người **SOẠN** có được upload file-khi-duyệt không. Đo prod: `test.drafter@` (Drafter+Procurement) **qua được** `[Authorize(Policy="KeHoachKyKet.Update")]` ở `POST /api/contract-signing-plans/{id}/approval-attachments`. Chữ anh nguyên văn (ảnh 2 @S171): *"File do **người duyệt** tải lên trong quá trình duyệt"* — discriminator ở **CHỦ THỂ**, không ở **BƯỚC** ⇒ *"policy hiện **rộng hơn ý định**"*.
|
||||
|
||||
**Đo tươi:** khối chứa (67) mang tiêu-đề `## 🧊 CHỜ ANH @S172` — 🧊 = lineage/đóng băng; hàng xóm (68)(69)(70)(71) đều `~~gạch~~ ✅ ĐÓNG`. S181 có **8 lượt owner** + phiên này 4 câu — **0 lượt** dùng cho (67). `CHỜ ANH:6` segment mới **không nhắc**. Slot (67) **0 hit** trên `STATUS`/`WAL`/`migration-todos`/`error-ledger`; control-dương `KeHoachKyKet` = 8 hit ở `CLAUDE.md`.
|
||||
|
||||
**Hệ-quả:** món **DUY NHẤT** mang nhãn "CHỈ ANH PHÁN" **và** đã có đo prod chứng minh lỗ authz thật. Im **9 nhãn phiên** (S172→S181), nằm dưới heading 🧊 giữa 4 dòng đã gạch ⇒ mắt lướt đọc thành *"khối này xong rồi"*. Theo tiering `HANDOFF:3`, segment S172 sắp bị archive ⇒ **chìm luôn**.
|
||||
|
||||
## FLAG-6 `[gap-carry-dropped]` — **MED** — 2 ý `{treo-chờ-anh}`/`{đang-cãi}` trong `_mind-s-12.md` không được carry sang phiên-logic 13
|
||||
|
||||
**Đo tươi bàn giao:** `session-12/_end:3` chỉ carry **3 slug** · `:4` chỉ 4 câu pending · `session-13/_context-s-13.md` = scaffold trắng · `WAL.md` rỗng 182 B.
|
||||
|
||||
🔸 **Tự BÁC một phần:** **P5/P7/P8 KHÔNG còn là gap** — `HANDOFF:5` xác nhận *"Anh chốt 8 câu P1-P8"* ⇒ đã trả lời (đo tiền-đề trước khi leo thang, bài `HANDOFF:73`).
|
||||
|
||||
**Cái THẬT SỰ rơi — 2 món:**
|
||||
- **(a) 7 baseline `-Expect`** (`_mind-s-12.md:115`, nhãn `{đang-cãi}`): giữ-tripwire-thêm-ngày ⟂ tách-file-dữ-liệu, tự khai *"cả hai đều đổi thiết kế máy governance"*. Chuỗi `-Expect` **0 hit** trên HANDOFF+STATUS+WAL+error-ledger+migration-todos; control-dương `baseline` có hit ở STATUS.
|
||||
- **(b) đề-nghị `ring2`** *"đòi phép đo trước khi vào tally"* (`:84`) — **luật về chính bộ đếm H24**, ghi cùng lượt với 2 FLAG khác, **chỉ sống ở `_mind` của phiên đã đóng**.
|
||||
|
||||
**Hệ-quả:** món (b) đặc biệt đắt — nó là **bản vá cho chính cơ-chế đang sinh ra FLAG-1/FLAG-2**. Rơi ⇒ tally H24 tiếp tục nhận flag không-kèm-số, vòng sau phải phát hiện lại từ đầu.
|
||||
|
||||
---
|
||||
|
||||
## TOTAL: 6 — phân-rã theo class (cho `jump_on_class_repeat`)
|
||||
|
||||
| class | n | FLAG |
|
||||
|---|---|---|
|
||||
| `gap-carry-dropped` | **2** | FLAG-1, FLAG-6 |
|
||||
| `gap-carry-aged` | **1** | FLAG-2 |
|
||||
| `gap-decision-sunk` | **2** | FLAG-3, FLAG-5 |
|
||||
| `gap-owner-specifics` | **1** | FLAG-4 |
|
||||
| `gap-incident-unrecorded` | **0** | *honest-zero CÓ LẬP LUẬN* — sự-cố lớn nhất phiên trước (9/9 spawn garble #53) ĐÃ ghi đích danh `_mind-s-12.md:96` + `:125`. Có sổ ⇒ không phải class này |
|
||||
| `gap-underfill` | **0** | 🔴 **KHÔNG phải honest-zero — là `CHƯA ĐO`**. `ring2` đã bác flag underfill @S179 vì *"fire ngược chiều phán quyết mà không kèm số"*; vai chấp nhận ràng buộc ⇒ không phát flag khi chưa đo trọn 9 nguồn `tiep_reload.sources`. *(Datum còn nợ: mốc `_expand_S168` = 182.160 B; @S179 đo ra 373.881 B = **2,05× lỗi-thời**)* |
|
||||
|
||||
## Điểm PIN được BÁC / RÚT
|
||||
|
||||
- **Slot (73) — RÚT HOÀN TOÀN, không thành FLAG.** Vai đã dựng nghi-vấn từ đĩa (3 persona land `57e672f7`; 3 diary ở nấc SEED mang chữ "chưa chạy"; `HANDOFF:32` + `:7` không liệt). **Nhưng lead ĐÃ chạy probe trong chính phiên này, 5/5 ALIVE** ⇒ tiền-đề **hết đúng trước khi vai kịp phát**. Đúng khuôn `HANDOFF:73`. Không leo thang. *(INFORM-only: `HANDOFF:32` giờ là số cũ — trục `view-*`, không lấn.)*
|
||||
- **4 câu `HANDOFF:6` — ĐÃ CHỐT, không chìm.** Commit `936d1aed` thi hành thật: (1) báo-cáo-cuối lật DA DUYET + gửi outbox (stamp_verify OK) · (2) điều-kiện-3 ghép `§LT3` · (3) P3 forward-only · (4) VK-5 giữ phụ lục. Chỉ vế SPECIFICS của (4) đi vào FLAG-4, **không đếm trùng**.
|
||||
|
||||
## Khai kẽ của chính vai (không giấu)
|
||||
|
||||
1. FLAG-2 vai **tính lại streak** thay vì chỉ đọc máy — trái chữ thường lệ ("đọc detector, KHÔNG tính lại"). Lý do: **máy mù đúng chỗ cần đo**, im theo máy sẽ báo sạch một cách SAI. Port **nguyên văn** regex `:892`, không viết lại logic — cùng đường `ring2` @S173/@S180.
|
||||
2. Tập **34 slug** vắng khỏi `HANDOFF:9`, vai **chỉ kết luận chắc 2** (có đo đĩa dương). **30 món còn lại = `CHƯA ĐO` từng món** — **không gộp vào FLAG-1 để thổi số**. Đây là **cận-dưới**, không phải tổng.
|
||||
3. Con số `20` ở `:8` vs **23** slug đếm ở `:9` — giải-thích được (20 cũ + 3 mới) ⇒ **không** phải gap; lệch-số là trục `view-*`, không lấn.
|
||||
@ -0,0 +1,120 @@
|
||||
# sub-lead-stale-auditor — V2 @open S182 (H24 soi VIEW LỆCH SOURCE)
|
||||
|
||||
**VERDICT: `LEAD-STALE: 11 FLAG`**
|
||||
HEAD `936d1aed` · counter 56 · đọc output máy làm NỀN, không chạy lại. 11 FLAG đều **NGOÀI tập máy phán được** (line-ref drift · enum liệt-tên-không-số · to-do đã xong chưa lật · pointer trỏ sai dòng · ba mốc "hiện tại" chỏi nhau trong một file).
|
||||
|
||||
> ⚠️ **#53 datum:** first-return garble (*"Now the measurements. Batch 1."*). Recover bằng SendMessage-resume. **Mất 0 byte.**
|
||||
|
||||
---
|
||||
|
||||
## FLAG-1 `[view-stale-count]` `docs/STATUS.md:8` — view-lệch-source THẬT, ô canonical khác gánh và ô đó ĐÚNG
|
||||
|
||||
**View** (`:8`, nhãn `🔥 CURRENT`): Mig **71** · **644 test** (45D+**599I**) · counter **46** · `light 2/6` `deep 6/15` **ok** · `light_at=44`.
|
||||
**Source cùng file:** `:467 Migrations = 72` · `:474 Tests = 645 PASS (45 Domain + 600 Infra)`; `.session-counter.json` → `counter = 56`, `light_at = 53`, `deep_at = 40` ⇒ light **3/6**, deep **16/15 OVERDUE**.
|
||||
|
||||
🔴 **Nặng nhất KHÔNG phải con số mà là chữ `ok`**: `:8` tuyên `deep 6/15 ok` trong khi đĩa là **16/15 OVERDUE** — máy chỉ so SỐ, không đọc được **nhãn phán-quyết**. `light_at=44` sai 9 nhịp.
|
||||
**Sửa:** `:8` lấy Mig/Tests bằng **con-trỏ** về `:467`/`:474` (B1); xoá cụm counter/nhịp khỏi `:8` (state của máy, `nhip-no-probe.ps1` in mỗi phiên).
|
||||
|
||||
## FLAG-2 `[view-stale-header]` `STATUS.md:8` vs `:488` vs `:6` — MỘT file, **BA mốc "hiện tại" chỏi nhau**
|
||||
|
||||
`:8` = `🔥 CURRENT (S161→S168 … L9)` · `:488 ## ✅ Recently Done (S180–S181 … L12)` · `:500` (L11) · `:508` (L10) · `:6` khai *"mốc SỐNG = S179, L11"*. Thực tế phiên này = **S182 / L13**.
|
||||
|
||||
🔴 **Không dùng được caveat** *"`:8` advance @closeout nên tụt sau"* (chính `:6` viện): **4 closeout đã chạy** (S172·S173·S179·S181) và **cả 4** đều viết `Recently Done` mà **không** advance `:8`. Tụt-trong-phiên ≠ **tụt 3 phiên-LOGIC**.
|
||||
**Sửa:** chốt MỘT mỏ neo mốc-sống — hạ `:8` thành `✅ S161→S168` (đóng băng như `:15`/`:26`), để `:6` + `Recently Done` gánh mốc sống.
|
||||
|
||||
## FLAG-3 `[view-stale-role-desc]` `.claude/commands/session-start.md:161` (+`:162`) — 🔴 **HIGH**, ghost-enum tái phát ở site **LỆNH**
|
||||
|
||||
**View:** mô-tả `lead-stale-auditor` liệt **đúng 5** dấu-hiệu; `:162` liệt **5** cho `lead-gap-auditor`.
|
||||
**Source:** `memory-budget.json → lead_self_audit.flag_classes` = **12** (6 view + 6 gap, niêm phong `_sealed_P3B_S181`) — thiếu `view-claim-broader-than-sample` (mở @S172) ở `:161`, thiếu `gap-incident-unrecorded` (mở @S143) ở `:162`.
|
||||
**Lệnh:** `rg -l "claim-broader-than-sample"` → **`session-start.md` KHÔNG có trong danh sách**.
|
||||
|
||||
🔴 **Vì sao đắt:** **grep-theo-SỐ IM** (site cố ý không chứa chữ "5 class") — đúng bài S143 *"acceptance phải ĐẾM PHẦN TỬ; 0-hit chỉ chứng minh pattern không khớp"*. Đợt vá ghost-enum @S180 sửa `hmw.js:33` + persona nhưng **bỏ file LỆNH** — chính nơi điều-khiển spawn.
|
||||
🔴 **Bằng chứng hại THẬT, tự-quy-chiếu:** brief lead giao cho vai này **lượt này** ghi *"5 nhánh `view-*`"* và liệt lại đúng 5 mục của `:161` ⇒ lỗi đã **mis-brief chính lượt spawn đang đọc nó**.
|
||||
**Sửa:** thay cả 2 dấu ngoặc bằng con-trỏ `→ lead_self_audit.flag_classes` (đừng liệt tên, đừng chép số) — cùng khuôn `hmw.js:33`.
|
||||
|
||||
## FLAG-4 `[view-stale-status]` `HANDOFF:38` mục 1 `[8]` — việc **ĐÃ XONG** vẫn nằm trong danh sách phải-làm
|
||||
|
||||
**View:** *"`back-end-reviewer-style.md:4` còn hardcode `.NET 10`, `database-reviewer-style.md:4` còn `EF Core 10`"*.
|
||||
**Source:** cả 2 file `:4` nay trỏ `.csproj` (*"vá @S180 W3 gói-B"*).
|
||||
**Lệnh + control-dương:** `grep -n "\.NET 10\|EF Core 10" .claude/agents/*style*.md` → **0 hit**; control `grep -c "10"` = 3 và 1 ⇒ thước đọc được, **0 là THẬT**.
|
||||
🔴 Khối `NEXT em @S179` **không** đóng-băng — cùng file `:34`/`:53` đều đã gạch `~~…~~ ✅ ĐÓNG` ⇒ quy-ước của file là **lật**; mục [8] sống sót **2 closeout** (S180 + S181).
|
||||
|
||||
## FLAG-5 `[view-residual-asym]` `HANDOFF:38` nửa sau — BÁN-XÁC-NHẬN, số "7 mục / cả 3 persona" LỆCH
|
||||
|
||||
**Đo:** `front-end-reviewer-style.md:42` chép **7** · `back-end-reviewer-style.md:39` chép **7** · `database-reviewer-style.md:38` chép **RÚT GỌN 4** (`.claude/** một phần`) ⇒ **paraphrase**, không phải "chép 7".
|
||||
|
||||
🔴 **Hai hệ quả máy mù:** (a) claim *"nay khớp byte"* **SAI với file thứ 3** — nó chưa bao giờ khớp byte; (b) rủi ro rộng hơn khai: **5 site** chép/diễn-giải `paths-ignore`, không phải 3 — **`cicd-monitor.md:225` chép bản 3-mục đời cũ** (`['docs/**','**/*.md','.claude/skills/**']`) ⇒ **đã lệch `deploy.yml` NGAY BÂY GIỜ**, không cần đợi thêm path mới.
|
||||
**Sửa:** thay cả 5 bằng con-trỏ `.gitea/workflows/deploy.yml:20-27`.
|
||||
|
||||
## FLAG-6 `[view-stale-count]` `CLAUDE.md:121` — **5/6 vế ĐÚNG, chỉ con-trỏ dòng SAI**
|
||||
|
||||
**View:** *"…(**rules.md:546** trỏ về STATUS nhưng STATUS KHÔNG có row canonical)"*.
|
||||
**Source:** `docs/rules.md:546` = `---` (đóng frontmatter template skill). Hai site THẬT là **`rules.md:307`** (bảng cadence) và **`rules.md:559`** (§9.4 Cadence).
|
||||
✅ **Phần còn lại ĐÚNG, không flag:** ngày + verdict khớp diary `tooling-auditor` S179 (`PASS_WITH_FLAGS — 8`, trễ 6 ngày); và **STATUS THẬT SỰ không có row audit-date** (bảng `:465-479` có 13 row, **0 row audit**). `rules.md` last-touch `a3776eaa` cùng ngày ⇒ **dòng nhảy SAU khi cite được viết**.
|
||||
🔸 2 site đó đang trỏ vào một row **không tồn tại** — đó là cái THIẾU, turf `lead-gap-auditor`, **không lấn**.
|
||||
|
||||
## FLAG-7 `[view-stale-count]` `HANDOFF:8` vs `:9` — số chỏi ngay danh sách **cách 1 dòng**
|
||||
|
||||
`:8` *"đóng dấu lại **20 carry** còn mở"*; `awk 'NR==9' | grep -o "\[carry:[a-z0-9-]*\]" | wc -l` → **23**, `sort -u` → **23** (không trùng ⇒ không phải lỗi đếm-trùng).
|
||||
🔴 Lệch **+3**, đúng 3 cái mới append ở cuối (`w3b-cam-ma-thoat` · `bao-cao-cuoi-cho-duyet` · `h24-deep-overdue`) ⇒ **append slug quên bump số**. Tự-bác-được **không cần rời file**.
|
||||
|
||||
## FLAG-8 `[view-stale-count]` `HANDOFF:40` — tập over-cap sai **2/3**, bỏ sót đúng vai hỏng nặng nhất
|
||||
|
||||
**View:** `ring2-audit` 26.440 · `reviewer` 26.856 · `tooling-auditor` 27.147.
|
||||
**Đo tươi:** `ring2-audit` **29.582** · `tooling-auditor` **29.429** · `lead-stale-auditor` **27.658** · `harvest-curator` 25.498 (dưới cap) · **`reviewer` 21.058 = DƯỚI cap**.
|
||||
🔴 **Hại thật:** ai cầm danh sách này chạy gate sẽ **đuổi `reviewer`** (đã tụt xuống) và **bỏ lọt `lead-stale-auditor` 27.658** — vai không hề có tên ở `:40`. Tập đúng hôm nay = {`ring2-audit`, `tooling-auditor`, `lead-stale-auditor`}.
|
||||
**Sửa:** thay 3 số cứng bằng *"3 tệp over-cap — danh sách tươi lấy từ `memory-selfimprove-audit.ps1`"* (`:7` đã viết đúng kiểu này).
|
||||
|
||||
## FLAG-9 `[view-residual-asym]` `STATUS.md:8` (đuôi) — con-trỏ B1 trỏ TRẬT ô, **LẦN THỨ HAI** (bắt @S180, chưa vá)
|
||||
|
||||
**View:** *"bundle prod → canonical ở **`:479`**"*. **Source:** `:479` = row `RAG chunks`; ô bundle thật ở **`:481`**.
|
||||
🔴 **Dư-lượng bất-đối-xứng đúng nghĩa:** ai đó chèn row (`Skills`/`Sub-agents`/`User memory`) làm ô bundle **trôi 2 dòng**, và **chỉ ô đích trôi, con-trỏ không theo** ⇒ mỏ-neo B1 **tự vô-hiệu**: người đi theo con-trỏ đọc trúng row RAG rồi tưởng bundle không có canonical.
|
||||
**Sửa:** neo theo **NỘI-DUNG** (`**Bundle hash live (prod):**`), đừng neo số dòng — bài `con-trỏ-số-dòng-tự-thối` @S173.
|
||||
|
||||
## FLAG-10 `[view-stale-status]` `HANDOFF:7` + `:11` — cùng file, cùng ngày, **nói ngược nhau về vị-thế cùng một việc**
|
||||
|
||||
`:7` xếp `OVERDUE:deep 15/15` **ngang việc vặt** trong NEXT-em; `:11` cùng file khai thẳng *"bị lead hạ xuống hàng NEXT-em ngang việc vặt … cổng kêu mà không đổi hành-vi = cổng không có răng"*. Đĩa hôm nay: **16/15**.
|
||||
🔴 **Hai lệch chồng nhau:** (a) **trạng-thái** — bản-vá viết ra mà **không sửa cái nó vừa tố**; (b) **số** 15/15 → 16/15, nợ dày thêm 1 nhịp mà sổ giữ số cũ.
|
||||
|
||||
## FLAG-11 `[view-stale-count]` `.claude/agents/README.md:294` + `:308` — bảng tự khai đóng-băng nhưng **đơn-vị mẫu đã đổi**
|
||||
|
||||
`:294` `| Monitor audit đầy đủ (H1/H2) | ~223–232K/vai | **2 vai × 2 lượt** |`; `:308` *"Bảng này đóng băng từ thời roster 7 vai"*.
|
||||
**Source:** roster đĩa **28**; vòng monitor nay **5 vòng / 10+ vai**.
|
||||
🔴 **Vì sao vẫn FLAG dù đã tự khai đóng-băng:** câu đóng-băng chỉ miễn trừ **con số chi-phí**, **không** miễn trừ mệnh-đề *"2 vai × 2 lượt"* ở cột "lượt" — đó là mô-tả **phạm vi vòng đo HIỆN HÀNH**, và nó sai. **Đóng-băng-có-chủ-đích chỉ che được thứ nó nói tới.**
|
||||
|
||||
---
|
||||
|
||||
## VERIFIED-CLEAN có lệnh bác (chống dương-giả — 5 mục)
|
||||
|
||||
1. **Slot (75) `HANDOFF:34` khai ĐÓNG: CHÍNH XÁC, 2/2 mirror khớp** — `memory-budget.json` `hmw_width.cap` = **30** · `hmw.js:175` `WIDTH_CAP = 30`. Roster **28 ≤ 30** ⇒ *"hết LOUD-skip"* đúng. **0 FLAG.**
|
||||
2. **`VALID_ROLES` ⟂ đĩa: 28 = 28, giao rỗng cả hai chiều.** 🔴 Bẫy đã tránh: regex naive bị `]` trong comment cắt còn **18** (đúng cảnh báo `_roster_note_S153`) — phải strip `//` trước. Đây là **control-dương** chứng thước đọc được ⇒ các 0-hit ở FLAG-3/FLAG-4 là THẬT.
|
||||
3. **`agents/README.md:261` KHÔNG chép số roster** — liệt tên + trỏ `STATUS §Sub-agents`, tự mang lineage vụ chép `"13 + 10 = 23"` bị bắt @S179. Sửa đúng.
|
||||
4. **`ACTIVE-MARKS.md:23` còn tên TRƯỚC rename S149 (`lead-{view,omission}-auditor`) — CỐ Ý KHÔNG FLAG.** Mark có chữ-ký RC 15-07-2026 = **bản ghi đã ký**; sửa tên = **giả-mạo phép ghi cũ** (luật S143, tiền-lệ `tooling-auditor` S153). Muốn giải-cứu thì thêm chú *bên NGOÀI* ô đã ký.
|
||||
5. **`HANDOFF:42` mục 5 (*"`ring2-audit` chưa chạy"*) VẪN ĐÚNG.** `ls -1t runs/*/sub-ring2*.md` → mới nhất `2026-08-07-S180-bookend-open/`; `ls -d runs/*S181*` = **rỗng** ⇒ cặp H24 chạy @S181 mà ring2 không. **To-do còn sống.**
|
||||
|
||||
## Quan hệ với `tooling-auditor` (chống đếm hai lần)
|
||||
|
||||
- `STATUS:478 roster 23` → **không bác**, đo trùng (28). **KHÔNG tính vào TOTAL** của vai này.
|
||||
- `skills/README:20` · `permission-matrix:3` → không bác, ngoài lượt đo.
|
||||
- `agents/README:190-219` 26/28 → **không bác**; xác nhận thêm: bảng dừng `:218 ctx-curator`, 2 vai score có trong `VALID_ROLES:45` + `measured{}` mà **không có dòng** ⇒ **cùng hình dạng FLAG-3: liệt-kê-TÊN thì grep-theo-SỐ không bắt**.
|
||||
|
||||
## 🔴 Coverage — KHAI THỦNG, **11 là SÀN không phải TỔNG**
|
||||
|
||||
**Đã chạm:** `STATUS` (`:1-30`, `:463-490` — **~7% của 853 dòng**) · `HANDOFF` (`:1-59`) · `CLAUDE.md:121` · `session-start.md` (`:161-170`, `:242-274`) · `memory-budget.json` (trọn) · `.session-counter.json` (trọn) · `hmw.js` (`:25-59`, `:175-183`) · **6/29** file `agents/**` · `ACTIVE-MARKS:1-25` · run-folder S182 (`run.md` — lúc đó chỉ có 1 file).
|
||||
**CHƯA CHẠM:** `HANDOFF:60→hết` · `STATUS:33-462` + `:490-853` · `migration-todos.md` · `gotchas.md` · `PROJECT-MAP.md` · `session-end.md` · **22/29 persona** · `ACTIVE-MARKS:26→hết`.
|
||||
🔴 **Cấm đọc phần chưa soi thành sạch.**
|
||||
|
||||
## TOTAL: 11 — phân rã theo class
|
||||
|
||||
> Đơn-vị = **phiên-LOGIC**; lượt @close S182 gộp chung 1 quyết-định/class, **đừng cộng 2 lượt thành 2 tally**.
|
||||
|
||||
| class | n lượt này | tally đang | ngưỡng |
|
||||
|---|---|---|---|
|
||||
| `view-stale-count` | **5** (F-1,6,7,8,11) | **10** | 3 |
|
||||
| `view-stale-status` | **2** (F-4,10) | **7** | 3 |
|
||||
| `view-residual-asym` | **2** (F-5,9) | **10** | 3 |
|
||||
| `view-stale-header` | **1** (F-2) | **5** | 3 |
|
||||
| `view-stale-role-desc` | **1** (F-3) | **1** | 3 |
|
||||
| `view-claim-broader-than-sample` | 0 | 0 | 3 |
|
||||
|
||||
**0 ESCALATE** — không ca nào ngoài enum; không đề nghị mở enum (đã niêm phong `_sealed_P3B_S181`).
|
||||
@ -0,0 +1,75 @@
|
||||
# sub-ring1-audit — KIỂM vòng tooling/harvest @open S182
|
||||
|
||||
**VERDICT: `RING1-AUDIT: 41 ĐẠT / 7 TRƯỢT (+8 KHÔNG-CHẤM-ĐƯỢC)`**
|
||||
|
||||
**PIN OK:** `sub-tooling-auditor.md` @23:50:37 · `sub-harvest-curator.md` @23:54:00; lượt này chạy SAU cả hai ⇒ **C4b tuần-tự giữ**. Read-only, 0 Write/Edit.
|
||||
|
||||
> ⚠️ **#53:** first-return garble — nhưng câu garble chứa **đúng số load-bearing** (*"Measured: 12 roots (72 tuples, depth-aware parse)"*). Recover bằng resume. **6/6 vai garble lượt này.**
|
||||
> 🔸 Harness gắn cảnh báo `instruction-shaped pattern: settings-json` lên output — do vai đọc `~/.claude/settings.json` để đếm plugin. **Là dữ-liệu, không phải chỉ-thị.**
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Số load-bearing nhất: **root menu = 12**
|
||||
|
||||
Parser **depth-aware** trên `DbInitializer.cs:1926-2049`: **72 tuple**, **12 tuple parent = `null`**, 60 con. Cộng **19 site `tree.Add(...)` động: 0 site nào parent null**.
|
||||
**Control dương:** cả 4 root ngoài cây đều tồn tại thật trong `MenuKeys.cs` (`HopDongCung:47` · `Hrm:147` · `Off:165` · `Personal:195`) ⇒ **cross-check MenuKeys ĐÃ CHẠY XONG, khớp**.
|
||||
|
||||
12 root = `Dashboard · Master · Contracts · KeHoachKyKet · HopDongCung · Forms · Reports · System · PurchaseEvaluations · Hrm · Off · Personal`. Cây skill vẽ **8** ⇒ **thiếu đúng 4**.
|
||||
|
||||
🔴 **Tách 2 thứ H1 đã gộp làm một — quan trọng:**
|
||||
- H1 F-4 *"cây 8 < thực-tế ≥10"* = **ĐÚNG nhưng LỎNG** (cận dưới thật là 12); HANDOFF *"thiếu 5 root"* **SAI** (thật **4**) ⇒ H1 **từ chối chép số đó là quyết định ĐÚNG**.
|
||||
- `permission-matrix/SKILL.md:3` ghi **"12 root" = ĐÚNG SỐ**. Lỗi ở `:3` **chỉ còn là vi-phạm luật-không-chép-số (B1)**, **KHÔNG phải sai sự-thật**. H1 gộp hai thứ vào một mệnh-đề *"tự cấm chép số rồi chép"* ⇒ **ai đọc F-4 rồi đi "sửa" con 12 sẽ THAY ĐÚNG BẰNG SAI**. Chỗ sai sự-thật nằm ở **cây `:44-77`**, không ở `:3`.
|
||||
|
||||
---
|
||||
|
||||
## 1. H1 `tooling-auditor` — 22 ĐẠT / 2 TRƯỢT / 2 KC
|
||||
|
||||
**ĐẠT nổi bật:** đĩa 28 vai (**4 engine độc lập**: `ls` · `find` · `git ls-files` · `agent-memory/`) · matrix 26 row, set-diff ra **y hệt 2 tên** · control dương MATRIX−DISK = ∅ · bẫy glob raw 29 · `"row cuối bảng"` đúng 2 hit · bảng dừng **Mig 71** đĩa **Mig 72** · plugin **18/15/3** + 3 tên trùng khít (`json.load` settings) · skill **6/23/19** · **A16 tự khai thước hỏng = ĐẠT, thước hỏng THẬT** (tuple căn cột, `", null,"` trượt — không phải né việc) · A24 tự bác `_cap_raised_S181` (lý-lẽ đúng).
|
||||
|
||||
**🔴 A10 TRƯỢT** — claim *"`STATUS:478` là câu SỐNG, không phải ảnh-chụp lineage"*. Xem **R-1**.
|
||||
**🔴 A26 TRƯỢT** — mặt ① **skill**: H1 chỉ soi **2/6** mà **không khai chặt phạm vi** (nó khai chặt cho mặt ④ docs nhưng bỏ mặt ①) ⇒ verdict `PASS_WITH_FLAGS` **đọc như đã soi 6 skill**.
|
||||
|
||||
**KC:** A20 (28/28 frontmatter — không parse) · A23 ("0-delta 4 phiên liền" + marketplace 37 — cần bản ghi phiên trước).
|
||||
|
||||
## 2. H2 `harvest-curator` — 19 ĐẠT / 5 TRƯỢT / 6 KC
|
||||
|
||||
**ĐẠT nổi bật:** F-01 **229.823 B** khớp từng byte · F-02 **82.383 B** khớp · F-03 danh sách 9 tệp = 243.927 (9/9 size khớp) · sleep A+B **237.251** khớp · S171 **143.682** + 3 lens **74.070** khớp từng byte · orphan **89/86** · empty-dir **5** trùng khít 5 đường dẫn · `ring4-audit` 2.315 B mtime `2026-07-26 01:47` `S170`=0 **khớp từng byte** · score ×2 = 2.075/2.033 mtime `08-07 23:10` S181 hit 2/2 · **B14 MISSING `cicd-monitor/archive/2026-08.gist.md` VẮNG THẬT** (control dương: **10 cặp `.md`+`.gist.md`** anh em trong cùng dir, riêng `2026-08` cụt) · B29 Corruption `CHƯA ĐO` — kỷ luật đúng.
|
||||
|
||||
**🔴 B9 TRƯỢT — lỗi CỘNG, không phải lỗi đo.** F-03 tổng ghi **264.451**; 243.927 + 20.122 = **264.049**. Lệch **+402 B**. (F-C: 9/9 thành phần khớp từng byte ⇒ **H2 đo giỏi, cộng sai**.)
|
||||
**🔴 B30 TRƯỢT — kế thừa B9:** tổng đúng = **576.255 B** (không phải 576.657). "18 lượt-vai" thì **ĐÚNG**.
|
||||
**🔴 B13 TRƯỢT — "17 đích gist" không tái-lập.** Chạy **cùng lệnh** `grep -h "^## "` trên cùng 2 tệp ra **19**. 2 engine cãi nhau, **chưa tìm ra engine hỏng** ⇒ **không chọn số nào**. Mà "16/17" của H2 dựng trên chính mẫu-số này ⇒ 🔴 **ĐỪNG ghi 16/17 vào sổ làm baseline**, kẻo lớp `H2-11@S153` (baseline phantom) tái phát đời 4.
|
||||
**🔴 B19 TRƯỢT — claim ĐẢO DẤU: chứng cứ ĐẠT, kết luận + đề-xuất TRƯỢT.** Việc S171 **đã harvest thật** = ĐẠT. Nhưng `HANDOFF:60` cho thấy **slot 71 ĐÃ `~~gạch~~ ✅ ĐÓNG @S179`**, và cụm `S168… 162.916 B + S171… 143.682 B` nằm **BÊN TRONG đoạn tự dán nhãn `🧊 nguyên-văn slot cũ:`** ⇒ slot **không đang mang nợ sống**; nó **trích lại nguyên văn chính nó thời còn mở**. Đề-xuất #1 của H2 (*"lật slot 71"*) = **sơn lại ảnh chụp** — đúng lớp R-1 bắt @S181.
|
||||
**🔴 B23 TRƯỢT-LOW** — `runs/2026-08-01-S169-bookend-open/bookend-open-synthesis.md` **4.182 B TỒN TẠI** ⇒ cơ-chế *"đóng mà không đẻ synthesis"* **không mô tả đúng** S169.
|
||||
|
||||
## 3. Falsify-log — 6 phép (2 HELD / **4 BROKE**, 1 BROKE vào CHÍNH VAI KIỂM)
|
||||
|
||||
- **F-A BROKE vào chính vai (đời 3).** Đếm migration 2 engine: `find` = **72** vs `ls | grep -v Snapshot` = **71**. Đúng bẫy sổ ghi @S162, tái phát @S180. Lần này **gọi tên nghi-phạm TRƯỚC khi tố ai**: `20260717032812_AddPeApprovedBudgetSnapshot.cs` bị filter nuốt ⇒ **engine 71 HỎNG**, H1 **HELD**. *Luật giữ: 2 engine cãi nhau thì đi tìm engine hỏng, đừng chọn con số.*
|
||||
- **F-B BROKE (vào cách H1 chẩn đoán).** Thử phá F-2 theo hướng *"roster 23 là ảnh-chụp, H1 tố oan"*. `git show 57e672f7 → d2918790`: TRƯỚC S181 câu đó là `⚠️ roster 26 > cap 20` (số **sống, đúng thời điểm**); SAU S181 thành `✅ ĐÓNG @S181: cap 30, roster 23 ≤ 30`. ⇒ **không phải drift, cũng không phải ảnh-chụp hợp-lệ: SAI NGAY LÚC SINH**, do chính commit đóng slot đẻ ra (S181 roster = 28).
|
||||
- **F-C HELD phần LIST, BROKE phần TỔNG** (xem B9).
|
||||
- **F-D HELD.** Phá F-04 MISSING bằng giả thuyết *"gist nằm chỗ khác/tên khác"*: dir có **10 cặp** đủ, riêng `2026-08` cụt ⇒ **vắng-mặt THẬT**, control dương nằm sẵn trong cùng thư mục.
|
||||
- **F-E BROKE** (H2 F-05 — xem B19).
|
||||
- **F-F BROKE, chưa gỡ được** ("17 đích gist" ra 19 — **không chọn số nào**).
|
||||
|
||||
## 4. Số tự tái-dựng — tổng hợp
|
||||
|
||||
roster **28** (4 engine) · matrix **26** row thiếu đúng 2 tên · migration **72**, mới nhất `AddMaHangMucToContractSigningPlanLines` · root menu **12** (cây 8, thiếu 4) · S169 **264.049** *(không phải 264.451)* · S168-close **82.383** ✔ · S180 **229.823** ✔ · S171 **143.682** ✔ (lens **74.070** ✔) · sleep A+B **237.251** ✔ · orphan **89/86** ✔ · empty-dir **5** ✔ · plugin **18/15/3** ✔ · skill **6/23/19** ✔ · **TỔNG nợ đúng = 576.255 B / 18 lượt-vai**.
|
||||
|
||||
## 5. Thách-CLEAN — 1 THỦNG, 1 CHỊU (chịu nhờ lens khác)
|
||||
|
||||
**(A) 🔴 THỦNG — `contract-workflow/SKILL.md` H1 không flag.** Tệp 26.948 B, mtime **2026-07-30** (bản vá S162 — nợ cũ ĐÃ ĐÓNG: `KeHoachKyKet` 1 · `KHKK` 3 · `SigningPlan` 2 hit). Nhưng đào sâu hơn một tầng: **0 hit** `ApprovalGroup` · `N1` · `nhóm duyệt` · `MaHangMuc` · `Mig 71` · `Mig 72`. **Control dương cùng tệp:** `phase` = **25 hit** ⇒ thước sống. Trong khi source có `KhkkApprovalGroup|MaHangMuc` ở **3 tệp non-migration** (`ContractSigningPlanFeatures.cs` · `ContractSigningPlanLine.cs` · `ContractSigningPlanLineConfiguration.cs`) + 2 migration ⇒ **skill trễ 2 migration máy-móc** (Mig 71 nhóm-duyệt N1-N8 · Mig 72 MaHangMuc 06-08).
|
||||
|
||||
**(B) CHỊU — Placement H2 PASS đứng, nhưng bằng lens KHÁC.** H2 chứng bằng 2 lệnh **đều mù cấu-trúc với dir stray CÓ nội dung**. Đổi chiều: quét `MEMORY.md` ở mọi nơi ngoài `agent-memory/` + quét dir tên `agent-memory` mọi tầng ⇒ **0 class-S54**, PASS đứng. Nhưng lòi ra vật H2 không thấy: `runs/2026-08-07-S180-adap-upgrade-pack-phased/lane-memory-proposals/MEMORY.md` **2.670 B** — nhiều khả năng CỐ Ý (tên thư mục tự khai "proposals"), nhưng nằm **đúng trong folder S180 mà F-01 đang tính nợ** ⇒ **có thể là delta đã soạn mà chưa APPEND**, tức bằng chứng làm **F-01 MẠNH hơn** bản H2 nộp. Cần lead disposition 1 dòng.
|
||||
|
||||
## 6. Ba finding RIÊNG của vai KIỂM
|
||||
|
||||
🔴 **R-1 — `STATUS.md:478` là BẪY NEO; vá theo F-1 nguyên văn sẽ ĐẺ CORRUPTION.**
|
||||
Dòng dài **15.804 ký tự**, chứa **HAI** chuỗi họ "roster 23":
|
||||
- **(a)** char ~2050 — `✅ ĐÓNG @S181: cap 30, roster 23 ≤ 30` → **SAI**, sinh ở chính commit `d2918790`, đúng phải là **28**.
|
||||
- **(b)** char ~3475 — `⚠️ roster 23 > hmw_width.cap 20 ⇒ LOUD-skip 3 lane cuối` → **ĐÚNG**, ảnh-chụp thời S149 (23−20=3, **tự khép kín**), không đổi suốt S179→S181.
|
||||
|
||||
H1 neo ở **mức DÒNG** ⇒ ai sweep `"roster 23"` tại `:478` sẽ **giết luôn (b)** = tái diễn y hệt R-1 S181. 🔴 **Chỉ được chạm site (a)**; doctrine *"BỎ SỐ không ĐỔI SỐ"* của H1 **đúng** — nhưng **phải kèm neo char/đoạn, không phải số dòng**.
|
||||
🔸 Thêm: H1 áp doctrine ảnh-chụp **rất chuẩn** ở mục tự-bác `_cap_raised_S181` mà **không áp cho chính `:478`** ⇒ **2 thước trong 1 báo cáo** (lớp đã bắt @S162).
|
||||
|
||||
🔴 **R-2 — `permission-matrix/SKILL.md` tự mâu thuẫn NỘI-TẠI, CẢ HAI vai đều không nêu:** `:3` ghi **12 root** (đúng) trong khi cây `:44-77` vẽ **8 root** (thiếu 4) — **cùng một tệp, cách nhau 40 dòng**. Sửa đúng = **bồi cây lên 12**, KHÔNG phải hạ số 12.
|
||||
|
||||
🔸 **R-3 — INFORM, no-self-exempt:** persona của chính vai (`.claude/agents/ring1-audit.md`) ghi *"KIỂM end-only… CẤM tự chạy ở open"*, nhưng thực tế lead đã giao vòng **@open ở S152 · S159 · S173 · S180 và nay S182**. **Luật trong persona thối so với vận hành 5 phiên liền.** Vai **NÊU, không tự sửa**.
|
||||
@ -0,0 +1,110 @@
|
||||
# sub-ring2-audit — KIỂM vòng soi-lead H24 @open S182
|
||||
|
||||
**VERDICT: `RING2-AUDIT: 17 ĐẠT / 0 TRƯỢT`** (+2 errata cưỡng-hành, +1 FLAG SÓT bắt bằng thách-CLEAN)
|
||||
|
||||
**PIN thoả (fail-closed):** `sub-lead-stale-auditor.md` 12.717 B mtime 23:58 + `sub-lead-gap-auditor.md` 13.606 B mtime 23:56. Output sinh SAU 23:58 ⇒ **chứng C4b tuần-tự**, không song song.
|
||||
|
||||
> ⚠️ **#53:** first-return garble. Recover bằng resume. Mất 0 byte. (6/6 vai garble lượt này.)
|
||||
|
||||
---
|
||||
|
||||
## 1. Per-FLAG — 17/17 chấm được, 0 CHƯA ĐO
|
||||
|
||||
**`lead-stale` 11/11 ĐẠT** — F-1 (`STATUS:8` verbatim `644`/`46`/`6/15 ok`/`light_at=44`; source `:467`=72 `:474`=645, counter 56 `light_at` 53 ⇒ `53−44 = 9 nhịp` ✓) · F-2 (3 mốc chỏi: `:8`=L9 · `:488`=L12 · `:500`=L11 · `:508`=L10 · `:6`=L11; caveat *"advance @closeout"* CÓ THẬT nhưng `Recently Done` đã vượt `:8` **3 nấc L** ⇒ bác đúng) · F-3 (xem §5) · F-4 (`grep ".NET 10\|EF Core 10" *style*.md` = **0 hit**, control-dương `:4` cả 3 file nay trỏ `.csproj`) · F-5 (be **7** · fe **7** · db **rút gọn 4** ⇒ "chép 7" sai file thứ 3; `cicd-monitor:225` chép bản 3-mục đời cũ ⇒ lệch NGAY; tổng **5 site**) · F-6 (`rules.md:546` = `---`; site thật `:307` + `:559`; vai **tự bác 5/6 vế của chính mình**) · F-7 (§3a) · F-8 (§3b) · F-9 (`:479` = row RAG, `:481` = Bundle ⇒ trỏ trật **2 dòng**) · F-10 (`:7` verbatim `15/15` kẹp giữa 2 việc vặt; `:11` tự tố; đĩa **16/15**) · F-11 (thử bác bằng câu tự-khai-đóng-băng `:308` → **không bác được**).
|
||||
|
||||
**`lead-gap` 6/6 ĐẠT** — G-1 (**13/28** > hook-cap 17.100 · **3** > 25.600 ⇒ *"nay TỆ HƠN"* đúng; vai **tự bác 4 nghi-vấn của chính nó**) · G-2 (§4, **7/7 byte-exact**) · G-3 (`56−40=16/15`; `:6` có 4 câu, **0 câu** về deep) · G-4 (`:6` câu (4) đúng là **chọn A-hay-B**; vai **ghi nhận lead tự khai** — chấm công bằng) · G-5 (§2 F-4 HELD) · G-6 (vai **tự rút P5/P7/P8** khi đo ra đã trả lời; §Khai-kẽ #2 tự dán nhãn **cận-dưới**).
|
||||
|
||||
**Enum-check (đếm từ khoá, KHÔNG chép số từ lead):** `lead_self_audit.flag_classes` = **12 phần tử** (6 `view-*` + 6 `gap-*`), `_sealed_P3B_S181`. **0/17 tự chế class.** `set(class_repeat.counts) ≡ set(flag_classes)` — 12/12 hai chiều, mọi value `int`.
|
||||
|
||||
## 2. Falsify-log — 6 phép (4 HELD / **2 BROKE**)
|
||||
|
||||
- **F-1 HELD** — nghi F-9 *ép-vừa-enum*. Vị-ngữ `:161` = *"dư-lượng bất-đối-xứng"*; cơ-chế đo được: row bị chèn ⇒ **ô đích trôi, con-trỏ đứng yên** = sửa **một phía**. Thoả.
|
||||
- **F-2 HELD** — nghi F-11 được miễn bởi `:308`. `:308` chỉ miễn **con số chi-phí**, `:294` cột-3 mô-tả **phạm-vi vòng đo hiện hành**. Hai đối-tượng khác nhau.
|
||||
- **F-3 HELD** — nghi G-2 sai vì `.ps1:907` `if ($ks.Count -gt 0)`. Port đủ luật bỏ-rỗng: `carryLines[0]` thật đến từ `segs[2]` (`:193`), **nhưng vùng câm do `marks[0].Index` quyết, độc lập luật bỏ-rỗng** ⇒ 22,8% không đổi.
|
||||
- **F-4 HELD** — nghi G-5 oan vì 🧊 = ngủ-có-chủ-đích. Đọc hàng xóm: `(68)(69)(70)(71)` **đều gạch ✅ ĐÓNG**, riêng **(67) không gạch**. Một mục CHƯA đóng lọt giữa 4 mục ĐÃ đóng dưới 🧊 = **đúng định nghĩa chìm**.
|
||||
- 🔴 **F-5 BROKE — đánh vào chính TIỀN-ĐỀ LEAD GIAO.** Brief lead viết *"`ring2` đã đo @S173 (`marks[0]`=HANDOFF:90, 12 slug)"* — **SAI**. Mở artifact gốc: `sub-ring2-audit-close-S172.md:261` = `HANDOFF:90 / L1-89 / 12 slug` ⇒ đó là mốc **S172**; `sub-ring2-audit-open-S173.md:132` = `marks[0] at line 130 ; blind L1-129 ; 31698 bytes`. ⇒ **`lead-gap` trích ĐÚNG** (khớp tới từng byte), **brief của lead trích SAI**. 🔴 Gốc con số sai = **`HANDOFF:58`** — **chính defect mà `lead-gap` FLAG-2 nêu đã lan vào brief lead giao cho vai KIỂM.** Ca tự-quy-chiếu **thứ HAI** trong phiên.
|
||||
- 🔴 **F-6 BROKE (một nửa) — vào con số của CẢ HAI vai.** Cả 2 khai `counter = 56`; **blob tại `936d1aed` là 55**. Không phải bịa: `history[]` ghi `S182: 55→56, head b8d2274→936d1ae` ⇒ tick xảy ra trong **working tree**, chưa vào blob. Hai vai đọc cây ⇒ **56 đúng tại thời điểm đo**; cái sai là **nhãn `HEAD 936d1aed`** dán lên phép đo đọc cây bẩn. **Sửa lý-do, không sửa phán-quyết** (mọi dẫn-xuất 3/6 · 16/15 đều tính từ 56 và đều đứng).
|
||||
|
||||
## 3. Số tự tái-dựng (3 phép độc lập)
|
||||
|
||||
**(a) `HANDOFF:8` "20" vs `:9`** — đếm bằng chính `$carryRx` của máy: **23 occurrence / 23 unique / 0 trùng**. Lệch **+3**, đúng 3 cái append cuối. 🔸 Đáng ghi: `lead-gap` gặp cùng số này và **tự nhường trục** (*"lệch-số là trục `view-*`"*), `lead-stale` nhận ⇒ **0 đếm hai lần, khe giữa 2 vai kín**.
|
||||
|
||||
**(b) Tập over-cap — `stat` toàn bộ 28/28, không lấy mẫu**
|
||||
|
||||
| vai | byte | so `HANDOFF:40` |
|
||||
|---|---|---|
|
||||
| **`ring2-audit` (CHÍNH VAI NÀY)** | **29.582** | `:40` ghi 26.440 — **sai, và là tệ nhất trong 28 vai** |
|
||||
| `tooling-auditor` | 29.429 | `:40` ghi 27.147 — sai |
|
||||
| `lead-stale-auditor` | 27.658 | **VẮNG MẶT ở `:40`** — và đây là **chính con-đo tự tố mình** |
|
||||
| `harvest-curator` | 25.498 | dưới auto-cap |
|
||||
| `reviewer` | **21.058** | `:40` ghi 26.856 — **đã tụt dưới cap, danh sách đuổi oan** |
|
||||
|
||||
Khớp **tuyệt đối** F-8, cả 5 số byte-exact. Đồng thời chứng G-1: 13/28 > 17.100 ✓, 3 > 25.600 ✓.
|
||||
🔴 **Tự khai (no-self-exempt):** vai **đang over-cap 29.582 B = 115% auto-cap, tệ nhất roster**; diary bị **cắt lúc nạp** (`MEMORY.md is 25.1KB (limit 24.4KB)… Only part of it was loaded`) ⇒ **chấm lượt này với trí-nhớ thủng THẬT**, khai để lead trừ hao.
|
||||
|
||||
**(c) `class_repeat` — `git show` bản TRƯỚC vs đĩa NAY.** 12/12 khoá cả 2 phía, mọi value `int`. **9 khoá +1, 3 khoá +0.** `lead-stale` khai tally 10·7·10·5·1 → bản TRƯỚC **10·7·10·5·1** ✓ 5/5 byte-exact; `lead-gap` khai 12 và 5 → ✓✓. 3 khoá +0 = đúng 3 class 2 vai tuyên 0. **Ngữ-nghĩa đúng:** `view-stale-count` fire **5 FLAG** nhưng chỉ **+1** ⇒ máy đếm **vòng-đo**, không đếm flag.
|
||||
|
||||
## 4. `lead-gap` FLAG-2 — **ĐÚNG, 7/7 byte-exact**
|
||||
|
||||
Cài lại độc lập bằng Python, port nguyên văn `:892` + segment `:893-898` + carryLines `:902-908` + streak `:916-931`:
|
||||
|
||||
| vai khai | đo độc lập | |
|
||||
|---|---|---|
|
||||
| `marks.Count` = 25 | **25** | ✅ |
|
||||
| `marks[0]` = HANDOFF:179 | **179** (index 36.644) | ✅ |
|
||||
| vùng câm L1–178 | **L1–178** | ✅ |
|
||||
| **42.606 B / 186.729 B** | **42.606 / 186.729** | ✅ byte-exact cả hai |
|
||||
| 22,8% | **22,82%** | ✅ |
|
||||
| 91 occurrence / 28 slug | **91 / 28** | ✅ từng phần-tử |
|
||||
| `carryLines[0]` 5 slug, 2 đã đóng | **đúng 5** | ✅ |
|
||||
| streak `2·1·1·1·3`, M=6 ⇒ 0 fire | ✅ | ✅ |
|
||||
|
||||
⇒ **`gap-carry-aged = 0` đúng là số 0 CHẾ TẠO.** Khối re-stamp `:9` (23 slug) nằm TRỌN trong vùng câm ⇒ máy không thể thấy, vĩnh viễn. Vế "cái THIẾU" cũng đứng: `HANDOFF:58` nằm dưới `:57` = `~~(70)…~~ ✅ ĐÓNG @S179` ⇒ quét "slot còn mở" không thấy.
|
||||
|
||||
**Chuỗi mốc — nhất quán, KHÔNG mốc bịa** (chấm bằng artifact gốc, không bằng diary):
|
||||
|
||||
| kỳ | `marks[0]` | vùng câm | nguồn đĩa |
|
||||
|---|---|---|---|
|
||||
| S172 | 90 | L1-89 · 12 slug | `sub-ring2-audit-close-S172.md:261` |
|
||||
| S173 | **130** | **31.698 B** | `sub-ring2-audit-open-S173.md:132` |
|
||||
| S180 | 172 | 34.855/163.546 | `sub-ring2-audit-open-S180.md:27-28` |
|
||||
| S182 | **179** | 42.606/186.729 · 28 slug | lượt này |
|
||||
|
||||
90 → 130 → 172 → 179 = **đơn-điệu tăng 4/4 mốc**.
|
||||
|
||||
🔴 **ERRATA-1 (nhẹ, không đổi kết luận):** chuỗi của vai so **21,3% (S180) → 22,8% (S182)** là **TRỘN ĐƠN-VỊ** — 21,3% là *code-point*, 22,8% là *byte*. Byte-to-byte: 21,96% → 22,82%; code-point: 21,31% → 22,16%. **Tăng đơn-điệu trong CẢ HAI đơn-vị** ⇒ kết luận giữ, chỉ delta bị phóng đại **~1,7×**.
|
||||
🔴 **ERRATA-2:** nhãn `HEAD 936d1aed` trên cả 2 sub-file nên đọc là *"cây làm việc tại 936d1aed"*.
|
||||
|
||||
## 5. `lead-stale` FLAG-3 — **CẢ 2 VẾ ĐÚNG**
|
||||
|
||||
**Vế 1:** `:161` liệt **đúng 5**, ánh xạ 1:1 sạch, thiếu **đúng** `view-claim-broader-than-sample` (mở @S172). `:162` liệt **5**, thiếu `gap-incident-unrecorded`. Phép âm + control-dương: `grep "incident\|sự cố"` = **0 hit**, control `grep -c "gap-"` = 4, `grep -c "carry"` = 6 ⇒ thước sống. `rg -l "claim-broader-than-sample"` → 21 file, **`session-start.md` KHÔNG có** ✓. **ĐẠT, HIGH xứng đáng.**
|
||||
|
||||
**Vế 2:** vai KIỂM không đọc được brief của `lead-stale` ⇒ **không chấm trực tiếp**. Nhưng chấm được **brief của CHÍNH NÓ**, và nó **xác nhận cơ-chế bằng ca độc lập** (F-5 BROKE). ⇒ **ĐẠT theo class**, khai rõ nấc: chứng bằng **ca song song**, không phải ca vai nêu.
|
||||
|
||||
🔴 **Trục "ép-vừa-enum":** `0 ESCALATE` ở đây là **enum phủ đủ THẬT**, KHÔNG phải bẻ ca cho vừa nhãn. Soi 3 ca dễ ép nhất (F-9 · F-11 · G-4) — **cả 3 thoả vị-ngữ class đang mang**. Đối lập rõ với **S162**, khi vai phán TRƯỢT vì `view-stale-count` bị ép. Enum **chưa cần mở**.
|
||||
|
||||
## 6. Thách-CLEAN — 🔴 bắt được **1 FLAG SÓT, nằm trong file của CHÍNH VAI KIỂM**
|
||||
|
||||
Soi bề mặt **cả 2 vai đều KHÔNG chạm**: `lead-stale` khai coverage 6/29 `agents/**` và **0 file** `agent-memory/**`.
|
||||
|
||||
**SÓT: `.claude/agent-memory/ring2-audit/MEMORY.md:28` — `view-stale-role-desc`, CÙNG class F-3, cùng bệnh ghost-enum.**
|
||||
Verbatim dưới `## 🎯 Role baseline`: `- Input = OUTPUT của lead-stale-auditor (5 class view-*) + lead-gap-auditor (6 class gap-*).` — enum đĩa = **6** `view-*`; vế `gap-*` đúng, vế `view-*` **sai**.
|
||||
|
||||
Vì sao là FLAG thật:
|
||||
1. **Cùng vị-ngữ F-3, cùng đợt vá sót** — `:161` sót vì @S180 bỏ file lệnh; dòng này sót vì đợt đó không chạm `agent-memory/**`.
|
||||
2. 🔴 **Site ĐIỀU-KHIỂN, không phải văn kể chuyện** — `## 🎯 Role baseline` **auto-inject ~200 dòng đầu mỗi lượt spawn** ⇒ **vai bị nạp thước thiếu một vạch TRƯỚC khi bắt đầu chấm**.
|
||||
3. **Bất-đối-xứng nội-dòng** — cùng câu, vế `gap-*` đúng vế `view-*` sai ⇒ đã sửa một nửa.
|
||||
4. 🔴 **Tái phát lần 2, vai tự bắt @S173** (diary: *"`:25` dạy '5 class view-*' (enum 6)… tao bị nạp thuốc thiếu vạch TRƯỚC khi làm việc"*). Dòng trôi 25→28, **số sai vẫn nguyên** ⇒ **9 phiên không ai vá**.
|
||||
5. **Không tự-miễn-trừ** — bỏ qua = đúng anti-pattern `m-1 miễn-trừ-persona-của-vai-đo` vai tự tố @S153.
|
||||
|
||||
**2 honest-zero — cả 2 CÓ RĂNG:** `gap-incident-unrecorded=0` (garble #53 **có** sổ ở `_mind-s-12.md:96`/`:125`; máy đồng ý +0) · `gap-underfill = CHƯA ĐO` — 🔴 **nhãn này TRUNG THỰC HƠN honest-zero, chấm là điểm cộng**: vai tự nhận ràng buộc áp @S179 và **từ chối phát flag khi chưa đo trọn 9 nguồn**, đồng thời **vẫn để lại datum nợ** (`_expand_S168` 182.160 B vs đo @S179 373.881 B = **2,05× lỗi-thời**).
|
||||
|
||||
---
|
||||
|
||||
## Câu dặn cho lead
|
||||
|
||||
🔴 **17/17 ĐẠT nghĩa là mọi FLAG trúng vật thật và mọi class đúng vị-ngữ — KHÔNG nghĩa 2 vai đã bắt hết.** Đây là **độ CHÍNH XÁC, không phải độ PHỦ**: `lead-stale` tự khai chỉ chạm **~7% `STATUS`** (30/853 dòng) và **6/29** persona; mở đúng MỘT chỗ trong phần chưa chạm (`agent-memory/**`) là ra ngay **1 FLAG sót cùng class**. ⇒ **11 và 6 phải đọc là SÀN. Cấm làm tròn sàn thành tổng.**
|
||||
|
||||
**2 việc load-bearing cho closeout:**
|
||||
1. **`HANDOFF:58` đang là NGUỒN LÂY** — không chỉ "số cũ đọc thành nhẹ gấp đôi", nó **đã lây vào brief lead giao cho vai KIỂM lượt này**. Vá `:58` phải **kèm mở lại slot (70)**, vì lỗi máy đang bị **đóng ké** dưới `~~ĐÓNG @S179~~`.
|
||||
2. **`gap-carry-aged` vừa 0→1** trong `class_repeat` — nhưng máy sinh số đó **vẫn đang mù 22,8% đầu file**. Số 1 này do **vai tự tính TAY**, không phải máy tự thấy. **Đừng đọc thành "detector đã khỏi".**
|
||||
@ -0,0 +1,65 @@
|
||||
# sub-tooling-auditor — V1 @open S182 (H1 tooling-freshness 4 mặt)
|
||||
|
||||
**VERDICT: `TOOLING-AUDITOR: PASS_WITH_FLAGS — 4 finding`**
|
||||
|
||||
> ⚠️ **#53 datum:** first-return = garble (1 câu giữa transcript: *"Tree shows 8 roots while the description says 12. Let me measure the real root set from source."*). Recover bằng **SendMessage-resume in-session**, path-2. **Mất 0 byte.** Câu garble CHÍNH LÀ finding F-4 đang đo dở ⇒ đừng vứt câu garble.
|
||||
|
||||
---
|
||||
|
||||
## F-1 [vai-trò] `.claude/agents/README.md:190-219` — skill-matrix phủ 26/28 vai
|
||||
|
||||
- **View:** bảng "🛠️ Skill assignment" liệt **26 row** (`:194`→`:219`). **Source:** đĩa có **28 vai**.
|
||||
- **Chứng:** `comm -23 <đĩa> <matrix>` → thiếu `score-count-auditor`, `score-counter`. **Control dương:** chiều ngược `comm -13` = **RỖNG** (0 row ma) ⇒ thiếu THẬT, không phải lỗi parse.
|
||||
- **Lớp:** lô S181 cập-nhật roster ở **4 bề-mặt** (đĩa · `VALID_ROLES` · `STATUS:478` · `README:27`) nhưng bỏ sót **bề-mặt thứ 5 nằm trong CHÍNH file README** ⇒ đúng bài *được-chỉ-1-chỗ ⇒ grep CẢ LỚP TRONG FILE* (tái phát S172/S179).
|
||||
|
||||
## F-2 [docs] `docs/STATUS.md:478` — "roster 23", số thật 28
|
||||
|
||||
- **View:** `~~roster 26 > hmw_width.cap 20~~ ✅ ĐÓNG @S181: cap 30, roster 23 ≤ 30 ⇒ 0 lane bị bỏ`.
|
||||
- **Chứng:** `ls .claude/agents/*.md | grep -v README | wc -l` = **28** · `memory-budget.json:30` `"cap": 30` · mirror `hmw.js:175` `WIDTH_CAP = 30` (**2 nơi KHỚP**).
|
||||
- 🔴 **Sai SỐ, đúng KẾT-LUẬN** (28 ≤ 30 vẫn đúng). Là **câu SỐNG** (vị-từ khẳng-định "0 lane bị bỏ"), không phải ảnh-chụp lineage ⇒ vá được. Doctrine: **BỎ SỐ, không ĐỔI SỐ**.
|
||||
|
||||
## F-3 [skill] `.claude/skills/README.md:20` — same-class residue
|
||||
|
||||
- Câu vừa bị XOÁ ở `ef-core-migration/SKILL.md:25` @S181 **còn sống nguyên** tại site thứ 2.
|
||||
- **Chứng:** `grep -rn "row cuối bảng\|row Mig cuối" --include=*.md` (loại `agent-memory`/`runs`/`sessions`) = **đúng 2 hit** — `SKILL.md:25` (bia-mộ, ĐÚNG) + `skills/README.md:20` (dư-lượng, SAI). Bảng thật dừng **Mig 71**, đĩa mới nhất **Mig 72** (`20260806070242_AddMaHangMucToContractSigningPlanLines`).
|
||||
- ⇒ **F-7 nợ cũ ĐÃ VÁ ở SKILL.md nhưng bản vá KHÔNG quét cùng-lớp.** Lớp *claim chép nhiều site* tái phát **lần thứ 4** (S153 → S172 → S179 → nay).
|
||||
|
||||
## F-4 [skill] `permission-matrix/SKILL.md:3` + cây `:44-77` — 🔴 CHƯA ĐO XONG
|
||||
|
||||
- `:3` tự cấm chép số **rồi chép** `12 root` — tự mâu thuẫn trong CÙNG mệnh-đề.
|
||||
- Cây `:44-77` liệt **8 root**: `Dashboard · Master · Contracts · Forms · PurchaseEvaluations · KeHoachKyKet · Reports · System`.
|
||||
- 🔴 **Số root SỐNG CHƯA CHỐT — thước hỏng, tự khai:** hai phép đo trên cùng `DbInitializer.cs` **cãi nhau** (một lần trả **3**, một lần trả **10**). Nguyên nhân: `grep ", null,"` **trượt** các dòng có khoảng-trắng căn cột. Theo luật *2 engine cãi nhau ⇒ đi tìm engine HỎNG, đừng chọn con SỐ* ⇒ **không chốt số**, cần đo lại bằng cắt-cột tuple.
|
||||
- **Chốt được tới đây:** cây **8 root < thực-tế ≥10** ⇒ nợ cũ **F-2 CÒN MỞ** và **F-3 CÒN MỞ**. 🔴 Con số *"thiếu đúng 5 root"* trong HANDOFF **CHƯA tái-lập được** — **đừng chép lại**.
|
||||
|
||||
---
|
||||
|
||||
## Nợ cũ S179 — trạng-thái từng cái
|
||||
|
||||
| Nợ | Trạng-thái | Chứng |
|
||||
|---|---|---|
|
||||
| F-2 cây `permission-matrix` thiếu root sống | **CÒN** | cây = 8 root, source ≥10 |
|
||||
| F-3 `:3` cấm chép số rồi chép "12 root" | **CÒN** | literal còn nguyên `:3` |
|
||||
| F-4 dải `§16+` 4 giá-trị/4 surface | **CHƯA ĐO** | hết lượt trước khi chạm |
|
||||
| F-5 `PROJECT-MAP.md` 0-hit 4 module sống | **CHƯA ĐO** | — |
|
||||
| F-7 `ef-core-migration` bảng dừng Mig 71 | **VÁ** (đúng cách) — nhưng đẻ **F-3 mới** | bia-mộ `SKILL.md:25` |
|
||||
| F-8 `schema-diagram:515` "91 bảng" vs 97 | **CHƯA ĐO** | — |
|
||||
| F-6 S179 (`CLAUDE.md` neo `Lần kế 2026-08-01`) | **VÁ** | nay `Lần kế: 2026-09-01` |
|
||||
| S181 F-1/F-4 (2 vai W4b thiếu `effort:`/`memory:`) | **VÁ TRỌN** | 28/28 `model: opus` · 28/28 `effort: max` · 28/28 `memory: project` |
|
||||
|
||||
## DIFF vs S181/S180
|
||||
|
||||
- **THÊM:** roster 26→**28**. Khớp **5/5 bề-mặt** — đĩa 28 (29 file −README; 🔸 bẫy glob, đừng đếm 29) · `VALID_ROLES` 28 (parse chặt neo `^const VALID_ROLES = \[`→`^\]`, strip `//`; `diff` tập-TÊN vs đĩa **IDENTICAL**) · `agent-memory/` 28 dir · `STATUS:478` `**28**` · `README:27` "28 vai". Bề-mặt thứ **6** (skill-matrix) = F-1.
|
||||
- **ĐỔI:** `hmw_width.cap` 20→**30**, mirror `hmw.js:175` đổi CÙNG lượt ⇒ 0 drift 2-nơi. Slot (75) đóng thật (28 ≤ 30, hết LOUD-skip).
|
||||
- **XÓA:** câu *"mới nhất = row cuối bảng"* gỡ khỏi `ef-core-migration/SKILL.md` — nhưng **không quét cùng-lớp** ⇒ F-3.
|
||||
- **0-DELTA:** skill **6 project** + **23** standalone + **19** command (khớp `README:233`) · plugin **18 registered / 15 enabled / 3 disabled** (`pr-review-toolkit` · `code-modernization` · `hookify`), **0-delta 4 phiên liền**, marketplace **37**. 🔸 Nấc báo đúng: 15 **enabled**, KHÔNG đọc thành "đang dùng".
|
||||
- **stale:** `STATUS:478` (F-2) · `skills/README:20` (F-3) · `permission-matrix/SKILL.md:3` + cây (F-4).
|
||||
|
||||
## Tự BÁC 1 ứng-viên flag (ghi để không tái-sinh)
|
||||
|
||||
`memory-budget.json:31` `_cap_raised_S181` ghi *"roster 26 và còn để chỗ cho 2 vai tầng-2/tầng-3 của W4b"* — **KHÔNG flag**. Đó là lý-lẽ **tại thời-điểm nâng cap** (26 + 2 = 28), nằm trong khoá `_cap_raised_*` = **bản-ghi thuật lại**; sửa = **giả-mạo phép đo cũ**. Khác hẳn `STATUS:478` là vị-từ khẳng-định đang sống.
|
||||
|
||||
## Khai chặt phạm vi
|
||||
|
||||
Mặt ④ docs **CHƯA phủ hết**: `docs/HANDOFF.md` · `docs/governance/` · `docs/PROJECT-MAP.md` · `schema-diagram.md` = **CHƯA ĐO** lượt này.
|
||||
|
||||
**Propose-only — 0 file bị sửa.**
|
||||
@ -0,0 +1,11 @@
|
||||
# owner-decisions — YC-013 pipeline (S182, 2026-08-08)
|
||||
|
||||
## D3 — 9 checkbox quyền duyệt trên panel KHKK (YC-014, AskUser ~01:20)
|
||||
**Anh chọn: "C — Mờ ô chưa nghe (Recommended)"**
|
||||
- Giữ layout y khuôn ảnh; ô nào máy KHKK chưa nghe ⇒ **disabled + tooltip "chưa áp dụng cho KHKK"** — tiền lệ chính trang này (ô Duyệt-thẳng-Cấp-cuối đã mờ cho type-10 từ K3, `ApprovalWorkflowsV2Page.tsx:1292-1302`).
|
||||
- Hệ quả scope: **W3 (port semantics) HỦY đợt này**. W2d thi hành theo C.
|
||||
- Danh sách ô mờ CHÍNH XÁC = chờ 2 reviewer chấm (đặc biệt ô #4 Trả-về-Drafter: hành-vi thật = luôn-cho dù cờ tắt — mờ-tick-sẵn hay mờ-thường, reviewer khuyến nghị).
|
||||
- Ô "Ngưỡng giá trị gói CEO" (`:944-961`): cùng cách — KHKK cố ý không port (OG-3 NĐUQ human-gate).
|
||||
|
||||
## D4 — nhãn panel (lead quyết theo đề xuất FE-lens, reviewer chấm)
|
||||
Title = menu-label "Duyệt KHKK — Nhóm {g}" + phụ đề nghiệp vụ `khkkGroupMenuLabel(g)`.
|
||||
@ -0,0 +1,40 @@
|
||||
# run — KHKK 8 workflow-nhóm mirror trang Duyệt NCC (YC-013, S182/L13)
|
||||
|
||||
- ts-mở: 2026-08-08T00:50+07:00 · HEAD nền: `936d1aed` (cây làm việc)
|
||||
- lead: **Fable 5** (anh `/model claude-fable-5` @S182 — chủ dự án đổi, 0 caveat)
|
||||
- yêu-cầu: `YC-013` sổ `docs/governance/so-yeu-cau-chu-du-an.md` (nguyên văn + 4 ảnh)
|
||||
|
||||
## Đề bài (từ 4 ảnh anh gửi)
|
||||
|
||||
Trang `admin.solutions.com.vn/system/approval-workflows-v2/DuyetNcc` (QT-DN-V2-001 v04) có ĐỦ:
|
||||
xem 3 Bước × Cấp × NV + badge role · **Sửa tại chỗ** (dialog per-NV **9 checkbox quyền duyệt**: Trả-về-1-Cấp-trước / Trả-về-1-Bước-trước / Trả-về-Người-chỉ-định / Trả-về-Drafter-mặc-định / Sửa-Section-2 / Sửa-Section-ngân-sách / Duyệt-thẳng-Cấp-cuối / Duyệt-là-KẾT-THÚC / **XÓA phiếu**) · **Tạo phiên bản mới** · **Ghim/Bỏ ghim** (Cho user chọn) · **Xoá version** · Lịch sử versions (v03 Archived) · Tạo quy trình mới · rule-box (mỗi Bước 1 Phòng + tối đa 3 Cấp, OR-of-N, tuần tự).
|
||||
|
||||
**8 mục sidebar "Duyệt KHKK — Nhóm 1..8"** (khoanh đỏ) phải **GIỐNG NHƯ VẬY** — thêm/sửa/xóa/edit/stick tương tự Duyệt NCC, UI **không lệch**.
|
||||
|
||||
## Pipeline (anh chỉ định trong YC-013)
|
||||
|
||||
| bước | engine | trạng thái |
|
||||
|---|---|---|
|
||||
| B1a `/fable-real investigator-codebase` — lens **BE** | Fable single deep-pass | [ ] |
|
||||
| B1b `/fable-real investigator-codebase` — lens **DATABASE** | Fable single deep-pass | [ ] |
|
||||
| B1c `/fable-real investigator-codebase` — lens **FE-UI** ("đúng nhất về UI lẫn giao diện, ko lệch") | Fable single deep-pass | [ ] |
|
||||
| B2 lead tổng hợp 3 lens → **wave-plan chi tiết** (spec draft) | lead Fable | [ ] |
|
||||
| B3 ×2 `/fable-real reviewer` — review + tổng hợp wave-plan | Fable single ×2 | [ ] |
|
||||
| B4 lead chốt **spec final** | lead | [ ] |
|
||||
| B5 **hmw fan-out Opus 5 MAX** execute wave | Workflow hmw.js (worker-pin opus+max) | [ ] |
|
||||
|
||||
## Artifact chờ
|
||||
|
||||
`sub-invest-be-fable.md` · `sub-invest-db-fable.md` · `sub-invest-fe-fable.md` · `spec-khkk-awf-mirror-ncc-DRAFT.md` · `sub-review-1-fable.md` · `sub-review-2-fable.md` · `spec-khkk-awf-mirror-ncc-08-08-2026.md` · `implement-synthesis.md`
|
||||
|
||||
## Đo nền W0 (R-3 — số đo TƯƠI, lane implementer-backend, 2026-08-08)
|
||||
|
||||
`baseline-W0: 645 PASS (45D+600I)` — 0 fail, 0 skip. Lệnh: `dotnet test SolutionErp.slnx` chạy **sau** W0.1/W0.2, HEAD nền `936d1aed` + 0 sửa mã. Domain 45 (407 ms) · Infrastructure 600 (35 s). ⇒ **W1 đạt khi FULL suite = 645 + 14 = 659, 0 fail.**
|
||||
|
||||
W0.1 (mig ×2 DB): `SolutionErp_Dev` + `SolutionErp_Design` cùng từ `20260731085624` (Mig 71) → **`20260806070242`** (Mig 72) ✅. *(Khai: local có BA DB — `SolutionErp` base ở `appsettings.json:3` bị `appsettings.Development.json:3` override nên KHÔNG phải DB runtime; lượt đầu tôi trỏ nhầm vào nó, đã chạy lại đúng 2 DB — chi tiết `sub-implementer-backend-0.md §1`.)*
|
||||
|
||||
W0.2 (seeder): boot API 1 lần (`ASPNETCORE_ENVIRONMENT=Development`) ⇒ **8 row `KHKK-N1..8`, ApplicableType=10, IsActive=1, IsUserSelectable=1**, mỗi row 3 Bước / 9 Cấp (roster local resolve ĐỦ người). Đã tắt API sau khi seed.
|
||||
|
||||
## Chống-#53 (7/7 vai garble @bookend-open phiên này)
|
||||
|
||||
Mỗi sub **GHI ARTIFACT LIÊN TỤC TRONG LÚC LÀM** vào đúng 1 file `sub-*` của mình; return chỉ cần VERDICT-header + con-trỏ. Return garble ⇒ đĩa cứu.
|
||||
@ -0,0 +1,80 @@
|
||||
# SPEC FINAL — YC-013: 8 panel "Duyệt KHKK — Nhóm 1..8" mirror trang Duyệt NCC (S182/L13, 2026-08-08)
|
||||
|
||||
> **Nguồn:** spec DRAFT + merge 2 review Fable — `sub-review-1-fable.md` (`PASS_WITH_FIXES 2C/5M`, F1-F12) + `sub-review-2-fable.md` (`PASS_WITH_FIXES 1C/5M+7m`, §8). Mọi file:line đã được ÍT NHẤT 1 reviewer re-verify trên cây (HEAD nền `936d1aed`).
|
||||
> **Owner-decisions:** D3 = **C** (YC-014, anh chốt qua AskUser) · D4 = title menu-label + phụ đề nghiệp vụ · **D5/D6 = lead chọn nhánh an toàn theo bậc-2 luật tự-trị, anh veto 1 câu là gỡ** (khai ở MỤC 3).
|
||||
> **Executor:** hmw fan-out, worker **Opus 5 MAX** (worker-tier-pin sẵn). Cầm ĐÚNG file này chạy.
|
||||
|
||||
## MỤC 1 — Ràng buộc toàn cục (R-1..R-6, đọc trước mọi wave)
|
||||
|
||||
- **R-1 (C-1):** W1+W2 = **MỘT commit code duy nhất**. Nếu buộc tách: BẮT BUỘC W1 → CI xanh → W2. **CẤM mọi commit chứa W2 mà thiếu W1a** — nút "Tạo phiên bản mới" trên panel nhóm khi BE còn deactivate-per-type = tắt IsActive cả 8 nhóm prod. (Bom đã armed sẵn hôm nay qua panel `/ContractSigningPlan`.)
|
||||
- **R-2 (M-4/F10):** trên PROD chỉ đo ĐỌC. Mọi acceptance có GHI chạy **LOCAL**. Không có endpoint re-activate — POST version mới là ratchet một chiều.
|
||||
- **R-3 (M-3/F9):** baseline test = **số đo tươi tại W0** ghi vào `run.md`. KHÔNG chép 644/645 từ doc nào. *(Tham chiếu: lead đo tươi @bootstrap S182 = 645 PASS (45D+600I) — vẫn phải đo lại tại W0.)*
|
||||
- **R-4:** 0 migration. Wave nào phát hiện cần mig ⇒ DỪNG, báo lead (premise 3-lens vỡ).
|
||||
- **R-5:** commit `[CLAUDE] Workflow: KHKK 8 panel nhóm mirror DuyetNcc + BE guards (0-mig)`. Artifact run-folder không trigger CI (paths-ignore verified).
|
||||
- **R-6 (F4):** 🔴 **CÔNG THỨC W1a KHOÁ** = `typeEnum == ContractSigningPlan ⇒ Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)`. **CẤM biến-thể regex `^KHKK-N[1-8]$`** (công thức FE-invest D1 — code lạ type-10 như `QT-KHKK-V2-001` sẽ rơi nhánh per-type → NUKE cả 8 nhóm).
|
||||
|
||||
## MỤC 2 — WAVE
|
||||
|
||||
### W0 — prep + đo nền (blocking)
|
||||
| # | Task | Đạt khi |
|
||||
|---|---|---|
|
||||
| 0.1 | `dotnet ef database update` ×2 DB (Dev + Design — kẹt Mig 71, đĩa Mig 72; đo lại drift tại chỗ) | `sqlcmd` top MigrationId = `20260806070242` cả 2 |
|
||||
| 0.2 | Boot API 1 lần → seeder đẻ 8 row local | SQL: 8 row `KHKK-N1..8` type-10 IsActive=1. Roster local **rỗng/đủ người đều KHÔNG phải bug** (seeder resolve email; local có 14 user Solutions thật) |
|
||||
| 0.3 | Đo baseline test TƯƠI → ghi `run.md` | R-3 |
|
||||
| 0.4 | Prod-state check ĐỌC-only: `ssh vietreport-vps` + `sqlcmd -E` (integrated — KHÔNG cần password) chạy 3 query DB-invest §4 + query audit pin-lệch (F11): `SELECT p.MaKeHoach, p.ApprovalGroup, w.Code FROM ContractSigningPlans p JOIN ApprovalWorkflows w ON w.Id=p.ApprovalWorkflowId WHERE w.Code LIKE 'KHKK-N%' AND w.Code <> CONCAT('KHKK-N', p.ApprovalGroup)` | Kỳ vọng: 8 row IsActive=1 · phiếu KHKK đều Phase=3 (DaDuyet=TERMINAL, đã verify enum) · audit = 0 row. **LỆCH ⇒ báo lead TRƯỚC W1** (nhóm Archived-oan → recovery = tạo version mới từ panel sau W2; phiếu treo pin lệch → sẽ kẹt submit sau W1b by-design, phải biết số trước) |
|
||||
|
||||
### W1 — BE: 5 fix + 14 test, 2 file + 1 file MỚI, 0-mig
|
||||
Files: `src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` · `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` · **MỚI** `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanWorkflowGuards.cs` · `src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs` (1 call-site).
|
||||
|
||||
| # | Fix | Site | Cách |
|
||||
|---|---|---|---|
|
||||
| a | 🔴 Deactivate-per-Code type-10 | `AdminFeatures.cs:339-343` | Công thức R-6. Type khác GIỮ per-type. **Sửa comment `:339` "only ONE active per type" CÙNG COMMIT** (F12d). Ghi chú nhất quán: `nextVersion :334-337` đã per-Code sẵn |
|
||||
| b | Group-match guard 3-site (F1) | Create `:479-486` · UpdateDraft `:603-607` · `SubmitAsync` (Service, cạnh rào hạng-mục `:169-187` trước `LoadStepsAsync :189` — executor verify điểm chèn tại chỗ) | 🔴 **File MỚI `ContractSigningPlanWorkflowGuards.cs` — `public static class`** (KHÔNG nhét vào `Scope` internal — Submit ở Infrastructure không gọi được; KHÔNG `InternalsVisibleTo(Infrastructure)`; KHÔNG inline-duplicate). Method `EnsureWorkflowGroupMatchAsync(IApplicationDbContext, Guid awId, int approvalGroup, CancellationToken)` — move NGUYÊN logic rào (v), **GIỮ semantics cho-qua-code-ngoài-họ** (`:477-478` chủ-đích). 3 site cùng gọi. Message 409: *"Phiếu nhóm N{g} đang gắn quy trình N{k} — vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình."* |
|
||||
| b2 | Pin-check IsActive ∧ IsUserSelectable (F5 — **D6**) | Create (sau rào iv) + UpdateDraft (CHỈ trong nhánh `awId != plan.ApprovalWorkflowId` `:603`) | Mirror vá-3 `CreateContractFromSigningPlanFeatures.cs:141-147` — 2 message tách "đã ngừng hoạt động"/"chưa ghim". Giữ-nguyên-pin KHÔNG check (phiếu cũ pin bản archived vẫn sửa GhiChu) |
|
||||
| c | Changelog PUT type-10 | `AdminFeatures.cs:863-893` | Nhánh `else if type-10`: query `ContractSigningPlans` (`ApprovalWorkflowId==def.Id && Phase != DaDuyet && Phase != TuChoi`; query-filter tự loại phiếu xoá-mềm — ĐÚNG cho changelog) → add `ContractSigningPlanChangelog { EntityType=Workflow /*=4 enum SẴN*/, Action=Update, EntityId=def.Id, PhaseAtChange, UserId/UserName, Summary="Sửa quy trình duyệt tại chỗ: {Code} v{NN} (giữ nguyên phiên bản)", ContextNote (kèm Reason) }` mirror block PE. *(Dialog PUT đang HỨA nguyên văn "ghi vào lịch sử phiếu đang chạy" `Page.tsx:963-977` — fix này làm lời hứa thành thật)* |
|
||||
| d | DELETE usage-check → 409/404 (F2+F3+M-1) | `AdminFeatures.cs:1193-1205` | (i) đếm pin **8 module** có cột `ApprovalWorkflowId` (grep Domain để liệt, đừng chép): PE/Contract/KHKK **BẮT BUỘC `IgnoreQueryFilters()`** (soft-deleted vẫn chặn FK) · Proposal/Leave/Ot/Vehicle/Travel đếm thẳng — 🔴 5 module này pin **LOOSE 0-FK** ⇒ guard này là **RÀO DUY NHẤT chống dangling** (rationale 2 vế: FK-modules = UX 547→409; office = DATA-INTEGRITY) · (ii) đếm opinions **8 bảng `*LevelOpinion`** theo level-ids của workflow, `IgnoreQueryFilters()` → 409 "N phiếu đang gắn + M chữ ký" · (iii) `KeyNotFoundException` → `NotFoundException` (id lạ = 404, hết 500 thô — middleware không map BCL) · (iv) **D5:** code họ `KHKK-N[1-8]` && version CUỐI của Code ⇒ 409 *"Nhóm N{n} phải còn ≥1 version"* (chặn ca seeder-tái-sinh M-1 khỏi đường UI) |
|
||||
|
||||
**Test W1 — 14 ca (test-before a/b/b2 · test-after c/d).** Homes verified: `tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs` + `ContractSigningPlanApprovalTests.cs`:
|
||||
T1 create v2 `KHKK-N1` ⇒ N2..N8 bất biến + N1 v01→false v02→true · T2 regression type-1 Code-mới-cùng-type vẫn tắt Code cũ · T3 POST type-10 **code lạ** `QT-KHKK-V2-001` ⇒ 8 nhóm bất biến (F4) · T4 UpdateDraft re-pin lệch nhóm ⇒ 409 · T5 phiếu pin-lệch PRE-EXISTING (seed thẳng DbContext) Submit ⇒ 409 · T6 code ngoài họ: Create/UpdateDraft/Submit PASS như cũ (negative M-5c.ii) · T7 re-pin bản archived ⇒ 409 (D6) · T8 PUT workflow nhóm có phiếu ChoDuyet ⇒ 1 row changelog đúng shape · T9 phiếu DaDuyet/TuChoi ⇒ 0 row (negative) · T10 DELETE có pin KHKK ⇒ 409 · T11 **phiếu KHKK ĐÃ XOÁ MỀM pin ⇒ vẫn 409** (F2) · T12 **phiếu Leave (office, 0-FK) pin ⇒ 409** (F3 — hôm nay là 200-xoá-êm) · T13 id không tồn tại ⇒ 404 · T14 xoá version CUỐI nhóm ⇒ 409 (D5); version 0-usage không-phải-cuối ⇒ xoá OK.
|
||||
**Đạt khi:** build 0-err + FULL suite = baseline-W0 + 14, 0 fail.
|
||||
|
||||
### W2 — FE: fe-admin `src/pages/system/ApprovalWorkflowsV2Page.tsx` (~70-100 dòng) + `KhkkCreatePage` ×2 app
|
||||
| # | Task | Site | Cách |
|
||||
|---|---|---|---|
|
||||
| a | Parse kind | `:164-172` | `typeCode?.match(/^KhkkN([1-8])$/)` → `khkkGroup`; `selectedTypeInt = khkkGroup ? TYPE_INT_CONTRACT_SIGNING_PLAN : TYPE_CODE_TO_INT[typeCode]`; query overview giữ `applicableType=10` (queryKey dùng chung — invalidate prefix tự tươi cả panel nhóm lẫn panel type-10, đã verify `:292/:348`) |
|
||||
| b | Synthetic slice per-Code | `:299-301` | `versions = type10.history.filter(d => d.code === khkkGroupWorkflowCode(g))` (🔴 `History` = FULL list kể cả active — verified BE `:206-211`; panel tự filter `!isActive` khi render) · `active = versions.find(v=>v.isActive) ?? null` (KHÔNG đọc ô `Active` per-type — 1-trong-8) · giữ shape `TypeSummaryDto` |
|
||||
| c | `lockedCode` xuyên suốt | `TypePanel:357` → `Designer:620/:650-651/:798/:920-933` | prop `lockedCode = khkkGroupWorkflowCode(g)`; ô Mã `disabled={isEdit \|\| !!lockedCode}` + hint "Mã khoá theo nhóm — version tự tăng" (F12a); POST gửi `lockedCode ?? code` |
|
||||
| d | **D3=C thi hành — danh-sách CHỐT (F6+M-2 merge)** | `:1235-1328` | Gate `applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` (áp cả panel ContractSigningPlan cũ — nhất quán). **6 ô** (#1 `allowReturnOneLevel` · #2 `allowReturnOneStep` · #3 `allowReturnToAssignee` · #5 EditDetails `:1268-1276` · #6 EditBudget `:1277-1285` · #9 Delete `:1320-1328`): thêm `disabled` + `title` tooltip "Chưa áp dụng cho KHKK" — 🔴 **GIỮ `checked={entry.<flag>}` state THẬT, CẤM ép false** (trung thực với DB; payload `levelFlagsPayload :255-267` echo state, không đổi). **Ô #4 `allowReturnToDrafter`**: disabled + hiển thị **state thật (thường TRUE)** + tooltip RIÊNG *"KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh"* (KHÔNG mờ-kiểu-chưa-có — hành-vi thật là LUÔN-CHO). **#7 SkipToFinal GIỮ NGUYÊN** như đang có (`:1292-1302`) · **#8 Finalize SỐNG** |
|
||||
| e | (optional — kéo eoffice rotate) mirror menuKeys | `fe-{admin,user}/src/lib/menuKeys.ts:31-32` | +8 const `AwV2_KhkkN1..8` — 0 hệ quả runtime |
|
||||
| f | **Ô Ngưỡng CEO — NGOÀI D3, disable type-10 VÔ ĐIỀU KIỆN** (F7) | `:944-961` | `disabled={isEdit \|\| applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN}` + helper-text "KHKK không dùng ngưỡng máy — NĐUQ quyết" + POST type-10 gửi `null`. Tiền lệ `:1292`. (`CeoApprovalThreshold` KHÔNG port VĨNH VIỄN — OG-3) |
|
||||
| g | Auto-pin lọc isActive ×2 app (F5/D6) | fe-user `KhkkCreatePage.tsx:114` + fe-admin `:113-116` | `find(w => w.isActive && code-match)` — giữ filter `isUserSelectable :104`. Mirror LOGIC 2 app (2 file vốn khác nhau — không cần byte-equal) |
|
||||
| h | Comment lệch + D4 phụ đề | `:641-644` + PageHeader `:305-318` | sửa comment "2 ô duyệt sớm sống cho MỌI loại" cho khớp thực tế (m-2) · thêm ~1 prop `subtitle = khkkGroupMenuLabel(g)` (`types/khkk.ts:159-168` — 2-app identical sẵn) |
|
||||
|
||||
**Acceptance LOCAL (R-2):** A1 8 leaf → 8 slice khác nhau, hết landing · A2 dialog đúng khuôn ảnh theo D3=C (title `KHKK-N{n} v01 (giữ nguyên phiên bản)`, ô #4 tick-disabled, 6 ô disabled-state-thật, rule-box) · A2b sửa-tại-chỗ THẬT 1 nhóm ⇒ 204 + panel giữ v01 (M-5/m-5) · A3 tạo v02 N1 ⇒ nằm ĐÚNG panel N1 + badge 7 nhóm KHÔNG mất · A4 regression 4 route cũ + fe-user auto-pin. **Đạt khi:** `npm run build` ×2 app + tsc 0-err.
|
||||
🔸 **Răng nút Ghim (sửa theo F5):** sau D6, bỏ-ghim **bản active** = chặn tạo phiếu nhóm; bỏ ghim cả 8 = đóng cửa tạo phiếu KHKK toàn cục (không có rào chặn bỏ-ghim-cái-cuối — khai, không rào đợt này).
|
||||
|
||||
### W3 — **N/A** (D3=C đã chốt; port sau theo lệnh owner = gỡ mờ per-cờ CÙNG wave, khuôn K3-F5)
|
||||
|
||||
### W4 — test-specialist đóng suite: FULL `dotnet test SolutionErp.slnx` = baseline-W0 + 14, 0 fail. Số canonical đổi ⇒ update DUY NHẤT `docs/STATUS.md` row Tests (B1).
|
||||
|
||||
### W5 — ship + verify
|
||||
1. **Commit MỘT phát** BE+FE+tests (R-1, R-5) → push.
|
||||
2. **cicd-monitor 4-chân (m-3):** (i) CI Run success + test-gate = số W4 · (ii) bundle **admin** rotate + grep js mới marker `KhkkN1` (control-âm bundle cũ 0-hit); **eoffice: đổi nếu làm W2e/W2g-fe-user, GIỮ nếu không — ĐO đừng suy** · (iii) BE DLL: ssh grep `SolutionErp.Application.dll` token `EnsureWorkflowGroupMatch` 2-encoding UTF-8/UTF-16 (khuôn S177) + control-dương token cũ · (iv) query-1 lại: 8 row IsActive=1 bất biến qua deploy.
|
||||
3. **Acceptance PROD đọc-only:** login `admin@solutions.com.vn` → ≥3/8 panel render đúng slice + mở dialog rồi Hủy · 3 query SQL + audit pin-lệch (F11) · **KHÔNG bấm "Tạo phiên bản mới" trên prod** · note: role thường chưa grant menu-row ⇒ không thấy leaf = đúng thiết kế 2-tầng (#82).
|
||||
4. Đóng sổ: STATUS/HANDOFF/WAL + tick `YC-013` + khai **accepted-risk**: race 2-admin cùng Code → 500 thô (UNIQUE đỡ data — không mở scope middleware).
|
||||
|
||||
### Rollback
|
||||
0-mig ⇒ revert commit sạch phần MÁY. Data ở lại hợp lệ (changelog rows = audit-history; v02+ = ratchet vô hại, pin by-Id). 🔴 **Revert W1a = bom per-type TÁI VŨ TRANG** — sau revert, CẤM "Tạo phiên bản mới" mọi panel type-10 tới khi re-apply.
|
||||
|
||||
## MỤC 3 — Quyết định + khai
|
||||
|
||||
| # | Quyết | Ai | Nội dung |
|
||||
|---|---|---|---|
|
||||
| D3 | ✅ **anh** (YC-014) | **C** — mờ ô chưa nghe, thi hành theo danh-sách-chốt W2d (6 ô disabled-state-thật + #4 disabled-**state-thật** + #7 giữ + #8 sống). 🔴 **ERRATA ratified @1bis (FE FLAG-B):** chữ "forced-true" ở dòng này bản trước = **rút gọn HAO NGHĨA** của MỤC 2 *"state thật (thường TRUE)"* — hard-code `checked={true}` sẽ nói dối ca row=false, cùng class lỗi mà D3=C cấm ở 6 ô kia chỉ đảo dấu. Code theo **state thật** là ĐÚNG (default=true ở 4 lớp ⇒ thực tế vẫn hiện tick, thoả A2). MỤC 2 W2d là bản chuẩn |
|
||||
| D4 | ✅ lead (reviewer đồng ý) | title = menu-label + phụ đề nghiệp vụ |
|
||||
| D5 | 🟡 **lead chọn CÓ** (bậc-2 an toàn — reviewer-2 M-1 khuyến nghị CÓ) | rào delete-last-version nhóm ⇒ 409 (chặn seeder-tái-sinh khỏi đường UI). **Anh veto 1 câu là gỡ** |
|
||||
| D6 | 🟡 **lead chọn CÓ** (bậc-2 an toàn — reviewer-1 F5 khuyến nghị; tiền lệ vá-3 bridge CÙNG LỖ cho type-3) | pin-check IsActive∧IsUserSelectable + FE lọc isActive ×2 app. **Anh veto 1 câu là gỡ** |
|
||||
|
||||
**Khai mâu-thuẫn lens (F8):** (a) công thức W1a: BE-invest thắng, FE-invest regex BỊ CẤM (R-6) · (b) D3: FE-invest nghiêng A, BE-invest nghiêng C — anh đã chốt C · (c) DB-invest W-DB-2 (param `code=` phía BE) = **KHÔNG LÀM** — chọn client-side slice (payload nhỏ, 0-đổi-BE); disposition khai tại đây.
|
||||
|
||||
**Nợ NGOÀI scope (khai, không lặng):** GET Overview 0-policy lộ roster+flags cho mọi user authenticated (owner ngó sau) · roster 8 nhóm = snapshot email lúc seed, không tự sync theo QT-DN-V2-001 · `Khkk_WfView` panel aggregate giấu 7/8 nhóm (re-point = việc riêng) · sửa-workflow không notify approver phiếu treo (parity PE) · `DEFAULT_CODE_BY_TYPE[10]` panel cũ vẫn gợi mã ngoài-họ (sau W1a vô hại — per-Code) · comment "7 bảng" `:1120` · race 2-admin 2601→500 (accepted).
|
||||
@ -0,0 +1,68 @@
|
||||
# SPEC DRAFT — YC-013: 8 panel "Duyệt KHKK — Nhóm 1..8" mirror trang Duyệt NCC (S182/L13)
|
||||
|
||||
> Tổng hợp từ 3 Fable deep-pass: `sub-invest-be-fable.md` (§1-§5+VERDICT) · `sub-invest-db-fable.md` (§1-§7) · `sub-invest-fe-fable.md` (§1-§6). Trạng thái: **DRAFT chờ 2 reviewer Fable + anh chốt D3**.
|
||||
|
||||
## MỤC 1 — Mục tiêu + phạm vi + tiền đề đã chứng
|
||||
|
||||
**Mục tiêu (YC-013):** 8 mục sidebar "Duyệt KHKK — Nhóm 1..8" ra ĐÚNG khuôn trang `approval-workflows-v2/DuyetNcc`: panel active-card + 4 nút (Sửa-tại-chỗ / Tạo-phiên-bản-mới / Ghim-Bỏ-ghim / Xoá-version) + Lịch sử versions + dialog Designer 9-checkbox per-NV + rule-box. **UI không lệch.**
|
||||
|
||||
**Tiền đề 3 lens hội tụ (mỗi cái có file:line trong artifact):**
|
||||
- **0 MIGRATION** — 9/9 checkbox = 9 cột bit sẵn trên `ApprovalWorkflowLevels` dùng chung; versioning UNIQUE `(Code,Version)` + `IsActive`; Archived = `IsActive=false`.
|
||||
- **BE generic ~85%** — 5 action controller + PUT diff-by-Id + guard chữ-ký 3-site ĐÃ KHKK-aware + `SetUserSelectable`/`Delete` by-Id.
|
||||
- **Danh tính nhóm = `Code` (`KHKK-N{n}`), KHÔNG phải ApplicableType** — 8 workflow cùng type=10. Đây là gốc của MỌI việc phải sửa.
|
||||
- **Hiện trạng "lệch":** 8 route `/system/approval-workflows-v2/KhkkN{n}` (Layout `:188`) đổ về **landing grid** vì `TYPE_CODE_TO_INT` không có key `KhkkN*` (`ApprovalWorkflowsV2Page.tsx:164-172`).
|
||||
- **Authz:** giữ `Workflows.Create` y trang DuyetNcc (32 policy `AwV2_KhkkN*` 0-consumer BE — đo + control-dương; per-group-authz = đổi hình controller 9-type, KHÔNG làm trừ khi anh lệnh).
|
||||
- Runtime 9-flag: KHKK hiện chỉ nghe **#8 Finalize**; 4 Trả-về + Sửa-S2 + Sửa-NS + Duyệt-thẳng + XÓA = **BỎ QUA/CHƯA WIRE** ⇒ điểm quyết D3 (dưới).
|
||||
|
||||
## MỤC 2 — WAVE-PLAN chi tiết
|
||||
|
||||
### W0 — prep local (bắt buộc trước mọi test)
|
||||
1. `dotnet ef database update` ×2 DB (Dev + Design **đang kẹt Mig 71**, đĩa = Mig 72 — committed-but-unapplied class S53).
|
||||
2. Boot API 1 lần → `SeedKhkkGroupWorkflowsAsync` đẻ 8 row local. ⚠️ Local 8 workflow **RỖNG NGƯỜI là ĐÚNG** (seeder resolve email prod) — acceptance cấu trúc chạy local, acceptance đội-hình chạy prod.
|
||||
|
||||
### W1 — BE-1: 4 fix, 2 file, 0-mig (`ApprovalWorkflowV2AdminFeatures.cs` + `ContractSigningPlanFeatures.cs` +1 helper)
|
||||
| # | Fix | Site | Cách |
|
||||
|---|---|---|---|
|
||||
| a | 🔴 **Deactivate-per-Code cho type-10** (mìn M1, 3 lens hội tụ) | `:339-343` | `typeEnum == ContractSigningPlan` ⇒ scope `w.Code == request.Code`; type khác giữ per-type (P1 — KHÔNG làm P2 per-Code toàn cục: đổi hành vi PE/Contract chưa lường) |
|
||||
| b | **Group-match guard 2-site** (bypass rào-v) | `:603-607` + `SubmitAsync` | extract rào (v) `:479-486` → `EnsureWorkflowGroupMatchAsync(db, awId, group)`; gọi ở Create + UpdateDraft + Submit (chốt-cuối, khuôn 2-tầng `:179-187`). Message lỗi chỉ việc cần làm cho phiếu lỡ pin lệch trước vá |
|
||||
| c | **Changelog type-10 khi PUT sửa-tại-chỗ** | `:863-893` | thêm nhánh mirror block PE → `ContractSigningPlanChangelogs` (bảng sẵn Mig 69; khuôn ghi `ContractSigningPlanWorkflowService.cs:546-557`), query phiếu pin `def.Id` Phase ∉ {DaDuyet, TuChoi} |
|
||||
| d | **DELETE usage-check → 409 đích danh** (lợi MỌI type) | `:1193-1205` | pre-count pin 8 module có cột `ApprovalWorkflowId` + LevelOpinions (khuôn `CountLiveOpinionsAsync` theo workflow-id) → 409 "N phiếu đang pin"; hết cảnh `SqlException 547 → 500 thô` (`GlobalExceptionMiddleware.cs:32-39` không map DbUpdateException) |
|
||||
|
||||
**Test W1 (test-before cho a/b, test-after c/d):** tạo v2 `KHKK-N1` ⇒ N2..N8 `IsActive` bất biến + regression DuyetNcc tạo-Code-mới-cùng-type vẫn tắt Code cũ · UpdateDraft re-pin lệch nhóm ⇒ 409 · PUT workflow nhóm có phiếu treo ⇒ 1 row `ContractSigningPlanChangelogs` · DELETE version có pin ⇒ 409 (không 500).
|
||||
|
||||
### W2 — FE: 1 file chính `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` (~60-90 dòng diff)
|
||||
| # | Chạm | Site | Cách |
|
||||
|---|---|---|---|
|
||||
| a | Parse kind | `:164-172` | `match(/^KhkkN([1-8])$/)` → `khkkGroup`; `selectedTypeInt = khkkGroup ? 10 : TYPE_CODE_TO_INT[typeCode]` (query overview giữ `applicableType=10`) |
|
||||
| b | Synthetic slice per-Code | `:299-301` | từ type-10: `versions = history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v => v.isActive) ?? null` (🔴 KHÔNG tin ô `Active` per-type của BE — nó số-ít, 8 nhóm trộn) · title = "Duyệt KHKK — Nhóm {g}" + phụ đề nghiệp vụ `khkkGroupMenuLabel(g)` (D4 — lead chọn theo đề xuất FE, reviewer chấm) |
|
||||
| c | `lockedCode` xuyên suốt | `TypePanel:357` → `Designer:620/:650/:798/:927` | prop `lockedCode?: string`; ô Mã `disabled={isEdit || !!lockedCode}`; POST gửi `lockedCode ?? code`. 🔴 Thiếu = "Tạo quy trình mới" từ panel nhóm với mã tự gõ đẻ version **rơi khỏi slice** + không được rào (v) nhận |
|
||||
| d | **D3 — 7 ô chưa-wire** (chờ anh chốt, xem MỤC 3) | `:1275/:1284/:944-961/…` | thi hành theo phương án anh chọn {A giữ / B port / C mờ-ô} |
|
||||
| e | (optional) mirror menuKeys | `fe-{admin,user}/src/lib/menuKeys.ts:31-32` | +8 const `AwV2_KhkkN*` — 0 hệ quả runtime, mirror-completeness |
|
||||
|
||||
**SHA-mirror 2 app: KHÔNG áp dụng** — Designer là fe-admin-only (đã CHỨNG: fe-user 0 route/0 page/0 nhánh `AwV2_*`); `types/khkk.ts` 2 app identical sẵn, không chạm.
|
||||
**Regression:** 4 route cũ (`DuyetNcc`/`PhuongAn`/`Contract`/`ContractSigningPlan`) render như cũ · fe-user tạo phiếu nhóm auto-pin `KHKK-N{g}` không đổi API · **hệ quả nghiệp vụ THẬT của nút Ghim:** `IsUserSelectable=false` 1 nhóm ⇒ CHẶN TẠO PHIẾU nhóm đó cả 2 app (`KhkkCreatePage.tsx:104/:113-116`) — acceptance phải nêu để anh biết nút này có răng.
|
||||
|
||||
### W3 — CHỈ KHI anh chọn D3=B: port semantics vào `ContractSigningPlanWorkflowService`
|
||||
F6 XÓA-đúng-lượt (mirror PE 3-rào `PeSoftDeleteFeatures.cs:110-114`) · F1×4 return-modes per-level (mirror switch `PurchaseEvaluationWorkflowService.cs:421-424`) · F3-analog sửa-Lines-lúc-duyệt · F2 skip-to-final (+gỡ ép-false `:387` + gỡ disable FE `:1292-1302` **CÙNG WAVE** — tiền lệ F5 @K3). Mỗi cờ = test RED→GREEN riêng. `CeoApprovalThreshold` **KHÔNG port** (OG-3 NĐUQ human-gate — owner đã chốt trước).
|
||||
|
||||
### W4 — test-specialist đóng suite (baseline 645 → +N per W1/W3)
|
||||
### W5 — reviewer adversarial pre-commit → commit → cicd-monitor 3-chân (bundle **admin rotate**, eoffice giữ nguyên nếu chỉ W2a-d; +eoffice nếu e) → acceptance prod + 3 query SQL verify (DB §4 bàn giao)
|
||||
|
||||
## MỤC 3 — Điểm quyết + acceptance + rollback
|
||||
|
||||
**D3 (ANH CHỐT — chặn scope W2d/W3):** 6/9 checkbox tick được mà KHKK-runtime KHÔNG nghe (#78 config-lie class):
|
||||
- **A** — giữ 9 ô tick được (uniform tuyệt đối với ảnh; cờ lưu DB nằm chờ; risk: người cấu hình tưởng có hiệu lực).
|
||||
- **B** — port cho sống thật (W3; đổi hành vi duyệt KHKK; nặng + cần test per-cờ).
|
||||
- **C** — mờ ô chưa-wire + tooltip "chưa áp dụng cho KHKK" (tiền lệ CHÍNH TRANG NÀY: SkipToFinal đã disabled cho type-10 `:1292-1302`; + ô Ngưỡng CEO cùng cách; trung thực #78; port sau gỡ dần).
|
||||
|
||||
**D4 (lead quyết, reviewer chấm):** title panel = menu-label "Duyệt KHKK — Nhóm {g}" + phụ đề nghiệp vụ từ `khkkGroupMenuLabel(g)`.
|
||||
|
||||
**Acceptance (đo được, từ FE §5):**
|
||||
1. 8 leaf sidebar → 8 slice panel khác nhau (URL `/KhkkN{n}`, header đúng nhóm, card `KHKK-N{n} v01` + badge + 3 Bước + 4 nút + Lịch sử) — hết landing grid.
|
||||
2. Dialog Sửa-tại-chỗ: title `KHKK-N{n} v{NN} (giữ nguyên phiên bản)`, khối cam 9 checkbox đúng khuôn (theo D3), Drafter tick sẵn, rule-box.
|
||||
3. "Tạo phiên bản mới" nhóm N1 ⇒ v02 nằm ĐÚNG panel N1; **badge "Đang áp dụng" 7 nhóm khác KHÔNG mất** (W1a).
|
||||
4. Regression 4 route cũ + fe-user auto-pin + 645 test cũ xanh.
|
||||
|
||||
**Rollback:** 0 mig ⇒ revert commit là sạch; W1a có regression-test 2 chiều giữ hành vi cũ cho type ≠ 10.
|
||||
|
||||
## Nợ NGOÀI scope (khai, không lặng): GET Overview 0-policy lộ roster+9-flag cho mọi user authenticated (BE §5.2 — information-disclosure mềm, owner ngó sau) · roster 8 nhóm = snapshot email lúc seed, KHÔNG tự sync theo QT-DN-V2-001 (BE §5.3) · `Khkk_WfView` panel aggregate giấu 7/8 nhóm (FE §6.1 — slice này vá cho 8 leaf mới; re-point mục cũ = việc riêng) · comment "7 bảng" stale `:1120`.
|
||||
@ -0,0 +1,221 @@
|
||||
# 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**
|
||||
@ -0,0 +1,88 @@
|
||||
# sub-implementer-backend-0 — ĐỢT-1 lane BE (W0 + W1) — S182 YC-013
|
||||
|
||||
> Artifact ghi LIÊN TỤC trong lúc làm (chống-#53). Spec nguồn: `spec-khkk-awf-mirror-ncc-08-08-2026.md` MỤC 2.
|
||||
> Trạng thái: **IN-PROGRESS** (khởi tạo lúc bắt đầu lane).
|
||||
|
||||
## 0. Kế hoạch lane
|
||||
- W0.1 `dotnet ef database update` ×2 DB (Dev + Design) → kỳ vọng top MigrationId `20260806070242`
|
||||
- W0.2 boot API 1 lần → verify 8 row `KHKK-N1..8` type-10 IsActive=1
|
||||
- W0.3 `dotnet test SolutionErp.slnx` baseline TƯƠI → ghi `run.md`
|
||||
- W1a deactivate-per-Code type-10 (`ApprovalWorkflowV2AdminFeatures.cs:339-343`) — CÔNG THỨC R-6 KHOÁ
|
||||
- W1b file MỚI `ContractSigningPlanWorkflowGuards.cs` + gọi 3 site
|
||||
- W1b2 pin-check IsActive ∧ IsUserSelectable (D6)
|
||||
- W1c changelog type-10 (`:863-893`)
|
||||
- W1d DELETE guard (`:1193-1205`)
|
||||
|
||||
## 1. W0 — prep + đo nền
|
||||
|
||||
### W0.1 — `dotnet ef database update` ×2 DB — ✅ DONE
|
||||
🔴 **Khai lệch-có-lý-do #1: local có BA DB, không phải hai.** Đo trước khi áp (đúng luật "đo drift tại chỗ"):
|
||||
| DB | nguồn conn | trước | sau |
|
||||
|---|---|---|---|
|
||||
| `SolutionErp` | `appsettings.json:3` Default (**base — KHÔNG dùng ở dev**, bị `appsettings.Development.json:3` override) | ~Mig 53 (tụt 19 mig) | `20260806070242` |
|
||||
| `SolutionErp_Dev` | `appsettings.Development.json:3` = **DB runtime THẬT của API local** | `20260731085624` (Mig 71) | **`20260806070242`** ✅ |
|
||||
| `SolutionErp_Design` | `DesignTimeDbContextFactory.cs:15` (ef CLI mặc định) | `20260731085624` (Mig 71) | **`20260806070242`** ✅ |
|
||||
|
||||
- Spec nói "Dev + Design"; memory-pack Pattern-2 định nghĩa Dev = `SolutionErp_Dev` (explicit conn) ⇒ **2 DB spec đòi = `_Dev` + `_Design`, cả 2 ĐÃ lên Mig 72.**
|
||||
- Lượt chạy ĐẦU tôi trỏ nhầm `--connection` vào `SolutionErp` (base). **Khai thẳng, không giấu:** không phá gì (DB đó vốn tụt 19 mig và không ai đọc ở dev), nhưng nó **không phải** 1 trong 2 DB spec đòi ⇒ đã chạy lại đúng 2 DB. Nếu chỉ nhìn lượt đầu mà tuyên "xong W0.1" thì đó là **PASS giả**.
|
||||
- Lệnh đo: `sqlcmd -S "(localdb)\MSSQLLocalDB" -d <db> -Q "SELECT TOP 1 MigrationId FROM __EFMigrationsHistory ORDER BY MigrationId DESC"`
|
||||
|
||||
### W0.2 — boot API → 8 row `KHKK-N1..8` — ✅ PASS
|
||||
- Trước boot: `SELECT ... WHERE Code LIKE 'KHKK-N%'` trên `SolutionErp_Dev` = **0 row** (control âm — thước sống).
|
||||
- Boot: `ASPNETCORE_ENVIRONMENT=Development ASPNETCORE_URLS=http://localhost:5443 dotnet run --project src/Backend/SolutionErp.Api --no-launch-profile`, chạy tới khi seeder xong rồi **kill** (`Stop-Process` pid 55784).
|
||||
- Sau boot: **8/8** row `KHKK-N1..N8` · `ApplicableType=10` · `IsActive=1` · `IsUserSelectable=1` · **3 Bước / 9 Cấp mỗi row** ⇒ roster local resolve ĐỦ 11 email (không rơi vào ca "workflow 0 bước" mà `DbInitializer.cs:544-548` cho phép).
|
||||
- Seeder: `SeedKhkkGroupWorkflowsAsync` `DbInitializer.cs:549`, idempotent PER-CODE `:610` `if (await db.ApprovalWorkflows.AnyAsync(w => w.Code == code)) continue;` — **KHÔNG đụng** (memory-pack cấm).
|
||||
|
||||
### W0.3 — baseline test TƯƠI — ✅ `645 PASS (45D+600I)`
|
||||
- Lệnh: `dotnet test SolutionErp.slnx`, chạy SAU W0.1/W0.2, **0 sửa mã** tại thời điểm đo.
|
||||
- Domain 45 / 407 ms · Infrastructure 600 / 35 s · **Failed 0 · Skipped 0**.
|
||||
- Ghi `run.md` mục "Đo nền W0" (R-3). Trùng số lead đo @bootstrap (645) nhưng **đo lại độc lập**, không chép.
|
||||
- ⇒ Cửa W1/W4: FULL suite = **645 + 14 = 659**, 0 fail.
|
||||
|
||||
## 2. W1 — 5 fix
|
||||
|
||||
> 🔴 **Khai lệch-có-lý-do #2 — ARTIFACT NÀY TỪNG NÓI DỐI (lag).** Mục này đọc "_(chưa bắt đầu)_"
|
||||
> trong khi **W1a ĐÃ nằm trên đĩa** (`git diff` = +21/−4 đúng khối `:339`). Lane trước bị cắt
|
||||
> SAU khi Edit land nhưng TRƯỚC khi ghi artifact ⇒ đúng lỗ hổng mà "ghi-đĩa-từng-phần" sinh ra
|
||||
> để bịt. Lane tiếp đã **TIN DIFF, không tin artifact** (lệnh lead). Bài học ghi §4.
|
||||
|
||||
### W1a — deactivate-per-Code cho type-10 — ✅ DONE (đã land từ lượt trước, verify lại lượt này)
|
||||
- Site: `ApprovalWorkflowV2AdminFeatures.cs:339-359` (khối `var actives = ...`).
|
||||
- Công thức **ĐÚNG R-6**, verify từng vế trên đĩa:
|
||||
`typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan ? Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive) : Where(w => w.ApplicableType == typeEnum && w.IsActive)`
|
||||
⇒ 3 vế đủ, **0 regex** trong nhánh này (grep `KHKK-N` trong file: chỉ 1 hit và nằm trong
|
||||
COMMENT giải thích lệnh cấm, không phải mã chạy — *use ⟂ mention*).
|
||||
- Comment `:339` cũ "only ONE active per type" **đã sửa** (F12d): thay bằng khối 2 gạch đầu
|
||||
dòng type-10 per-CODE / loại khác per-TYPE + ghi chú `nextVersion :334-337` vốn đã per-Code.
|
||||
|
||||
### W1b — file MỚI `ContractSigningPlanWorkflowGuards.cs` + gọi 3 site — ✅ DONE
|
||||
- **File mới:** `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanWorkflowGuards.cs`
|
||||
— `public static class`, 2 method: `ParseGroup(string?)` + `EnsureWorkflowGroupMatchAsync(IApplicationDbContext, Guid, int, CancellationToken)`.
|
||||
Thân = **MOVE nguyên** logic rào (v) cũ; **GIỮ semantics cho-qua-code-ngoài-họ** (viết thành
|
||||
doc-comment nêu lý do, để không ai "sửa" thành chặn).
|
||||
- **Site 1 Create** `ContractSigningPlanFeatures.cs:475-489` — 14 dòng regex inline XOÁ, thay bằng 1 call.
|
||||
- **Site 2 UpdateDraft** `:617-630` — rào MỚI (cửa này trước KHÔNG có), đặt TRONG nhánh `awId != plan.ApprovalWorkflowId`.
|
||||
- **Site 3 SubmitAsync** `ContractSigningPlanWorkflowService.cs:189-197` — chèn SAU rào hạng-mục
|
||||
(`okCount != catalogIds.Count` `:185-187`) và NGAY TRƯỚC `LoadStepsAsync` `:198`. **Điểm chèn đã
|
||||
verify tại chỗ** đúng lệnh (spec đoán `:169-187`; số thật sau khi chèn = `:189`). Thêm
|
||||
`using SolutionErp.Application.ContractSigningPlans;` vào service.
|
||||
- Message 409 dùng **nguyên văn spec**, 1 message chung 3 site.
|
||||
|
||||
### W1b2 (D6) — pin-check `IsActive ∧ IsUserSelectable` — ✅ DONE
|
||||
- Helper `ContractSigningPlanScope.EnsureWorkflowPinnableAsync` (`ContractSigningPlanFeatures.cs:322-347`)
|
||||
— mirror vá-3 `CreateContractFromSigningPlanFeatures.cs:141-147`, **2 message TÁCH** ("đã ngừng
|
||||
hoạt động" / "chưa ghim") đúng lý do vá-3 nêu.
|
||||
- Gọi 2 site: Create sau rào (iv) (`:478`) · UpdateDraft **CHỈ** trong nhánh `awId != plan.ApprovalWorkflowId` (`:626`).
|
||||
- Giữ-nguyên-pin KHÔNG check (phiếu cũ pin bản archived vẫn sửa được GhiChu) — đúng D6.
|
||||
|
||||
### W1c — changelog PUT type-10 — ✅ DONE
|
||||
_(chi tiết ghi ngay dưới, sau khi land)_
|
||||
|
||||
### W1d — DELETE usage-check → 409/404 — ✅ DONE
|
||||
_(chi tiết ghi ngay dưới, sau khi land)_
|
||||
|
||||
## 3. Verify
|
||||
_(chưa)_
|
||||
|
||||
## 4. Ambiguity / lệch spec khai
|
||||
_(chưa)_
|
||||
@ -0,0 +1,205 @@
|
||||
# sub-implementer-frontend-1 — S182 W2 (a-h) lane FE
|
||||
|
||||
> ⚠️ TÊN FILE: TASK text ghi `sub-implementer-frontend-0.md`, RUN-TRACE block + structured-output ghi `sub-implementer-frontend-1.md`.
|
||||
> Chọn `-1` (bản RUN-TRACE = bản em main đọc qua `subMdPath`). Khai ở đây để lead biết.
|
||||
|
||||
## §0 — Trạng thái lượt (append liên tục, chống-#53)
|
||||
- [x] Đọc SPEC FINAL MỤC 2 W2 (a-h) + MỤC 1 R-1..R-6
|
||||
- [x] Đọc file đích + đo tiền-đề từ ĐĨA (line-number trong spec có thể lệch)
|
||||
- [x] W2a parse kind
|
||||
- [x] W2b synthetic slice
|
||||
- [x] W2c lockedCode
|
||||
- [x] W2d D3=C 6 ô + #4
|
||||
- [x] W2e menuKeys ×2 app
|
||||
- [x] W2f ngưỡng CEO
|
||||
- [x] W2g auto-pin isActive ×2 app
|
||||
- [x] W2h comment + subtitle
|
||||
- [x] build fe-admin — exit 0
|
||||
- [x] build fe-user — exit 0
|
||||
|
||||
**Lượt 2 (vá-nốt):** chẩn-đoán TS trong TASK là **ảnh cũ** — đo lại từ đĩa thì đã sạch ⇒ **0 dòng code sửa**, lượt này là ĐO + CHỨNG. Chi tiết §3.3.
|
||||
- [x] tsc thật ×2 app (lệnh trong spec là phép đo CÂM — §3.1)
|
||||
- [x] control-dương chứng tsc có răng (§3.2)
|
||||
- [x] proof-harness 27/27 + anti-Goodhart (§3.4)
|
||||
- [x] đối chiếu code-vs-spec W2a-h (§3.5)
|
||||
- [x] mirror proof SHA + 4-place (§3.6)
|
||||
|
||||
## §1 — Đo tiền-đề từ đĩa (TRƯỚC khi gõ)
|
||||
|
||||
| # | Tiền-đề spec | Đo trên đĩa | Verdict |
|
||||
|---|---|---|---|
|
||||
| P1 | `:164-172` TYPE_CODE_TO_INT | `ApprovalWorkflowsV2Page.tsx:164-172` ĐÚNG (`TYPE_INT_CONTRACT_SIGNING_PLAN=10` `:149`) | HELD |
|
||||
| P2 | `:299-301` currentType | `:299-301` `overview.data?.types.find(...)` ĐÚNG | HELD |
|
||||
| P3 | `:357/:620/:650-651/:798/:920-933` | `TypePanel :357` · `Designer :620` · `defaultCode :650-651` · POST code `:798` · ô Mã `:920-933` — ĐÚNG cả 5 | HELD |
|
||||
| P4 | 6 ô D3 `:1235-1266`, `:1268-1276`, `:1277-1285`, `:1320-1328` | #1 `:1232-1240` · #2 `:1241-1249` · #3 `:1250-1258` · #4 `:1259-1267` · #5 `:1268-1276` · #6 `:1277-1285` · #7 `:1292-1302` · #8 `:1307-1315` · #9 `:1320-1328` | HELD (số ô #1-#4 lệch ±3 dòng so spec, khớp nội dung) |
|
||||
| P5 | `:944-961` ngưỡng CEO | `:943-961` ĐÚNG | HELD |
|
||||
| P6 | `menuKeys.ts:31-32` = 2 const `AwV2_*` | fe-admin `:31-32` + fe-user `:31-32` ĐÚNG, 0 key KhkkN | HELD |
|
||||
| P7 | BE const để mirror | `MenuKeys.cs:127-134` `AwV2_KhkkN1..8` (VÀO `All`) | HELD |
|
||||
| P8 | 4-place Layout staticMap | `fe-admin/Layout.tsx:188` `/^KhkkN[1-8]$/` ĐÃ CÓ (K3 S164/S165) ⇒ **chỗ-4 KHÔNG cần chạm** | HELD |
|
||||
| P9 | `khkkGroupWorkflowCode` / `khkkGroupMenuLabel` | `types/khkk.ts:159-168`, 2 app SHA identical `b26bf674…` | HELD |
|
||||
| P10 | 🔴 spec W2g *"2 file KhkkCreatePage vốn khác nhau — không cần byte-equal"* | **SAI**: `sha256sum` 2 file = `332986ae6bf1904a4db434c796164caa2768beaaebdc7843dd2b1cdd729d17da` **BẰNG NHAU** + `diff` 0 dòng | **LỆCH** → sửa mirror BYTE-EQUAL, giữ SHA-pair |
|
||||
| P11 | `WorkflowDto` (local) có `isActive`? | `KhkkCreatePage.tsx:57-62` = `{id, code, name, isUserSelectable}` — **THIẾU `isActive`** | phải thêm field (BE có: `DefinitionDto.isActive :79`) |
|
||||
| P12 | 🔴 spec W2h *"thêm ~1 prop `subtitle`"* | `components/PageHeader.tsx:3-11` chỉ có `title/description/actions` (bài S37 lặp lần 3) | **LỆCH** → xem §4 FLAG-1 |
|
||||
| P13 | build = phép thử TS thật | `package.json` build = `tsc -b && vite build`, `noUnusedLocals`+`noUnusedParameters`+`erasableSyntaxOnly` ON | HELD |
|
||||
|
||||
## §2 — Thay đổi từng task (đã gõ xong, chờ build)
|
||||
|
||||
**File chạm (4):**
|
||||
1. `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` (a,b,c,d,f,h)
|
||||
2. `fe-admin/src/lib/menuKeys.ts` (e)
|
||||
3. `fe-user/src/lib/menuKeys.ts` (e)
|
||||
4. `fe-admin/src/pages/khkk/KhkkCreatePage.tsx` + `fe-user/…` (g, `cp` → SHA-pair)
|
||||
|
||||
### W2a — parse kind
|
||||
- `khkkGroupMatch = typeCode?.match(/^KhkkN([1-8])$/) ?? null` → `khkkGroup: number|null`
|
||||
- `selectedTypeInt = khkkGroup !== null ? TYPE_INT_CONTRACT_SIGNING_PLAN : typeCode ? TYPE_CODE_TO_INT[typeCode] : null`
|
||||
- queryKey GIỮ NGUYÊN (`['approval-workflow-v2-overview', selectedTypeInt]`) ⇒ 8 panel nhóm + panel type-10 **dùng chung 1 ô cache**, invalidate làm tươi cả 9. Khai trong comment.
|
||||
- Regex CHUỖI (không `Number()`) — `KhkkN03` không lọt (bài học L1 #11).
|
||||
- +import `{ khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'` (không chép chuỗi `KHKK-N`).
|
||||
|
||||
### W2b — synthetic slice
|
||||
- Tách `typeBucket` (raw BE) ⟂ `currentType` (lát cắt). `useMemo([khkkGroup, typeBucket])`.
|
||||
- `versions = typeBucket.history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v=>v.isActive) ?? null` · **KHÔNG đọc `typeBucket.active`** (1-trong-8).
|
||||
- Giữ shape `TypeSummaryDto` ⇒ TypePanel/Designer không biết mình đang xem nhóm hay loại.
|
||||
|
||||
### W2c — lockedCode xuyên suốt
|
||||
- Page: `lockedCode = khkkGroup !== null ? khkkGroupWorkflowCode(khkkGroup) : null`
|
||||
- `TypePanel({type, lockedCode, onSaved})` → `<Designer lockedCode={lockedCode} …>`
|
||||
- `defaultCode = lockedCode ?? DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'` — ô hiện ĐÚNG mã sẽ gửi (không hiện `QT-KHKK-V2-001` rồi gửi `KHKK-N3`).
|
||||
- Ô Mã `disabled={isEdit || lockedCode !== null}` + hint "Mã khoá theo nhóm — version tự tăng."
|
||||
- POST `code: lockedCode ?? code` (bất-biến theo CẤU TRÚC, không dựa vào input disabled).
|
||||
- **+`key={typeCode ?? 'all'}` trên `<TypePanel>`** — NGOÀI spec, lý do: chuyển N1→N3 khi Designer đang mở thì `cloneFrom/editTarget` (state trong TypePanel) còn trỏ bản N1 trong khi `lockedCode` đã là N3 ⇒ POST đẻ `KHKK-N3` mang ruột N1. Remount xoá state. Khai ở đây.
|
||||
|
||||
### W2d — D3=C danh-sách-chốt
|
||||
Cờ chung `const isKhkk = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` (1 nguồn, 8 chỗ dùng).
|
||||
| Ô | Cờ | Xử lý |
|
||||
|---|---|---|
|
||||
| #1 | allowReturnOneLevel | `disabled={isKhkk}` + title chung + **`checked={entry.…}` GIỮ** + mờ text |
|
||||
| #2 | allowReturnOneStep | ↑ |
|
||||
| #3 | allowReturnToAssignee | ↑ |
|
||||
| #4 | allowReturnToDrafter | `disabled` + `checked` THẬT + **title RIÊNG** "KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh" + **KHÔNG mờ** (giữ `text-slate-700`) |
|
||||
| #5 | allowApproverEditDetails | như #1 |
|
||||
| #6 | allowApproverEditBudget | như #1 |
|
||||
| #7 | allowApproverSkipToFinal | **GIỮ hành vi** (kể cả `checked` ép-false của K3 S165); chỉ thay 3 biểu thức trùng → `isKhkk`, giá trị y hệt |
|
||||
| #8 | allowApproverFinalize | **SỐNG** — 0 chạm |
|
||||
| #9 | allowApproverDelete | như #1 (rose→slate khi mờ) |
|
||||
- `levelFlagsPayload` **0 dòng đổi** (echo state thật).
|
||||
- +1 dòng giải thích trong hộp tím (chỉ khi `isKhkk`) — nói rõ ô mờ vẫn hiện đúng giá trị DB, và 2 ô còn tác dụng.
|
||||
|
||||
### W2e — menuKeys ×2 app
|
||||
+8 const `AwV2_KhkkN1..8` mirror `MenuKeys.cs:127-134`, sau `:32` cả 2 app. Comment viết theo app (fe-admin trỏ `Layout.tsx:188`; fe-user khai "cố ý không có Designer") — **cố ý lệch chữ, không lệch const**.
|
||||
|
||||
### W2f — Ngưỡng CEO
|
||||
`disabled={isEdit || isKhkk}` + helper-text "KHKK không dùng ngưỡng máy — NĐUQ quyết…" + POST `ceoApprovalThreshold: isKhkk ? null : (…)`.
|
||||
|
||||
### W2g — auto-pin lọc isActive ×2 app
|
||||
- `WorkflowDto` (local, `:57-62`) **+`isActive: boolean`** — BE có (`AwAdminOverviewDto`, đối chiếu `DefinitionDto.isActive`).
|
||||
- `find(w => w.isActive && code-match)`; filter `isUserSelectable` ở query GIỮ.
|
||||
- Sửa 2 câu thông báo cho khớp điều-kiện MỚI (3 vế: đúng mã ∧ đang áp dụng ∧ đã ghim) — nếu không sẽ nói "chưa có quy trình" trong khi nó đang nằm đó ở bản archived (sai-thầm-lặng).
|
||||
- `cp` fe-admin → fe-user, SHA-pair `89dfa607…` **BẰNG NHAU**.
|
||||
|
||||
### W2h — comment + tiêu đề
|
||||
- Vá comment `:641-644`: câu cũ "2 ô duyệt sớm sống cho MỌI loại" **SAI** — #7 vẫn khoá cho type-10 từ K3 S165. Viết lại + trỏ tại-chỗ.
|
||||
- PageHeader: title = `Quy trình duyệt (Mới): {khkkGroupMenuLabel(g)}` · description = phụ đề nghiệp vụ (loại + nhóm N{n} + mã khoá + "7 nhóm còn lại không đổi"). Xem FLAG-1.
|
||||
|
||||
## §3 — Verify (build ×2 + proof)
|
||||
|
||||
### 3.1 🔴 Lệnh acceptance trong spec/TASK là PHÉP ĐO CÂM — phải đổi
|
||||
`npx tsc --noEmit` (nguyên văn acceptance R-2 + TASK lượt 2) chạy ra **EXIT=0, 0 dòng output** ở CẢ 2 app **kể cả khi code sai**: `fe-{admin,user}/tsconfig.json` là solution-style `{"files": [], "references": [...]}` ⇒ tsc **không biên dịch file nào** rồi thoát 0. Xanh vì CÂM, không phải vì đúng.
|
||||
**Lệnh THẬT** (đã dùng dưới đây): `npx tsc -p tsconfig.app.json --noEmit` — hoặc `tsc -b` (chính `npm run build`).
|
||||
|
||||
| # | Lệnh | Exit | Ghi chú |
|
||||
|---|---|---|---|
|
||||
| V1 | `fe-admin$ npx tsc -p tsconfig.app.json --noEmit` | **0** | 114 file `src/` trong tập biên dịch (`--listFiles`), có `ApprovalWorkflowsV2Page.tsx` + `types/khkk.ts` |
|
||||
| V2 | `fe-user$ npx tsc -p tsconfig.app.json --noEmit` | **0** | |
|
||||
| V3 | `fe-admin$ npm run build` (`tsc -b && vite build`) | **0** | ✓ 1964 modules · `index-CmEoYZiH.js` 1.761,63 kB |
|
||||
| V4 | `fe-user$ npm run build` | **0** | ✓ 1953 modules · `index-CT5Y_xQ4.js` 1.677,49 kB |
|
||||
|
||||
V3/V4 chạy **SAU** khi đã hoàn tác tiêm-lỗi ⇒ đo đúng trạng thái cuối trên đĩa.
|
||||
Warning PRE-EXISTING (không phải nợ mới): chunk >500 kB · `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` · CSS `@import` order.
|
||||
|
||||
### 3.2 Control-DƯƠNG — chứng phép đo có RĂNG
|
||||
Tiêm `const __ctrl_neg: number = khkkGroupWorkflowCode(3)` vào cuối chính file đã ship → tsc nổ **2 lỗi**:
|
||||
`TS2322 Type 'string' is not assignable to type 'number'` + **`TS6133 '__ctrl_neg' is declared but its value is never read`**.
|
||||
🔴 Ý nghĩa: **TS6133 (`noUnusedLocals`) CÒN SỐNG** — đúng class lỗi mà 3 chẩn-đoán "declared-never-read" (`lockedCode :417` · `isKhkk :714` · `khkkNotYetHint :717`) thuộc về. Nên "0 lỗi" ở V1 = **thật sự hết**, không phải đo hụt.
|
||||
Hoàn tác: SHA trước/sau `dda4be1f9d4dabd…b69b7f71` **BẰNG NHAU** (byte-identical).
|
||||
|
||||
### 3.3 🔴 Chẩn-đoán trong TASK là ẢNH CŨ — 0 dòng code phải vá
|
||||
6 lỗi TS mà TASK liệt (thiếu import `khkkGroupWorkflowCode`/`khkkGroupMenuLabel` `:330/:343/:355` · `lockedCode` chưa khai ở signature `:403/:517` · 3 biến never-read) **KHÔNG còn tồn tại trên đĩa**: `import { khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'` ở `:36`; `lockedCode` khai đủ ở CẢ 2 signature (`TypePanel :417/:421` · `Designer :690/:699`); 3 biến đều có chỗ dùng thật (`isKhkk` 20 call-site, `khkkNotYetHint` 6, `lockedCode` 6).
|
||||
⇒ Đây là **IDE-diagnostic chụp giữa lúc lane trước đang gõ** (gotcha #68). Lượt này **KHÔNG sửa 1 dòng code nào** — chỉ đo + chứng. `git diff --stat` giữ đúng **5 file**, 0 file untracked.
|
||||
|
||||
### 3.4 Proof-harness — chạy CHÍNH text đã ship (extract-by-anchor, neo lệch thì script tự chết)
|
||||
`node proof-w2.mjs` → **27 PASS / 0 FAIL**:
|
||||
- **T1 (8/8)** dây xuyên suốt `AwV2_KhkkN{n}` → `Layout.tsx:188` → `/system/approval-workflows-v2/KhkkN{n}` → regex page parse nhóm `{n}` → `lockedCode = KHKK-N{n}`, đối chiếu đủ 3 nguồn BE+admin+user.
|
||||
- **T2 (7/7) control-ÂM:** `KhkkN0` · `KhkkN9` · **`KhkkN03`** · `KhkkN10` · `khkkn1` · `"KhkkN1 "` · `KhkkN` — bị chặn ở CẢ 2 regex (regex CHUỖI, không `Number()`).
|
||||
- **T3 (2/2)** control-âm cho chính T1: key bịa `AwV2_KhkkN9/N0` vắng ở cả 3 nguồn.
|
||||
- **T4/T5/T6 (10/10)** D3=C — xem 3.5.
|
||||
|
||||
**Anti-Goodhart** (`proof-control.mjs`, exit 0): áp ĐÚNG vị ngữ T4 và T5 lên ô **#7** (ô cố ý khoá + ép-false từ K3 S165) ⇒ **cả hai đều `false`**. Phép đo phân biệt được ô-sống ⟂ ô-khoá và bắt được ép-false; không phải luôn-luôn-PASS.
|
||||
|
||||
### 3.5 Đối chiếu code-vs-spec W2a–h
|
||||
| Task | Verdict | Bằng chứng |
|
||||
|---|---|---|
|
||||
| W2a parse kind | ✅ | `:302-306` regex chuỗi; queryKey giữ nguyên (9 panel chung 1 ô cache — khai tại `:300-301`) |
|
||||
| W2b synthetic slice | ✅ **và là BẮT BUỘC** | BE `active = versions.FirstOrDefault(v => v.IsActive)` (`:210`) — với type-10 nay có **8 bản active song song** (BE lane đổi sang tắt **per-CODE** `:336-360`) ⇒ đọc `bucket.active` sẽ sai cho 7/8 panel. `history` xác nhận **KHÔNG lọc** `IsActive` (`definitions` `:118-134` chỉ lọc Type + IsUserSelectable) |
|
||||
| W2c lockedCode | ✅ | signature 2 nơi · `defaultCode :733` · ô Mã `disabled :1020` · **POST `code: lockedCode ?? code` `:884`** (bất-biến theo CẤU TRÚC). Khớp khoá với vị ngữ `w.Code == request.Code` bên BE |
|
||||
| W2d D3=C | ✅ **đúng danh-sách-chốt** | 6 ô `disabled={isKhkk}` + `checked={entry.…}` THẬT · #4 tooltip riêng + KHÔNG mờ · #7 giữ nguyên · **#8 `allowApproverFinalize` 0 chạm** (T4) · `levelFlagsPayload` 0 dòng đổi |
|
||||
| W2e menuKeys ×2 | ✅ | 8 const khớp `MenuKeys.cs:127-134`; khối const 2 app **diff 0 dòng**, chỉ comment cố ý lệch |
|
||||
| W2f ngưỡng CEO | ✅ | `disabled={isEdit \|\| isKhkk} :1052` + helper-text `:1055` + **POST `null` `:891`** |
|
||||
| W2g auto-pin isActive | ✅ | `isActive` **CÓ THẬT** trong payload (`ToDto` `:176`) — không phải field bịa; 2 câu thông báo đổi sang nói ĐIỀU KIỆN (rỗng-vì-lọc ⟂ rỗng-vì-chưa-có) |
|
||||
| W2h comment + tiêu đề | ✅ | subtitle gộp vào `description` (xem FLAG-1) |
|
||||
|
||||
### 3.6 Mirror proof (Pattern 16-bis + SHA)
|
||||
- `KhkkCreatePage.tsx` admin ⟂ user: `89dfa607061cd3d560dcbbdb8fbad157d38cae1786be3cd41a81bd8e9c1d1390` **BẰNG NHAU** (cặp `cp`, W2g).
|
||||
- `types/khkk.ts` admin ⟂ user: `b26bf67492821b1003c3d523585e2ce563a26b04ef728867689911263e5a33d3` **BẰNG NHAU** — file **không sửa** ⇒ **đối chứng DƯƠNG** cho phép đo hash.
|
||||
- **4-place:** ① page+types ✅ · ② `App.tsx:80` route `:typeCode` đã có (0 route mới) ✅ · ③ `menuKeys.ts` ×2 ✅ · ④ `Layout.tsx:188` staticMap **đã có sẵn từ K3 S164/S165** ⇒ chỗ-4 KHÔNG cần chạm (P8 đúng), regex Layout khớp regex page.
|
||||
- **Lệch CÓ CHỦ ĐÍCH:** fe-user **không có** `ApprovalWorkflowsV2Page.tsx` + **0** nhánh `AwV2` trong `Layout.tsx` (màn cấu hình là của admin) ⇒ 8 const bên fe-user là mirror hằng-số thuần, không sinh menu.
|
||||
|
||||
## §4 — Nợ / FLAG cho lead
|
||||
|
||||
- 🔴 **FLAG-A (acceptance hỏng — cần sửa spec):** acceptance R-2 ghi *"`npm run build` ×2 app + **tsc 0-err**"*. Nếu ai chạy `npx tsc --noEmit` đúng nguyên văn thì **luôn xanh kể cả code sai** (§3.1). Đề nghị sửa acceptance thành `npx tsc -p tsconfig.app.json --noEmit`. Ảnh hưởng **mọi wave FE trước/sau**, không riêng S182.
|
||||
- 🔴 **FLAG-B (spec tự mâu-thuẫn — đã chọn nhánh, xin lead ratify):** ô **#4 `allowReturnToDrafter`**. MỤC 2 dòng `d` (bảng thi-hành, có neo dòng) ghi *"hiển thị **state thật (thường TRUE)**"*; MỤC 3 bảng D3 ghi bản rút gọn *"#4 **forced-true**-disabled"*. Code đang theo **state thật** (`checked={entry.allowReturnToDrafter}`), vì:
|
||||
(1) đo đĩa: mặc định `true` ở **4 lớp độc lập** — `ApprovalWorkflow.cs:129 = true` · EF `HasDefaultValue(true)` · DTO `:232/:465` · FE hydrate `?? true` (`:213`) ⇒ thực tế **vẫn hiện TICK**, thoả acceptance A2 *"ô #4 tick-disabled"*;
|
||||
(2) hard-code `checked={true}` sẽ **nói dối** đúng ca hàng row có `false` — cùng class lỗi mà chính D3=C cấm ở 6 ô kia (*"CẤM ép false"*), chỉ đảo dấu.
|
||||
⇒ Đọc *"forced-true"* là **rút gọn hao nghĩa** của *"state thật (thường TRUE)"*. **Nếu lead thật sự muốn ép cứng `true`** thì nói 1 câu, sửa 1 dòng — nhưng nên biết là đánh đổi tính trung thực với DB.
|
||||
- ⚠️ **FLAG-1 (P12, đã xử lý — không còn là nợ):** `@/components/PageHeader` **không có** prop `subtitle` (`title/description/actions`, cả 3 đều `ReactNode`). **Không thêm prop** — phụ đề nghiệp vụ gộp vào `description`. Lý do: PageHeader dùng chung toàn app, đẻ prop cho 1 trang là mở rộng API cho cả trăm call-site. (Bài S37, lặp lần 3 — mỗi lần lại tưởng có `subtitle`.)
|
||||
- ⚠️ **Ngoài spec, đã khai:** `key={typeCode ?? 'all'}` trên `<TypePanel>` (`:401`) — remount khi đổi nhóm; thiếu nó thì `cloneFrom/editTarget` còn trỏ bản N1 trong khi `lockedCode` đã sang N3 ⇒ POST đẻ `KHKK-N3` mang ruột N1.
|
||||
- ⚠️ **Nợ nhận-thức (không chặn):** 8 panel nhóm + panel `/ContractSigningPlan` **dùng CHUNG 1 ô cache** (queryKey không có `khkkGroup` — cố ý, vì lọc client-side trên payload đã tải trọn). Invalidate sau khi lưu làm tươi cả 9. Nếu sau này BE nhận `code=` làm tham số **request** thì BẮT BUỘC thêm `code` vào queryKey, nếu không 9 màn dùng chung cache sai CÂM.
|
||||
- ⚠️ **Chưa chạy (ngoài tay lane FE):** acceptance A1/A2b/A3/A4 là **UAT động** (bấm thật trên 8 leaf, sửa-tại-chỗ ⇒ 204, tạo v02 N1 kiểm 7 nhóm không mất badge). Lane này chỉ chứng được phần **tĩnh**. A3 phụ thuộc BE lane (tắt active per-CODE) — 2 nửa phải test CÙNG nhau.
|
||||
- ✅ **Không nợ build/mirror:** 0 lỗi TS ×2 app · SHA-pair 2/2 · 4-place 4/4 · 27/27 proof + 3 control.
|
||||
|
||||
## §5 — vá MAJOR-1 (reviewer pre-commit, lượt sau)
|
||||
|
||||
**Bệnh:** `title=` nằm trên chính `<input type="checkbox" disabled={isKhkk}>`. Chromium (Chrome/Edge = trình duyệt đích của owner) **nuốt sự kiện chuột của form-control disabled** ⇒ tooltip KHÔNG bao giờ bật; `<label>` bao ngoài không mang `title` nên rê vào chữ cũng câm. Quyết định D3=C của owner là *"mờ ô chưa nghe **+ tooltip**"* ⇒ mất vế tooltip thì admin chỉ còn 7 ô xám vô cớ. Nặng nhất là ô **#4 `allowReturnToDrafter`**: câu *"KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh"* là thông tin **DUY NHẤT** phân biệt #4 với 6 ô "chưa áp dụng" — câm ⇒ #4 trông y hệt 6 ô kia = **ngược nghĩa**.
|
||||
|
||||
**(a) Dời `title` từ `<input>` lên `<label>` — 8/8 site** (label không disabled nên hover chữ là hiện):
|
||||
|
||||
| # | cờ | title cũ ở input | title mới ở label |
|
||||
|---|---|---|---|
|
||||
| 1 | `allowReturnOneLevel` | :1361 | :1366 (label @:1364) |
|
||||
| 2 | `allowReturnOneStep` | :1372 | :1379 (@:1377) |
|
||||
| 3 | `allowReturnToAssignee` | :1383 | :1392 (@:1390) |
|
||||
| 4 | `allowReturnToDrafter` | :1399 | :1410 (@:1408) |
|
||||
| 5 | `allowApproverEditDetails` | :1410 | :1432 (@:1430) |
|
||||
| 6 | `allowApproverEditBudget` | :1421 | :1445 (@:1443) |
|
||||
| 7 | `allowApproverSkipToFinal` | :1443 ← **tiền lệ CÙNG BỆNH từ K3**, reviewer chỉ đích danh | :1469 (@:1467) |
|
||||
| 9 | `allowApproverDelete` | :1471 | :1499 (@:1497) |
|
||||
|
||||
**(b) RIÊNG ô #4 — thêm chữ NHÌN THẤY ĐƯỢC** (`fe-admin/.../ApprovalWorkflowsV2Page.tsx:1421-1429`, ngay dưới `</label>` #4 @:1420): `{isKhkk && <div className="col-start-2 -mt-0.5 pl-4 text-[10px] leading-snug text-slate-500">{khkkDrafterHint}</div>}`. Thông tin phân-biệt **không được phụ thuộc hover**. Dùng `col-start-2` (không phải sibling thường) vì #4 nằm ô **hàng 2 cột 2** của `grid grid-cols-2`: sibling auto-place sẽ rơi xuống **cột 1** ⇒ đọc nhầm sang ô #3 và đẩy lệch 5 ô `col-span-2` phía dưới. `col-start-2` ghim dòng chữ đúng dưới ô của nó; 5 ô `col-span-2` kế tiếp vẫn auto-place xuống hàng mới, 0 reflow.
|
||||
|
||||
**Chuỗi #4 gom thành 1 const** `khkkDrafterHint` (`:722`, cạnh `khkkNotYetHint`) vì nay dùng ở **2 chỗ** (tooltip + chữ hiện) — 2 bản literal sẽ trôi lệch nhau. Kèm comment tại `:715-721` ghi rõ **vì sao** `title` phải ở label, để lần sau không ai dời ngược lên input rồi làm câm lại.
|
||||
|
||||
**Vùng CẤM — đo lại sau khi vá, xác nhận nguyên vẹn:**
|
||||
- 9/9 `checked=` **byte-identical** với bản trước khi vá (`:1371 :1384 :1397 :1415 :1437 :1450 :1474 :1488 :1504`) — gồm cả `checked={isKhkk ? false : entry.allowApproverSkipToFinal}` (quyết định K3, giữ) và `checked={entry.allowReturnToDrafter}` (**FLAG-B §4 vẫn treo y nguyên, lượt này KHÔNG động**).
|
||||
- Ô **#8 `allowApproverFinalize`** (`:1484-1492`): **0** `disabled`, **0** `title` — không chạm.
|
||||
- `levelFlagsPayload` (`:258`, call `:880/:920`): không chạm. `isKhkk` (`:714`): không chạm.
|
||||
|
||||
**Proof — checker có RĂNG (control-âm):** script parse tag-owner của mọi `title=` trong file (`scratchpad/check-title-owner.js`).
|
||||
- Bản đã ship: `title=` tổng **22** · tham chiếu `isKhkk` **8** · owner=`<label>` **8/8** · owner=`<input>` **0**.
|
||||
- **CONTROL-ÂM:** tiêm lại 1 `title` vào `<input>` ô #1 ⇒ checker **nổ đúng 1** (`owner=<input> :1373`). ⇒ 0-hit ở bản thật là **im-vì-đúng**, không phải im-vì-câm.
|
||||
|
||||
**Build:** `npx tsc --noEmit -p tsconfig.app.json` → **exit 0** (🔸 dùng `-p`; `npx tsc --noEmit` TRẦN biên dịch 0 file = xanh giả — đúng FLAG-A §4). `npm run build` fe-admin → **✓ built in 30.18s, 0 lỗi TS**; warning `>500 kB` + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` + `@import` = **PRE-EXISTING**.
|
||||
|
||||
**Mirror:** `ApprovalWorkflowsV2Page.tsx` **chỉ tồn tại ở fe-admin** (fe-user: file không có, `ls pages/system | grep -i workflow` = 0, `grep khkkNotYetHint|khkkDrafterHint fe-user/src` = **0 hit**) ⇒ **không có cặp để mirror**, đúng lệch-có-chủ-đích đã khai §3.6. Lượt này chạm **đúng 1 file**.
|
||||
|
||||
**⚠️ FLAG-C (cùng lớp, KHÔNG chạm — xin lead quyết):** grep cả file thấy **1 site nữa cùng bệnh** ngoài phạm vi MAJOR-1: nút **"+ Thêm NV"** (`:1226-1248`) có `disabled={addDisabled}` + `title=` giải thích **lý do bị khoá** (*"Cấp N-1 phải có ≥1 NV trước"* / *"Hết NV khả dụng…"*) đặt **trên chính `<button disabled>`** ⇒ cùng Chromium suppress, admin không biết vì sao nút xám. Đây là bug **có sẵn từ trước S182**, không thuộc D3=C, và TASK ghi rõ *"1 việc DUY NHẤT — KHÔNG làm gì khác"* ⇒ **để nguyên, chỉ khai**. (Hai nút `title="Lên"/"Xuống"` `:1155/:1164` cũng disabled ở rìa nhưng title chỉ là **nhãn**, không mang thông tin lý-do ⇒ không cùng lớp thiệt hại.)
|
||||
@ -0,0 +1,119 @@
|
||||
# INVEST-BE — KHKK 8 nhóm workflow admin mirror DuyetNcc (S182, Fable deep-pass)
|
||||
|
||||
> Trạng thái: ĐANG ĐO — đổ ruột liên tục. Mỗi claim kèm file:line.
|
||||
|
||||
## §1 Endpoint map trang approval-workflows-v2 (admin)
|
||||
|
||||
**Controller duy nhất:** `src/Backend/SolutionErp.Api/Controllers/ApprovalWorkflowsV2Controller.cs` — route `api/approval-workflows-v2` (:17), class-level `[Authorize]` trần (:18).
|
||||
|
||||
| Action | Verb+Route | Authz | file:line |
|
||||
|---|---|---|---|
|
||||
| Overview (list all types + versions + history) | `GET ?applicableType=&isUserSelectable=` | **authenticated-only, 0 policy** (CỐ Ý — Drafter cần list để pick lúc create, comment :10-15) | :21-26 |
|
||||
| Tạo version mới | `POST` | `Workflows.Create` | :28-34 |
|
||||
| Sửa-tại-chỗ (giữ Id/Version/IsActive) | `PUT {id}` | `Workflows.Create` | :45-51 |
|
||||
| Ghim/Bỏ ghim (`IsUserSelectable`) | `PATCH {id}/user-selectable` | `Workflows.Create` | :56-62 |
|
||||
| Xoá version | `DELETE {id}` | `Workflows.Create` | :64-70 |
|
||||
|
||||
**Tham số phân loại = `ApplicableType` (int enum), KHÔNG code-prefix.** Enum `ApprovalWorkflowApplicableType` @ `Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:53-71`: DuyetNcc=1 · DuyetNccPhuongAn=2 · Contract=3 · Proposal/Leave/Ot/Vehicle/ItTicket/Travel=4-9 · **ContractSigningPlan=10** (:70). Trang DuyetNcc = `?applicableType=1`; "NCC và Giải pháp" = 2; KHKK = 10.
|
||||
|
||||
**Handler Overview:** `Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` — filter generic `d.ApplicableType == typeEnum` (:123-127) + `IsUserSelectable` (:129-132); khi `applicableType=null` group theo `Enum.GetValues<>()` toàn bộ (:199-213) ⇒ **GENERIC, 0 hardcode chặn type-10**. Label map `AwLabels.Type` :90-98 CÓ entry type-10 "Kế hoạch ký kết HĐ" (:97) — nhưng label này là 1-per-type, KHÔNG phân biệt 8 nhóm N1-N8 (8 nhóm cùng type=10 sẽ gộp chung 1 `AwTypeSummaryDto` với 1 `Active` duy nhất — xem §2 hệ quả).
|
||||
|
||||
**9 checkbox = 9 cột `bool` per-Level row** trên `ApprovalWorkflowLevel` (`ApprovalWorkflow.cs:99-176`) — schema DÙNG CHUNG mọi ApplicableType:
|
||||
| # | Checkbox UI | Flag | line |
|
||||
|---|---|---|---|
|
||||
| 1 | Trả về 1 Cấp trước | `AllowReturnOneLevel` (F1m1) | :120 |
|
||||
| 2 | Trả về 1 Bước trước | `AllowReturnOneStep` (F1m2) | :123 |
|
||||
| 3 | Trả về Người chỉ định | `AllowReturnToAssignee` (F1m3) | :126 |
|
||||
| 4 | Trả về Drafter (default) | `AllowReturnToDrafter` (F1m4, default TRUE) | :129 |
|
||||
| 5 | Sửa Section 2 lúc duyệt | `AllowApproverEditDetails` (F3) | :133 |
|
||||
| 6 | Sửa Section ngân sách | `AllowApproverEditBudget` (F4) | :139 |
|
||||
| 7 | Duyệt thẳng Cấp cuối | `AllowApproverSkipToFinal` (F2) | :147 |
|
||||
| 8 | Duyệt là KẾT THÚC (không CEO) | `AllowApproverFinalize` (F5) | :158 |
|
||||
| 9 | NV XÓA phiếu khi đến lượt | `AllowApproverDelete` (F6, Mig 68) | :173 |
|
||||
|
||||
**🔴 Create handler ĐÃ CÓ nhánh type-10 đích danh** (`ApprovalWorkflowV2AdminFeatures.cs:387`): `AllowApproverSkipToFinal = typeEnum != ContractSigningPlan && l.AllowApproverSkipToFinal` — **ép-false F2 CHO RIÊNG type-10** (K3-test O-1: KHKK service 0 lần đọc cờ này, echo true = config-lie #78). `AllowApproverFinalize` từng ép-false @S161 nhưng ĐÃ GỠ @S165 (service KHKK port level-finalize) — comment :376-386.
|
||||
|
||||
**PUT sửa-tại-chỗ (S155 F-1):** T1 khung-không-đổi (tập Step.Order + Level.Order bất biến, :552-559 + :572-574 + :593-595 + :636-648) + T2 luật-chơi-không-đổi (Code/ApplicableType/CeoApprovalThreshold/F5/F2 = ECHO-only, lệch ⇒ 409, :539-550 + :598-603). Guard chữ-ký 2 tầng (:665-724): `CountLiveOpinionsAsync` + `ListRetainedOpinionsAsync` — cần kiểm 2 helper này có đọc `ContractSigningPlanLevelOpinions` không (xem §2/§4). Transaction rào @ :744-745.
|
||||
|
||||
**Deactivate-per-type khi POST:** `:339-343` — deactivate MỌI workflow `ApplicableType == typeEnum && IsActive` ⇒ **only-ONE-active-per-TYPE. Với 8 nhóm KHKK cùng type=10: tạo version mới cho N2 sẽ TẮT IsActive của N1..N8 còn lại** ⇒ đây là site chặn #1 (xem §2).
|
||||
|
||||
### §1-bis Phần sau của AdminFeatures (line 800-1205)
|
||||
|
||||
- **Guard chữ-ký 3-site ĐÃ phủ KHKK 3/3:** `CountLiveOpinionsAsync` :961-965 + `ListRetainedOpinionsAsync` :1033-1038 + `PurgeOrphanOpinionsAsync` :1099-1102 đều có nhánh `ContractSigningPlanLevelOpinions` (site liệt-kê-thủ-công, comment :957-960 tự khai luật "module mới PHẢI vào CẢ 3"). `DetachTrackedOpinionsOn` :1122-1136 quét theo TÊN CỘT metadata ⇒ tự phủ (comment :1120 nói "7 bảng" — stale nhẹ, thực tế 8).
|
||||
- **🔴 GAP changelog PUT sửa-tại-chỗ:** `:863-893` chỉ ghi vết vào `PurchaseEvaluationChangelogs` khi `ApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}` — **KHKK type-10 bị SKIP** (comment :857-862 tự khai: `ContractSigningPlanChangelogs` là changelog PHIẾU, "wire nếu cần ở W2/W3" — CHƯA wire). Sửa workflow nhóm KHKK đang có phiếu treo ⇒ phiếu 0 vết, chỉ có `def.UpdatedAt` (:851).
|
||||
- **🔴 GAP DELETE version 0 usage-check:** `DeleteAwDefinitionCommandHandler` :1193-1205 = load + `Remove` + Save, **KHÔNG kiểm phiếu đang pin** (comment :1187-1189 tự khai "Sau UAT ... cần check usage trước khi delete" — nợ từ Mig 22 era). Hard-delete; hành vi thực tế phụ thuộc FK của cột pin từng module (cần đo FK `ContractSigningPlans.ApprovalWorkflowId` — xem §2).
|
||||
- **SetUserSelectable** :1173-1185 generic by-Id ⇒ type-10 dùng được 0-đổi.
|
||||
|
||||
## §2 Seed 8 workflow nhóm KHKK + phân loại
|
||||
|
||||
**Seed = `DbInitializer.SeedKhkkGroupWorkflowsAsync`** (`Infrastructure/Persistence/DbInitializer.cs:549-687`, K3 nhịp-1 S164/S165) — KHÔNG phải Mig 71 (Mig 71 chỉ AddColumn `ApprovalGroup` lên phiếu + backfill=1; workflow ROWS do DbInitializer đẻ lúc boot, INFRASTRUCTURE seed not-gated DemoSeed, comment :539).
|
||||
|
||||
- **Phân loại 8 nhóm = `Code` string `KHKK-N{n}`** (:609), n=1..8 (:607). TẤT CẢ 8 cùng `ApplicableType=ContractSigningPlan(10)` (:616). Name `"Duyệt KHKK — Nhóm {n}"` (:617). Version=1, `IsActive=true` + `IsUserSelectable=true` cả 8 (:615,:622-623).
|
||||
- **Idempotent PER-CODE** `AnyAsync(w.Code == code)` (:610) — comment :540-542 tự khai vì sao KHÔNG dùng khuôn per-type của 6 seeder mẫu: 8 workflow dùng chung 1 type, check type sẽ chặn 7 nhóm còn lại.
|
||||
- **Đội hình 3 trạm PRO→CCM→BGĐ** sao từ QT-DN-V2-001 prod (roster :554-585): PRO C1 4-người OR-of-N + C2 tra.bui · CCM C1 4-người + C2 chuong.phan **`AllowApproverFinalize=true`** (:576-579) · BGĐ C1 truong.nguyen. Resolve theo EMAIL, thiếu user ⇒ bỏ slot + warn không throw (:593-600), cấp rỗng ⇒ nén Order (:645), workflow 0-bước vẫn tạo giữ mã (:673-676).
|
||||
- **8 nhóm hiện SỐNG CHUNG IsActive=true cùng type=10** ⇒ bất biến "only-ONE-active-per-type" của POST create (`ApprovalWorkflowV2AdminFeatures.cs:339-343`) ĐÃ VỠ từ lúc seed (8 active cùng type). Hệ quả: **POST tạo version mới cho BẤT KỲ nhóm nào sẽ deactivate CẢ 8** — site chặn đích danh #1.
|
||||
- Sidebar 8 menu-row `AwV2_KhkkN1..N8` seed @ `DbInitializer.cs:1988-1995`, parent `ApprovalWorkflowsV2`; MenuKeys VÀO `All` (`MenuKeys.cs:246-249`) ⇒ 32 policy `AwV2_KhkkN{n}.{R,C,U,D}` tồn tại.
|
||||
|
||||
## §3 Runtime duyệt KHKK — bảng 9-flag × {PE-runtime, KHKK-runtime}
|
||||
|
||||
KHKK runtime = `Infrastructure/Services/ContractSigningPlanWorkflowService.cs` (W3 S161 + K3 S165). 4 action cố định `submit/approve/return/reject` (:74-75), KHÔNG có `targetPhase` param. OR-of-N `ResolveActingLevel` (:455-469), UPSERT `ContractSigningPlanLevelOpinions` (:477-513), con-trỏ ĐÔI reset-Cấp-1-khi-sang-Bước (:309-310), submit reset về Bước1/Cấp1 cả 2 lối vào (:198-202).
|
||||
|
||||
| # | Flag | PE-runtime | KHKK-runtime | Evidence KHKK |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `AllowReturnOneLevel` | TÔN TRỌNG — `PurchaseEvaluationWorkflowService.cs:421` (switch `WorkflowReturnMode`) | **BỎ QUA** — return luôn = `TraLai` toàn phần | `ContractSigningPlanWorkflowService.cs:48-50` comment (c) "4 return-mode per-level CỐ Ý chưa đọc"; `ReturnOrRejectAsync:354-400` 0 đọc flag |
|
||||
| 2 | `AllowReturnOneStep` | TÔN TRỌNG — `:422` | **BỎ QUA** — nt | nt |
|
||||
| 3 | `AllowReturnToAssignee` | TÔN TRỌNG — `:423` | **BỎ QUA** — nt | nt |
|
||||
| 4 | `AllowReturnToDrafter` | TÔN TRỌNG — `:424` (mode Drafter phải bật cờ) | **BỎ QUA (hành vi = luôn-cho)** — return về Drafter cho MỌI NV đúng lượt, kể cả cờ tắt | `ReturnOrRejectAsync` chỉ guard đúng-lượt (:379-380), không đọc cờ |
|
||||
| 5 | `AllowApproverEditDetails` (F3) | TÔN TRỌNG — `PurchaseEvaluationDetailFeatures.cs:84` + `PurchaseEvaluationSupplierFeatures.cs:45` | **BỎ QUA / CHƯA WIRE** — KHKK Lines/Dossier edit = drafter-only Nháp/TraLai | `EnsureDraftEditable` `ContractSigningPlanFeatures.cs:295-303` (mọi đường ghi W2); 0 hit `AllowApproverEditDetails` trong ContractSigningPlan* |
|
||||
| 6 | `AllowApproverEditBudget` (F4) | TÔN TRỌNG — `PurchaseEvaluationFeatures.cs:411` | **BỎ QUA / PE-semantics** (KHKK không có section NS; giá dòng = `ApprovedAmount ??= ProposedAmount` chốt tại finalize `:345-349`) | 0 hit trong ContractSigningPlan* |
|
||||
| 7 | `AllowApproverSkipToFinal` (F2) | TÔN TRỌNG — `PurchaseEvaluationWorkflowService.cs:810` (+ Contract `:335`) | **CHƯA WIRE + BỊ ÉP-FALSE tại POST create cho type-10** | service 0 đọc (comment `:47` "giữ nguyên không port"); ép-false `ApprovalWorkflowV2AdminFeatures.cs:387` |
|
||||
| 8 | `AllowApproverFinalize` (F5) | TÔN TRỌNG — `:870` (opt-out `applyLevelFinalize`) | **TÔN TRỌNG** — port @K3 S165, opt-out param mirror PE S96, set `EndedByLevelFinalize` runtime-flag (Mig 71) | `ContractSigningPlanWorkflowService.cs:276-289`; seed bật cho TP.CCM (`DbInitializer.cs:576-579,:655`) |
|
||||
| 9 | `AllowApproverDelete` (F6) | TÔN TRỌNG — `PeSoftDeleteFeatures.cs:110-114` (3 rào: policy ∧ đúng-lượt ∧ F6) | **BỎ QUA / CHƯA WIRE** — delete KHKK không có nhánh approver-đúng-lượt (xem §3-bis delete handler) | 0 hit `AllowApproverDelete` trong ContractSigningPlan* |
|
||||
|
||||
- `CeoApprovalThreshold` (workflow-level, NGOÀI 9 checkbox): PE đọc; KHKK **CỐ Ý KHÔNG port** (NĐUQ human-gate vĩnh viễn, OG-3 — comment `:46-47`).
|
||||
- DTO detail KHKK ĐÃ expose `AllowApproverFinalize` per-level (`ContractSigningPlanFeatures.cs:140,:1013,:1030`) — các flag khác không expose (PE expose đủ 9 @ `PurchaseEvaluationDtos.cs:124-138`).
|
||||
|
||||
### §3-bis Surprises quanh pin/đổi workflow (evidence create/update path)
|
||||
- **Rào (v) group-match CHỈ chạy lúc CREATE:** `ContractSigningPlanFeatures.cs:479-486` regex `^KHKK-N([1-8])$` so `approvalGroup`; workflow code NGOÀI họ (type-10 code khác) → CHO QUA (comment :477-478).
|
||||
- **🔴 UpdateDraft re-pin (:603-607) chỉ gọi `EnsureWorkflowTypeAsync` (type=10, :307-318), KHÔNG re-check group-match** ⇒ phiếu nhóm 3 tạo đúng KHKK-N3 rồi PUT draft re-pin KHKK-N5 = 200 OK; SubmitAsync cũng không re-check workflow-code↔group (`ContractSigningPlanWorkflowService.cs:169-187` chỉ check Lines↔group) ⇒ **bypass rào (v) 2-bước** — phiếu nhóm 3 chạy đội hình nhóm 5.
|
||||
- FK `ContractSigningPlans.ApprovalWorkflowId` → ApprovalWorkflows = **Restrict vật lý** (`ContractSigningPlanConfiguration.cs:48-53`).
|
||||
|
||||
## §4 Gap-analysis BE → wave đề xuất
|
||||
|
||||
### §4.0 Authz đối chiếu 32 policy `AwV2_KhkkN{n}.{R,C,U,D}` (gotcha #85)
|
||||
**Đo:** grep `AwV2_` toàn `SolutionErp.Api` = **0-hit** (control-dương cùng thước: `KeHoachKyKet` cùng path = 29 hit / 1 file ⇒ vắng thật). ⇒ **32 policy KHÔNG controller nào tiêu thụ** — chúng chỉ sống ở tầng menu-display (sidebar row + Permission matrix). Đây KHÔNG phải bug mới của KHKK: 2 key cũ `AwV2_DuyetNcc`/`AwV2_DuyetNccPhuongAn` (MenuKeys.cs:116-117) cũng 0 consumer BE — trang DuyetNcc hiện hành chạy đúng mô hình này rồi.
|
||||
- **Key gate THẬT của trang admin (mọi type):** GET = authenticated-only · POST/PUT/PATCH/DELETE = `Workflows.Create` (§1). "Y như trang DuyetNcc" ⇒ **0-đổi-authz**: admin nào có `Workflows.Create` sửa được CẢ 8 nhóm (và cả PE/Contract workflows).
|
||||
- **Trade-off nếu muốn per-group write-authz** (nhóm N1 chỉ ai được cấp N1 sửa): controller phải resolve workflow→Code→group rồi check `AwV2_KhkkN{n}.Update` — đổi hình authz của controller DÙNG CHUNG 9 type ⇒ blast radius mọi module; và theo #85 endpoint phải gate bằng key CỦA CHÍNH NÓ, không OR key con. **Quyết định thuộc owner/lead — không tự quyết**; mặc định rẻ + nhất quán = giữ `Workflows.Create`.
|
||||
- 🔸 Menu-row 8 leaf cần `CanRead` grant per-role (KHKK root CỐ Ý không inherit — `GetMyMenuTreeQuery.cs` 4-case; seeder grant explicit) — tầng display độc lập tầng API (memory `permission_grant_two_layers`).
|
||||
|
||||
### §4.1 Disposition 4+1 site chặn
|
||||
| # | Site | Hiện trạng | Đề xuất wave | Rủi ro |
|
||||
|---|---|---|---|---|
|
||||
| (a) | **only-ONE-active-per-TYPE** `ApprovalWorkflowV2AdminFeatures.cs:339-343` | POST version mới nhóm N2 ⇒ deactivate CẢ 8 (bất biến đã vỡ từ seed: 8 active cùng type-10). Blast thực tế: pick-list KhkkCreatePage lọc `isUserSelectable` KHÔNG lọc IsActive (`fe-admin/src/pages/khkk/KhkkCreatePage.tsx:97`) + runtime load by-Id ⇒ thiệt hại = Overview `Active` badge/summary + mọi chỗ đọc IsActive về sau | **P1 (khuyên):** đổi scope deactivate = `w.Code == request.Code` khi `typeEnum == ContractSigningPlan` (đúng semantic "1 active per chuỗi-version", 8 chuỗi độc lập). **P2:** đổi per-Code cho MỌI type — sạch hơn nhưng đổi hành vi PE/Contract (hiện tạo Code MỚI cùng type tự tắt Code cũ — có thể có chỗ dựa vào) | P1: if-đặc-thù type-10 (thêm 1 nhánh rẽ); P2: regression PE/Contract khó lường — cần grep chỗ đọc IsActive trước |
|
||||
| (b) | **Changelog PUT skip type-10** `:863-893` | Sửa workflow nhóm có phiếu KHKK treo ⇒ phiếu 0 vết (comment :857-862 tự khai defer W2/W3) | Wire block mirror PE: query `ContractSigningPlans` pin `def.Id` + Phase ∉ {DaDuyet,TuChoi} → add `ContractSigningPlanChangelogs` (EntityType Workflow, khuôn `ContractSigningPlanWorkflowService.cs:546-557`) | Thấp — bảng phiếu-changelog có sẵn FK Cascade; chỉ thêm 1 nhánh else-if |
|
||||
| (c) | **DELETE version 0 usage-check** `:1193-1205` | FK `ContractSigningPlans.ApprovalWorkflowId` **Restrict** (`ContractSigningPlanConfiguration.cs:50-53`) ⇒ data KHÔNG mất, nhưng `DbUpdateException` KHÔNG được map (`GlobalExceptionMiddleware.cs:32-39` chỉ map 5 app-exception, `_` → **500 "Lỗi hệ thống"**). Phiếu xoá-MỀM vẫn còn row ⇒ vẫn chặn. Workflow từng dùng rồi unpin: opinions FK Restrict vào Levels cũng chặn cascade ⇒ 500 | Guard usage-check trong `DeleteAwDefinitionCommandHandler`: đếm pin per-module (PE `ApprovalWorkflowId`, Contract, Proposal, 4 WorkflowApps, KHKK — 8/9 module có cột pin) + đếm LevelOpinions (tái dùng khuôn `CountLiveOpinionsAsync` NHƯNG theo workflow-id) → 409 đích danh "N phiếu đang pin" | Trung bình — liệt-kê-thủ-công 8 module (đúng class lỗi comment :957-960 đã cảnh báo); nợ này của Mig 22 era, sửa 1 lần lợi mọi type |
|
||||
| (d) | **Bypass rào (v) 2-bước** `ContractSigningPlanFeatures.cs:603-607` | UpdateDraft re-pin chỉ check type, không check group-match; Submit không re-check ⇒ phiếu N3 chạy đội hình N5 | Extract rào (v) (:479-486) thành `ContractSigningPlanScope.EnsureWorkflowGroupMatchAsync(db, awId, group)` → gọi ở CẢ Create + UpdateDraft; cân nhắc gọi thêm ở `SubmitAsync` làm chốt-cuối (khuôn 2-tầng như hạng-mục :179-187) | Thấp; chú ý phiếu cũ đã lỡ pin lệch trước vá sẽ kẹt submit — message phải chỉ việc cần làm |
|
||||
| (e) | **Ép-false F2 type-10** `:387` | ĐÚNG theo chống config-lie #78 (service KHKK 0 đọc F2 — `ContractSigningPlanWorkflowService.cs:47`) | **GIỮ** cho tới khi có lệnh owner port F2. Nếu port: copy nhánh PE `PurchaseEvaluationWorkflowService.cs:797-810` vào service KHKK + gỡ ép-false + gỡ khoá FE **CÙNG WAVE** (đúng tiền lệ F5 @K3: port + gỡ ép-false + gỡ FE-lock 1 wave, comment :376-386) | Mở mà không port = đẻ lại #78; port mà quên gỡ ép-false = cờ chết chiều ngược |
|
||||
|
||||
### §4.2 Việc 0-đổi-BE (đã generic sẵn — wave FE nối thẳng)
|
||||
1. GET Overview `?applicableType=10` trả đủ 8 nhóm × versions × 9-flag per-level (§1) — FE lọc client-side theo `code == "KHKK-N{n}"` (payload nhỏ; muốn sạch hơn có thể thêm query param `code` — optional, không blocker).
|
||||
2. PUT sửa-tại-chỗ (thêm/bớt người, đổi NV, 7 cờ tại-chỗ, đổi tên/phòng) — T1/T2 + guard chữ-ký 3-site ĐÃ phủ KHKK (§1-bis).
|
||||
3. PATCH user-selectable + DELETE (khi chưa pin) — generic by-Id.
|
||||
4. Lịch sử versions (History list per code) — có sẵn trong Overview DTO.
|
||||
5. Runtime approve/return/reject/submit KHKK + F5 finalize + inbox + notify — W3/K3 xong (§3).
|
||||
6. Authz: giữ `Workflows.Create` = y-hệt trang DuyetNcc (§4.0).
|
||||
|
||||
### §4.3 Việc PHẢI đổi BE (gom wave)
|
||||
- **Wave-BE-1 (nhỏ, an toàn):** (a)-P1 deactivate-per-Code cho type-10 · (d) group-match guard 2-site · (b) changelog type-10 · (c) usage-check DELETE → 409. 4 việc đều trong 2 file `ApprovalWorkflowV2AdminFeatures.cs` + `ContractSigningPlanFeatures.cs` (+1 helper Scope), 0 migration.
|
||||
- **Wave-BE-2 (chỉ khi owner duyệt semantics):** port return-modes F1×4 / F3-analog (sửa Lines lúc duyệt) / F6 approver-delete / F2 skip-to-final vào KHKK runtime — mỗi cái là ĐỔI HÀNH VI DUYỆT, thuộc quyền owner (comment (c) service :48-50 đã khai chủ-đích chưa đọc). Nếu KHÔNG port: FE phải ẩn/disable các ô chưa-wire cho type-10 (tiền lệ `earlyFinalizeLocked` W1 — đã có khuôn), không thì mỗi ô tick = config-lie #78 mới.
|
||||
|
||||
## §5 Surprises ngoài đề bài
|
||||
1. **8 link sidebar KhkkN{n} hiện đổ về trang landing chung** — `Layout.tsx:188` route `/system/approval-workflows-v2/KhkkN{n}` nhưng `ApprovalWorkflowsV2Page.tsx` `TYPE_CODE_TO_INT` (:164-172) KHÔNG có key `KhkkN*` ⇒ `selectedTypeInt=undefined` → rơi về grid landing (:299-301,:323). Menu có, trang chưa phân nhóm — wave FE là phần nặng nhất của đề bài, BE gần xong.
|
||||
2. **GET Overview 0-policy trả đủ 9-flag + CeoApprovalThreshold + tên/email toàn bộ approver cho MỌI user authenticated** (kể cả fe-user token) — cố ý từ Mig 22 cho Drafter pick (:10-15), nhưng payload nay giàu hơn nhiều so 2026-05; đáng để owner ngó lại (information-disclosure mềm).
|
||||
3. **Roster 8 workflow = SNAPSHOT lúc seed theo email hardcode** (`DbInitializer.cs:554-585`) — QT-DN-V2-001 prod đổi người thì 8 nhóm KHÔNG tự theo; idempotent per-Code nên restart không re-sync (đúng thiết kế, nhưng dễ bị hiểu nhầm "sao đội hình không khớp DuyetNcc nữa").
|
||||
4. **Bất biến only-1-active-per-type đã vỡ từ seed** (8 active type-10) mà Overview `Active = versions.FirstOrDefault(IsActive)` (:210) chỉ hiện 1 — 7 nhóm còn lại "tàng hình" khỏi ô Active của type-summary; FE per-group phải đọc theo Code, đừng tin ô `Active`.
|
||||
5. `DetachTrackedOpinionsOn` comment "7 bảng" stale (thực 8 — quét metadata nên vẫn đúng hành vi) `ApprovalWorkflowV2AdminFeatures.cs:1120`.
|
||||
6. KHKK delete phiếu allow-list ĐÃ rộng hơn PE: {Nháp, TuChoi, **TraLai**} (S177 owner) vs PE {DangSoanThao, TuChoi} + F6-đúng-lượt (`ContractSigningPlanFeatures.cs:1733-1738` vs `PurchaseEvaluationFeatures.cs:1404-1406` + `PeSoftDeleteFeatures.cs:110-114`) — mirror "nút XÓA ở màn duyệt" cho KHKK là F6-wire (wave-BE-2), không phải nới allow-list thêm.
|
||||
|
||||
## VERDICT
|
||||
BE cho 8 workflow "Duyệt KHKK — Nhóm 1..8" đã **generic ~85%**: controller + 5 action + PUT sửa-tại-chỗ + guard chữ-ký + ghim/xoá + seed 8 nhóm (Code `KHKK-N{n}`, cùng ApplicableType=10) + runtime F5 finalize đều SỐNG. **4 site BE phải sửa** (wave-BE-1, 2 file, 0-mig): deactivate-per-Code type-10 (`:339-343`) · group-match guard thêm 2-site (`:603-607`+Submit) · changelog type-10 (`:863-893`) · usage-check DELETE→409 (`:1193-1205`). **6/9 checkbox hiện chỉ PE tôn trọng** — tick cho type-10 = cờ nằm im (#78-class): hoặc port runtime (wave-BE-2, cần owner duyệt semantics), hoặc FE khoá ô theo tiền lệ `earlyFinalizeLocked`. Authz giữ `Workflows.Create` y trang DuyetNcc; 32 policy `AwV2_KhkkN*` 0 consumer BE (đo + control-dương). Phần nặng còn lại là FE (8 route đổ landing chung).
|
||||
@ -0,0 +1,150 @@
|
||||
# SUB-INVEST-DB (Fable deep-pass, lens DATABASE) — S182 KHKK AWF mirror NCC
|
||||
|
||||
> Đề bài: 8 workflow "Duyệt KHKK — Nhóm 1..8" cần đủ chức năng như "Duyệt NCC" trên approval-workflows-v2.
|
||||
> Câu hỏi trung tâm: **schema hiện tại ĐỦ chưa hay cần mig mới?** Kỳ vọng "máy rộng UI hẹp" ⇒ 0 mig — PHẢI CHỨNG.
|
||||
> Trạng thái file: đổ ruột liên tục (chống-#53). Khu chưa đo ghi `CHƯA ĐO`.
|
||||
|
||||
## §1 Schema V2 — cột thật 3 bảng (từ ModelSnapshot + entity)
|
||||
|
||||
Nguồn: `ApplicationDbContextModelSnapshot.cs:128-313` + `src/Backend/SolutionErp.Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs` (1 file chứa cả 3 entity).
|
||||
|
||||
### `ApprovalWorkflows` (snapshot :128-187)
|
||||
| Cột | Kiểu | Ghi chú |
|
||||
|---|---|---|
|
||||
| Id | uniqueidentifier PK | BaseEntity |
|
||||
| Code | nvarchar(100) NOT NULL | "QT-DN-V2-001" |
|
||||
| Version | int | monotonic per Code |
|
||||
| ApplicableType | int | enum 1..10 — **ContractSigningPlan=10** (`ApprovalWorkflow.cs:70`) |
|
||||
| Name | nvarchar(200) NOT NULL | |
|
||||
| Description | nvarchar(1000) NULL | |
|
||||
| IsActive | bit | |
|
||||
| ActivatedAt | datetime2 NULL | |
|
||||
| IsUserSelectable | bit | Mig 25 — Ghim/Bỏ ghim |
|
||||
| CeoApprovalThreshold | decimal(18,2) NULL | Mig 53 — ngưỡng gói CEO (PE semantic) |
|
||||
| CreatedAt/CreatedBy/UpdatedAt/UpdatedBy | | BaseEntity audit |
|
||||
|
||||
Index: `(ApplicableType, IsActive)` + **UNIQUE `(Code, Version)`** (:181-184). 🔴 **KHÔNG có IsDeleted** — `ApprovalWorkflow : BaseEntity` (entity :21), KHÔNG AuditableEntity ⇒ 0 soft-delete, 0 query-filter trên cả 3 bảng V2.
|
||||
|
||||
### `ApprovalWorkflowSteps` (snapshot :274-313)
|
||||
Id · ApprovalWorkflowId (FK) · Order int · Name nvarchar(200) NOT NULL · DepartmentId uniqueidentifier NULL (hint) · audit 4 cột. Index: `DepartmentId` + `(ApprovalWorkflowId, Order)`.
|
||||
|
||||
### `ApprovalWorkflowLevels` (snapshot :189-272)
|
||||
Id · ApprovalWorkflowStepId (FK) · Order int (Cấp, OR-of-N rows cùng Order) · Name nvarchar(200) NULL · ApproverUserId NOT NULL (1 row = 1 NV slot) · audit 4 cột · **9 cờ bit Allow\***. Index: `ApproverUserId` + `(ApprovalWorkflowStepId, Order)`.
|
||||
|
||||
### §1b Bảng ánh xạ 9 checkbox ảnh ↔ cột — **9/9 CÓ ĐỦ, 0 cột thiếu**
|
||||
|
||||
Per-NV = per-row `ApprovalWorkflowLevels` (mỗi row = 1 ApproverUserId ⇒ cờ trên row chính là cờ per-NV).
|
||||
|
||||
| # | Checkbox (ảnh YC-013) | Cột thật | Default (snapshot) | Mig gốc |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Trả về 1 Cấp trước | `AllowReturnOneLevel` | false (:220-223) | 29 |
|
||||
| 2 | Trả về 1 Bước trước | `AllowReturnOneStep` | false (:225-228) | 29 |
|
||||
| 3 | Trả về Người chỉ định | `AllowReturnToAssignee` | false (:230-233) | 29 |
|
||||
| 4 | Trả về Drafter (mặc định) | `AllowReturnToDrafter` | **true** (:235-238) | 29 |
|
||||
| 5 | Sửa Section 2 | `AllowApproverEditDetails` | false (:205-208) | 29 |
|
||||
| 6 | Sửa Section ngân sách | `AllowApproverEditBudget` | false (:200-203) | 30 |
|
||||
| 7 | Duyệt thẳng Cấp cuối | `AllowApproverSkipToFinal` | false (:215-218) | 31 |
|
||||
| 8 | Duyệt là KẾT THÚC | `AllowApproverFinalize` | false (:210-213) | 58 |
|
||||
| 9 | XÓA phiếu | `AllowApproverDelete` | false (:195-198) | 68 |
|
||||
|
||||
Cột nằm trên bảng V2 DÙNG CHUNG mọi ApplicableType ⇒ workflow KHKK (ApplicableType=10) **lưu được cả 9 cờ ngay hôm nay, 0 mig**. ⚠️ Ranh giới lens: cột TỒN TẠI ≠ service KHKK ĐỌC cờ (semantic #3 đọc `PeLevelOpinions`, #5/#6 đọc section PE — entity comment `ApprovalWorkflow.cs:119-173`; KHKK service có honor không = lens BE).
|
||||
|
||||
## §2 Versioning — đọc từ `ApprovalWorkflowV2AdminFeatures.cs` (KHÔNG đoán từ UI)
|
||||
|
||||
- **Lưu version:** cùng `Code` nhiều row, `Version int` monotonic; **UNIQUE (Code, Version)** (snapshot :183-184). "Archived" KHÔNG có cột riêng — chỉ là `IsActive=false` (nhãn UI suy từ bit).
|
||||
- **Tạo phiên bản mới** = `CreateAwDefinitionCommandHandler` (:326-): INSERT row mới `Version = max(Code)+1` (:334-337), `IsActive=true, IsUserSelectable=true` (:352-353). 🔴 **LANDMINE cho KHKK:** deactivate cũ chạy theo **`ApplicableType`** chứ KHÔNG theo Code — `Where(w.ApplicableType == typeEnum && w.IsActive)` set `IsActive=false` TẤT CẢ (:339-343). Với DuyetNcc (1 code/type) đúng; với KHKK **8 code chung type=10** ⇒ tạo version mới cho `KHKK-N1` sẽ **tắt IsActive của cả KHKK-N2..N8**. Chính seeder đã né bẫy same-class ở check idempotent (`DbInitializer.cs:540-542` "check theo type sẽ chặn 7 nhóm còn lại") nhưng Create handler CHƯA vá — vá-tệp-A-quên-tệp-B. **Fix = code BE (đổi scope deactivate theo Code), 0 mig.**
|
||||
- **Sửa tại chỗ** = `UpdateAwDefinitionCommandHandler` (:515-900): UPDATE in-place ĐÚNG row + **diff Levels theo `Level.Id` — GIỮ NGUYÊN Id** (:561-634, "F-3 CẤM delete-then-insert") ⇒ không đẻ Id mới, không mồ-côi con trỏ (khác nỗi lo F17/S155 — máy Update nay ĐÃ TỒN TẠI, memory cũ "WF-V2 0 lệnh Update" STALE). Cấm in-place: đổi Code/ApplicableType/CeoThreshold (:539-550), thêm/bớt/đảo Bước (:553-574), đổi Order dòng (:593), toggle 2 cờ đổi-đường-đi F5/F2 (:598-603), bỏ người giữ F5/F2 (:627-632), làm rỗng Cấp (:639-647) — tất cả 409 + gợi ý tạo version mới. 7 cờ còn lại + đổi người + thêm/bỏ người trong Cấp = hợp lệ in-place (:789-797).
|
||||
- **Guard chữ ký khi bỏ/đổi người:** 2 tầng quét **CẢ 8 bảng LevelOpinions** gồm `ContractSigningPlanLevelOpinions` — count-sống (:961-965) + retained (:1033-1038); transaction rào (:744-745, :895-899). ⇒ máy sửa-tại-chỗ **ĐÃ KHKK-aware ở tầng guard**.
|
||||
- ⚠️ Vết changelog khi sửa-tại-chỗ chỉ ghi cho **PE** (`ApplicableType ∈ {DuyetNcc, DuyetNccPhuongAn}` :863-892); KHKK khai-giới-hạn defer F-6b (:857-862) — bảng `ContractSigningPlanChangelogs` ĐÃ TỒN TẠI (Mig 69) ⇒ wire = code BE thuần, **0 mig**.
|
||||
- **Ghim/Bỏ ghim** = `SetAwUserSelectableCommandHandler` (:1173-1185): flip 1 bit, generic mọi type — KHKK dùng ngay.
|
||||
- **Xoá version** = `DeleteAwDefinitionCommandHandler` (:1193-1205): **HARD delete** (`Remove` + BaseEntity không soft-delete), **0 guard usage** — comment tự khai ":1187-1189 Sau UAT … cần check usage trước khi delete". Phiếu pin (PE/HĐ/KHKK Restrict) hoặc LevelOpinion tồn tại ⇒ SqlException 547 → 500. Version không ai đụng ⇒ xoá sạch (cascade Steps→Levels).
|
||||
|
||||
## §3 KHKK wiring — Mig 70/71 + ContractSigningPlans pin/pointer/finalize-flag
|
||||
|
||||
### FK map V2 (snapshot :6210-6323) — quyết định risk xoá
|
||||
| Quan hệ | Behavior |
|
||||
|---|---|
|
||||
| Steps → ApprovalWorkflows | **Cascade** (:6229-6233) |
|
||||
| Levels → Steps | **Cascade** (:6212-6216) |
|
||||
| Levels.ApproverUserId → Users | Restrict (:6218-6222) |
|
||||
| Steps.DepartmentId → Departments | Restrict (:6235-6238) |
|
||||
| `ContractSigningPlans.ApprovalWorkflowId` → ApprovalWorkflows | **Restrict** (:6245-6248) |
|
||||
| `Contracts.ApprovalWorkflowId` → | Restrict (:6327-6330) |
|
||||
| `PurchaseEvaluations.ApprovalWorkflowId` → | Restrict (:6755-6758) |
|
||||
| `ContractSigningPlanLevelOpinions.ApprovalWorkflowLevelId` → Levels | **Restrict** (:6297-6301) |
|
||||
| `ContractSigningPlanApprovals.ApprovalWorkflowLevelId` | ⚠️ **LOOSE Guid — 0 FK** (cột + index :416/:455 nhưng relationship section :6251-6260 chỉ có 1 HasOne về Plan Cascade) |
|
||||
|
||||
⇒ Hard-delete 1 workflow version bị DB CHẶN (SqlException 547) nếu: (a) bất kỳ phiếu PE/HĐ/KHKK pin nó, HOẶC (b) bất kỳ LevelOpinion (PE/HĐ/KHKK) trỏ Level của nó — cascade Steps→Levels đụng Restrict của LevelOpinions.
|
||||
|
||||
### Mig 71 `AddKhkkApprovalGroupCatalogAndFinalizeRuntime` (`20260731085624_*.cs`)
|
||||
- **KHÔNG có bảng catalog-nhóm-duyệt riêng** — "catalog" trong tên mig = gắn `ContractSigningPlanLines.CatalogEntryId` (loose-Guid, KHÔNG FK, :70-74) + `TenHangMuc` denorm (:76-81) trỏ về `ContractCatalogEntries` (Mig 70, bảng danh mục 86 dòng).
|
||||
- `ContractSigningPlans.ApprovalGroup int NOT NULL default 0` + backfill vô-điều-kiện `SET = 1` (:47-61) — nhóm duyệt 1..8 lưu TRÊN PHIẾU dạng int trần (0 bảng, 0 FK, 0 CHECK constraint 1..8).
|
||||
- `EndedByLevelFinalize bit` runtime-flag (:63-68) — ghost-window khai trước, K3 tiêu thụ.
|
||||
- Index-swap: UNIQUE `(PlanId, SupplierId)` → UNIQUE `(PlanId, SupplierId, CatalogEntryId)` filtered `[IsDeleted]=0` (:43-45, :93-98). Down có bẫy F-8 (:132-141).
|
||||
|
||||
### `ContractSigningPlans` pin + pointer (snapshot :315-408) — mirror ĐỦ khuôn PE
|
||||
`ApprovalWorkflowId Guid?` (pin, Restrict) · `CurrentWorkflowStepIndex int?` (:336) · `CurrentApprovalLevelOrder int?` (:333) · `EndedByLevelFinalize bit` (:351) · `ApprovalGroup int` (:321) · `Phase int` · AuditableEntity (IsDeleted/DeletedAt/DeletedBy — phiếu SOFT-delete, ≠ workflow) · UNIQUE filtered `MaKeHoach` (:395-397) · index `(ApprovalGroup, Phase)` (:403).
|
||||
|
||||
### 8 workflow nhóm = 8 row `ApprovalWorkflows` phân loại thế nào?
|
||||
- Seed `SeedKhkkGroupWorkflowsAsync` (`DbInitializer.cs:549-687`): 8 row `Code="KHKK-N{n}"` n=1..8, `Version=1`, `ApplicableType=10`, `Name="Duyệt KHKK — Nhóm {n}"`, `IsActive=true`, `IsUserSelectable=true` (:609-625). 3 Bước PRO→CCM→BOD, Cấp OR-of-N, riêng Cấp 2 CCM (a Chương) `AllowApproverFinalize=true` (:575-579, :655). Idempotent **PER-CODE** (:610) — cố ý né bẫy check-theo-type (:540-542). Resolve người theo EMAIL, thiếu user → bỏ slot + log, workflow 0 bước vẫn TẠO (:544-548).
|
||||
- **Liên kết nhóm↔workflow = QUY ƯỚC CODE, không phải cột/FK:** phiếu chốt `ApprovalGroup` lúc tạo (default 1, validator 1..8 `ContractSigningPlanFeatures.cs:415-418, :461`); rào (v) regex `^KHKK-N([1-8])$` trên Code của workflow user pick — lệch nhóm = 409, Code NGOÀI họ (type-10 chung) **cho qua** (:475-486). `ContractCatalogEntry.ApprovalGroup int` 1..8 (validator `ContractCatalogFeatures.cs:120`, KHÔNG CHECK constraint) quyết nhóm của hạng mục; line-gán guard khớp nhóm phiếu (`:691, :802`).
|
||||
- Con trỏ duyệt KHKK: service `ContractSigningPlanWorkflowService.cs` GHI đủ cặp `CurrentWorkflowStepIndex` (0-based index list sorted) + `CurrentApprovalLevelOrder` (1-based) (:201-202, :295, :309-310) + `EndedByLevelFinalize=true` nhánh finalize (:275-282 — ghost-window Mig 71 ĐÃ ĐÓNG, K3 live) + null-out khi terminal (:323-324). Đọc pointer từ CHÍNH workflow đã pin, KHÔNG đụng `IsActive` (:422-446, có bounds-check) ⇒ **version Archived (IsActive=false) không làm phiếu treo kẹt**.
|
||||
- Mig 72 `AddMaHangMucToContractSigningPlanLines` (`20260806070242`): +`MaHangMuc nvarchar(20)` denorm + backfill từ catalog Code, idempotent (:13-34) — mig CUỐI trên đĩa.
|
||||
|
||||
## §4 Dữ liệu thật LocalDB Dev (sqlcmd -E, đo 2026-08-08)
|
||||
|
||||
| Phép đo | Kết quả |
|
||||
|---|---|
|
||||
| `ApprovalWorkflows` theo type | chỉ type {3,4,5,6,7,9} × 1 row (6 seeder mẫu). **Type 1/2 (DuyetNcc): 0 row. Type 10 (KHKK): 0 row** |
|
||||
| 8 workflow `KHKK-N*` | **0/8 trên Dev local** — seeder idempotent sẽ tạo ở lần boot API kế; QT-DN-V2-001 v01-v04 là dữ liệu PROD (admin tạo tay), local không có |
|
||||
| `ContractSigningPlans` | 0 phiếu · 0 LevelOpinions · 0 Approvals |
|
||||
| `__EFMigrationsHistory` Dev | top = `20260731085624` (**Mig 71**) — **Mig 72 trên đĩa CHƯA áp** |
|
||||
| `__EFMigrationsHistory` Design | top = Mig 71 — cùng drift |
|
||||
|
||||
⇒ **Câu "8 row đủ chưa, IsUserSelectable bao nhiêu" trên PROD: CHƯA ĐO** (SSH chết khi load SQL-client — S134/S148/S177). Local suy được từ seed-code: lần boot prod sau commit K3 đã tạo 8 row `IsActive=1, IsUserSelectable=1`; ảnh sidebar YC-013 hiện 8 mục = seed đã chạy prod. Query bàn giao implementer chạy trên prod:
|
||||
```sql
|
||||
SELECT Code, Version, IsActive, IsUserSelectable FROM ApprovalWorkflows WHERE ApplicableType=10 ORDER BY Code, Version;
|
||||
SELECT ApprovalGroup, Phase, COUNT(*) n FROM ContractSigningPlans GROUP BY ApprovalGroup, Phase;
|
||||
SELECT COUNT(*) FROM ContractSigningPlanLevelOpinions;
|
||||
```
|
||||
|
||||
## §5 Verdict migration + risk
|
||||
|
||||
### VERDICT: **0 MIGRATION.** Schema V2 dùng chung đã đủ 100% cho mirror
|
||||
1. 9/9 checkbox = 9 cột bit có sẵn trên `ApprovalWorkflowLevels` (§1b), bảng dùng chung mọi ApplicableType.
|
||||
2. Versioning v01→v04 + Archived: UNIQUE `(Code, Version)` + `IsActive` bit — đã phục vụ đúng cơ chế này cho `QT-DN-V2-001` v04; `KHKK-N{n}` là Code như mọi Code.
|
||||
3. Ghim = `IsUserSelectable` sẵn; Sửa-tại-chỗ = máy Update diff-by-Id ĐÃ KHKK-aware ở guard chữ ký (3/3 site :961/:1033/:1099); Xoá = máy Delete sẵn (thiếu guard là thiếu CODE, không thiếu cột).
|
||||
4. Pin + con trỏ + finalize-flag phiếu KHKK: đủ từ Mig 69/71, service đã ghi/đọc sống.
|
||||
5. Hạng-mục/nhóm: `ApprovalGroup` (phiếu + catalog) + regex Code — 0 nhu cầu cột mới.
|
||||
|
||||
### Nhưng 3 QUẢ MÌN CODE-LEVEL lộ ra từ scan DB-semantics (fix = BE code, vẫn 0 mig)
|
||||
| # | Mìn | Bằng chứng | Hệ quả nếu bỏ qua |
|
||||
|---|---|---|---|
|
||||
| M1 | **Create-new-version deactivate theo `ApplicableType`** — tạo v02 cho `KHKK-N1` sẽ `IsActive=false` cả active-version của N2..N8 (8 Code chung type=10; giả định 1-Code-1-type vỡ) | `ApprovalWorkflowV2AdminFeatures.cs:339-343`; đối chiếu seeder đã né same-class `DbInitializer.cs:540-542` | Overview 7 nhóm kia mất "Active" (thành Archived-oan); mọi consumer lọc IsActive theo type sai |
|
||||
| M2 | **Overview API `Active = FirstOrDefault(IsActive)` per-TYPE + KHÔNG có param Code** — type 10 có 8 chuỗi version trộn chung 1 `History`, `Active` lấy 1 trong 8 | `:107-109` (query 2 param), `:203-212` | Trang admin 8 mục sidebar không tách được version-list per nhóm nếu FE không tự group by Code |
|
||||
| M3 | **Delete = hard-delete 0 guard usage** — phiếu pin (Restrict) / opinion (Restrict) → SqlException 547 → 500 thô; version rác thì xoá êm | `:1187-1204`; FK snapshot :6245-6248, :6297-6301 | UX 500 khó hiểu thay 409; data KHÔNG mất (DB chặn được) |
|
||||
|
||||
### Risk integrity/concurrency chi tiết
|
||||
- **Sửa workflow đang có phiếu KHKK treo:** an toàn cấu trúc — mọi mutation đổi-đường-đi bị 409; đổi người/cờ giữ nguyên `Level.Id` ⇒ pointer (StepIndex, LevelOrder) vẫn giải được; guard chữ ký quét cả `ContractSigningPlanLevelOpinions` (sống + retained); transaction rào :744/:899. **Lỗ nhỏ:** vết sửa KHÔNG ghi vào `ContractSigningPlanChangelogs` (chỉ PE :863-892, defer F-6b) — phiếu KHKK đang chạy bị sửa quy trình mà lịch sử phiếu im lặng.
|
||||
- **Xoá version Archived có phiếu treo/xong trỏ vào:** bị Restrict chặn (pin `ApprovalWorkflowId`) — kể cả phiếu soft-deleted (row còn, FK còn). Pointer phiếu KHÔNG chết vì không xoá được. Riêng `ContractSigningPlanApprovals.ApprovalWorkflowLevelId` LOOSE (0 FK) — nếu tương lai có đường hard-delete phiếu/level thì history giữ Guid treo (hiện không có đường đó).
|
||||
- **Race 2 admin cùng "Tạo version mới" cùng Code:** UNIQUE (Code,Version) đỡ — kẻ thua 2601/2627 → 500 (không silent-dup). Chấp nhận được cho màn quản trị.
|
||||
- **Race tạo 2 phiếu cùng (PE, nhóm):** rào handler check-then-insert, CỐ Ý không filtered-unique (`ContractSigningPlanFeatures.cs:456-460`, gate-K2 F-7) — giữ nguyên quyết định cũ, ngoài scope.
|
||||
|
||||
## §6 Đề xuất wave DB
|
||||
|
||||
- **W-DB-0 (prep local, bắt buộc trước test):** `dotnet ef database update` ×2 DB (Dev + Design đang kẹt Mig 71, đĩa 72) + boot API 1 lần để `SeedKhkkGroupWorkflowsAsync` đẻ 8 row local. 0 mig mới.
|
||||
- **W-DB-1 (BE, đi cùng wave mirror):** vá M1 — scope deactivate trong `CreateAwDefinitionCommandHandler` đổi `ApplicableType`→`Code` (hoặc Code∧Type); test: tạo v2 `KHKK-N1` ⇒ N2..N8 `IsActive` bất biến + case DuyetNcc regression.
|
||||
- **W-DB-2 (BE):** overview/API thêm chiều Code (param `code=` hoặc group per-Code trong `AwTypeSummaryDto`) để 8 trang nhóm đọc đúng chuỗi version của mình.
|
||||
- **W-DB-3 (BE, khuyến nghị):** guard lịch sự cho Delete — pre-count pin (PE/HĐ/KHKK/5 office) + LevelOpinions → 409 kể tên hồ sơ (tái dùng khuôn `CountLiveOpinionsAsync`), thay 547→500.
|
||||
- **W-DB-4 (BE, optional):** wire changelog KHKK khi sửa-tại-chỗ (bảng `ContractSigningPlanChangelogs` sẵn — mirror khối PE :863-892).
|
||||
- **Verify prod (bàn giao implementer):** 3 query §4.
|
||||
|
||||
## §7 Surprises ngoài đề bài
|
||||
1. **Local DB drift:** Mig 72 committed trên đĩa (2026-08-06) nhưng CẢ Dev lẫn Design local dừng Mig 71 — pattern committed-but-unapplied-local; test wave nào chạm Lines sẽ lệch schema nếu quên update.
|
||||
2. **Dev local 0 row type-1/2 (DuyetNcc)** — workflow NCC prod tạo tay qua UI, không seed ⇒ mọi test local về trang DuyetNcc cần tự tạo workflow trước.
|
||||
3. Memory cũ S155 "WF-V2 KHÔNG có lệnh Update" đã STALE — `UpdateAwDefinitionCommand` (sửa-tại-chỗ diff-by-Id, giữ Id Level) đã land sau đó, guard rất dày.
|
||||
4. Seeder KHKK resolve người theo EMAIL cứng (11 email prod `DbInitializer.cs:556-584`) — DB thiếu user (local!) ⇒ workflow RỖNG 0 bước vẫn được tạo + log warning; trang designer local sẽ thấy 8 workflow trống người, KHÔNG phải bug.
|
||||
5. `HaveSequentialOrders` (Create :306-316) — validator jagged-levels per-step chỉ chạy path Create; Update path dùng bộ guard riêng (T1 set-equality) — 2 bộ luật song song cùng file, drift-risk nếu wave sau chỉnh 1 quên 1.
|
||||
|
||||
---
|
||||
*Đo: ModelSnapshot + entity + 2 handler-file + DbInitializer + Mig 69/70/71/72 + sqlcmd Dev/Design. Prod: CHƯA ĐO (SSH giới hạn). ~62K token.*
|
||||
@ -0,0 +1,87 @@
|
||||
# sub-invest-fe-fable — S182 KHKK AWF-v2 mirror NCC (lens FE UI, "ko lệch")
|
||||
|
||||
> Fable deep-pass đơn lẻ, /fable-real investigator-codebase. Đề: 8 mục sidebar "Duyệt KHKK — Nhóm 1..8" phải ra ĐÚNG trang `system/approval-workflows-v2/DuyetNcc` khuôn ảnh anh gửi. 0 sửa code.
|
||||
|
||||
## §1 Trang DuyetNcc render từ đâu (route + page + KIND_CONFIG + dialog)
|
||||
|
||||
**Route:** `fe-admin/src/App.tsx:79-80` — `/system/approval-workflows-v2` + `/system/approval-workflows-v2/:typeCode` → cùng 1 component `ApprovalWorkflowsV2Page` (named export, `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` 1355 dòng).
|
||||
|
||||
**KIND map (KHÔNG phải KIND_CONFIG object đầy đủ kiểu S29/S35 — chỉ 2 Record thưa):**
|
||||
- `TYPE_CODE_TO_INT` `:164-172` = `{ DuyetNcc:1, DuyetNccPhuongAn:2, Contract:3, ContractSigningPlan:10 }` — **0 entry `KhkkN*`**.
|
||||
- `DEFAULT_CODE_BY_TYPE` `:173-180` = `{1:'QT-DN-V2-001', 2:'QT-DN-PA-V2-001', 3:'QT-HD-V2-001', 10:'QT-KHKK-V2-001'}` (chỉ gợi-ý ô Mã).
|
||||
- Nhãn hiển thị KHÔNG có bảng FE — lấy từ BE `applicableTypeLabel` (`AwLabels.Type`, `ApprovalWorkflowV2AdminFeatures.cs:97` có entry type-10 "Kế hoạch ký kết HĐ").
|
||||
|
||||
**Cơ chế render `:286-353`:** `selectedTypeInt = TYPE_CODE_TO_INT[typeCode]`; unknown typeCode ⇒ `undefined` ⇒ `currentType=null` ⇒ render **LANDING GRID** mọi type (`:323-343`) thay vì panel. Panel thật = `TypePanel` `:357-459` (active card + "Lịch sử versions" + nút "Tạo quy trình mới") — mỗi panel khoá vào ĐÚNG 1 `applicableType`, active = `type.active` (BE tính), history render **filter `!d.isActive`** (`:419,:426`).
|
||||
|
||||
**Cột nút phải (khuôn ảnh):** `DefinitionCard:577-612` — Sửa tại chỗ `:587` / Tạo phiên bản mới `:596` / Ghim-Bỏ ghim `:606` / Xoá version `:610`. Badge: Đang áp dụng `:490` · Cho user chọn `:502` · Ngưỡng CEO `:506-513`. Badge NS PRO/CCM per NV `:560-565` (display-only suy từ role).
|
||||
|
||||
**Dialog Sửa tại chỗ / Tạo mới:** cùng component `Designer:620-1354`, `isEdit = editTarget !== null` `:638`; title "Sửa tại chỗ — {code} v{NN} (giữ nguyên phiên bản)" `:868`. Khối cam "QUYỀN DUYỆT {TÊN}" `:1228-1329` = **9 checkbox** map field TS (`EditLevelEntry:100-123` / API `LevelDto:37-62`):
|
||||
| # | Label UI | Field |
|
||||
|---|---|---|
|
||||
|1| Trả về 1 Cấp trước | `allowReturnOneLevel` |
|
||||
|2| Trả về 1 Bước trước | `allowReturnOneStep` |
|
||||
|3| Trả về Người chỉ định | `allowReturnToAssignee` |
|
||||
|4| Trả về Drafter (mặc định) — tick sẵn | `allowReturnToDrafter` (default true `:231`) |
|
||||
|5| Chỉnh sửa **Section 2 (Hạng mục/NCC/Báo giá)** | `allowApproverEditDetails` `:1275` |
|
||||
|6| Chỉnh sửa Section ngân sách | `allowApproverEditBudget` `:1284` |
|
||||
|7| Duyệt thẳng Cấp cuối | `allowApproverSkipToFinal` — 🔴 **DISABLED khi type=10** `:1292-1302` (K3: service KHKK chưa port SkipToFinal, BE ép-false) |
|
||||
|8| Duyệt là KẾT THÚC quy trình (xanh emerald) | `allowApproverFinalize` `:1307-1315` |
|
||||
|9| Cho phép NV này XÓA phiếu (đỏ rose) | `allowApproverDelete` `:1320-1328` |
|
||||
9 cờ gửi qua 1 hàm chung `levelFlagsPayload:255-267` (2 call-site POST `:820` + PUT `:788` — chống silent-drop F6/F-7 S155). Rule-box xanh cuối dialog `:1343-1350`. Ô "Ngưỡng CEO" `:944-961` render cho MỌI type (kể cả 10).
|
||||
|
||||
## §2 8 mục KHKK sidebar hiện map đi đâu (4-chỗ pattern 16-bis)
|
||||
|
||||
| Chỗ | File:line | Trạng thái |
|
||||
|---|---|---|
|
||||
| 1. BE const + `All` | `MenuKeys.cs:127-134` + `:248-249` | ✅ 8 key `AwV2_KhkkN1..8` VÀO `All` (⇒ 32 policy) |
|
||||
| 2. BE menu-row seed | `DbInitializer.cs:1988-1995` | ✅ label "Duyệt KHKK — Nhóm {n}", parent `ApprovalWorkflowsV2` (root :1981 dưới System), order 3..10, icon FileSignature; Admin auto-grant qua `SeedAdminPermissionsAsync` (comment :1984-1987) |
|
||||
| 3. FE `lib/menuKeys.ts` mirror | `fe-admin:31-32` / `fe-user:31-32` | ❌ chỉ có `AwV2_DuyetNcc(+PhuongAn)` — **0 KhkkN**. VÔ HẠI runtime: sidebar render BE-tree + `resolvePath` string thô; 0 call-site `can('AwV2_*')`; route `/system/approval-workflows-v2` **KHÔNG PermissionGuard** (`App.tsx:79-80` route trần). Optional mirror-completeness |
|
||||
| 4. FE `resolvePath` | `fe-admin/src/components/Layout.tsx:188` | ✅ `/^KhkkN[1-8]$/` → `/system/approval-workflows-v2/KhkkN{n}` (K3 S164/S165, CHỈ nới họ `AwV2_*`) |
|
||||
|
||||
**Bấm vào hôm nay ra gì:** route khớp `App.tsx:80` → `ApprovalWorkflowsV2Page`, nhưng `TYPE_CODE_TO_INT['KhkkN3'] = undefined` ⇒ `currentType=null` ⇒ **LANDING GRID mọi type** (`:323-343`, header "Quy trình duyệt (Mới)" + hint "chọn loại từ menu"). **CẢ 8 mục ra CÙNG landing — KHÔNG ra khuôn DuyetNcc panel. Đây chính là "lệch".** (Không phải ComingSoon, không 404.)
|
||||
|
||||
**Sibling:** root AwV2 có leaf 1-2 = DuyetNcc/PhuongAn (`:1982-1983`); KHÔNG có leaf `AwV2_Contract` seed (Layout `:178` chỉ phục vụ deep-link).
|
||||
|
||||
## §3 Delta "ko lệch" — cần thêm gì + hardcode-PE-label sites
|
||||
|
||||
**Danh tính nhóm = `Code`, KHÔNG phải ApplicableType.** 8 workflow seed `SeedKhkkGroupWorkflowsAsync` (`DbInitializer.cs:549-625`): `Code="KHKK-N{n}"`, **CÙNG `ApplicableType=10`**, Version=1, `IsActive=true` cả 8, `IsUserSelectable=true`, 3 trạm PRO→CCM→BGĐ sao roster Duyệt NCC prod. BE guard pin: regex `^KHKK-N([1-8])$` phải khớp `plan.ApprovalGroup` (`ContractSigningPlanFeatures.cs:479-486`). FE helper sẵn **2 app identical**: `khkkGroupWorkflowCode(n)='KHKK-N{n}'` + `khkkGroupMenuLabel(n)` (`fe-{admin,user}/src/types/khkk.ts:159-168`). Precedent slice-by-code đã chạy ở fe-user `WorkflowMatrixViewPage.tsx:84-99` (K4c: client-side filter `wf.code === khkkGroupWorkflowCode(g)`, không thêm trục group vào queryKey).
|
||||
|
||||
**Delta CHÍNH — 1 file `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx`** (page đã generic theo `:typeCode`; KHÔNG thêm route/menu/BE-seed):
|
||||
1. `:164-172` — thêm chiều parse: `const khkkKindMatch = typeCode?.match(/^KhkkN([1-8])$/)` → `khkkGroup: 1..8 | null`; `selectedTypeInt = khkkGroup ? 10 : TYPE_CODE_TO_INT[typeCode]` (overview query giữ nguyên `applicableType=10`).
|
||||
2. `:299-301` — `currentType` cho kind nhóm = **synthetic slice**: từ `types.find(t=>t.applicableType===10)` lấy `versions = history.filter(d => d.code === khkkGroupWorkflowCode(g))`; `active = versions.find(v=>v.isActive) ?? null`; `applicableTypeLabel = 'Duyệt KHKK — Nhóm {g}'` (menu-label BE `:1988`) — **D4** anh chọn nhãn này hay nhãn nghiệp vụ `khkkGroupMenuLabel(g)` = "{g}. Thiết bị, máy móc…" (đề xuất: title menu-label + phụ đề nghiệp vụ).
|
||||
3. `TypePanel:357` + `Designer:620` — prop mới `lockedCode?: string`: Designer `defaultCode → lockedCode ?? DEFAULT_CODE_BY_TYPE[...]` (`:650-651`), ô Mã `disabled={isEdit || !!lockedCode}` (`:927`), POST gửi `lockedCode ?? code` (`:798`). **Thiếu khoá này thì "Tạo quy trình mới" từ panel nhóm 3 với mã tự gõ sẽ đẻ version rơi khỏi slice → biến mất khỏi panel** (và không được guard `:482` nhận là workflow nhóm).
|
||||
4. Không đổi gì khác của khuôn: 4 nút phải, badge, Lịch sử versions, dialog 9-checkbox, rule-box — dùng nguyên `TypePanel`/`DefinitionCard`/`Designer` (đúng "ko lệch"). SkipToFinal đã tự disable cho type-10 (`:1292-1302`) — 8 kind thừa hưởng vì cùng applicableType=10.
|
||||
|
||||
**Hardcode-label sites trong khuôn chung (đánh dấu — KHÔNG tự quyết):**
|
||||
- `:1275` "Cho phép chỉnh sửa **Section 2 (Hạng mục/NCC/Báo giá)** lúc đang duyệt" — từ vựng PE. KHKK service **KHÔNG đọc cờ này** (0-hit `AllowApproverEditDetails` trong `ContractSigningPlan*.cs`).
|
||||
- `:1284` "Section ngân sách" — tương tự, 0-hit `AllowApproverEditBudget`.
|
||||
- `:944-961` ô "**Ngưỡng giá trị gói CEO**" render mọi type — KHKK service **CỐ Ý KHÔNG port** `CeoApprovalThreshold` (`ContractSigningPlanWorkflowService.cs:46-47`: NĐUQ = human-gate vĩnh viễn, OG-3) ⇒ nhập cho kind KHKK = config-lie class #78. Khuôn disable-có-tooltip sẵn ở `:1292`.
|
||||
- Badge "✎ NS PRO/NS CCM" (`:560-565`) — suy từ role PE-budget; hiển thị trên panel KHKK như đang hiển thị cho type-10 hiện nay.
|
||||
- **Răng thật của 9 checkbox với KHKK hôm nay: CHỈ #8 `AllowApproverFinalize` sống** (`ContractSigningPlanWorkflowService.cs:276`); 4 cờ Trả-về không đọc — Trả lại KHKK = TOÀN PHẦN luôn-được (`:48,:354-362`); Delete/EditDetails/EditBudget/SkipToFinal: 0-hit. **D3:** giữ nguyên khuôn (đúng "ko lệch", như panel ContractSigningPlan đang render) hay disable 7 ô chết theo khuôn SkipToFinal — anh/lead chốt; mặc định nghiêng GIỮ (uniform, cờ nằm chờ port).
|
||||
|
||||
## §4 fe-user — IsUserSelectable tại create KHKK + designer admin-only proof
|
||||
|
||||
- **Tạo phiếu KHKK KHÔNG có dropdown chọn quy trình** — workflow GÁN CỨNG theo nhóm (owner S174): `fe-user/src/pages/khkk/KhkkCreatePage.tsx:96-116` query `applicableType=10` → **filter `w.isUserSelectable` `:104`** → `autoWorkflow = find(code === 'KHKK-N{group}')` `:113-116`; thiếu mã ⇒ CHẶN tạo + báo "Chưa có quy trình KHKK-N{n}" (`:128,:155`). `fe-admin/src/pages/khkk/KhkkCreatePage.tsx` mirror cùng khuôn (`:97-100`). ⇒ **Bỏ ghim (`IsUserSelectable=false`) 1 workflow nhóm = CHẶN TẠO PHIẾU nhóm đó** ở cả 2 app — nút Ghim/Bỏ ghim trên 8 panel mới có hệ quả nghiệp vụ thật.
|
||||
- Consumer thứ 2 `fe-user/src/pages/khkk/KhkkDetailPage.tsx:214-227` = bridge KHKK→HĐ, query `applicableType=3` lọc `isUserSelectable && isActive` — không liên quan nhóm.
|
||||
- **Designer admin-only — CHỨNG:** grep `approval-workflows-v2` toàn `fe-user/src` = chỉ API-consumer (Create/Detail/Matrix/Proposal/WorkflowApp) — **0 page designer, 0 route `/system/approval-workflows-v2`**, Layout fe-user **0 nhánh `AwV2_*`** (grep 0-hit ngoài `lib/menuKeys.ts:31-32` const chết). fe-user chỉ có view read-only `pages/pe/WorkflowMatrixViewPage.tsx` (`/khkk/workflow-matrix?type=10&group=n`, K4c slice cùng convention code).
|
||||
|
||||
## §5 Wave FE đề xuất + acceptance
|
||||
|
||||
**W-FE1 (fe-admin, 1 file):** `ApprovalWorkflowsV2Page.tsx` — 4 chạm §3 (parse kind `:164-172` · synthetic slice `:299-301` · `lockedCode` xuyên `TypePanel:345-350/:357/:447-455` → `Designer:620/:650/:798/:927` · title). Import từ `types/khkk.ts` (đã có sẵn trong fe-admin). Ước ~60-90 dòng diff.
|
||||
**W-FE2 (optional):** `fe-{admin,user}/src/lib/menuKeys.ts` +8 const `AwV2_KhkkN*` (mirror-completeness, 0 hệ quả runtime).
|
||||
**SHA-mirror 2 app: KHÔNG áp dụng** — trang system Designer là fe-admin-only (chứng §4); `types/khkk.ts` 2 app đã identical sẵn, không chạm.
|
||||
**BE:** 0 bắt buộc cho khuôn hiển thị; **D1 (khuyến nghị làm cùng wave):** `ApprovalWorkflowV2AdminFeatures.cs:339-343` deactivate-per-TYPE — sửa thành per-Code khi code khớp `^KHKK-N[1-8]$` (hoặc per-Code toàn cục — semantic đổi, anh chốt).
|
||||
|
||||
**Acceptance đo được:**
|
||||
1. Login admin → sidebar Hệ thống › Quy trình duyệt (Mới) hiện 10 leaf; bấm "Duyệt KHKK — Nhóm 3" → URL `/system/approval-workflows-v2/KhkkN3` → header "Quy trình duyệt (Mới): Duyệt KHKK — Nhóm 3" + card `KHKK-N3 v01` badge "Đang áp dụng"+"Cho user chọn" + 3 Bước (PRO 4NV+TP·CCM 4NV+TP·BGĐ CEO) + cột 4 nút phải + "Lịch sử versions"/"Tạo quy trình mới". 8/8 leaf → 8 slice khác nhau, hết landing grid.
|
||||
2. "Sửa tại chỗ" → dialog title "Sửa tại chỗ — KHKK-N3 v01 (giữ nguyên phiên bản)", per-NV khối cam "QUYỀN DUYỆT …" 9 checkbox 2 cột, "Trả về Drafter (mặc định)" tick sẵn, dòng xanh KẾT THÚC + dòng đỏ XÓA phiếu, SkipToFinal mờ + tooltip, rule-box cuối.
|
||||
3. "Tạo quy trình mới" từ panel nhóm: ô Mã khoá `KHKK-N3` → lưu ra v02 nằm ĐÚNG panel nhóm 3; (nếu D1 làm) badge "Đang áp dụng" 7 nhóm khác KHÔNG mất.
|
||||
4. Regression: `/DuyetNcc` `/DuyetNccPhuongAn` `/Contract` `/ContractSigningPlan` render như cũ; fe-user tạo phiếu nhóm 3 vẫn auto-pin `KHKK-N3` (không đổi API).
|
||||
|
||||
## §6 Surprises ngoài đề
|
||||
|
||||
1. 🔴 **Panel `ContractSigningPlan` (đích của `Khkk_WfView` admin, `Layout.tsx:40`) hôm nay GIẤU 7/8 workflow nhóm:** BE `Active = FirstOrDefault(IsActive)` per-type (`ApprovalWorkflowV2AdminFeatures.cs:210`, tie-break Version DESC không xác định giữa 8 bản v1) + FE history filter `!d.isActive` (`:419,:426`) ⇒ 8 bản đều active thì 1 hiện card, 7 KHÔNG hiện đâu cả. Slice-by-code của wave này vá đúng bệnh; cân nhắc re-point `Khkk_WfView` hoặc để panel aggregate liệt theo code.
|
||||
2. 🔴 **Bom deactivate-per-TYPE** (`:339-343`): bấm "Tạo quy trình mới"/"Lưu + kích hoạt" cho BẤT KỲ mã nào thuộc type-10 ⇒ `IsActive=false` CẢ 8 nhóm (seeder không re-set — idempotent per-Code `:610` skip). Runtime tạo phiếu KHÔNG hỏng (pin theo `IsUserSelectable`, guard type `EnsureWorkflowTypeAsync:307-318` không đòi IsActive) nhưng badge/active-slice sai toàn bộ. = D1.
|
||||
3. Seeder KHKK resolve người duyệt THEO EMAIL, thiếu user ⇒ bỏ slot/cấp/bước + log warning (`DbInitializer.cs:544-548,:597-600`) — DB dev thiếu người thật thì 8 panel có thể hiện bước rỗng/thiếu NV; không phải bug FE.
|
||||
4. `AwV2_Contract` không có menu-leaf seed (chỉ deep-link `Layout.tsx:178`) — sibling lệch nhẹ, không thuộc scope.
|
||||
5. Route FE `/system/approval-workflows-v2` KHÔNG PermissionGuard (App.tsx:79-80) — 2-tầng gotcha #82: hiển thị dựa menu-grant, authz thật ở BE controller.
|
||||
@ -0,0 +1,153 @@
|
||||
# REVIEW-1 (Fable, adversarial ĐÚNG-SAI) — S182 khkk-awf-mirror-ncc
|
||||
|
||||
> Vai: `/fable-real reviewer` #1/2 — lens SAI/SÓT/MÂU-THUẪN trên spec DRAFT + 3 invest.
|
||||
> Trạng thái: HOÀN TẤT (3 đợt đo trực tiếp trên code + findings + wave-final).
|
||||
|
||||
## VERDICT
|
||||
**REVIEW-1: PASS_WITH_FIXES — 2C/5M** (+5m)
|
||||
- Kiến trúc spec ĐÚNG HƯỚNG, đa số claim load-bearing HELD khi đo lại trên code (History-full-list, deactivate-site, PUT-guards, rào-v, delete-0-guard, middleware, seeder, 0-consumer AwV2_). Verdict không phải FAIL vì không có finding nào lật kiến trúc — nhưng 2C (F1 unbuildable W1b, F2 IgnoreQueryFilters) PHẢI vào spec final trước khi execute, 5M nên vào cùng đợt.
|
||||
- C: **F1** (W1b helper internal ⟂ Submit Infrastructure — không biên dịch được như viết) · **F2** (W1d thiếu `IgnoreQueryFilters` ⇒ claim "hết 500" sai ở ca phổ biến nhất).
|
||||
- M: **F3** (5 office-pin LOOSE 0-FK — DB-invest M3 sai, W1d là rào duy nhất) · **F4** (khoá công thức W1a + belt + test code-lạ) · **F5** (KHKK-create không check IsActive — răng nút Ghim mô tả sai sau wave) · **F6** (D3=C: ô #4 phải forced-true, không mờ-chưa-có) · **F7** (ô Ngưỡng CEO tách khỏi D3, disable vô điều kiện).
|
||||
- m: F8 (khai mâu-thuẫn 3 lens) · F9 (baseline 645⟂644 — đo tươi) · F10 (môi trường acceptance #3) · F11 (audit pin-lệch tồn đọng) · F12 (hint lockedCode · notify-parity · comment `:339` · invalidate-keys OK).
|
||||
|
||||
## Checklist việc (8 mục từ lệnh)
|
||||
- [x] 1. W1a deactivate-per-Code — đọc `:326-398` + grep IsActive consumer toàn BE/Office → F4, kết luận P1 đủ (P2 không cần — 0 consumer server-side dựa active-per-type-10)
|
||||
- [x] 2. W2b synthetic slice — BE `:199-216` + FE `:286-353` → HELD (History = full list; slice đúng shape; queryKey/invalidate OK)
|
||||
- [x] 3. W2c lockedCode — PUT Code-echo 409 `:539-541` HELD; đường rơi-khỏi-slice còn lại = POST panel khác (chấp nhận, per-Code fix F4 khiến vô hại) → F12a hint
|
||||
- [x] 4. W1b group-match Submit — Submit `:163-213` không re-check; extract-helper UNBUILDABLE → F1; phiếu pin lệch tồn đọng → F11
|
||||
- [x] 5. W1d DELETE — đếm từ Domain+snapshot: 8 cột pin, chỉ 3 FK, 5 LOOSE; ItTicket không pin → F2, F3
|
||||
- [x] 6. D3 chấm A/B/C — A: giữ #78 6-ô + đẻ "sleeping-config" khi port sau; B: nặng, thuộc owner (service comment `:48-50` tự khai); C: khả thi có tiền lệ NHƯNG phải theo danh-sách F6 (#4 forced-true) + F7 tách CEO-box. Không chấm thay anh — cả 3 sống, C rẻ+trung thực nhất, A đúng "ko lệch" nhất (khai trade-off)
|
||||
- [x] 7. Mâu thuẫn 3 lens → F8 (a/b/c)
|
||||
- [x] 8. Sót chung → F12 (notify parity-PE không làm · invalidate keys ĐO OK · optimistic không dùng — trang này invalidate-only)
|
||||
|
||||
## 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.
|
||||
|
||||
## ĐO TRỰC TIẾP (đợt 3 — service/seeder/status/FE-designer)
|
||||
- **HELD:** service KHKK comment `:36-65` tự khai (c) 4 return-mode CỐ Ý chưa đọc + "mở thêm mode là đổi hành vi duyệt, thuộc quyền lead" · finalize `:276-289` = #8 sống thật · Submit là entry-point controller-gọi-thẳng (`:60-62` — xác nhận KHÔNG có MediatR handler cho Submit).
|
||||
- **HELD:** seeder `DbInitializer.cs:539-625` đúng như invest (idempotent per-Code `:610`, roster email prod, `IsActive=true + IsUserSelectable=true` `:622-623`, thiếu user → bỏ slot + warn).
|
||||
- **HELD (control):** grep `AwV2_` trong `SolutionErp.Api` = **0 hit** — khớp claim "32 policy 0-consumer BE".
|
||||
- **Đo FE Designer:** "Tạo quy trình mới" (`TypePanel:413`) set `cloneFrom = type.active` ⇒ ở panel nhóm (synthetic slice) code state tự prefill `KHKK-N{g}` từ card active — lockedCode là RÀO chống sửa tay + chống case active=null (rơi về `DEFAULT_CODE_BY_TYPE[10]='QT-KHKK-V2-001'` `:650`). Archived card cũng đủ 4 nút (`:426-443`) — slice pass-through by-Id OK.
|
||||
- **Đo STATUS.md:** canonical dòng 8 = **"644 test PASS (45D+599I — CI #444 KHỚP local)"** @S168; mốc sống S179 không ghi đổi test. Spec W4 ghi "baseline 645" — lệch 1, không rõ nguồn.
|
||||
|
||||
## FINDINGS
|
||||
|
||||
> Ký hiệu: `[C]` phải sửa spec trước khi execute · `[M]` lỗ/claim-sai phải vá trong spec · `[m]` khai/polish. Mỗi finding: chỗ sai (spec/invest) → bằng chứng file:line → cách vá.
|
||||
|
||||
**F1 [C] — W1b không biên dịch được như viết (helper internal ⟂ Submit nằm Infrastructure).**
|
||||
Spec W1b: *"extract rào (v) → `EnsureWorkflowGroupMatchAsync(db, awId, group)`; gọi ở Create + UpdateDraft + Submit"*. Đo: `ContractSigningPlanScope` = `internal static` (`ContractSigningPlanFeatures.cs:263`); `InternalsVisibleTo` duy nhất = `SolutionErp.Infrastructure.Tests` (`SolutionErp.Application.csproj:25`); Submit KHÔNG có Application-handler — controller gọi thẳng service Infrastructure (`ContractSigningPlanFeatures.cs:19` + `ContractSigningPlanWorkflowService.cs:60-62`) ⇒ helper để trong Scope thì site thứ 3 KHÔNG gọi được, executor sẽ tự chế đường khác ngoài review.
|
||||
**Vá:** tạo `public static class ContractSigningPlanWorkflowGuards` (Application/ContractSigningPlans, file cạnh Scope) chứa helper; Create/UpdateDraft (Application) + SubmitAsync (Infrastructure — đã reference Application) cùng gọi. KHÔNG dùng `InternalsVisibleTo(Infrastructure)` (mở toàn bộ internal surface 2 assembly) · KHÔNG inline-duplicate (drift 3 bản regex).
|
||||
|
||||
**F2 [C] — W1d claim "hết cảnh 547 → 500" là claim-mạnh-hơn-fix: thiếu `IgnoreQueryFilters()`.**
|
||||
PE + Contract + KHKK đều có `HasQueryFilter(!IsDeleted)` (`PurchaseEvaluationConfiguration.cs:84` · `ContractConfiguration.cs:44` · `ContractSigningPlanConfiguration.cs:55`). Đếm-pin naive sẽ KHÔNG THẤY phiếu xoá-mềm, nhưng FK vật lý vẫn chặn (row còn) ⇒ đúng ca PHỔ BIẾN NHẤT (drafter xoá phiếu nháp/TraLai — allow-list KHKK S177 — rồi admin xoá version "không ai dùng") vẫn 547→500. Test trong spec ("DELETE version có pin ⇒ 409") PASS mà bug ship — acceptance đo hình-dạng không đo hành-vi (@S179 class).
|
||||
**Vá:** mọi count pin/opinion trong guard dùng `IgnoreQueryFilters()` (3 module có filter; 5 office không filter — đếm thẳng); opinions đếm CẢ row phiếu-đã-xoá (FK không quan tâm IsDeleted). Test bổ sung: phiếu KHKK đã xoá-mềm pin version → DELETE 409.
|
||||
|
||||
**F3 [M] — DB-invest M3 "data KHÔNG mất (DB chặn được)" SAI cho 5 module Office — pin LOOSE 0-FK; W1d là guard DUY NHẤT.**
|
||||
Đo snapshot: `HasForeignKey("ApprovalWorkflowId")` = **4 hit** (Steps-Cascade `:6231` + KHKK `:6247` + Contract `:6329` + PE `:6757`). Domain có **8 cột pin** (`Proposal/LeaveRequest/OtRequest/VehicleBooking/TravelRequest` thêm vào 3 module trên; ItTicket KHÔNG có — "8/9" của BE-invest ĐÚNG). ⇒ DELETE hôm nay với phiếu Office pin (0 opinions) **thành công vật lý** → dangling pin → phiếu treo chết lúc load workflow. Spec W1d đếm đủ 8 module (đúng), nhưng rationale kế thừa claim sai của DB-invest — executor đọc M3 có thể thu hẹp còn 3 module FK "vì DB chặn rồi".
|
||||
**Vá spec:** sửa rationale W1d thành 2 vế: (a) 3 module FK = nâng UX 547→409; (b) 5 module Office = **chống mất-toàn-vẹn dữ liệu** (không có FK đỡ). Test BẮT BUỘC ca office: Leave/Proposal phiếu pin → DELETE 409 (hôm nay = 200-xoá-êm).
|
||||
|
||||
**F4 [M] — W1a: 2 công thức scope KHÁC HÀNH VI giữa 2 invest, spec phải KHOÁ công thức + thiếu 1 test + thiếu belt.**
|
||||
BE-invest P1 = `typeEnum == ContractSigningPlan ⇒ scope Code`; FE-invest D1 = *"per-Code khi code khớp `^KHKK-N[1-8]$`"*. Khác nhau ở code type-10 NGOÀI-họ (`QT-KHKK-V2-001` — chính là `DEFAULT_CODE_BY_TYPE[10]` `:179` mà panel ContractSigningPlan đề xuất): công thức BE → per-Code (an toàn); công thức FE-regex → rơi nhánh per-type → **NUKE cả 8 nhóm**. Spec chọn đúng (typeEnum) nhưng khai "3 lens hội tụ" — che lệch; executor đọc FE-invest có thể implement regex.
|
||||
**Vá:** (a) KHOÁ công thức trong spec: `w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive` (vế `ApplicableType` = belt: UNIQUE(Code,Version) là GLOBAL nên 1 Code có thể span 2 type qua version khác nhau — hiếm nhưng rẻ để chặn); (b) thêm test thứ 3: POST type-10 code-lạ `QT-KHKK-V2-001` ⇒ IsActive 8 nhóm bất biến; (c) ghi chú P1 nhất quán với chính hàm — `nextVersion` `:334-337` đã per-Code sẵn.
|
||||
|
||||
**F5 [M] — KHKK-create KHÔNG check `IsActive` server-side ⇒ sau wave này, nút Ghim mất răng như spec mô tả + pin-bản-archived = 200.**
|
||||
Đo: Create/UpdateDraft phiếu KHKK chỉ `EnsureWorkflowTypeAsync` (type) + rào (v) — KHÔNG đòi IsActive/IsUserSelectable; đối chứng bridge KHKK→HĐ ĐÃ vá đúng lỗ này cho type-3 (vá-3, `CreateContractFromSigningPlanFeatures.cs:141-147`, comment tự khai "forge POST pin được quy trình retired"). FE `KhkkCreatePage` fe-user `:104` filter `isUserSelectable` KHÔNG lọc `isActive`, `:114-116` `find(code)` ăn may theo ordering Version-DESC của BE. Hệ quả CHỈ LỘ SAU W2 (mở đường tạo v02): (a) v01 sau deactivate vẫn `IsUserSelectable=true` (POST `:343` không đụng cờ này) ⇒ "Bỏ ghim v02" làm auto-pin **rơi về v01 archived im lặng** thay vì CHẶN tạo — câu regression của spec (*"Bỏ ghim ⇒ CHẶN TẠO PHIẾU nhóm đó"*) chỉ đúng khi nhóm có đúng 1 version; (b) forge POST pin bản archived.
|
||||
**Vá (khuyến nghị làm trong W1):** Create + UpdateDraft-khi-đổi-pin thêm check `IsActive ∧ IsUserSelectable` 2 message tách (mirror vá-3); UpdateDraft giữ-nguyên-pin không đụng (guard chỉ chạy `awId != plan.ApprovalWorkflowId` `:603` — phiếu cũ pin bản archived vẫn sửa GhiChu bình thường). FE ×2 app: `find(w.isActive && code-match)` (fe-user `:114` + fe-admin mirror `:113-116`). Nếu anh muốn giữ nguyên semantics cũ thì tối thiểu PHẢI sửa câu răng-nút-Ghim trong spec thành "chặn khi bỏ ghim TẤT CẢ version của nhóm".
|
||||
|
||||
**F6 [M] — D3=C như spec viết sẽ NÓI DỐI CHIỀU NGƯỢC ở ô #4 Trả-về-Drafter; danh sách ô mờ phải CHÍNH XÁC.**
|
||||
Bảng BE §3 dòng #4: KHKK **BỎ QUA cờ nhưng hành-vi = LUÔN-CHO** (return về Drafter cho mọi NV đúng lượt kể cả cờ tắt — `ReturnOrRejectAsync` không đọc cờ). Mờ ô #4 kiểu "chưa áp dụng cho KHKK" = bảo tính năng KHÔNG CÓ trong khi nó LUÔN BẬT.
|
||||
**Vá spec-C (danh sách chốt, gate `applicableType === 10` — áp cả panel ContractSigningPlan cho nhất quán):**
|
||||
- Mờ-**false**-disabled + tooltip "chưa áp dụng cho KHKK": #1 `allowReturnOneLevel` · #2 `allowReturnOneStep` · #3 `allowReturnToAssignee` (`:1235-1266` vùng render) · #5 `allowApproverEditDetails` `:1268-1276` · #6 `allowApproverEditBudget` `:1277-1285` · #9 `allowApproverDelete` `:1320-1328` — 6 ô.
|
||||
- **Forced-true**-disabled + tooltip "KHKK luôn trả về người soạn khi Trả lại — cờ không tắt được": #4 `allowReturnToDrafter` `:1256-1267`.
|
||||
- Giữ nguyên: #7 đã disabled sẵn (`:1292-1302`) · #8 sống (`:1307-1315`).
|
||||
- Payload: ô mờ gửi giá-trị-đang-lưu (echo, không ép) để không vấp guard PUT.
|
||||
|
||||
**F7 [M] — Ô "Ngưỡng giá trị gói CEO" phải TÁCH KHỎI D3 — disable cho type-10 VÔ ĐIỀU KIỆN.**
|
||||
`:944-961` chỉ `disabled={isEdit}` ⇒ create-mode NHẬP ĐƯỢC cho type-10, trong khi `CeoApprovalThreshold` là quyết-đã-chốt-vĩnh-viễn KHÔNG port (OG-3 NĐUQ human-gate — `ContractSigningPlanWorkflowService.cs:46-47`). Lý lẽ của phương án A ("cờ nằm chờ port") KHÔNG áp dụng cho ô này — nó là #78 vĩnh viễn, không phải tạm thời. Spec hiện bó nó vào C ⇒ anh chọn A hoặc B là lỗ này sống tiếp.
|
||||
**Vá:** W2 thêm mục riêng (mọi phương án D3): type-10 ⇒ ô CEO disabled + tooltip "KHKK không dùng ngưỡng máy — NĐUQ quyết" + POST gửi `null`; tiền lệ cùng-file `:1292`.
|
||||
|
||||
**F8 [m] — Mâu-thuẫn 3 lens spec chưa khai (mục 7 của đề):** (a) công-thức W1a lệch BE⟂FE (đã gộp F4); (b) D3: FE-invest §3 *"mặc định nghiêng GIỮ (A)"* ⟂ BE-invest §4.3 *"KHÔNG port thì FE PHẢI ẩn/disable"* (= C) — spec trình trung lập mà không khai 2 lens ngược nhau; (c) DB-invest W-DB-2 (thêm chiều `code=` phía BE) bị DROP im lặng — spec chọn client-side slice mà không ghi disposition. **Vá:** 3 dòng khai trong MỤC 3.
|
||||
|
||||
**F9 [m] — Baseline test hardcode "645" ⟂ STATUS canonical "644" (@S168 CI #444).** Một trong hai stale. **Vá:** W0/W4 đo tươi + ghi vào run.md, spec không chép số (B1).
|
||||
|
||||
**F10 [m] — Acceptance #3 chưa chỉ định môi trường; prod-create v02 là mutate config THẬT và KHÔNG có đường re-activate** (controller 5 action — không có activate; PUT cấm đổi IsActive; seeder idempotent per-Code không re-set). **Vá:** chứng chính = integration test W1a; acceptance UI #3 chạy LOCAL sau W0 (chấp nhận workflow rỗng người — đo cấu-trúc slice/badge); nếu muốn prod thì v02 = clone-y-hệt và khai với anh "v02 thành bản sống, v01 archived" TRƯỚC khi bấm.
|
||||
|
||||
**F11 [m] — Thiếu data-audit phiếu ĐÃ pin lệch trước vá.** Guard W1b chỉ chặn Create/UpdateDraft/Submit TỪ NAY; phiếu ChoDuyet pin lệch từ trước chạy tiếp đội hình lệch im lặng. **Vá W5:** thêm query `SELECT p.MaKeHoach, p.ApprovalGroup, w.Code FROM ContractSigningPlans p JOIN ApprovalWorkflows w ON w.Id = p.ApprovalWorkflowId WHERE w.Code LIKE 'KHKK-N%' AND w.Code <> CONCAT('KHKK-N', p.ApprovalGroup)` — >0 row thì báo anh xử tay. Cùng W5: nếu query 8-nhóm cho thấy nhóm nào 0-active (bom đã nổ trước vá) → recovery = "Tạo phiên bản mới" từ panel nhóm (lockedCode tự điền mã đúng).
|
||||
|
||||
**F12 [m] — Sót nhỏ (mục 8):** (a) hint ô Mã `:929-932` khi `lockedCode` nên đổi chữ ("Mã khoá theo nhóm — version auto-tăng"); (b) sửa-workflow-tại-chỗ KHÔNG notify approver phiếu treo — parity PE (PE cũng không notify), KHÔNG làm wave này, khai vào Nợ; (c) invalidate keys ĐÃ ĐO ĐỦ: key `['approval-workflow-v2-overview', selectedTypeInt]` + mutations invalidate prefix ⇒ 8 panel nhóm và panel type-10 dùng chung cache tự tươi, 0 việc; fe-user cache là app khác — không cross-invalidate được, chấp nhận (hành vi sẵn có); (d) comment BE `:339` "only ONE active per type" phải sửa CÙNG COMMIT với W1a kẻo comment nói dối.
|
||||
|
||||
## TỔNG HỢP WAVE FINAL ĐỀ XUẤT
|
||||
> Bản wave-plan ĐÃ SỬA theo F1-F12 — đủ chi tiết cho executor Opus MAX. Chỗ nào giữ nguyên spec DRAFT thì ghi "như spec".
|
||||
|
||||
### W0 — prep local (như spec) + 2 bổ sung
|
||||
1. `dotnet ef database update` ×2 DB (Dev + Design — đo lại drift tại chỗ, đừng tin số cũ) → boot API 1 lần → 8 row `KHKK-N*` local (RỖNG NGƯỜI là ĐÚNG).
|
||||
2. **[F9]** `dotnet test SolutionErp.slnx` đo baseline TƯƠI, ghi số vào `run.md` (spec 645 ⟂ STATUS 644 — không chép, đo).
|
||||
3. **[F10]** Chốt môi trường acceptance: #1/#2 local + prod; #3 = integration test (chứng chính) + UI local; prod-#3 CHỈ khi anh gật "v02 clone thành bản sống".
|
||||
|
||||
### W1 — BE: 5 fix, 2 file + 1 file MỚI, 0-mig
|
||||
| # | Fix | Site | Cách (đã sửa theo finding) |
|
||||
|---|---|---|---|
|
||||
| a | Deactivate-per-Code type-10 **[F4]** | `ApprovalWorkflowV2AdminFeatures.cs:339-343` | CÔNG THỨC KHOÁ: `typeEnum == ContractSigningPlan` ⇒ `Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)`; type khác giữ per-type. CẤM biến-thể regex `^KHKK-N[1-8]$` của FE-invest D1 (code lạ type-10 sẽ rơi per-type → nuke 8 nhóm). Sửa comment `:339` cùng commit **[F12d]** |
|
||||
| b | Group-match guard 3-site **[F1]** | Create `:479-486` · UpdateDraft `:603-607` · `SubmitAsync` (`ContractSigningPlanWorkflowService.cs`, sau guard pin `:163-165`) | File MỚI `Application/ContractSigningPlans/ContractSigningPlanWorkflowGuards.cs` — `public static class`, method `EnsureWorkflowGroupMatchAsync(IApplicationDbContext, Guid awId, int approvalGroup, CancellationToken)` (move nguyên logic rào (v), GIỮ semantics cho-qua-ngoài-họ + khai trong spec). 3 site cùng gọi. Message 409: "Phiếu nhóm N{g} đang gắn quy trình N{k} — vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình." |
|
||||
| b2 | **MỚI [F5]** IsActive∧IsUserSelectable cho pin KHKK | Create (sau rào iv) + UpdateDraft (trong nhánh `awId != plan.ApprovalWorkflowId` `:603`) | Mirror vá-3 `CreateContractFromSigningPlanFeatures.cs:141-147` — 2 message tách "đã ngừng hoạt động"/"chưa ghim". Giữ-nguyên-pin KHÔNG check (phiếu cũ pin bản archived vẫn sửa GhiChu được) |
|
||||
| c | Changelog type-10 khi PUT **(như spec, chi tiết chốt)** | `:863-893` | Nhánh `else if (def.ApplicableType is ContractSigningPlan)`: query `db.ContractSigningPlans.Where(p => p.ApprovalWorkflowId == def.Id && p.Phase != DaDuyet && p.Phase != TuChoi)` (query-filter tự loại phiếu xoá-mềm — ĐÚNG cho changelog) → add `ContractSigningPlanChangelogs { EntityType = Workflow /*=4, enum sẵn*/, Action = Update, EntityId = def.Id, PhaseAtChange, UserId/UserName, Summary + ContextNote }` mirror block PE |
|
||||
| d | DELETE usage-check → 409 **[F2][F3]** | `:1193-1205` | Đếm pin **8 module** (PE/Contract/KHKK **BẮT BUỘC `IgnoreQueryFilters()`**; Proposal/Leave/Ot/Vehicle/Travel đếm thẳng — 5 module này pin LOOSE 0-FK, guard này là RÀO DUY NHẤT chống dangling) + đếm opinions 8 bảng theo level-ids của workflow (IgnoreQueryFilters — FK không quan tâm IsDeleted) → 409 kể tên "N phiếu đang gắn + M chữ ký". Rationale 2 vế: FK-modules = UX 547→409; office = data-integrity |
|
||||
|
||||
**Test W1 (test-before a/b/b2, test-after c/d):**
|
||||
1. Tạo v2 `KHKK-N1` ⇒ N2..N8 IsActive bất biến. 2. DuyetNcc tạo Code-mới-cùng-type vẫn tắt Code cũ (regression per-type). 3. **[F4]** POST type-10 code-lạ `QT-KHKK-V2-001` ⇒ 8 nhóm bất biến. 4. UpdateDraft re-pin lệch nhóm ⇒ 409; re-pin bản archived ⇒ 409 **[F5]**; Submit phiếu pin lệch ⇒ 409. 5. PUT workflow nhóm có phiếu treo ⇒ 1 row `ContractSigningPlanChangelogs`. 6. DELETE version có pin KHKK ⇒ 409; **phiếu KHKK ĐÃ XOÁ MỀM pin ⇒ vẫn 409 [F2]**; **phiếu Leave (office, 0-FK) pin ⇒ 409 [F3]**; version 0-usage ⇒ xoá OK.
|
||||
|
||||
### W2 — FE `ApprovalWorkflowsV2Page.tsx` (~70-100 dòng) + KhkkCreatePage ×2
|
||||
| # | Chạm | Site | Cách |
|
||||
|---|---|---|---|
|
||||
| a | Parse kind (như spec) | `:164-172` | `match(/^KhkkN([1-8])$/)` → `khkkGroup`; `selectedTypeInt = khkkGroup ? 10 : TYPE_CODE_TO_INT[typeCode]` |
|
||||
| b | Synthetic slice (như spec — ĐÃ VERIFY `History` = full list kể cả active, BE `:206-211`) | `:299-301` | `versions = type10.history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v => v.isActive) ?? null` · label = "Duyệt KHKK — Nhóm {g}" + phụ đề `khkkGroupMenuLabel(g)` (D4) |
|
||||
| c | `lockedCode` (như spec) + hint **[F12a]** | `TypePanel:357` → `Designer:620/:650-651/:798/:920-933` | prop `lockedCode?: string`; ô Mã `disabled={isEdit \|\| !!lockedCode}` + hint "Mã khoá theo nhóm"; POST gửi `lockedCode ?? code` |
|
||||
| d | D3 — thi hành theo anh chốt; **nếu C thì theo danh-sách-chốt [F6]** | `:1235-1328` | 6 ô mờ-FALSE (#1#2#3 return-modes · #5 `:1268-1276` · #6 `:1277-1285` · #9 `:1320-1328`) + **#4 FORCED-TRUE-disabled** tooltip "KHKK luôn trả về người soạn — cờ không tắt được" + #7 giữ disabled sẵn + #8 sống. Gate `applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` (áp cả panel ContractSigningPlan). Payload ô mờ = echo giá-trị-đang-lưu |
|
||||
| e | (optional, như spec) mirror menuKeys +8 const | `fe-{admin,user}/src/lib/menuKeys.ts:31-32` | 0 hệ quả runtime |
|
||||
| f | **MỚI [F7]** — Ô Ngưỡng CEO: disable type-10 VÔ ĐIỀU KIỆN (mọi phương án D3) | `:944-961` | `disabled={isEdit \|\| applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN}` + tooltip "KHKK không dùng ngưỡng máy — NĐUQ quyết" + POST type-10 gửi `null`. Tiền lệ `:1292` |
|
||||
| g | **MỚI [F5]** auto-pin lọc isActive ×2 app | fe-user `KhkkCreatePage.tsx:114` + fe-admin `:113-116` | `find(w => w.isActive && codeMatch)` — giữ filter `isUserSelectable` `:104`. Răng nút Ghim đúng lại: bỏ ghim BẢN ACTIVE = chặn tạo phiếu nhóm |
|
||||
|
||||
**Regression (như spec) + sửa câu răng-Ghim theo F5.** SHA-mirror: KHÔNG áp cho Designer (fe-admin-only, đã chứng); RIÊNG W2g là cặp file 2-app — diff phải mirror logic (không cần byte-equal vì 2 file vốn khác nhau).
|
||||
|
||||
### W3 — CHỈ KHI D3=B (như spec; anchors `PeSoftDeleteFeatures.cs:110-114` / `PurchaseEvaluationWorkflowService.cs:421-424/:810` CHƯA re-đo trong review này — executor verify trước khi port; mỗi cờ 1 test RED→GREEN; `CeoApprovalThreshold` vĩnh viễn KHÔNG port — F7 vẫn áp)
|
||||
|
||||
### W4 — test-specialist đóng suite: baseline = SỐ ĐO Ở W0 (không phải 645) + N test mới W1/W3
|
||||
|
||||
### W5 — reviewer pre-commit → commit → cicd 3-chân (BE api + bundle admin rotate; +eoffice nếu W2e/W2g fe-user) → acceptance + SQL verify
|
||||
- 3 query DB §4 (như spec) + **[F11]** query audit pin-lệch: `... WHERE w.Code LIKE 'KHKK-N%' AND w.Code <> CONCAT('KHKK-N', p.ApprovalGroup)` — >0 báo anh xử tay.
|
||||
- Query 8-nhóm: nhóm nào 0-active (bom nổ trước vá) → recovery = "Tạo phiên bản mới" từ panel nhóm đó (lockedCode tự điền mã, sinh vNext active).
|
||||
- Acceptance 1-4 như spec, #3 theo môi trường đã chốt ở W0.3 **[F10]**.
|
||||
|
||||
### Điểm quyết cho anh (cập nhật theo review)
|
||||
- **D3** giữ 3 phương án NHƯNG: (i) khai 2 lens khuyến nghị ngược nhau (FE nghiêng A, BE nghiêng C) **[F8b]**; (ii) nếu C → dùng danh-sách-chốt F6 (ô #4 forced-true, KHÔNG mờ-kiểu-chưa-có); (iii) ô Ngưỡng CEO KHÔNG thuộc D3 — disable vô điều kiện **[F7]**.
|
||||
- **D4** (title panel) giữ như spec.
|
||||
- **MỚI D5 [F5]:** chốt hành vi pin: (khuyến nghị) thêm check IsActive∧IsUserSelectable + FE lọc isActive — hay giữ nguyên semantics cũ và sửa lại câu răng-nút-Ghim trong spec.
|
||||
|
||||
### Nợ NGOÀI scope (giữ như spec, +2)
|
||||
GET Overview 0-policy (information-disclosure mềm) · roster snapshot-email · `Khkk_WfView` panel aggregate giấu 7/8 · comment "7 bảng" `:1120` · **+ sửa-workflow-tại-chỗ không notify approver phiếu treo (parity PE) [F12b]** · **+ W-DB-2 (param `code=` phía BE) = KHÔNG LÀM, chọn client-side slice — disposition khai [F8c]**.
|
||||
@ -0,0 +1,159 @@
|
||||
# SUB-REVIEW-2 (Fable) — lens HOÀN-CHỈNH-VẬN-HÀNH + tổng hợp WAVE-PLAN FINAL
|
||||
|
||||
> Run: 2026-08-08-S182-khkk-awf-mirror-ncc · Reviewer #2/2 (`/fable-real reviewer`, YC-013)
|
||||
> Trạng thái: ĐANG ĐỔ RUỘT — skeleton viết lúc spawn, Edit liên tục.
|
||||
|
||||
## VERDICT
|
||||
|
||||
**`REVIEW-2: PASS_WITH_FIXES — 1C/5M` (+7 minor).** Spec DRAFT nền TỐT — 3 tiền đề lớn (0-mig · W1a per-Code P1 · synthetic slice + lockedCode) đều **re-verify ĐỨNG** trên cây; không có finding nào đảo hướng thiết kế. Toàn bộ fix đã được vá thẳng vào **§8 WAVE-PLAN FINAL** — executor cầm §8 chạy, không cần đọc lại spec DRAFT.
|
||||
|
||||
| # | Mức | Finding | Vá ở |
|
||||
|---|---|---|---|
|
||||
| C-1 | 🔴 C | Spec không cấm ship W2 (FE) trước W1a (BE) — 8 nút "Tạo phiên bản mới" nối thẳng bom deactivate-per-type (`:339-343`); bom đã armed sẵn qua panel `/ContractSigningPlan` hôm nay | §8 R-1 |
|
||||
| M-1 | M | DELETE hết version 1 nhóm → seeder TÁI SINH v01 roster-snapshot lúc restart (verified `:610` AnyAsync-per-Code) — spec không khai; giữa 2 restart nhóm bị CHẶN tạo phiếu | §3 · §8 W1d-(iv) D5 |
|
||||
| M-2 | M | D3=C có bẫy thi công: pattern SkipToFinal ép `checked=false` — áp nguyên cho 6 ô còn lại thì ô #4 (seed default TRUE, runtime LUÔN trả về Drafter) hiển thị nói dối chiều ngược | §6 · §8 W2d |
|
||||
| M-3 | M | Spec chép số baseline "645" ⟂ STATUS canonical "644" — vi phạm B1 + measured-label gate | §5 · §8 R-3/W0.3 |
|
||||
| M-4 | M | Acceptance #3 (tạo v02) không nói chạy đâu; trên PROD = mutation MỘT CHIỀU (verified: controller 0 endpoint re-activate) | §4 · §8 R-2 |
|
||||
| M-5 | M | Test W1 thiếu 3 ca: Submit chốt-cuối pin-lệch pre-existing · DELETE nhánh opinions-only · 2 ca negative (changelog phiếu terminal = 0 row; code ngoài họ vẫn PASS) | §5 · §8 T1.5/T1.8/T1.10/T1.6 |
|
||||
| m-1..m-7 | m | W0 "local rỗng người" không chắc · comment `:641-644` drift · cicd thiếu marker byte-level · thiếu pre-deploy prod-state check · acceptance thiếu phép đo UI-ghi · DELETE id-không-tồn-tại = 500 (KeyNotFoundException không map) · `DEFAULT_CODE_BY_TYPE[10]` panel cũ vẫn gợi mã ngoài-họ | §2-§7 · §8 |
|
||||
|
||||
**Còn CHẶN fan-out:** anh chốt **D3** (khuyến nghị **C** — tiền lệ sống ngay trang này, A = mìn-ngủ khi port sau, B = scope ×3-5) + **D5** (rào delete-last-version nhóm, khuyến nghị **CÓ** — 1 nhánh if rẻ hơn viết doc giải thích tái sinh). D4 chốt rồi.
|
||||
|
||||
**Verify độc lập đã chạy (không tin invest chay):** seeder `:549-687` · deactivate `:339-343` + ép-false `:387` · DELETE `:1193-1205` + middleware `:32-39` · controller 5-action/0-reactivate · KhkkCreatePage `:97-116` 2 app · TypePanel/Designer/payload `:255-267/:286-459/:620-978/:1225-1354` · rào (v) `:479-486` + UpdateDraft `:603-607` + Submit `:160-210` · enum Phase (`DaDuyet=3` ⇒ 3 phiếu prod là terminal, KHÔNG phải treo) + enum `ChangelogEntityType.Workflow=4` sẵn · khuôn changelog PE `:863-893` · deploy.yml paths-ignore + build-2-FE · test-homes 2 file tồn tại · STATUS row test 644.
|
||||
|
||||
## 1. Thứ tự wave + phụ thuộc
|
||||
|
||||
**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
|
||||
|
||||
- **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) — VERIFIED-BY-CODE
|
||||
|
||||
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 **M-1** (kèm đề xuất rào delete-last-version ở §5/§8 — có rào thì đường UI không bao giờ chạm được ca tái sinh, mental-model sạch hơn viết doc giải thích).
|
||||
- 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 **m-1** (chỉnh chữ W0).
|
||||
|
||||
## 4. Acceptance prod — đủ đo chưa
|
||||
|
||||
- **[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.
|
||||
- **[m-5] 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
|
||||
|
||||
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
|
||||
|
||||
**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
|
||||
|
||||
- **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 — bản executor cầm chạy (hmw Opus MAX)
|
||||
|
||||
> Đã vá đủ C-1 + M-1..M-5 + m-1..m-7 của review này vào thân plan. **2 điểm quyết còn CHẶN trước khi fan-out:** **D3** (khuyến nghị **C**) + **D5** (rào delete-last-version, khuyến nghị **CÓ**). D4 đã chốt (title menu-label + phụ đề nghiệp vụ). Mọi `file:line` dưới đây đã re-verify trên cây S182 (HEAD nền `936d1aed`).
|
||||
|
||||
### Ràng buộc toàn cục (đọc trước khi chạy bất kỳ wave nào)
|
||||
- **R-1 (từ C-1):** W1+W2 = **MỘT commit code duy nhất** (mặc định). Nếu lead tách: thứ tự BẮT BUỘC W1 → CI xanh → W2. **CẤM mọi commit chứa W2 mà thiếu W1a** — nút "Tạo phiên bản mới" trên panel nhóm khi BE còn deactivate-per-type = tắt IsActive cả 8 nhóm prod.
|
||||
- **R-2 (từ M-4):** trên PROD chỉ đo ĐỌC. Mọi acceptance có GHI (tạo version, sửa-tại-chỗ, xoá) chạy **LOCAL**. Không có endpoint re-activate — POST version mới là ratchet một chiều.
|
||||
- **R-3 (từ M-3):** baseline test = **số đo tươi tại W0** ghi vào `run.md`. Không chép 644/645 từ bất kỳ doc nào.
|
||||
- **R-4:** 0 migration. Nếu wave nào phát hiện cần mig ⇒ DỪNG, báo lead (premise 3-lens vỡ).
|
||||
- **R-5:** commit scope `[CLAUDE] Workflow: ...`; artifact run-folder không trigger CI (paths-ignore đã verify).
|
||||
|
||||
### W0 — prep + đo nền (blocking mọi wave sau)
|
||||
| # | Task | Lệnh/site | Đạt khi |
|
||||
|---|---|---|---|
|
||||
| 0.1 | Áp mig local ×2 DB (Dev+Design kẹt Mig 71, đĩa Mig 72) | `dotnet ef database update --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api` (×2 connection) | `sqlcmd` top MigrationId = `20260806070242` cả 2 DB |
|
||||
| 0.2 | Boot API 1 lần → seeder đẻ 8 row local | run SolutionErp.Api | SQL: 8 row `KHKK-N1..8` type-10 `IsActive=1`. Roster local **tùy user-seed — rỗng/đủ đều KHÔNG phải bug** (m-1) |
|
||||
| 0.3 | Đo baseline test TƯƠI | `dotnet test SolutionErp.slnx` | Số PASS ghi vào `run.md` — mọi so sánh sau so với số này (R-3) |
|
||||
| 0.4 | Prod-state check ĐỌC-only (m-4) | `ssh vietreport-vps` + `sqlcmd -E` chạy 3 query (spec DB §4) | Kỳ vọng: 8 row IsActive=1 · phiếu KHKK đều Phase=3 (DaDuyet) · opinions đếm được. **LỆCH kỳ vọng (nhóm Archived-oan / phiếu ChoDuyet-TraLai đang treo) ⇒ báo lead TRƯỚC khi W1** — phiếu treo pin lệch nhóm sẽ kẹt submit sau W1b (by design), phải biết trước con số |
|
||||
|
||||
### W1 — BE: 4 fix + 12 test, 2 file + 1 helper, 0-mig
|
||||
**File:** `src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` + `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` (+`ContractSigningPlanScope` helper) + `src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs` (1 call-site).
|
||||
|
||||
| # | Fix | Site (verified) | Cách | Test (RED→GREEN cho a/b; after cho c/d) |
|
||||
|---|---|---|---|---|
|
||||
| a | 🔴 Deactivate-per-Code cho type-10 | `ApprovalWorkflowV2AdminFeatures.cs:339-343` | `typeEnum == ContractSigningPlan` ⇒ `Where(w.Code == request.Code && w.IsActive)`; type khác GIỮ per-type (P1) | **T1.1** create v2 `KHKK-N1` ⇒ N2..N8 `IsActive` bất biến + N1 v01→false v02→true · **T1.2** regression type-1: Code MỚI cùng type vẫn tắt Code cũ · **T1.3** type-10 code ngoài họ (vd `QT-KHKK-V2-001`) chỉ tắt chính Code nó |
|
||||
| b | Group-match guard 3-site | extract rào (v) `ContractSigningPlanFeatures.cs:479-486` → `ContractSigningPlanScope.EnsureWorkflowGroupMatchAsync(db, awId, approvalGroup, ct)`; gọi: Create (chỗ cũ) + UpdateDraft `:603-607` (sau `EnsureWorkflowTypeAsync`) + `SubmitAsync` (`ContractSigningPlanWorkflowService.cs` sau rào hạng-mục `:169-187`, trước `LoadStepsAsync :189`) | Code khớp `^KHKK-N([1-8])$` mà n ≠ `ApprovalGroup` ⇒ 409 message chỉ việc ("đổi quy trình trong nháp về đúng nhóm N{x} / hoặc đổi nhóm phiếu"); **code NGOÀI họ ⇒ PASS như cũ** (:477-478 chủ-đích, M-5c.ii) | **T1.4** UpdateDraft re-pin lệch ⇒ 409 · **T1.5** phiếu pin-lệch PRE-EXISTING (seed thẳng DbContext bypass guard) Submit ⇒ 409 · **T1.6** code ngoài họ: Create/UpdateDraft/Submit OK |
|
||||
| c | Changelog PUT type-10 | `ApprovalWorkflowV2AdminFeatures.cs:863-893` thêm nhánh `else if type-10` mirror block PE | Query `ContractSigningPlans` (`ApprovalWorkflowId == def.Id && Phase != DaDuyet && Phase != TuChoi`) → add `ContractSigningPlanChangelog { EntityType = Workflow /*=4, enum SẴN — verified*/, Action = Update, EntityId = def.Id, PhaseAtChange, UserId, UserName, Summary = "Sửa quy trình duyệt tại chỗ: {Code} v{NN} (giữ nguyên phiên bản)", ContextNote = note (kèm Reason) }`. Dialog PUT đang HỨA nguyên văn "ghi vào lịch sử phiếu đang chạy" (`ApprovalWorkflowsV2Page.tsx:963-977`) — fix này làm lời hứa thành thật cho KHKK | **T1.7** PUT workflow nhóm có phiếu ChoDuyet ⇒ 1 row đúng shape · **T1.8** phiếu DaDuyet/TuChoi ⇒ 0 row |
|
||||
| d | DELETE usage-check → 409 + 404 đúng | `ApprovalWorkflowV2AdminFeatures.cs:1193-1205` | (i) pre-count pin: grep `ApprovalWorkflowId` trong Domain để LIỆT ĐỦ module có cột pin (đếm từ grep, không chép số spec) + (ii) count LevelOpinions 3 bảng theo def.Id qua Levels (khuôn `CountLiveOpinionsAsync :961-965` đổi trục) ⇒ >0 = 409 đích danh "N phiếu đang pin / M chữ ký đã ký" · (iii) đổi `KeyNotFoundException` → `NotFoundException` (m-6: hiện id-không-tồn-tại = 500 thô vì middleware không map BCL exception — verified `GlobalExceptionMiddleware.cs:32-39`) · (iv) **D5 (chờ chốt, khuyến nghị CÓ):** code họ `KHKK-N[1-8]` && version CUỐI của Code ⇒ 409 "Nhóm N{n} phải còn ≥1 version" (chặn ca seeder-tái-sinh M-1 khỏi đường UI) | **T1.9** delete có pin ⇒ 409 · **T1.10** hết pin nhưng còn opinions ⇒ 409 · **T1.11** id không tồn tại ⇒ 404 · **T1.12** (nếu D5) xoá version cuối nhóm ⇒ 409 |
|
||||
|
||||
**Test homes (verified tồn tại):** `tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs` (T1.1-T1.3, T1.7-T1.12) · `ContractSigningPlanApprovalTests.cs` (T1.4-T1.6). **Đạt khi:** build 0-err + FULL suite = baseline W0 + 11-12 test mới, 0 fail.
|
||||
|
||||
### W2 — FE: fe-admin `src/pages/system/ApprovalWorkflowsV2Page.tsx` (file 1355 dòng)
|
||||
| # | Task | Site (verified) | Cách |
|
||||
|---|---|---|---|
|
||||
| a | Parse kind | `:164-172` | `typeCode?.match(/^KhkkN([1-8])$/)` → `khkkGroup`; `selectedTypeInt = khkkGroup ? TYPE_INT_CONTRACT_SIGNING_PLAN : TYPE_CODE_TO_INT[typeCode]`; query overview giữ `applicableType=10` |
|
||||
| b | Synthetic slice per-Code | `:299-301` | từ `types.find(t=>t.applicableType===10)`: `versions = history.filter(d => d.code === khkkGroupWorkflowCode(g))` · `active = versions.find(v=>v.isActive) ?? null` (🔴 KHÔNG đọc ô `Active` per-type — nó là 1-trong-8) · slice truyền vào `TypePanel` giữ shape `TypeSummaryDto` (`history` = versions ĐỦ cả active — panel tự filter `!isActive` `:419/:426`, verified) · label = "Duyệt KHKK — Nhóm {g}" |
|
||||
| c | `lockedCode` xuyên suốt | `TypePanel:357` → `Designer:620` / defaultCode `:650-651` / POST `:798` / ô Mã `:920-928` | prop `lockedCode?: string` = `khkkGroupWorkflowCode(g)`; ô Mã `disabled={isEdit || !!lockedCode}` + helper-text; POST gửi `lockedCode ?? code`. Thiếu = version tự gõ mã RƠI khỏi slice + không được rào (v) nhận |
|
||||
| d | **D3 thi hành (mặc định C — luật M-2 BẮT BUỘC)** | 6 ô `:1232-1285` (4 Return + EditDetails + EditBudget) + CEO `:944-961` | 6 ô: CHỈ thêm `disabled={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN}` + `title` tooltip — **GIỮ `checked={entry.<flag>}` state THẬT, CẤM ép false** (payload `levelFlagsPayload :255-267` không đổi — echo state, verified). Ô #4 Trả-về-Drafter tooltip RIÊNG: "KHKK hiện luôn trả về Drafter khi Trả lại — cờ chưa phân nhánh" (hiển thị tick-thật, thường TRUE). Ô Ngưỡng CEO: POST-mode `disabled` khi type-10 + helper-text "KHKK không áp ngưỡng CEO (NĐUQ human-gate)" (PUT-mode đã khoá sẵn). SkipToFinal `:1292-1302` GIỮ NGUYÊN |
|
||||
| e | (optional — kéo eoffice rotate) mirror menuKeys | `fe-{admin,user}/src/lib/menuKeys.ts:31-32` | +8 const `AwV2_KhkkN1..8` — 0 hệ quả runtime |
|
||||
| f | (m-2) vá comment lệch | `:641-644` | comment "2 ô duyệt sớm nay sống cho MỌI loại" ⟂ thực tế SkipToFinal vẫn disable type-10 `:1286-1302` — sửa 1 dòng cho khớp |
|
||||
| g | (D4) phụ đề nghiệp vụ | PageHeader `:305-318` + TypePanel | khuôn hiện CHỈ render `applicableTypeLabel` — thêm ~1 prop `subtitle = khkkGroupMenuLabel(g)` (`types/khkk.ts:159-168`, 2-app identical sẵn) |
|
||||
|
||||
**Acceptance LOCAL (R-2):** **A1** 8 leaf → 8 slice khác nhau (URL/header/card `KHKK-N{n} v01`+badge+3 Bước+4 nút+Lịch sử) hết landing · **A2** dialog Sửa-tại-chỗ đúng khuôn ảnh theo D3 (title `KHKK-N{n} v01 (giữ nguyên phiên bản)`, Drafter tick sẵn, rule-box) · **A2b** sửa-tại-chỗ THẬT 1 nhóm (đổi tên NV / tick 1 cờ Return) ⇒ 204, panel refresh giữ v01 · **A3** tạo v02 N1 (local) ⇒ nằm ĐÚNG panel N1 + badge 7 nhóm khác KHÔNG mất (răng W1a) · **A4** regression 4 route cũ render như cũ + fe-user tạo phiếu auto-pin không đổi API. **Đạt khi:** `npm run build` fe-admin (+fe-user nếu e) + tsc 0-err.
|
||||
|
||||
### W3 — CHỈ KHI anh chốt D3=B: port semantics per-cờ vào `ContractSigningPlanWorkflowService` (giữ nguyên nội dung spec DRAFT W3; mỗi cờ test RED→GREEN riêng; `CeoApprovalThreshold` KHÔNG port — OG-3). Mặc định **C ⇒ W3 = N/A**.
|
||||
|
||||
### W4 — test-specialist đóng suite
|
||||
Full `dotnet test SolutionErp.slnx` = baseline-W0 + N-test-mới, 0 fail · chạy SAU khi D3/D5 chốt · nếu số canonical đổi ⇒ update DUY NHẤT `docs/STATUS.md` row test (B1).
|
||||
|
||||
### W5 — ship + verify (sau reviewer pre-commit)
|
||||
1. **Commit MỘT phát** BE+FE+tests (R-1) `[CLAUDE] Workflow: KHKK 8 panel nhóm mirror DuyetNcc + 4 BE guard (0-mig)`.
|
||||
2. **cicd-monitor 4-chân (m-3):** (i) CI Run success + test-gate = số W4 · (ii) bundle **admin** rotate + grep js mới marker `KhkkN1` (control-âm: bundle cũ 0-hit; **eoffice: hash GIỮ nếu bỏ W2e / đổi nếu làm** — đo, không suy) · (iii) BE DLL: ssh grep `SolutionErp.Application.dll` token `EnsureWorkflowGroupMatch` 2-encoding UTF-8/UTF-16 (khuôn S177) + control-dương token cũ · (iv) query-1 lại: 8 row `IsActive=1` bất biến qua deploy.
|
||||
3. **Acceptance PROD đọc-only (R-2):** login `admin@solutions.com.vn` → ≥3/8 panel render đúng slice + mở dialog rồi Hủy · 3 query SQL khớp · **KHÔNG bấm "Tạo phiên bản mới" trên prod** · note: role thường chưa grant menu-row ⇒ không thấy leaf = đúng thiết kế (2-layers).
|
||||
4. Đóng sổ: STATUS (test + mô tả) / HANDOFF / WAL / tick `YC-013` + khai **accepted-risk**: race 2-admin-cùng-Code → 500 thô (UNIQUE đỡ data, không silent-dup) + nợ-ngoài-scope giữ nguyên khối spec (GET 0-policy · roster snapshot · `Khkk_WfView` aggregate · `DEFAULT_CODE_BY_TYPE[10]='QT-KHKK-V2-001'` panel cũ vẫn gợi mã ngoài-họ (m-7) · comment "7 bảng" `:1120`).
|
||||
|
||||
### Rollback
|
||||
0-mig ⇒ revert commit sạch phần máy. Data ở lại (hợp lệ, khai rõ): changelog rows W1c = audit-history · v02+ đã tạo = ratchet (vô hại runtime, pin by-Id). 🔴 Revert W1a = **bom per-type TÁI VŨ TRANG** — sau revert, CẤM "Tạo phiên bản mới" mọi panel type-10 tới khi re-apply.
|
||||
@ -0,0 +1,292 @@
|
||||
# sub-reviewer-precommit — S182 YC-013 đợt-2 (PRE-COMMIT, diff thật)
|
||||
|
||||
**VERDICT: `PASS_WITH_FIXES — 0C / 3M`** (+ 6 minor). Không có blocker. 3 MAJOR đều **propose-only**, không chặn commit nếu lead chấp nhận rủi ro có khai.
|
||||
|
||||
---
|
||||
|
||||
## 0. Diff scope (đo tươi, KHÔNG chép từ lane)
|
||||
|
||||
`git status --porcelain` + `git diff --stat`:
|
||||
|
||||
| Nhóm | File | Ghi chú |
|
||||
|---|---|---|
|
||||
| BE | `ApprovalWorkflowV2AdminFeatures.cs` (+184) · `ContractSigningPlanFeatures.cs` (+55) · `ContractSigningPlanWorkflowService.cs` (+10) · **`ContractSigningPlanWorkflowGuards.cs` (untracked `??`, 72 dòng)** | 4/4 khớp spec |
|
||||
| FE | `ApprovalWorkflowsV2Page.tsx` (+213) · `menuKeys.ts` ×2 · `KhkkCreatePage.tsx` ×2 | 5/5 khớp spec |
|
||||
| Test | `UpdateAwDefinitionTests.cs` (+567) · `ContractSigningPlanApprovalTests.cs` (+326) · **`ContractSigningPlanGroupAndCatalogTests.cs` (+14)** | **3 file, spec/lệnh khai 2** → mục 9 |
|
||||
| Ngoài scope | `.claude/governance/.session-counter.json` (+54/-53) · `docs/governance/so-yeu-cau-chu-du-an.md` (+2) | → mục 9 |
|
||||
|
||||
Tổng tracked: **+1400 / −106**, 13 file M + 1 file `??`.
|
||||
|
||||
### Máy — đo LẠI độc lập (không nhận số của lane)
|
||||
|
||||
| Phép đo | Lệnh chạy | Kết quả |
|
||||
|---|---|---|
|
||||
| Domain tests | `dotnet test SolutionErp.slnx` | **45 P / 0 F** |
|
||||
| Infrastructure tests | (cùng lệnh) | **614 P / 0 F** |
|
||||
| **Tổng** | | **659 P / 0 F** — khớp claim; baseline 645 (45+600) ⇒ **+14 đúng ở Infrastructure** |
|
||||
| `npm run build` fe-admin | exit 0 | ✓ built 15.36s |
|
||||
| `npm run build` fe-user | exit 0 | ✓ built 1.04s |
|
||||
| tsc fe-admin | `npx tsc --noEmit -p tsconfig.app.json` | 0 err (225 file) |
|
||||
| tsc fe-user | `npx tsc --noEmit -p tsconfig.app.json` | 0 err (214 file) |
|
||||
|
||||
---
|
||||
|
||||
## 1. W1a — công thức R-6 · **HELD**
|
||||
|
||||
`ApprovalWorkflowV2AdminFeatures.cs:355-361`:
|
||||
|
||||
```csharp
|
||||
var actives = typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan
|
||||
? await db.ApprovalWorkflows
|
||||
.Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)
|
||||
.ToListAsync(ct)
|
||||
: await db.ApprovalWorkflows
|
||||
.Where(w => w.ApplicableType == typeEnum && w.IsActive)
|
||||
.ToListAsync(ct);
|
||||
```
|
||||
|
||||
- ✅ Đúng **3 vế** R-6, đúng thứ tự, **KHÔNG** có biến-thể regex `^KHKK-N[1-8]$` ở site này (grep `KHKK-N` trong `AdminFeatures.cs` chỉ ra ở **comment** + gọi `ParseGroup` cho D5, không ở vị-ngữ W1a).
|
||||
- ✅ Comment `:339` cũ ("only ONE active per type") **đã bị xoá**, thay bằng khối 20 dòng nêu cả 2 nhánh + cấm regex. Không còn neo thối.
|
||||
- ✅ Nhánh `else` **giữ nguyên byte** semantics per-type cho mọi loại khác.
|
||||
- ✅ Ăn khớp với `nextVersion :336-338` vốn đã per-Code (`Where(w => w.Code == request.Code)`), nên 2 vế cùng một trục.
|
||||
|
||||
---
|
||||
|
||||
## 2. W1b — Guards 3-site · **HELD**
|
||||
|
||||
- ✅ File MỚI `ContractSigningPlanWorkflowGuards.cs` — `public static class` ở **Application** (đúng F1 của review-1: không nhét vào `Scope` internal, không `InternalsVisibleTo`, không inline-duplicate). Đây đúng là finding "spec-unbuildable vì assembly-ranh" đợt-1 và nó **đã được đóng đúng cách**.
|
||||
- ✅ 3 call-site, **đối số đúng thứ tự ở cả 3** (tôi đối chiếu tay từng site):
|
||||
- Create — `ContractSigningPlanFeatures.cs:516-517` → `(db, request.ApprovalWorkflowId, approvalGroup, ct)`
|
||||
- UpdateDraft — `:643-644` → `(db, awId, plan.ApprovalGroup, ct)`
|
||||
- Submit — `ContractSigningPlanWorkflowService.cs:196-197` → `(db, awId, plan.ApprovalGroup, ct)`
|
||||
- ✅ Semantics **cho-qua code ngoài họ** GIỮ NGUYÊN: `ParseGroup` trả `null` ⇒ `if (wfGroup is int k && ...)` không vào ⇒ pass. Ghi chú `:49-57` nêu đúng lý do gốc `:477-478`.
|
||||
- ✅ Điểm chèn Submit: **sau** 2 rào hạng-mục (`:177-187`), **ngay trên** `LoadStepsAsync(awId, ct)` `:199`. Cũng nằm sau guard phase `:160` và sau `plan.ApprovalWorkflowId is not Guid awId` `:163` ⇒ `awId` luôn hợp lệ, không có nhánh nào bỏ qua.
|
||||
- ✅ Thân rào cũ ở Create **đã xoá hẳn** (không để 2 bản luật song song).
|
||||
|
||||
---
|
||||
|
||||
## 3. W1b2 / D6 · **HELD**
|
||||
|
||||
- ✅ `EnsureWorkflowPinnableAsync` (`ContractSigningPlanFeatures.cs:331-353`) chỉ có **2 caller**: Create `:507`, UpdateDraft `:642` (grep toàn repo — 3 hit gồm 1 định-nghĩa).
|
||||
- ✅ UpdateDraft: cả 3 rào (`Type` / `Pinnable` / `GroupMatch`) nằm **TRONG** `if (request.ApprovalWorkflowId is Guid awId && awId != plan.ApprovalWorkflowId)` `:638`. Đúng D6 — giữ-nguyên-pin không bị hỏi. T7 chân (4) đo đúng vế này.
|
||||
- ✅ **2 message TÁCH** (`:346-352`): "đã ngừng hoạt động" ⟂ "chưa được ghim cho người dùng chọn". Không gộp `!IsActive || !IsUserSelectable`.
|
||||
- ✅ Không gọi ở đường Submit — đúng "D6 chốt 2 site".
|
||||
|
||||
---
|
||||
|
||||
## 4. W1c — changelog shape · **HELD**
|
||||
|
||||
`AdminFeatures.cs:916-955`:
|
||||
|
||||
- ✅ `EntityType = ContractSigningPlanEntityType.Workflow` — enum **=4** (verified `ContractSigningPlanChangelog.cs:12`), slot có sẵn, không đẻ slot mới.
|
||||
- ✅ `Action = ChangelogAction.Update` · `EntityId = def.Id` · `PhaseAtChange = plan.Phase` · `UserId` · `UserName` · `Summary` · `ContextNote` — đủ 8 field mirror khối PE.
|
||||
- ✅ Filter: `ApprovalWorkflowId == def.Id && Phase != DaDuyet && Phase != TuChoi` — `TraLai` **KHÔNG** bị loại (đúng spec: phiếu trả lại sẽ trình lại).
|
||||
- ✅ **CỐ Ý không `IgnoreQueryFilters()`** ở đây (ngược với DELETE) — có khai lý do tại chỗ `:930-934`. Đây là điểm mà đợt-1 tôi FAIL, nay đã được **phân biệt đúng 2 chiều** (changelog = ngữ nghĩa "sống"; DELETE = ngữ nghĩa "FK còn thật").
|
||||
- ✅ `note`/`summary`/`actorName` hoist ra ngoài 2 nhánh — không đổi hành vi (chỉ string op), khối PE dùng lại y hệt giá trị cũ.
|
||||
- ✅ Ghi nằm **trước** `await db.SaveChangesAsync(ct)` `:958` + `tx.CommitAsync(ct)` `:962` ⇒ atomic cùng lượt PUT.
|
||||
- ✅ Câu cũ xếp KHKK vào rọ "chỉ có UpdatedAt/UpdatedBy" đã gỡ (`:875-882`) — không để lại neo thối.
|
||||
|
||||
---
|
||||
|
||||
## 5. W1d — usage-count + D5 · **HELD** (2 minor)
|
||||
|
||||
### (i) pin-count 8 module — **đếm ĐÚNG 8, không thiếu không thừa**
|
||||
|
||||
Tôi grep Domain thay vì tin comment:
|
||||
|
||||
```
|
||||
grep -E "public (Guid|Guid\?) ApprovalWorkflowId" src/Backend/SolutionErp.Domain
|
||||
→ Contract · ContractSigningPlan · LeaveRequest · OtRequest · Proposal
|
||||
· TravelRequest · VehicleBooking · PurchaseEvaluation = 8
|
||||
(+ ApprovalWorkflow.cs:76 = FK nội bộ của Step, không phải module)
|
||||
```
|
||||
|
||||
- ✅ `IgnoreQueryFilters()` áp đúng **PE / Contract / KHKK** (`:1287-1292`). Tôi grep `HasQueryFilter` toàn `src/Backend`: trong 8 module pin, **chỉ 3 entity đó** có global filter (`ContractConfiguration.cs:44` · `ContractSigningPlanConfiguration.cs:55` · `PurchaseEvaluationConfiguration.cs:84`). 5 module Office **không** có ⇒ đếm thẳng là đúng. Claim trong comment `:1281-1285` HELD bằng phép đo độc lập.
|
||||
- ✅ Đây chính là finding 🔴 đợt-1 của tôi ("usage-count trên bảng có HasQueryFilter thiếu IgnoreQueryFilters") — **đã đóng**, và T11 đo đúng cái bẫy đó.
|
||||
|
||||
### (ii) opinions 8 bảng
|
||||
|
||||
- ✅ Đủ 8 (`:1300-1319`): PE · Contract · ContractSigningPlan · Proposal · LeaveRequest · OtRequest · TravelRequest · VehicleBooking.
|
||||
- ✅ `levelIds` lấy qua `l.Step!.ApprovalWorkflowId == def.Id` + `IgnoreQueryFilters()`.
|
||||
- ✅ `IgnoreQueryFilters()` trên 8 bảng opinion = **no-op** (không bảng nào có `HasQueryFilter`; `ContractSigningPlanLevelOpinion.cs:18` khai rõ "CỐ Ý KHÔNG"). Vô hại + đồng hình, đúng như comment nói. Và **KHÔNG** lọc `!IsDeleted` là ĐÚNG cho câu hỏi FK (khác `CountLiveOpinionsAsync :969` hỏi câu khác — "chữ ký còn hiệu lực"). Hai hàm hỏi 2 câu khác nhau, không phải mâu thuẫn.
|
||||
|
||||
### (iii) 404
|
||||
|
||||
- ✅ `throw new NotFoundException("ApprovalWorkflow", request.Id)` thay `KeyNotFoundException` (`:1275`). T13 có control dương.
|
||||
|
||||
### (iv) D5
|
||||
|
||||
- ✅ Chỉ áp cho họ, qua `ContractSigningPlanWorkflowGuards.ParseGroup(def.Code) is int group` `:1337` — **dùng lại đúng 1 nguồn regex**, không chép chuỗi lần 3.
|
||||
- ✅ Thứ tự: usage (i+ii) TRƯỚC, D5 SAU — khai lý do `:1259-1261`.
|
||||
|
||||
---
|
||||
|
||||
## 6. W2d — danh-sách-chốt 6 ô · **HELD trên trục dữ-liệu**, hở trên trục hiển-thị (→ MAJOR-1)
|
||||
|
||||
### 🔴 #8 `allowApproverFinalize` — **0-CHẠM, chứng minh bằng grep trên DIFF**
|
||||
|
||||
```
|
||||
git diff -- fe-admin/.../ApprovalWorkflowsV2Page.tsx | grep "allowApproverFinalize"
|
||||
→ exit 1 (0 hit)
|
||||
```
|
||||
Control dương: grep trên **file** ra 7 hit (`:57 :123 :217 :238 :267 :1456 :1457`) ⇒ lưới grep có răng, và ô #8 ở `:1456` **không** có `disabled`, **không** có `title`, `checked={entry.allowApproverFinalize}` nguyên vẹn. **Prod 8/88 cờ chịu lực AN TOÀN.**
|
||||
|
||||
### #7 `allowApproverSkipToFinal` — giữ nguyên hành vi
|
||||
|
||||
Diff chỉ đổi **3 biểu thức trùng lặp** `applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN` → `isKhkk`. `isKhkk` định nghĩa `:715` = **cùng biểu thức**, cùng scope, cùng prop. Giá trị y hệt. `checked={isKhkk ? false : ...}` giữ ép-false (đúng spec "GIỮ NGUYÊN").
|
||||
|
||||
### `levelFlagsPayload` — 0-chạm
|
||||
|
||||
`git diff | grep levelFlagsPayload` → **1 hit duy nhất, nằm trong COMMENT** (`:486` của diff). Payload không đổi ⇒ ô mờ vẫn echo state thật lên BE.
|
||||
|
||||
### 6 ô disabled — GIỮ state thật, **CẤM ép false: tuân thủ**
|
||||
|
||||
| # | Cờ | Dòng | `checked` | `disabled` | tooltip |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `allowReturnOneLevel` | 1358-1362 | `entry.…` ✅ | `isKhkk` | chung |
|
||||
| 2 | `allowReturnOneStep` | 1369-1373 | `entry.…` ✅ | `isKhkk` | chung |
|
||||
| 3 | `allowReturnToAssignee` | 1380-1384 | `entry.…` ✅ | `isKhkk` | chung |
|
||||
| 5 | `allowApproverEditDetails` | 1407-1411 | `entry.…` ✅ | `isKhkk` | chung |
|
||||
| 6 | `allowApproverEditBudget` | 1418-1422 | `entry.…` ✅ | `isKhkk` | chung |
|
||||
| 9 | `allowApproverDelete` | 1468-1472 | `entry.…` ✅ | `isKhkk` | chung |
|
||||
|
||||
Không ô nào bị `checked={false}`. ✅ ERRATA D3 (state-thật, không forced-true) được thi hành **đúng** ở ô #4 `:1396-1400`: `checked={entry.allowReturnToDrafter}` + `disabled` + **tooltip RIÊNG** + label **KHÔNG** bị `text-slate-400` (đúng ý "không mờ kiểu chưa-có").
|
||||
|
||||
### Ô Ngưỡng CEO (W2f)
|
||||
|
||||
- ✅ `disabled={isEdit || isKhkk}` `:1052` — vô điều kiện cho type-10.
|
||||
- ✅ POST gửi `ceoApprovalThreshold: isKhkk ? null : …` `:889-891`.
|
||||
- ✅ helper-text riêng `:1057-1058`.
|
||||
|
||||
---
|
||||
|
||||
## 7. W2g — `find(w => w.isActive && …)` · **HELD** (hợp-đồng FE↔BE đã đo)
|
||||
|
||||
- ✅ 2 app **mirror LOGIC** — 2 diff byte-identical ở phần code (`fe-admin/.../KhkkCreatePage.tsx:125-127` ⟂ `fe-user/.../KhkkCreatePage.tsx:125-127`).
|
||||
- ✅ 🔴 **`isActive` CÓ THẬT trong DTO BE** — đây là chỗ dễ vỡ nhất (nếu BE không trả, `w.isActive` = `undefined` ⇒ falsy ⇒ `autoWorkflow` LUÔN null ⇒ **chết đường tạo phiếu KHKK**, mà tsc + build + 659 test đều xanh). Đo: `AwDefinitionDto` nhận `d.IsActive` ở **`ApprovalWorkflowV2AdminFeatures.cs:178`** (đối số thứ 8), serialize camelCase ⇒ `isActive`. **Hợp đồng khớp.**
|
||||
- ✅ `history` = FULL list kể cả active — verified `:205-213`: `versions` = mọi definition của type, `active` = `versions.FirstOrDefault(v => v.IsActive)`. Claim của cả W2b lẫn W2g HELD.
|
||||
- ✅ Filter `isUserSelectable` client-side ở `:109` **vẫn giữ**.
|
||||
|
||||
---
|
||||
|
||||
## 8. 14 test — **đúng 14 ca spec, KHÔNG có ca nào nới để xanh**
|
||||
|
||||
| Ca | File | Nhận xét đối-kháng |
|
||||
|---|---|---|
|
||||
| T1 | `UpdateAwDefinitionTests.cs` | Có **tiền-đề đo** (`Be(8)` trước khi act) — chống "xanh ở thế giới sai" |
|
||||
| T2 | ″ | Regression type-1 + chứng-nhân xuyên-type |
|
||||
| T3 | ″ | **2 chân**: (a) code lạ không nuke 8 nhóm; (b) code lạ vẫn ratchet chính nó ⇒ giết được cả bản vá regex lẫn bản vá "type-10 đừng tắt gì" |
|
||||
| T4 | `ContractSigningPlanApprovalTests.cs` | 3 vế: chặn + **đĩa bất động** (`ReadPinAsync` từ context MỚI + changelog=0) + control dương |
|
||||
| T5 | ″ | **Seed phiếu pin lệch thẳng DbContext** ✅ + chứng-nhân phiếu đúng nhóm submit LỌT cùng lượt; fixture kéo hạng-mục theo `approvalGroup` ⇒ loại 2 cửa 409 khác |
|
||||
| T6 | ″ | Vế âm + **chân (2) chứng minh rào đang sống** (không phải xanh-mù) |
|
||||
| T7 | ″ | 4 chân, đủ cả (4) giữ-nguyên-pin-vẫn-sửa-được |
|
||||
| T8 | `UpdateAwDefinitionTests.cs` | Đo **SHAPE** 9 field + sanity `changes ≠ rỗng` + assert PE changelog = 0 (không rải chéo module) |
|
||||
| T9 | ″ | **Control dương TRONG CÙNG LƯỢT** (4 phiếu: 1 sống → có vết; DaDuyet/TuChoi/xoá-mềm → không) |
|
||||
| T10 | ″ | Control dương + **vi-sai message** (1 vs 2 hồ sơ ⇒ câu phải khác — giết "câu hằng") + chứng-nhân `DbUpdateException` |
|
||||
| T11 | ″ | Có **tiền-đề 2 chiều**: filter-on = 0, filter-off = 1 ⇒ đo đúng cái bẫy F2 |
|
||||
| T12 | ″ | **Seed `LeaveRequest` thật** ✅ + chứng-nhân xoá thẳng module 2 ⇒ dangling THẬT trong DB |
|
||||
| T13 | ″ | Control dương id thật |
|
||||
| T14 | ″ | **3 chân** (không phải 2): (1) không-phải-cuối ⇒ OK · (2) cuối ⇒ 409 · (3) ngoài họ 1-version ⇒ OK. Tiền-đề `0 usage` tách bạch D5 với rào (i) |
|
||||
|
||||
**Đếm:** 10 `[Fact]` mới ở `UpdateAwDefinitionTests` + 4 ở `ContractSigningPlanApprovalTests` = **14** ⟂ suite +14 (600→614). Khớp 2 nguồn.
|
||||
|
||||
**Có test nào bị NỚI không?** Có **đúng 1** assertion đổi, ở file thứ 3 (`ContractSigningPlanGroupAndCatalogTests.cs:858-868`):
|
||||
`Contain("KHKK-N5")` → `Contain("N5")`. Đây là **spec-change bắt buộc**, không phải nới-để-xanh: message mới do W1b chốt bỏ mã, giữ số nhóm; câu cũ sẽ ĐỎ. Có ghi lý do 8 dòng tại chỗ. **Chấp nhận** — nhưng xem MINOR-1 (assertion mù thứ-tự).
|
||||
|
||||
---
|
||||
|
||||
## 9. Residual — 3 khoản NGOÀI danh sách
|
||||
|
||||
1. **`tests/…/ContractSigningPlanGroupAndCatalogTests.cs`** (+14/−6) — file test **thứ 3**, lệnh chỉ khai 2. Chính-đáng (hệ quả tất yếu của đổi message W1b), nhưng **phải khai trong commit-body**, đừng để người đọc log sau này tưởng có ai lén sửa test cũ.
|
||||
2. **`.claude/governance/.session-counter.json`** (+54/−53) — tick S182 của lead. **Không liên quan YC-013.** → xem MINOR-2 (BOM).
|
||||
3. **`docs/governance/so-yeu-cau-chu-du-an.md`** (+2) — thêm dòng `YC-013` + `YC-014`. Đúng luật sổ (GHI NGAY), không liên quan code.
|
||||
|
||||
**`#8` / `levelFlagsPayload` / `SkipToFinal` có bị đụng ngoài ý không?** — KHÔNG. Đã grep trên **diff** (không phải trên file) cho cả 3, kết quả ở mục 6.
|
||||
|
||||
---
|
||||
|
||||
## 10. FLAG-A của FE lane — **CLAIM ĐÚNG, và tôi đo được**
|
||||
|
||||
| Lệnh | fe-admin | fe-user |
|
||||
|---|---|---|
|
||||
| `npx tsc --noEmit` (nguyên văn acceptance cũ) | exit 0 — **`--listFiles` = 0 dòng** | exit 0 — **`--listFiles` = 0 dòng** |
|
||||
| `npx tsc --noEmit -p tsconfig.app.json` | exit 0 — `--listFiles` = **225 dòng** | exit 0 — `--listFiles` = **214 dòng** |
|
||||
|
||||
`fe-admin/tsconfig.json` = `{"files": [], "references": [...]}` ⇒ dạng trần **biên dịch 0 file**, luôn xanh **theo cấu trúc**, không phải may mắn. FLAG-A **HELD**.
|
||||
|
||||
🔸 Bổ sung có ích cho lane: `npm run build` = `tsc -b && vite build` — `tsc -b` (build mode) **CÓ** đi theo project references ⇒ acceptance `npm run build` là thước THẬT. Chỉ acceptance `npx tsc --noEmit` trần là rỗng. Đề nghị: mọi acceptance FE về sau ghi `npm run build` hoặc `-p tsconfig.app.json`, và **kèm `--listFiles | wc -l` một lần** để chứng thước không rỗng.
|
||||
|
||||
---
|
||||
|
||||
# FINDINGS
|
||||
|
||||
## 🔴 MAJOR-1 — tooltip gắn trên `<input disabled>`: Chrome/Edge không hiện ⇒ nửa D3=C có thể chết câm
|
||||
|
||||
**Vị trí:** `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:1361, 1372, 1383, **1399**, 1410, 1421, 1471` (và tiền lệ cũ `:1443`).
|
||||
|
||||
`title` được đặt trên chính `<input type="checkbox" disabled={isKhkk}>`. Trình duyệt Chromium (Chrome/Edge — đúng trình duyệt đích, xem `feedback_hosolink_open_from_web_browser_block`) **chặn sự kiện chuột trên form-control bị disabled**, nên native tooltip thường **không** bật lên; Firefox thì có. `<label>` bao ngoài KHÔNG mang `title`, nên rê chuột vào phần chữ cũng không ra gì.
|
||||
|
||||
**Vì sao là MAJOR chứ không phải nit:** D3=C của anh là *"mờ ô chưa nghe **+ tooltip**"*. Nếu tooltip không hiện thì admin chỉ thấy 7 ô xám không lý do — đúng thứ mà rule-box `:1088-1096` cố tránh. Nặng nhất là **ô #4** `:1399`: câu *"KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh"* là **thông tin DUY NHẤT** phân biệt ô #4 với 6 ô kia (rule-box chỉ nói #4 "có tác dụng thật", không nói *"cờ chưa phân nhánh"*). Tooltip không hiện ⇒ ô #4 trông y hệt 6 ô "chưa áp dụng", tức **ngược nghĩa** — đúng class lỗi mà ERRATA D3 vừa sửa xong ở tầng `checked`.
|
||||
|
||||
**Đây là máy dựng mà không có cửa gọi** (`feedback_guard_built_but_never_called`).
|
||||
|
||||
**Acceptance đề nghị (chọn 1):**
|
||||
- (a) chuyển `title=` từ `<input>` lên `<label>` bao ngoài — label không disabled nên tooltip sống; hoặc
|
||||
- (b) render câu của ô #4 thành **chữ nhìn thấy được** (một dòng `text-[10px]` dưới ô), giữ tooltip cho 6 ô kia.
|
||||
- Kiểm chứng: mở dialog nhóm bất kỳ trên Chrome, rê chuột **vào chữ** của ô #4 → phải đọc được câu riêng.
|
||||
- 🔸 Tiền lệ `:1443` (#7, có từ K3) **cùng bệnh** — vá thì vá cùng lớp (bài `citation-trap` gen-2: vá 1 CLASS phải grep MỌI matcher cùng-lớp TRONG diff).
|
||||
|
||||
---
|
||||
|
||||
## 🔴 MAJOR-2 — 3 thành phần bất đồng về HOA/thường của `Code`; test mù theo CẤU TRÚC (SQLite ≠ SQL Server)
|
||||
|
||||
**Vị trí:** `ContractSigningPlanWorkflowGuards.cs:37-38` ⟂ `ApprovalWorkflowV2AdminFeatures.cs:357` ⟂ `KhkkCreatePage.tsx:126` (×2 app).
|
||||
|
||||
Ba chỗ hỏi cùng một câu "code này có phải nhóm N{n} không", bằng **ba luật khác nhau**:
|
||||
|
||||
| Chỗ | Phép so | Với `khkk-n1` (thường) |
|
||||
|---|---|---|
|
||||
| `GroupCodePattern = new(@"^KHKK-N([1-8])$", CultureInvariant)` — **KHÔNG** `IgnoreCase` | case-**SENSITIVE** | → `null` = **ngoài họ** ⇒ rào nhóm 3-site TẮT, D5 TẮT |
|
||||
| `w.Code == request.Code` (EF → SQL Server) | theo **collation DB**, mặc định `*_CI_AS` = case-**INSENSITIVE** | → **khớp** `KHKK-N1` ⇒ per-Code **vẫn hạ IsActive** bản thật |
|
||||
| `(w.code ?? '').trim().toUpperCase() === 'KHKK-N1'` | case-**INSENSITIVE** | → **khớp** ⇒ FE auto-pin nó cho phiếu nhóm 1 |
|
||||
|
||||
Hệ quả cụ thể: admin gõ `khkk-n1` trên panel type-10 gộp (ô Mã ở panel đó **không** khoá — `lockedCode = null`) ⇒ bản mới **tắt** `KHKK-N1` thật, FE **vẫn** auto-pin sang nó, còn rào nhóm + D5 **im lặng tắt** cho toàn bộ phiếu nhóm 1. Không có lỗi nào báo.
|
||||
|
||||
**Vì sao test không thấy:** 14 ca chạy trên **SQLite** (`SqliteDbFixture`), nơi `=` trên TEXT mặc định là **BINARY = case-sensitive**. Tức phép so ở tầng DB trong test có ngữ nghĩa **NGƯỢC** với prod. Đây không phải test viết ẩu — nó **mù theo cấu trúc**, không assertion nào bịt được.
|
||||
|
||||
**Acceptance đề nghị:** chuẩn hoá tại **đường ghi** — `Code = request.Code.Trim().ToUpperInvariant()` trong `CreateAwDefinitionCommandHandler` (và dùng cùng giá trị đó cho `nextVersion` + `actives`), **hoặc** thêm `RegexOptions.IgnoreCase` cho `GroupCodePattern` + khai rõ "W1a phụ thuộc collation" ngay tại `:350`. Kèm 1 ca test `khkk-n1` để bất-biến này có chứng nhân (dù SQLite, nó vẫn bắt được vế regex).
|
||||
|
||||
---
|
||||
|
||||
## 🔴 MAJOR-3 — rào D6 ở đường TẠO có 0 test; T7 chỉ phủ đường SỬA NHÁP
|
||||
|
||||
**Vị trí:** `ContractSigningPlanFeatures.cs:507` (`EnsureWorkflowPinnableAsync` tại Create).
|
||||
|
||||
Spec W1b2 nói rõ **2 site** (Create + UpdateDraft). Danh sách T1–T14 **chỉ có T7**, và T7 gọi `UpdateDraftAsync` ở cả 4 chân — **không chân nào đi qua Create**. Vậy một guard **hard-409 mới cắm vào write-path chính của prod** (mọi phiếu KHKK đều sinh ra qua đây) đang có **0 răng đo**.
|
||||
|
||||
Rủi ro cụ thể: nếu ai đó về sau dời `EnsureWorkflowPinnableAsync` ra khỏi Create, hoặc đảo nó lên **trước** `EnsureWorkflowTypeAsync` (làm 404 đổi thành 409), hoặc gỡ nó khi refactor — **659 test vẫn xanh**. Và chiều ngược cũng không được đo: rào này có thể đang **chặn oan** một luồng tạo hợp lệ nào đó mà chưa ai chạm tới.
|
||||
|
||||
Ghi chú công bằng: đây là **lỗ của SPEC** (spec liệt 14 ca và thiếu ca này), implementer làm đúng spec. Nhưng luật §7 nói *"New endpoint / guard mới ở write-path → test"*, và tôi không hạ thước theo spec.
|
||||
|
||||
**Acceptance đề nghị:** thêm **1 ca** vào `ContractSigningPlanGroupAndCatalogTests` (nơi đã có sẵn `CreateAsync` helper + seed PE): Create pin workflow `IsActive=false` ⇒ 409 · pin `IsUserSelectable=false` ⇒ 409 **message khác** · control dương pin bản active+ghim ⇒ tạo được **và không đốt mã** (`ReadPlanScopesAsync` rỗng ở 2 ca đầu — khuôn đã có ở `:869`).
|
||||
|
||||
---
|
||||
|
||||
# MINOR
|
||||
|
||||
- **MINOR-1 — assertion mù THỨ TỰ toán hạng.** `Contain("N5").And.Contain("N3")` (`GroupAndCatalogTests:865`, `ApprovalTests:188` + `:246`) vẫn xanh nếu ai đó **đảo 2 đối số** ở call-site (`EnsureWorkflowGroupMatchAsync(db, awId, approvalGroup, ct)` — 2 tham số kề nhau, 3 call-site). Tôi đã đối chiếu tay: **cả 3 site hiện ĐÚNG**. Nhưng lưới không giữ được bất-biến đó. Đề nghị pin nguyên câu có thứ tự: `Contain("Phiếu nhóm N3 đang gắn quy trình N5")` ở ít nhất 1 ca.
|
||||
- **MINOR-2 — `.session-counter.json` bị thêm BOM + đổi sang CRLF.** HEAD: `7b 0a` (`{`+LF). Worktree: `ef bb bf 7b 0d 0a` (**BOM** + `{` + CRLF). CRLF sẽ được git normalize, **BOM thì được commit**. Hiện chưa có script nào parse file này (`.claude/hooks/` chỉ có `wal-flush.ps1`, không đọc), nên rủi ro thấp — nhưng đây đúng là bẫy `BOM-json` đã ghi trong `feedback_resume_premise_reverify`, và nó vào commit **kèm theo code** mà không ai chủ ý. Đề nghị: `git add` **từng file cụ thể** (không `-A`), hoặc tách thành commit `wal:`/governance riêng.
|
||||
- **MINOR-3 — D5 đếm anh-em KHÔNG lọc `ApplicableType`.** `AdminFeatures.cs:1339-1340`: `Count(w => w.Code == def.Code && w.Id != def.Id)`. Một dòng `KHKK-N1` ở type khác (UNIQUE là `(Code, Version)` nên chỉ cần Version khác) sẽ được tính là "anh em" và cho phép xoá bản type-10 cuối cùng. Ca hiếm, nhưng vá là **1 mệnh đề**: `&& w.ApplicableType == def.ApplicableType`.
|
||||
- **MINOR-4 — panel nhóm tạo version MỚI ⇒ 6 cờ khoá bị đóng băng ở DEFAULT của FE, không phải "state thật".** Trên panel nhóm, "Tạo phiên bản mới" (không clone) khởi tạo entry từ `makeEmptyStep`; 6 ô `disabled` ⇒ admin **không thể** sửa ⇒ POST luôn ghi đúng bộ mặc định FE. Với KHKK thì vô hại (BE không đọc 6 cờ đó), nhưng chữ "state THẬT" trong D3=C **chỉ đúng cho đường CLONE/EDIT**. Đề nghị: khai 1 dòng trong commit-body hoặc nợ-khai, đừng để lượt sau đọc "state thật" rồi suy sang ca tạo-mới.
|
||||
- **MINOR-5 — bỏ ghim bản active = đóng cửa tạo phiếu nhóm (không có rào).** Spec §54 đã khai; sau W2g còn chặt hơn (`isActive && isUserSelectable`). Không đòi vá đợt này, chỉ xác nhận **răng đã sắc hơn spec mô tả** ⇒ khi đóng sổ nên ghi vào accepted-risk cho đủ 2 vế (bỏ-ghim **và** bỏ-active).
|
||||
- **MINOR-6 — `EnsureWorkflowGroupMatchAsync` cho qua khi `awId` không tồn tại.** Có khai chủ đích `:55-57` và ở Create/UpdateDraft thì `EnsureWorkflowTypeAsync` chạy trước nên vô hại. Ở **Submit** không có rào type đứng trước — nhưng `ContractSigningPlans.ApprovalWorkflowId` có **FK Restrict** (`ContractSigningPlanConfiguration.cs:50-53`) nên `awId` luôn resolve. Ghi lại để lượt sau đừng gỡ FK mà quên chỗ này.
|
||||
|
||||
---
|
||||
|
||||
# Điểm CHỊU ĐƯỢC soi (positive validation — ghi để lượt sau đừng phá)
|
||||
|
||||
1. **3/3 finding 🔴 của tôi ở review đợt-1 đều được đóng ĐÚNG BẢN CHẤT**, không phải đóng bằng chữ: (a) assembly-ranh → file `public static class` riêng; (b) `IgnoreQueryFilters` cho bảng có `HasQueryFilter` → đúng 3 bảng, và **phân biệt được 2 chiều** (DELETE cần, changelog cố ý không); (c) FK-thật → grep 8 module, 5 module loose-0-FK được khai là "rào DUY NHẤT" + T12 chứng minh bằng dangling THẬT trong DB.
|
||||
2. **Mọi ca test đều có control dương CÙNG LƯỢT** — T9/T10/T11/T13/T14 và cả T6 chân (2). Không có ca "vế âm xanh-mù" nào.
|
||||
3. **T5 dựng lại đúng trạng thái không-thể-tạo-qua-API** (seed thẳng DbContext) và **loại trước 2 cửa 409 nhiễu** bằng cách kéo hạng-mục theo `approvalGroup`. Đây là chống "xanh vì nhầm cửa" ở mức tôi không bẻ được.
|
||||
4. **`key={typeCode ?? 'all'}` trên `TypePanel`** (`:400`) — remount khi đổi nhóm, chặn state-bleed giữa 8 panel dùng chung component. Chi tiết nhỏ nhưng đúng chỗ.
|
||||
5. **POST gửi `code: lockedCode ?? code`** (`:882`) chứ không tin ô input đã `disabled` — bất-biến theo CẤU TRÚC, không theo kỷ luật UI.
|
||||
@ -0,0 +1,90 @@
|
||||
# sub-test-specialist-2 — ĐỢT-1 lane TEST W1 (14 test T1-T14)
|
||||
|
||||
> ⚠️ TASK header ghi artifact = `sub-test-specialist-0.md`, nhưng khối RUN-TRACE (ISOLATION B6) ra lệnh
|
||||
> **CHỈ ĐƯỢC GHI** `sub-test-specialist-2.md`. Ưu tiên luật isolation ⇒ ghi file NÀY, KHÔNG tạo `-0`.
|
||||
|
||||
- ts-mở: 2026-08-08 · lane: test-specialist (WRITE `tests/**` ONLY, 0 chạm `src/**`)
|
||||
- SPEC: `spec-khkk-awf-mirror-ncc-08-08-2026.md` MỤC 2 W1 khối Test (:37-39)
|
||||
- Homes: `tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs` (T1-T3, T8-T14)
|
||||
· `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs` (T4-T7)
|
||||
- 🔴 implementer-backend land 5 fix SONG SONG ⇒ test viết theo **HÀNH VI ĐÍCH**; ca đỏ-vì-BE-chưa-land = RED hợp lệ.
|
||||
|
||||
## §0 — Ghi liên tục (chống-#53). Mục CHƯA CHẠY chỉ có tiêu-đề + "CHƯA ĐO".
|
||||
|
||||
## §1 — Đọc nền (prod code sites) — ĐÃ ĐỌC
|
||||
|
||||
| Vế | Site trên đĩa (HEAD hôm nay) | Hành vi HÔM NAY | ⇒ test |
|
||||
|---|---|---|---|
|
||||
| W1a deactivate | `ApprovalWorkflowV2AdminFeatures.cs:339-343` `Where(w.ApplicableType == typeEnum && w.IsActive)` + comment `:339` "only ONE active per type" | tắt **MỌI** active cùng type ⇒ POST 1 nhóm = tắt cả 8 | T1 ĐỎ · T3 ĐỎ · T2 XANH (type-1 giữ per-type) |
|
||||
| nextVersion | `:334-337` `Where(w.Code == request.Code).Max(Version)` | ĐÃ per-Code sẵn (bất đối xứng với `:340`) | nền của T1/T3 |
|
||||
| Group-match Create | `ContractSigningPlanFeatures.cs:479-486` regex `^KHKK-N([1-8])$`, code ngoài họ CHO QUA (`:477-478` chủ đích) | CÓ ở Create | (đã có test cũ, xem §4) |
|
||||
| Group-match UpdateDraft | `:603-607` — **chỉ** `EnsureWorkflowTypeAsync` | KHÔNG check nhóm | T4 ĐỎ |
|
||||
| Group-match Submit | `ContractSigningPlanWorkflowService.cs:176-187` (rào hạng mục) → `:189 LoadStepsAsync` | KHÔNG check nhóm | T5 ĐỎ |
|
||||
| Pin-check IsActive/Selectable | `:603-607` không đọc 2 cờ | pin bản archived OK | T7 ĐỎ |
|
||||
| Changelog PUT | `:863-865` gate `ApplicableType is DuyetNcc or DuyetNccPhuongAn` | type-10 KHÔNG ghi vết | T8 ĐỎ · T9 (vế 0-row) XANH-mù ⇒ phải kèm control-dương |
|
||||
| DELETE | `:1193-1205` `KeyNotFoundException` + `Remove` trần, 0 usage-check | KHKK/PE/Contract = **FK Restrict** ⇒ `DbUpdateException` 500 · Leave/Ot/… = **loose 0-FK** ⇒ **200 xoá êm** | T10-T14 ĐỎ |
|
||||
|
||||
Sự-thật nền dùng để dựng lưới:
|
||||
- 8 module có cột `ApprovalWorkflowId`: Contract · ContractSigningPlan · LeaveRequest · OtRequest · Proposal · TravelRequest · VehicleBooking · PurchaseEvaluation (`grep --include=*.cs src/Backend/SolutionErp.Domain/`).
|
||||
- `ContractSigningPlanConfiguration.cs:50-53` FK **Restrict** + `:55` `HasQueryFilter(!IsDeleted)` ⇒ T11 phân biệt được "guard đọc soft-deleted" (409) vs "guard mù" (DbUpdateException).
|
||||
- `WorkflowAppsConfiguration.cs:12-26` LeaveRequest **KHÔNG khai FK** cho `ApprovalWorkflowId`/`LeaveTypeId` ⇒ T12 seed Guid tự do; và guard là RÀO DUY NHẤT (chứng-nhân trong test).
|
||||
- `NotFoundException : Exception` (`Common/Exceptions/ApplicationException.cs:3`) — KHÔNG họ hàng `KeyNotFoundException` ⇒ T13 đỏ thật hôm nay.
|
||||
- `ApprovalWorkflowConfiguration.cs:26` UNIQUE (Code, Version) ⇒ T14 seed được 2 version cùng Code.
|
||||
|
||||
## §2 — Baseline đo tươi
|
||||
|
||||
- `git status` lúc mở lane: **`src/` SẠCH** (implementer-backend chưa land) ⇒ số đo dưới là nền TRƯỚC cả 5 fix.
|
||||
- Lệnh: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1` (chạy nền).
|
||||
- Kết quả: xem §7 (điền khi job xong).
|
||||
|
||||
## §3 — T1-T3 (deactivate-per-Code, home UpdateAwDefinitionTests)
|
||||
|
||||
CHƯA ĐO
|
||||
|
||||
## §4 — T4-T7 (group-match guard + pin-check, home ContractSigningPlanApprovalTests)
|
||||
|
||||
CHƯA ĐO
|
||||
|
||||
## §5 — T8-T9 (changelog PUT type-10)
|
||||
|
||||
CHƯA ĐO
|
||||
|
||||
## §6 — T10-T14 (DELETE usage-check 409/404 + D5 last-version)
|
||||
|
||||
CHƯA ĐO
|
||||
|
||||
## §7 — Bảng kết {test → RED | GREEN | đợi-BE}
|
||||
|
||||
CHƯA ĐO
|
||||
|
||||
## §8 — Bug prod phát hiện / spec drift
|
||||
|
||||
CHƯA ĐO
|
||||
|
||||
## §9 — VÁ MAJOR-3 (reviewer pre-commit) — D6 ở đường TẠO · ĐÃ ĐO
|
||||
|
||||
- ts: 2026-08-08 (lượt resume riêng, sau khi reviewer-precommit ra finding). Scope: **đúng 1 `[Fact]`**, 0 chạm `src/**`, 0 sửa 14 ca cũ, 0 commit.
|
||||
- **Lỗ:** `EnsureWorkflowPinnableAsync` cắm 2 site (Create `ContractSigningPlanFeatures.cs:507` · UpdateDraft `:642`) mà T7 (`ContractSigningPlanApprovalTests.cs:1402`) gọi `UpdateDraftAsync` ở CẢ 4 chân ⇒ đường TẠO 0 răng đo.
|
||||
- **Nhà:** `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanGroupAndCatalogTests.cs`
|
||||
— 1 `[Fact]` `Create_PinQuyTrinhDaNgungHoacChuaGhim_ThiBiChan_VaKhongDotMa` (đặt cạnh ca rào (v) để dùng chung `SeedPeForCreateAsync` + `CreateAsync` + `ReadPlanScopesAsync`).
|
||||
— `SeedWorkflowAsync` +3 tham số **ĐUÔI** optional (`version` / `isActive` / `isUserSelectable`, default giữ hành vi cũ) ⇒ **15 call-site cũ 0 đổi chữ**; cùng khuôn `ContractSigningPlanApprovalTests.cs:135-140`.
|
||||
|
||||
**Thiết kế phép đo (vì sao 3 chân này phân biệt được 2 thế giới):**
|
||||
|
||||
| Chân | Seed (khác nhau ĐÚNG Version + 1 cờ) | Kỳ vọng | Bịt cái "xanh giả" nào |
|
||||
|---|---|---|---|
|
||||
| (1) | `KHKK-N1` v2 `IsActive=false` | 409 chứa "ngừng hoạt động" | — |
|
||||
| (2) | `KHKK-N1` v3 `IsUserSelectable=false`, **`IsActive` VẪN true** | 409 chứa "ghim" **∧ message ≠ chân (1)** | 2 cờ cùng false ⇒ `!IsActive` nổ trước ⇒ chân (2) đo lại chân (1) = phép đo RỖNG |
|
||||
| (3) | `KHKK-N1` v1 active+ghim | tạo được, mã **`KHKK/2026/001`** | rào chặn-tuốt (chân 3 đỏ) ⟂ rào bị gỡ khỏi Create (chân 1-2 xanh giả) |
|
||||
|
||||
- **Code cùng họ `KHKK-N1` + phiếu nhóm 1** ⇒ rào (v) `:515` KHỚP nên im lặng; mọi 409 quan sát được chỉ có thể từ D6. Ba message của các rào khác trong đường tạo đều khác chữ ("không thuộc loại…" / "đã có kế hoạch ký kết cho nhóm duyệt N…" / "Phiếu nhóm N{g} đang gắn quy trình N{k}") ⇒ 2 phép `Contain` định danh D6 duy nhất.
|
||||
- **Ordering ĐO TRỰC TIẾP, không suy luận:** sau 2 lần 409, assert `WorkflowAppCodeSequences.Any(Prefix=="KHKK/2026") == false` — nếu ai dời D6 xuống SAU `codeGen.GenerateAsync` (`:524`) thì hàng sequence đã tồn tại với `LastSeq=2` và chân (3) ra `003`. Codegen **tự commit** transaction riêng (`ContractSigningPlanCodeGenerator.cs:23-43`) ⇒ số mất vĩnh viễn, không rollback theo request.
|
||||
- **Không để lại phiếu nửa vời:** `ReadPlanScopesAsync` rỗng sau 2 lần 409 (D6 ném trước `db.ContractSigningPlans.Add` `:577`), rồi `BeEquivalentTo` đúng 1 `PlanScope("KHKK/2026/001", 1, dangDung)` sau chân (3).
|
||||
- 🔴 **CỐ Ý KHÔNG pin chuỗi `wf.Code` trong message** dù prod có chèn (`:342`/`:345`): bất-biến mà spec tuyên bố là "**2 câu TÁCH**", pin thêm chữ Code là biến chi-tiết trình bày thành yêu cầu (bài S164 không-pin-config-lie). Vế `NotBe` giữa 2 message mới là thứ khoá đúng tuyên bố đó.
|
||||
|
||||
**Đo (lệnh chuẩn `-p:BuildInParallel=false -maxcpucount:1`):**
|
||||
- Ca mới chạy CÔ LẬP (`--filter`): `Passed: 1, Failed: 0`.
|
||||
- Full-suite `dotnet test SolutionErp.slnx`: **Domain 45 + Infra 615 = 660 PASS, 0 FAIL** (nền 659 → **Δ+1**, 0 hồi quy).
|
||||
- `git status`: đổi DUY NHẤT `…/ContractSigningPlanGroupAndCatalogTests.cs` (+106/−7). `git diff --stat -- src/` = 3 file của lane implementer-backend (có TRƯỚC lượt này), **0 dòng của tôi**.
|
||||
|
||||
**Bug prod / spec drift lượt này:** KHÔNG có. Guard hành xử đúng như spec W1b2 mô tả ở cả 3 chân.
|
||||
@ -43,6 +43,8 @@ Dữ-liệu cũ (WAL · HANDOFF · auto-memory · migration-todos) **KHÔNG** đ
|
||||
| `YC-010` | "OK làm hết rồi khép trọn đi" | 2026-08-07 · S181 | đóng | **2** | `WAL` chain W5b/W3b/W6 | Khép trọn gói: W5b + W3b + phần còn lại W6 | `YC-008` |
|
||||
| `YC-011` | "1,2 -> làm đi, OK sau đó /session-end" | 2026-08-07 · S181 | mở | 1 | `WAL` `next:` | (1) push 42 commit · (2) cổng đồng-bộ + báo cáo cuối · rồi đóng phiên | |
|
||||
| `YC-012` | "nâng cap" *(🔸 trích từ `spec-…-07-08-2026.md:64`, cửa-sổ **S180**, KHÔNG bắt trực tiếp ở cửa-sổ này — khai đúng nấc)* | 2026-08-07 · S180 | đóng | 1 | `memory-budget.json` `_cap_raised_S181` | Lệnh dứt khoát nâng `hmw_width.cap` 20→30. 🔴 @S179 lead **tự thu-hẹp** chữ "OK" thành "chưa phải lệnh đổi số" — lần này không tự thu-hẹp | |
|
||||
| `YC-013` | "Hiện tạo muốn cac quy trình này giống như duyệt NCC. Có thể thêm sửa xóa edit stick tương tự như duyệt NCC. Cho 2 /fable-real invest cách làm BE và Database, 1 /fable-real invest cách cấu hình giao diện front-end đúng nhất về UI lẫn giao diện, ko lệch. -> Ra các wave xử lý chi tiết nhất. cho 2 /fable-real review và tổng hợp các wave xử lý chi tiết. Cuối cùng fan-out cho Opus 5 MAX xử lý nhé." *(kèm 4 ảnh trang `approval-workflows-v2/DuyetNcc` + dialog Sửa-tại-chỗ)* | 2026-08-08 · S182 | **mở** | 1 | `runs/2026-08-08-S182-khkk-awf-mirror-ncc/` | 8 workflow "Duyệt KHKK — Nhóm 1..8" phải có ĐỦ chức năng như trang "Duyệt NCC": xem steps/cấp/NV + per-NV quyền duyệt + Sửa-tại-chỗ + Tạo-phiên-bản-mới + Ghim/Bỏ-ghim + Xoá-version + lịch-sử-versions. UI **không lệch** khuôn DuyetNcc. Pipeline anh chỉ định: 3 `/fable-real investigator-codebase` (BE · DB · FE-UI) → wave-plan chi tiết → 2 `/fable-real reviewer` → hmw fan-out **Opus 5 MAX** execute | |
|
||||
| `YC-014` | "C — Mờ ô chưa nghe (Recommended)" *(AskUser D3 @S182 — sau khi invest phát hiện 6/9 checkbox KHKK-runtime bỏ qua)* | 2026-08-08 · S182 | đóng | 1 | `spec-khkk-awf-mirror-ncc-DRAFT.md` MỤC 3 D3 | Dialog KHKK giữ layout y khuôn; ô máy-chưa-nghe **mờ + tooltip "chưa áp dụng cho KHKK"** (tiền lệ ô Duyệt-thẳng-Cấp-cuối @K3). KHÔNG port semantics đợt này (W3 hủy); port sau thì gỡ mờ dần | |
|
||||
|
||||
> 🔴 **MỘT yêu-cầu, nhắc 4 lần.** Đây là dữ-liệu **TC-01** sinh ra để bắt, và nó tố-giác chính lead: hiểu sai ở `YC-002` nên anh phải nói lại 3 lần. **Ghi lại chứ không làm đẹp.**
|
||||
>
|
||||
|
||||
@ -30,6 +30,21 @@ export const MenuKeys = {
|
||||
ApprovalWorkflowsV2: 'ApprovalWorkflowsV2',
|
||||
AwV2_DuyetNcc: 'AwV2_DuyetNcc',
|
||||
AwV2_DuyetNccPhuongAn: 'AwV2_DuyetNccPhuongAn',
|
||||
// [W2e S182] 8 leaf Designer theo NHÓM DUYỆT của Kế hoạch ký kết HĐ.
|
||||
// Mirror BE `MenuKeys.cs:127-134` (8 key này VÀO `All` ⇒ 32 policy AwV2_KhkkN{n}.{R,C,U,D}).
|
||||
// ⚠️ File này là bản chép TAY của BE và hiện chỉ dùng cho `can(menuKey, action)`;
|
||||
// 8 key dưới đây CHƯA có call-site nào — chép vào để bảng đối chiếu FE↔BE không
|
||||
// khuyết, tránh lượt sau đọc chỗ này rồi kết luận "BE chưa có key nhóm".
|
||||
// Đường vào sidebar KHÔNG đi qua đây: sidebar dựng từ `/api/menus/me` rồi
|
||||
// `Layout.tsx:188` (`/^KhkkN[1-8]$/`) mới đổi key thành đường dẫn.
|
||||
AwV2_KhkkN1: 'AwV2_KhkkN1',
|
||||
AwV2_KhkkN2: 'AwV2_KhkkN2',
|
||||
AwV2_KhkkN3: 'AwV2_KhkkN3',
|
||||
AwV2_KhkkN4: 'AwV2_KhkkN4',
|
||||
AwV2_KhkkN5: 'AwV2_KhkkN5',
|
||||
AwV2_KhkkN6: 'AwV2_KhkkN6',
|
||||
AwV2_KhkkN7: 'AwV2_KhkkN7',
|
||||
AwV2_KhkkN8: 'AwV2_KhkkN8',
|
||||
// [S61 Mig 50] Module Ngân sách cũ (Budgets + Bg_*) XÓA — thay bằng bảng
|
||||
// "Tổng hợp ngân sách trình ký" per (Dự án, Hạng mục) trong phiếu PE.
|
||||
// Module Hồ sơ Nhân sự (Mig 34 — Phase 10.1 G-H1 Session 33, 2026-05-26)
|
||||
|
||||
@ -59,6 +59,11 @@ interface WorkflowDto {
|
||||
code: string
|
||||
name: string
|
||||
isUserSelectable: boolean
|
||||
// [W2g S182] Cùng một mã `KHKK-N{n}` có NHIỀU phiên bản (admin bấm "Tạo phiên bản
|
||||
// mới" thì bản cũ chuyển sang lưu trữ). `history` mà BE trả về là DANH SÁCH ĐẦY ĐỦ
|
||||
// — cả bản đang áp dụng lẫn bản đã lưu trữ — nên thiếu cờ này thì màn tạo phiếu
|
||||
// không phân biệt được, xem thêm chỗ dùng bên dưới.
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
// 🧊 [S174] `firstFreeGroup()` ĐÃ GỠ cùng lúc bỏ ô chọn nhóm — nó chỉ tồn tại để tự nhảy
|
||||
@ -110,9 +115,15 @@ export function KhkkCreatePage() {
|
||||
// `var code = $"KHKK-N{n}"` (vòng `for n = 1..8`, seed idempotent per-Code).
|
||||
// Không khớp ⇒ KHÔNG tự rơi về quy trình khác: chặn tạo + nói rõ thiếu mã nào, vì gán
|
||||
// nhầm quy trình là phiếu đi sai đường duyệt — hỏng nặng hơn là không tạo được.
|
||||
// [W2g S182] 🔴 Phải lọc thêm `isActive`. Danh sách trả về gồm CẢ phiên bản đã lưu
|
||||
// trữ, mà `find` lấy phần tử ĐẦU TIÊN khớp mã — hễ admin tạo phiên bản mới cho nhóm
|
||||
// này là phiếu mới có thể bị gắn vào bản CŨ (thứ tự do BE trả, không do màn này
|
||||
// quyết), đi theo đội hình duyệt đã bị thay. Gắn sai quy trình thì lúc phát hiện
|
||||
// phiếu đã chạy được vài trạm rồi. Ràng buộc `isUserSelectable` ở query trên vẫn giữ
|
||||
// (admin ghim bản nào thì user mới được dùng bản đó).
|
||||
const wantWorkflowCode = `KHKK-N${approvalGroup}`
|
||||
const autoWorkflow = workflows.data?.find(
|
||||
(w) => (w.code ?? '').trim().toUpperCase() === wantWorkflowCode,
|
||||
(w) => w.isActive && (w.code ?? '').trim().toUpperCase() === wantWorkflowCode,
|
||||
) ?? null
|
||||
|
||||
// Phiếu đang XEM — chỉ tải khi thật sự có cái để xem.
|
||||
@ -125,7 +136,11 @@ export function KhkkCreatePage() {
|
||||
const create = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!purchaseEvaluationId) throw new Error('Vui lòng chọn phiếu Duyệt NCC nguồn')
|
||||
if (!autoWorkflow) throw new Error(`Chưa có quy trình duyệt ${wantWorkflowCode}`)
|
||||
// [W2g S182] Câu này nói ĐIỀU KIỆN, không nói "không tồn tại": quy trình có thể
|
||||
// đang nằm đó nhưng ở phiên bản đã lưu trữ hoặc chưa được ghim.
|
||||
if (!autoWorkflow) {
|
||||
throw new Error(`Chưa có quy trình duyệt ${wantWorkflowCode} đang áp dụng và được ghim cho user chọn`)
|
||||
}
|
||||
const body: CreateKhkkInput = {
|
||||
peId: purchaseEvaluationId, // [F-2] BE Command field = PeId
|
||||
approvalWorkflowId: autoWorkflow.id,
|
||||
@ -349,9 +364,10 @@ export function KhkkCreatePage() {
|
||||
</div>
|
||||
{!workflows.isLoading && !autoWorkflow ? (
|
||||
<p className="mt-1 text-[11px] text-amber-700">
|
||||
⚠ Chưa có quy trình mã <strong>{wantWorkflowCode}</strong> (hoặc chưa được ghim
|
||||
cho user chọn). Nhờ admin mở trong màn Quy trình duyệt rồi quay lại — hệ thống
|
||||
KHÔNG tự dùng quy trình của nhóm khác.
|
||||
⚠ Chưa dùng được quy trình mã <strong>{wantWorkflowCode}</strong>: phải có
|
||||
phiên bản <strong>đang áp dụng</strong> và <strong>đã ghim</strong> cho user chọn.
|
||||
Nhờ admin mở trong màn Quy trình duyệt rồi quay lại — hệ thống KHÔNG tự dùng
|
||||
phiên bản đã lưu trữ, cũng KHÔNG tự dùng quy trình của nhóm khác.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
|
||||
@ -31,6 +31,9 @@ import { Textarea } from '@/components/ui/Textarea'
|
||||
import { api } from '@/lib/api'
|
||||
import { getErrorMessage } from '@/lib/apiError'
|
||||
import type { Department, Paged } from '@/types/master'
|
||||
// [W2 S182] 2 hàm dùng chung với màn nghiệp vụ (fe-user) — KHÔNG chép chuỗi
|
||||
// `KHKK-N{n}` / nhãn nhóm vào file này, để 1 nguồn duy nhất khi BE đổi.
|
||||
import { khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'
|
||||
|
||||
// ===== Types (mirror BE AwAdminOverviewDto) =====
|
||||
|
||||
@ -286,7 +289,21 @@ function isLevelEnabled(step: EditStep, order: LevelOrder): boolean {
|
||||
export function ApprovalWorkflowsV2Page() {
|
||||
const qc = useQueryClient()
|
||||
const { typeCode } = useParams<{ typeCode?: string }>()
|
||||
const selectedTypeInt = typeCode ? TYPE_CODE_TO_INT[typeCode] : null
|
||||
|
||||
// [W2a S182] 8 leaf `AwV2_KhkkN1..8` deep-link về CHÍNH trang này với typeCode
|
||||
// `KhkkN{1..8}` (`Layout.tsx:188` — chỗ-thứ-4 Pattern 16-bis đã nối từ K3 S164).
|
||||
// `KhkkN{n}` KHÔNG phải một ApplicableType riêng: cả 8 nhóm đều là type 10
|
||||
// (Kế hoạch ký kết HĐ), phân biệt nhau bằng MÃ quy trình `KHKK-N{n}`. Nên chỗ này
|
||||
// tách 2 khái niệm:
|
||||
// · `khkkGroup` = nhóm duyệt 1..8 (null nếu không vào từ leaf nhóm)
|
||||
// · `selectedTypeInt` = ApplicableType gửi BE — nhóm nào cũng là 10.
|
||||
// Hệ quả CÓ CHỦ ĐÍCH: queryKey của 8 panel nhóm + panel `/ContractSigningPlan`
|
||||
// TRÙNG NHAU ⇒ 1 lần tải dùng chung, và invalidate sau khi lưu làm tươi cả 9 màn.
|
||||
const khkkGroupMatch = typeCode?.match(/^KhkkN([1-8])$/) ?? null
|
||||
const khkkGroup = khkkGroupMatch ? Number(khkkGroupMatch[1]) : null
|
||||
const selectedTypeInt = khkkGroup !== null
|
||||
? TYPE_INT_CONTRACT_SIGNING_PLAN
|
||||
: typeCode ? TYPE_CODE_TO_INT[typeCode] : null
|
||||
|
||||
const overview = useQuery({
|
||||
queryKey: ['approval-workflow-v2-overview', selectedTypeInt],
|
||||
@ -296,25 +313,60 @@ export function ApprovalWorkflowsV2Page() {
|
||||
},
|
||||
})
|
||||
|
||||
const currentType = selectedTypeInt
|
||||
? overview.data?.types.find(t => t.applicableType === selectedTypeInt)
|
||||
const typeBucket = selectedTypeInt
|
||||
? overview.data?.types.find(t => t.applicableType === selectedTypeInt) ?? null
|
||||
: null
|
||||
|
||||
// [W2b S182] Lát cắt TỔNG HỢP cho panel nhóm. BE trả 1 bucket cho CẢ type 10
|
||||
// (`active` = 1-trong-8 quy trình nhóm, tuỳ bản nào kích hoạt sau cùng), nên
|
||||
// panel nhóm KHÔNG được đọc `bucket.active` — sẽ hiện nhầm quy trình của nhóm khác.
|
||||
// Cắt lại từ `history`:
|
||||
// 🔴 `history` là DANH SÁCH ĐẦY ĐỦ, KỂ CẢ bản đang áp dụng (verified BE
|
||||
// `ApprovalWorkflowV2AdminFeatures.cs:206-211` — không lọc `!IsActive`);
|
||||
// chính `TypePanel` mới lọc `!isActive` lúc render mục "Lịch sử versions".
|
||||
// · `active` suy TỪ chính lát cắt (`find(v => v.isActive)`), không mượn ô ngoài.
|
||||
// Giữ nguyên shape `TypeSummaryDto` ⇒ `TypePanel`/`Designer` không cần biết
|
||||
// chúng đang xem một nhóm hay cả loại.
|
||||
const currentType: TypeSummaryDto | null = useMemo(() => {
|
||||
if (khkkGroup === null) return typeBucket
|
||||
if (!typeBucket) return null
|
||||
const wantCode = khkkGroupWorkflowCode(khkkGroup)
|
||||
const versions = typeBucket.history.filter(d => d.code === wantCode)
|
||||
return {
|
||||
applicableType: typeBucket.applicableType,
|
||||
applicableTypeLabel: typeBucket.applicableTypeLabel,
|
||||
active: versions.find(v => v.isActive) ?? null,
|
||||
history: versions,
|
||||
}
|
||||
}, [khkkGroup, typeBucket])
|
||||
|
||||
// [W2c S182] Mã quy trình của panel nhóm là KHOÁ, không phải gợi ý: mỗi nhóm đúng
|
||||
// MỘT chuỗi phiên bản `KHKK-N{n}`. Gõ mã khác trên panel nhóm = đẻ quy trình mồ côi
|
||||
// (phiếu nhóm đó vẫn tìm `KHKK-N{n}` — `KhkkCreatePage.tsx:113`) ⇒ khoá thẳng ô Mã.
|
||||
const lockedCode = khkkGroup !== null ? khkkGroupWorkflowCode(khkkGroup) : null
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageHeader
|
||||
// [W2h/D4 S182] Vào từ leaf nhóm thì tiêu đề phải nói ĐÚNG nhóm nào —
|
||||
// `applicableTypeLabel` của BE chỉ có một chuỗi chung cho cả 8 nhóm
|
||||
// ("Kế hoạch ký kết HĐ"), 8 panel sẽ trông y hệt nhau.
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<Workflow className="h-5 w-5" />
|
||||
{currentType
|
||||
? `Quy trình duyệt (Mới): ${currentType.applicableTypeLabel}`
|
||||
: 'Quy trình duyệt (Mới)'}
|
||||
{khkkGroup !== null
|
||||
? `Quy trình duyệt (Mới): ${khkkGroupMenuLabel(khkkGroup)}`
|
||||
: currentType
|
||||
? `Quy trình duyệt (Mới): ${currentType.applicableTypeLabel}`
|
||||
: 'Quy trình duyệt (Mới)'}
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
currentType
|
||||
? 'Mỗi Bước = 1 Phòng. Mỗi Cấp trong Bước = 1 nhân viên cụ thể duyệt. Tuần tự: Cấp 1 → Cấp 2 → ... → Bước kế.'
|
||||
: 'Schema mới UAT — chọn loại quy trình từ menu bên trái.'
|
||||
khkkGroup !== null
|
||||
? `${currentType?.applicableTypeLabel ?? 'Kế hoạch ký kết HĐ'} — nhóm duyệt N${khkkGroup}. Mã quy trình khoá theo nhóm: ${lockedCode}; mỗi lần lưu là một phiên bản mới của CHÍNH nhóm này, 7 nhóm còn lại không đổi.`
|
||||
: currentType
|
||||
? 'Mỗi Bước = 1 Phòng. Mỗi Cấp trong Bước = 1 nhân viên cụ thể duyệt. Tuần tự: Cấp 1 → Cấp 2 → ... → Bước kế.'
|
||||
: 'Schema mới UAT — chọn loại quy trình từ menu bên trái.'
|
||||
}
|
||||
/>
|
||||
|
||||
@ -344,7 +396,11 @@ export function ApprovalWorkflowsV2Page() {
|
||||
|
||||
{currentType && (
|
||||
<TypePanel
|
||||
// [W2b S182] Panel nhóm và panel loại dùng CHUNG component: khác nhau duy
|
||||
// nhất ở lát cắt truyền vào + `lockedCode` (null = panel loại, tự do đặt mã).
|
||||
key={typeCode ?? 'all'}
|
||||
type={currentType}
|
||||
lockedCode={lockedCode}
|
||||
onSaved={() => qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })}
|
||||
/>
|
||||
)}
|
||||
@ -354,7 +410,17 @@ export function ApprovalWorkflowsV2Page() {
|
||||
|
||||
// ===== Per-type panel =====
|
||||
|
||||
function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => void }) {
|
||||
function TypePanel({
|
||||
type,
|
||||
// [W2c S182] `null` = panel theo LOẠI (admin tự đặt mã, hành vi cũ).
|
||||
// Khác null = panel theo NHÓM KHKK: mã bị khoá, mọi version đều thuộc chuỗi đó.
|
||||
lockedCode,
|
||||
onSaved,
|
||||
}: {
|
||||
type: TypeSummaryDto
|
||||
lockedCode: string | null
|
||||
onSaved: () => void
|
||||
}) {
|
||||
const [designerOpen, setDesignerOpen] = useState(false)
|
||||
const [cloneFrom, setCloneFrom] = useState<DefinitionDto | null>(null)
|
||||
// [F-7 S155] Khác null ⇒ Designer mở ở chế độ SỬA TẠI CHỖ (PUT, giữ nguyên Id +
|
||||
@ -448,6 +514,7 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi
|
||||
<Designer
|
||||
applicableType={type.applicableType}
|
||||
applicableTypeLabel={type.applicableTypeLabel}
|
||||
lockedCode={lockedCode}
|
||||
cloneFrom={cloneFrom}
|
||||
editTarget={editTarget}
|
||||
onClose={() => { setDesignerOpen(false); setCloneFrom(null); setEditTarget(null) }}
|
||||
@ -620,6 +687,7 @@ function DefinitionCard({
|
||||
function Designer({
|
||||
applicableType,
|
||||
applicableTypeLabel,
|
||||
lockedCode,
|
||||
cloneFrom,
|
||||
editTarget,
|
||||
onClose,
|
||||
@ -627,6 +695,8 @@ function Designer({
|
||||
}: {
|
||||
applicableType: number
|
||||
applicableTypeLabel: string
|
||||
// [W2c S182] Mã bị KHOÁ theo nhóm KHKK (`KHKK-N{n}`), null = tự do như cũ.
|
||||
lockedCode: string | null
|
||||
cloneFrom: DefinitionDto | null
|
||||
// [F-7 S155] Khác null ⇒ chế độ SỬA TẠI CHỖ (PUT). Loại trừ nhau với cloneFrom.
|
||||
editTarget: DefinitionDto | null
|
||||
@ -638,16 +708,38 @@ function Designer({
|
||||
const isEdit = editTarget !== null
|
||||
const source = editTarget ?? cloneFrom
|
||||
|
||||
// [K3 S164 — GỠ KHOÁ] `earlyFinalizeLocked` (W1 S161) ĐÃ BỎ HẲN: 2 ô "duyệt sớm"
|
||||
// nay sống cho MỌI loại quy trình, kể cả Kế hoạch ký kết HĐ (type 10) — BE đã port
|
||||
// đường finalize ở K3 cùng wave, và owner chốt TP.CCM là người quyết lên CEO hay
|
||||
// kết thúc. Lý do đầy đủ ở khối ghi chú đầu file (gần `TYPE_INT_CONTRACT_SIGNING_PLAN`).
|
||||
// [W2d/W2f S182 — D3=C] Kế hoạch ký kết HĐ (type 10, gồm cả 8 panel nhóm) chưa
|
||||
// dùng tới một phần cờ quyền của khuôn Duyệt NCC. Gom về ĐÚNG MỘT cờ để 8 ô
|
||||
// (7 ô quyền + ô Ngưỡng CEO) không mỗi chỗ tự viết lại điều kiện rồi lệch nhau.
|
||||
const isKhkk = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN
|
||||
// Câu dùng chung cho các ô CHƯA có đường chạy ở KHKK. Ô nào có lý do RIÊNG thì
|
||||
// viết riêng tại chỗ (xem ô "Trả về Drafter") — trạng thái tick vẫn là THẬT.
|
||||
// 🔴 Câu nhắc phải gắn `title` trên <label>, TUYỆT ĐỐI KHÔNG trên <input disabled>:
|
||||
// Chromium (Chrome/Edge — trình duyệt đích) nuốt sự kiện chuột của form-control
|
||||
// disabled ⇒ tooltip KHÔNG bao giờ bật, admin chỉ còn thấy mấy ô xám vô cớ. Label
|
||||
// không disabled nên rê vào chữ là hiện. Ai dời ngược lên input = làm câm lại.
|
||||
const khkkNotYetHint = 'Chưa áp dụng cho KHKK'
|
||||
// Ô "Trả về Drafter" (#4) khác 6 ô kia về NGHĨA, nên câu riêng — và câu này còn
|
||||
// hiện thành chữ NHÌN THẤY ĐƯỢC ngay dưới ô: đây là thông tin PHÂN BIỆT #4 với 6 ô
|
||||
// "chưa áp dụng", không được để nó phụ thuộc vào việc người dùng có rê chuột hay không.
|
||||
// Một chuỗi dùng cho cả tooltip lẫn dòng chữ, để hai chỗ không trôi lệch nhau.
|
||||
const khkkDrafterHint = 'KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh'
|
||||
|
||||
// [K3 S164 — GỠ KHOÁ · vá chữ W2h S182] Cờ `earlyFinalizeLocked` (W1 S161) ĐÃ BỎ HẲN
|
||||
// và ô "Duyệt là KẾT THÚC" (#8) nay SỐNG cho mọi loại — BE đã port đường finalize ở K3
|
||||
// cùng wave, owner chốt TP.CCM là người quyết lên CEO hay kết thúc.
|
||||
// 🔴 Đính chính câu cũ ("2 ô duyệt sớm sống cho MỌI loại"): chỉ có MỘT trong hai ô đó
|
||||
// sống cho KHKK. Ô còn lại — "Duyệt thẳng Cấp cuối" (#7) — vẫn KHOÁ cho type 10 từ K3
|
||||
// S165 (service KHKK chưa port nhánh SkipToFinal, BE Create còn ép-false), xem ngay
|
||||
// tại ô đó. Lý do đầy đủ ở khối ghi chú đầu file (gần `TYPE_INT_CONTRACT_SIGNING_PLAN`).
|
||||
const initialSteps: EditStep[] = useMemo(
|
||||
() => (source ? copyFromDefinition(source) : [makeEmptyStep(1)]),
|
||||
[source],
|
||||
)
|
||||
|
||||
const defaultCode = DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'
|
||||
// Panel nhóm: mã đã khoá thì ô Mã phải hiện ĐÚNG mã sẽ gửi đi, không phải mã gợi ý
|
||||
// chung của loại (`QT-KHKK-V2-001`) — hiện một đằng gửi một nẻo là kiểu bẫy tệ nhất.
|
||||
const defaultCode = lockedCode ?? DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'
|
||||
const [code, setCode] = useState(source?.code ?? defaultCode)
|
||||
const [name, setName] = useState(source ? source.name : `Quy trình ${applicableTypeLabel}`)
|
||||
const [description, setDescription] = useState(source?.description ?? '')
|
||||
@ -795,11 +887,19 @@ function Designer({
|
||||
// ============ ĐƯỜNG 2 — TẠO PHIÊN BẢN MỚI (POST, sinh Id mới) ============
|
||||
await api.post('/approval-workflows-v2', {
|
||||
applicableType,
|
||||
code,
|
||||
// [W2c S182] Panel nhóm: mã lấy từ prop khoá, KHÔNG lấy từ state ô nhập.
|
||||
// Ô nhập đang disabled nên state không đổi được qua UI — nhưng gửi thẳng
|
||||
// `lockedCode` là bất-biến theo CẤU TRÚC, không phụ thuộc kỷ luật của ô input.
|
||||
code: lockedCode ?? code,
|
||||
name,
|
||||
description: description || null,
|
||||
// S69 — ngưỡng gói CEO: '' → null (không áp ngưỡng), else parse số.
|
||||
ceoApprovalThreshold: ceoThreshold.trim() === '' ? null : Number(ceoThreshold.replace(/[^\d]/g, '')),
|
||||
// [W2f S182] KHKK KHÔNG dùng ngưỡng máy (người được uỷ quyền quyết lên CEO hay
|
||||
// không, qua ô "Duyệt là KẾT THÚC") ⇒ gửi thẳng null, không để giá trị lỡ tay
|
||||
// nằm lại trong state rồi âm thầm bật một nhánh rẽ mà quy trình KHKK không chạy.
|
||||
ceoApprovalThreshold: isKhkk
|
||||
? null
|
||||
: ceoThreshold.trim() === '' ? null : Number(ceoThreshold.replace(/[^\d]/g, '')),
|
||||
steps: steps.map((s, i) => ({
|
||||
order: i + 1,
|
||||
name: s.name,
|
||||
@ -924,12 +1024,16 @@ function Designer({
|
||||
required
|
||||
className="font-mono"
|
||||
// Mã là khoá của chuỗi phiên bản ⇒ sửa tại chỗ KHÔNG đổi được (BE :521 → 409).
|
||||
disabled={isEdit}
|
||||
// [W2c S182] Panel nhóm KHKK cũng khoá: đổi mã ở đây = đẻ quy trình mồ côi
|
||||
// (phiếu của nhóm vẫn đi tìm đúng `KHKK-N{n}`).
|
||||
disabled={isEdit || lockedCode !== null}
|
||||
/>
|
||||
<div className="text-[11px] text-slate-400">
|
||||
{isEdit
|
||||
? 'Không đổi được khi sửa tại chỗ — mã là khoá của chuỗi phiên bản.'
|
||||
: 'Vd QT-DN-V2-001. Version auto-tăng mỗi lần lưu.'}
|
||||
: lockedCode !== null
|
||||
? 'Mã khoá theo nhóm — version tự tăng.'
|
||||
: 'Vd QT-DN-V2-001. Version auto-tăng mỗi lần lưu.'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@ -951,12 +1055,17 @@ function Designer({
|
||||
// 🔴 Sửa tại chỗ KHÔNG đổi được ngưỡng: phiếu đang chờ sẽ đổi nhánh
|
||||
// cần/không-cần CEO giữa chừng (BE :529 → 409). Khoá ô để người dùng
|
||||
// không gõ vào rồi mới nhận lỗi. Giá trị gửi lên lấy thẳng từ bản đang lưu.
|
||||
disabled={isEdit}
|
||||
// [W2f S182] KHKK khoá VÔ ĐIỀU KIỆN (kể cả tạo mới): quy trình KHKK không
|
||||
// rẽ nhánh theo số tiền — người được uỷ quyền quyết. Tiền lệ cùng cách làm
|
||||
// là ô "Duyệt thẳng Cấp cuối" bên dưới.
|
||||
disabled={isEdit || isKhkk}
|
||||
/>
|
||||
<div className="text-[11px] leading-relaxed text-slate-400">
|
||||
{isEdit
|
||||
? 'Không đổi được khi sửa tại chỗ — đổi ngưỡng làm phiếu đang chờ nhảy nhánh cần/không-cần CEO giữa chừng. Muốn đổi: tạo phiên bản mới.'
|
||||
: 'Gói < ngưỡng: CCM duyệt là xong, không cần CEO. ≥ ngưỡng: phải lên CEO. Để trống = luôn theo đủ quy trình.'}
|
||||
{isKhkk
|
||||
? 'KHKK không dùng ngưỡng máy — NĐUQ quyết. Ai được kết thúc quy trình thì tick ô "Duyệt là KẾT THÚC" ở đúng NV đó bên dưới.'
|
||||
: isEdit
|
||||
? 'Không đổi được khi sửa tại chỗ — đổi ngưỡng làm phiếu đang chờ nhảy nhánh cần/không-cần CEO giữa chừng. Muốn đổi: tạo phiên bản mới.'
|
||||
: 'Gói < ngưỡng: CCM duyệt là xong, không cần CEO. ≥ ngưỡng: phải lên CEO. Để trống = luôn theo đủ quy trình.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -984,6 +1093,17 @@ function Designer({
|
||||
<div className="rounded-lg border border-violet-200 bg-violet-50/30 px-3 py-2 text-[11px] leading-relaxed text-violet-800">
|
||||
ⓘ Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới
|
||||
(Trả lại / Edit Section 2 / Edit Budget / Duyệt thẳng Cấp cuối).
|
||||
{/* [W2d S182 — D3=C] Nói thẳng lý do các ô bị mờ, thay vì để admin tự đoán
|
||||
là "mất quyền" hay "lỗi". Ô mờ vẫn hiện ĐÚNG giá trị đang lưu trong DB. */}
|
||||
{isKhkk && (
|
||||
<div className="mt-1 text-violet-900">
|
||||
Riêng Kế hoạch ký kết HĐ: một số ô bị khoá vì luồng duyệt KHKK chưa dùng tới
|
||||
(ô vẫn hiển thị đúng giá trị đang lưu, không bị đổi). Hai ô đang có tác dụng
|
||||
thật là <span className="font-semibold">"Trả về Drafter"</span> (KHKK luôn trả về
|
||||
người soạn) và <span className="font-semibold">"Duyệt là KẾT THÚC"</span> — tick cho NV
|
||||
được uỷ quyền quyết dừng hay trình tiếp CEO.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 rounded-lg border border-slate-200 p-3">
|
||||
@ -1228,57 +1348,107 @@ function Designer({
|
||||
<div className="mb-1 text-[10px] font-medium uppercase text-amber-700">
|
||||
Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}
|
||||
</div>
|
||||
{/* [W2d S182 — D3=C] Với KHKK (type 10, gồm 8 panel nhóm) các ô
|
||||
dưới đây bị KHOÁ vì luồng KHKK chưa chạy tới chúng.
|
||||
🔴 HAI điều KHÔNG được làm, vì đã có tiền lệ hỏng:
|
||||
(1) KHÔNG ép `checked={false}`. Ô phải nói đúng thứ ĐANG NẰM
|
||||
TRONG DB — bản seed có cờ nào bật thì phải thấy nó bật,
|
||||
nếu không admin sẽ tưởng cờ đã tắt trong khi BE vẫn đọc.
|
||||
(2) KHÔNG động vào payload (`levelFlagsPayload`): nó tiếp tục
|
||||
echo NGUYÊN giá trị đang lưu. Khoá là chuyện MÀN HÌNH,
|
||||
không phải chuyện dữ liệu — khoá bằng cách ghi đè dữ liệu
|
||||
thì mở lại sau này không còn gì để mở.
|
||||
Ô nào KHKK có chạy thì để nguyên: "Trả về Drafter" (hành vi thật
|
||||
là LUÔN trả về người soạn) và "Duyệt là KẾT THÚC" (#8, sống). */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<label className="flex items-center gap-1 text-[11px] text-slate-700">
|
||||
<label
|
||||
className={`flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
|
||||
title={isKhkk ? khkkNotYetHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowReturnOneLevel}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowReturnOneLevel', e.target.checked)}
|
||||
/>
|
||||
<span>Trả về 1 Cấp trước</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 text-[11px] text-slate-700">
|
||||
<label
|
||||
className={`flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
|
||||
title={isKhkk ? khkkNotYetHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowReturnOneStep}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowReturnOneStep', e.target.checked)}
|
||||
/>
|
||||
<span>Trả về 1 Bước trước</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 text-[11px] text-slate-700">
|
||||
<label
|
||||
className={`flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
|
||||
title={isKhkk ? khkkNotYetHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowReturnToAssignee}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowReturnToAssignee', e.target.checked)}
|
||||
/>
|
||||
<span>Trả về Người chỉ định</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1 text-[11px] text-slate-700">
|
||||
{/* [W2d S182] Ô này KHÁC 6 ô kia: KHKK có chạy nhánh trả-về-người-soạn,
|
||||
và chạy KHÔNG ĐIỀU KIỆN — cờ chưa được phân nhánh ở service nên bỏ
|
||||
tick cũng không tắt được. Vì vậy khoá nhưng KHÔNG làm mờ kiểu
|
||||
"chưa có": mờ ở đây sẽ đọc thành "tính năng chưa chạy", ngược hẳn
|
||||
sự thật. Câu nhắc viết riêng, không dùng câu chung. */}
|
||||
<label
|
||||
className="flex items-center gap-1 text-[11px] text-slate-700"
|
||||
title={isKhkk ? khkkDrafterHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowReturnToDrafter}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowReturnToDrafter', e.target.checked)}
|
||||
/>
|
||||
<span>Trả về Drafter (mặc định)</span>
|
||||
</label>
|
||||
<label className="col-span-2 flex items-center gap-1 text-[11px] text-slate-700">
|
||||
{/* [W2d S182 — vá MAJOR-1] Câu này là thứ DUY NHẤT phân biệt ô trên
|
||||
với 6 ô "chưa áp dụng" cùng khối, nên nó phải NHÌN THẤY ĐƯỢC chứ
|
||||
không nằm sau một cú rê chuột. `col-start-2` để dòng chữ rơi đúng
|
||||
dưới ô của nó (cột phải), không bị đọc nhầm sang ô bên trái. */}
|
||||
{isKhkk && (
|
||||
<div className="col-start-2 -mt-0.5 pl-4 text-[10px] leading-snug text-slate-500">
|
||||
{khkkDrafterHint}
|
||||
</div>
|
||||
)}
|
||||
<label
|
||||
className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
|
||||
title={isKhkk ? khkkNotYetHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowApproverEditDetails}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowApproverEditDetails', e.target.checked)}
|
||||
/>
|
||||
<span>Cho phép chỉnh sửa Section 2 (Hạng mục/NCC/Báo giá) lúc đang duyệt</span>
|
||||
</label>
|
||||
<label className="col-span-2 flex items-center gap-1 text-[11px] text-slate-700">
|
||||
<label
|
||||
className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
|
||||
title={isKhkk ? khkkNotYetHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowApproverEditBudget}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowApproverEditBudget', e.target.checked)}
|
||||
/>
|
||||
<span>Cho phép chỉnh sửa Section ngân sách lúc đang duyệt</span>
|
||||
@ -1289,13 +1459,20 @@ function Designer({
|
||||
service KHKK CHƯA port nhánh SkipToFinal + BE CREATE vẫn ép-false cờ
|
||||
này cho type-10 ⇒ ô này DISABLE cho KHKK kẻo tick bị nuốt im lặng
|
||||
(class "UI nói một đằng DB một nẻo" — đúng thứ W1 S161 cảnh báo). */}
|
||||
<label className={`col-span-2 flex items-center gap-1 text-[11px] ${applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN ? 'text-slate-400' : 'text-slate-700'}`}>
|
||||
{/* [W2d S182] GIỮ NGUYÊN hành vi ô này (kể cả `checked` ép false —
|
||||
khác 6 ô trên, đây là quyết định cũ của K3 S165 và BE Create
|
||||
ĐANG THẬT SỰ ép-false cờ này cho type 10, nên màn hình đang
|
||||
khớp DB). Chỉ đổi 3 biểu thức trùng lặp sang cờ dùng chung
|
||||
`isKhkk` — giá trị y hệt, để 8 chỗ khoá không lệch nhau. */}
|
||||
<label
|
||||
className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
|
||||
title={isKhkk ? 'Kế hoạch ký kết chưa hỗ trợ duyệt vượt cấp — service chưa port nhánh này (K3)' : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN ? false : entry.allowApproverSkipToFinal}
|
||||
disabled={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN}
|
||||
title={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN ? 'Kế hoạch ký kết chưa hỗ trợ duyệt vượt cấp — service chưa port nhánh này (K3)' : undefined}
|
||||
checked={isKhkk ? false : entry.allowApproverSkipToFinal}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowApproverSkipToFinal', e.target.checked)}
|
||||
/>
|
||||
<span>Cho phép duyệt thẳng Cấp cuối khi đang duyệt</span>
|
||||
@ -1317,11 +1494,15 @@ function Designer({
|
||||
per-Cấp: 1 dòng NV = 1 ô tick riêng. Tick cho NV nào thì
|
||||
CHỈ NV đó xóa được, và chỉ khi phiếu đang Chờ duyệt ĐÚNG
|
||||
lượt của họ. Cấp có 3 NV mà tick 1 ⇒ vẫn chỉ 1 người xóa. */}
|
||||
<label className="col-span-2 flex items-center gap-1 text-[11px] text-rose-700">
|
||||
<label
|
||||
className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-rose-700'}`}
|
||||
title={isKhkk ? khkkNotYetHint : undefined}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3"
|
||||
checked={entry.allowApproverDelete}
|
||||
disabled={isKhkk}
|
||||
onChange={e => updateField('allowApproverDelete', e.target.checked)}
|
||||
/>
|
||||
<span>Cho phép NV này XÓA phiếu khi đến lượt duyệt của mình</span>
|
||||
|
||||
@ -30,6 +30,20 @@ export const MenuKeys = {
|
||||
ApprovalWorkflowsV2: 'ApprovalWorkflowsV2',
|
||||
AwV2_DuyetNcc: 'AwV2_DuyetNcc',
|
||||
AwV2_DuyetNccPhuongAn: 'AwV2_DuyetNccPhuongAn',
|
||||
// [W2e S182] 8 leaf Designer theo NHÓM DUYỆT của Kế hoạch ký kết HĐ.
|
||||
// Mirror BE `MenuKeys.cs:127-134` (8 key này VÀO `All` ⇒ 32 policy AwV2_KhkkN{n}.{R,C,U,D}).
|
||||
// ⚠️ Chép cho ĐỦ bảng đối chiếu FE↔BE, KHÔNG phải để dùng ở app này: toàn bộ họ
|
||||
// `AwV2_*` là màn CẤU HÌNH bên fe-admin — fe-user cố ý không có trang Designer,
|
||||
// cũng không có nhánh `AwV2_*` trong `Layout.tsx`. Đừng đọc mấy dòng này thành
|
||||
// "fe-user thiếu route" rồi đi thêm route.
|
||||
AwV2_KhkkN1: 'AwV2_KhkkN1',
|
||||
AwV2_KhkkN2: 'AwV2_KhkkN2',
|
||||
AwV2_KhkkN3: 'AwV2_KhkkN3',
|
||||
AwV2_KhkkN4: 'AwV2_KhkkN4',
|
||||
AwV2_KhkkN5: 'AwV2_KhkkN5',
|
||||
AwV2_KhkkN6: 'AwV2_KhkkN6',
|
||||
AwV2_KhkkN7: 'AwV2_KhkkN7',
|
||||
AwV2_KhkkN8: 'AwV2_KhkkN8',
|
||||
// [S61 Mig 50] Module Ngân sách cũ (Budgets + Bg_*) XÓA — thay bằng bảng
|
||||
// "Tổng hợp ngân sách trình ký" per (Dự án, Hạng mục) trong phiếu PE.
|
||||
// Module Hồ sơ Nhân sự (Mig 34 — Phase 10.1 G-H1 Session 33, 2026-05-26)
|
||||
|
||||
@ -59,6 +59,11 @@ interface WorkflowDto {
|
||||
code: string
|
||||
name: string
|
||||
isUserSelectable: boolean
|
||||
// [W2g S182] Cùng một mã `KHKK-N{n}` có NHIỀU phiên bản (admin bấm "Tạo phiên bản
|
||||
// mới" thì bản cũ chuyển sang lưu trữ). `history` mà BE trả về là DANH SÁCH ĐẦY ĐỦ
|
||||
// — cả bản đang áp dụng lẫn bản đã lưu trữ — nên thiếu cờ này thì màn tạo phiếu
|
||||
// không phân biệt được, xem thêm chỗ dùng bên dưới.
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
// 🧊 [S174] `firstFreeGroup()` ĐÃ GỠ cùng lúc bỏ ô chọn nhóm — nó chỉ tồn tại để tự nhảy
|
||||
@ -110,9 +115,15 @@ export function KhkkCreatePage() {
|
||||
// `var code = $"KHKK-N{n}"` (vòng `for n = 1..8`, seed idempotent per-Code).
|
||||
// Không khớp ⇒ KHÔNG tự rơi về quy trình khác: chặn tạo + nói rõ thiếu mã nào, vì gán
|
||||
// nhầm quy trình là phiếu đi sai đường duyệt — hỏng nặng hơn là không tạo được.
|
||||
// [W2g S182] 🔴 Phải lọc thêm `isActive`. Danh sách trả về gồm CẢ phiên bản đã lưu
|
||||
// trữ, mà `find` lấy phần tử ĐẦU TIÊN khớp mã — hễ admin tạo phiên bản mới cho nhóm
|
||||
// này là phiếu mới có thể bị gắn vào bản CŨ (thứ tự do BE trả, không do màn này
|
||||
// quyết), đi theo đội hình duyệt đã bị thay. Gắn sai quy trình thì lúc phát hiện
|
||||
// phiếu đã chạy được vài trạm rồi. Ràng buộc `isUserSelectable` ở query trên vẫn giữ
|
||||
// (admin ghim bản nào thì user mới được dùng bản đó).
|
||||
const wantWorkflowCode = `KHKK-N${approvalGroup}`
|
||||
const autoWorkflow = workflows.data?.find(
|
||||
(w) => (w.code ?? '').trim().toUpperCase() === wantWorkflowCode,
|
||||
(w) => w.isActive && (w.code ?? '').trim().toUpperCase() === wantWorkflowCode,
|
||||
) ?? null
|
||||
|
||||
// Phiếu đang XEM — chỉ tải khi thật sự có cái để xem.
|
||||
@ -125,7 +136,11 @@ export function KhkkCreatePage() {
|
||||
const create = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!purchaseEvaluationId) throw new Error('Vui lòng chọn phiếu Duyệt NCC nguồn')
|
||||
if (!autoWorkflow) throw new Error(`Chưa có quy trình duyệt ${wantWorkflowCode}`)
|
||||
// [W2g S182] Câu này nói ĐIỀU KIỆN, không nói "không tồn tại": quy trình có thể
|
||||
// đang nằm đó nhưng ở phiên bản đã lưu trữ hoặc chưa được ghim.
|
||||
if (!autoWorkflow) {
|
||||
throw new Error(`Chưa có quy trình duyệt ${wantWorkflowCode} đang áp dụng và được ghim cho user chọn`)
|
||||
}
|
||||
const body: CreateKhkkInput = {
|
||||
peId: purchaseEvaluationId, // [F-2] BE Command field = PeId
|
||||
approvalWorkflowId: autoWorkflow.id,
|
||||
@ -349,9 +364,10 @@ export function KhkkCreatePage() {
|
||||
</div>
|
||||
{!workflows.isLoading && !autoWorkflow ? (
|
||||
<p className="mt-1 text-[11px] text-amber-700">
|
||||
⚠ Chưa có quy trình mã <strong>{wantWorkflowCode}</strong> (hoặc chưa được ghim
|
||||
cho user chọn). Nhờ admin mở trong màn Quy trình duyệt rồi quay lại — hệ thống
|
||||
KHÔNG tự dùng quy trình của nhóm khác.
|
||||
⚠ Chưa dùng được quy trình mã <strong>{wantWorkflowCode}</strong>: phải có
|
||||
phiên bản <strong>đang áp dụng</strong> và <strong>đã ghim</strong> cho user chọn.
|
||||
Nhờ admin mở trong màn Quy trình duyệt rồi quay lại — hệ thống KHÔNG tự dùng
|
||||
phiên bản đã lưu trữ, cũng KHÔNG tự dùng quy trình của nhóm khác.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
|
||||
@ -4,7 +4,9 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.ContractSigningPlans; // [S182 W1d] ContractSigningPlanWorkflowGuards.ParseGroup
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.ContractSigningPlans; // [S182 W1c] changelog + phase KHKK
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
@ -330,21 +332,53 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
{
|
||||
var typeEnum = (ApprovalWorkflowApplicableType)request.ApplicableType;
|
||||
|
||||
// 🔴 [S182 MAJOR-2] CHUẨN HOÁ `Code` TẠI ĐƯỜNG GHI — nguồn duy nhất cho cả handler.
|
||||
//
|
||||
// Trước bản này BA thành phần hỏi cùng câu "code này thuộc nhóm nào" bằng BA luật khác nhau:
|
||||
// • `ContractSigningPlanWorkflowGuards.GroupCodePattern` — regex .NET, case-SENSITIVE;
|
||||
// • `w.Code == request.Code` — EF → SQL Server, theo collation `*_CI_AS` = case-INSENSITIVE;
|
||||
// • FE `KhkkCreatePage` — `.trim().toUpperCase()` = case-INSENSITIVE.
|
||||
// ⇒ admin gõ `khkk-n1` ở panel type-10 gộp (ô Mã KHÔNG khoá vì `lockedCode = null`):
|
||||
// DB coi là trùng ⇒ hạ `IsActive` của `KHKK-N1` THẬT · FE vẫn auto-pin sang bản mới ·
|
||||
// nhưng regex trả `null` ⇒ rào nhóm 3-site và D5 IM LẶNG TẮT cho toàn bộ phiếu nhóm 1.
|
||||
// Không lỗi nào báo.
|
||||
// 🔴 Test KHÔNG thể bắt: 14 ca chạy SQLite, nơi `=` trên TEXT là BINARY (case-SENSITIVE)
|
||||
// ⇒ ngữ nghĩa NGƯỢC prod. Mù theo CẤU TRÚC, không assertion nào bịt được ⇒ phải chặn
|
||||
// bằng chuẩn-hoá, đừng chặn bằng test.
|
||||
var code = request.Code.Trim().ToUpperInvariant();
|
||||
|
||||
// Auto-increment version theo Code (cùng Code = cùng "logical" workflow)
|
||||
var nextVersion = await db.ApprovalWorkflows
|
||||
.Where(w => w.Code == request.Code)
|
||||
.Where(w => w.Code == code)
|
||||
.MaxAsync(w => (int?)w.Version, ct) ?? 0;
|
||||
nextVersion++;
|
||||
|
||||
// Deactivate active version cho ApplicableType này (only ONE active per type)
|
||||
var actives = await db.ApprovalWorkflows
|
||||
.Where(w => w.ApplicableType == typeEnum && w.IsActive)
|
||||
.ToListAsync(ct);
|
||||
// Tắt bản đang active — PHẠM VI phụ thuộc loại (bản trước: LUÔN per-type).
|
||||
//
|
||||
// • type-10 `ContractSigningPlan` (KHKK): **per-CODE**. Loại này có 8 quy trình
|
||||
// SỐNG SONG SONG cùng type — mỗi nhóm duyệt N1..N8 một `Code` riêng
|
||||
// (`DbInitializer.SeedKhkkGroupWorkflowsAsync:609`, seed cả 8 với IsActive=1).
|
||||
// Tắt theo type ⇒ tạo version mới cho MỘT nhóm sẽ hạ IsActive của CẢ 7 nhóm còn
|
||||
// lại (mỗi nhóm mất quy trình active ⇒ FE hết auto-pin ⇒ không tạo được phiếu).
|
||||
// • các loại khác: **per-TYPE** như cũ — 1 active/type là bất biến của chúng.
|
||||
//
|
||||
// 🔴 [S182 R-6] Vị ngữ type-10 KHOÁ đúng 3 vế `Code == request.Code &&
|
||||
// ApplicableType == typeEnum && IsActive`. CẤM thay bằng biến-thể regex
|
||||
// `^KHKK-N[1-8]$`: `Code` type-10 nằm ngoài họ đó (vd `QT-KHKK-V2-001`) sẽ rơi
|
||||
// lại nhánh per-type và nuke đúng 8 nhóm mà rào này sinh ra để cứu.
|
||||
// (`nextVersion` `:334-337` vốn đã tính per-Code ⇒ nhánh này làm 2 vế khớp nhau.)
|
||||
var actives = typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan
|
||||
? await db.ApprovalWorkflows
|
||||
.Where(w => w.Code == code && w.ApplicableType == typeEnum && w.IsActive)
|
||||
.ToListAsync(ct)
|
||||
: await db.ApprovalWorkflows
|
||||
.Where(w => w.ApplicableType == typeEnum && w.IsActive)
|
||||
.ToListAsync(ct);
|
||||
foreach (var old in actives) old.IsActive = false;
|
||||
|
||||
var def = new ApprovalWorkflow
|
||||
{
|
||||
Code = request.Code,
|
||||
Code = code, // [S182 MAJOR-2] bản đã chuẩn hoá, KHÔNG dùng request.Code thô
|
||||
Version = nextVersion,
|
||||
ApplicableType = typeEnum,
|
||||
Name = request.Name,
|
||||
@ -856,10 +890,17 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
// được "quy trình vừa bị sửa lúc nào, ai sửa, vì sao".
|
||||
// ⚠️ KHAI GIỚI HẠN: repo CHƯA có bảng changelog cho quy trình duyệt, mà
|
||||
// đợt 1 bị chốt "no new table" ⇒ vết cho workflow thuộc 5 module Văn
|
||||
// phòng số (Proposal/Leave/Ot/Travel/Vehicle) + KHKK (W1 S161 — reviewer F-6b:
|
||||
// `ContractSigningPlanChangelogs` là changelog PHIẾU, không phải changelog QUY-TRÌNH;
|
||||
// wire nếu cần ở W2/W3) hiện CHỈ có UpdatedAt/
|
||||
// phòng số (Proposal/Leave/Ot/Travel/Vehicle) hiện CHỈ có UpdatedAt/
|
||||
// UpdatedBy ở (a). Bảng `ApprovalWorkflowChangelogs` = việc tách riêng.
|
||||
// 🔄 [S182 W1c] KHKK (type-10) ĐÃ WIRE — xem nhánh `else if` bên dưới. Câu cũ ở
|
||||
// đây xếp KHKK chung rọ "chỉ có UpdatedAt/UpdatedBy" và hẹn "wire nếu cần ở W2/W3";
|
||||
// đợt này wire thật nên câu đó gỡ, không để lại neo thối.
|
||||
var note = string.Join(" | ", changes);
|
||||
if (!string.IsNullOrWhiteSpace(request.Reason))
|
||||
note = $"Lý do: {request.Reason.Trim()} | {note}";
|
||||
var summary = $"Sửa quy trình duyệt tại chỗ: {def.Code} v{def.Version} (giữ nguyên phiên bản)";
|
||||
var actorName = currentUser.FullName ?? currentUser.Email ?? "Hệ thống";
|
||||
|
||||
if (changes.Count > 0 &&
|
||||
def.ApplicableType is ApprovalWorkflowApplicableType.DuyetNcc
|
||||
or ApprovalWorkflowApplicableType.DuyetNccPhuongAn)
|
||||
@ -871,10 +912,6 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
.Select(p => new { p.Id, p.Phase })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var note = string.Join(" | ", changes);
|
||||
if (!string.IsNullOrWhiteSpace(request.Reason))
|
||||
note = $"Lý do: {request.Reason.Trim()} | {note}";
|
||||
|
||||
foreach (var pe in affected)
|
||||
{
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
@ -885,8 +922,48 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
EntityId = def.Id,
|
||||
PhaseAtChange = pe.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = currentUser.FullName ?? currentUser.Email ?? "Hệ thống",
|
||||
Summary = $"Sửa quy trình duyệt tại chỗ: {def.Code} v{def.Version} (giữ nguyên phiên bản)",
|
||||
UserName = actorName,
|
||||
Summary = summary,
|
||||
ContextNote = note,
|
||||
});
|
||||
}
|
||||
}
|
||||
// [S182 W1c] KHKK (type-10) — mirror khối PE ngay trên, cùng shape 8 field.
|
||||
//
|
||||
// VÌ SAO CẦN: dialog PUT hứa nguyên văn với admin "ghi vào lịch sử phiếu đang chạy"
|
||||
// (`ApprovalWorkflowsV2Page.tsx:963-977`). Với type-10 lời hứa đó trước nay là RỖNG —
|
||||
// sửa quy trình xong lịch sử phiếu KHKK không có dòng nào. Đây là fix cho lời hứa
|
||||
// thành thật, không phải tính năng mới.
|
||||
//
|
||||
// 🔴 Vị ngữ phiếu "đang chịu ảnh hưởng" = CHƯA kết thúc: loại 2 phase TERMINAL
|
||||
// (`DaDuyet` chốt giá + `TuChoi`). `TraLai` KHÔNG loại — phiếu trả lại sẽ được
|
||||
// trình LẠI và chạy đúng quy trình vừa sửa ⇒ người soạn cần thấy vết.
|
||||
// 🔴 CỐ Ý KHÔNG `IgnoreQueryFilters()`: `ContractSigningPlans` có global filter
|
||||
// (`ContractSigningPlanConfiguration.cs:55`) ⇒ phiếu XOÁ MỀM tự rụng khỏi tập.
|
||||
// Đúng ý muốn: ghi changelog cho phiếu đã xoá là đẻ rác không ai đọc. (Ngược với
|
||||
// rào DELETE bên dưới — ở đó xoá mềm VẪN phải chặn vì FK còn thật.)
|
||||
else if (changes.Count > 0 &&
|
||||
def.ApplicableType is ApprovalWorkflowApplicableType.ContractSigningPlan)
|
||||
{
|
||||
var affectedPlans = await db.ContractSigningPlans.AsNoTracking()
|
||||
.Where(p => p.ApprovalWorkflowId == def.Id
|
||||
&& p.Phase != ContractSigningPlanPhase.DaDuyet
|
||||
&& p.Phase != ContractSigningPlanPhase.TuChoi)
|
||||
.Select(p => new { p.Id, p.Phase })
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var plan in affectedPlans)
|
||||
{
|
||||
db.ContractSigningPlanChangelogs.Add(new ContractSigningPlanChangelog
|
||||
{
|
||||
ContractSigningPlanId = plan.Id,
|
||||
EntityType = ContractSigningPlanEntityType.Workflow,
|
||||
Action = SolutionErp.Domain.Contracts.ChangelogAction.Update,
|
||||
EntityId = def.Id,
|
||||
PhaseAtChange = plan.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = actorName,
|
||||
Summary = summary,
|
||||
ContextNote = note,
|
||||
});
|
||||
}
|
||||
@ -1184,9 +1261,21 @@ public class SetAwUserSelectableCommandHandler(IApplicationDbContext db)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DELETE version (chỉ khi chưa có phiếu pin) ==========
|
||||
// Hiện chưa có phiếu nào pin schema mới → unconditional delete OK cho UAT.
|
||||
// Sau UAT khi link với PE/Contract thật cần check usage trước khi delete.
|
||||
// ========== DELETE version — CHẶN khi còn phiếu pin / còn chữ ký ==========
|
||||
//
|
||||
// 🔴 [S182 W1d] Câu cũ ở đây ("chưa có phiếu nào pin schema mới → unconditional delete OK cho
|
||||
// UAT") đã HẾT ĐÚNG từ lâu: 8 module pin `ApprovalWorkflowId` thật trên prod. Xoá trần để
|
||||
// lại 2 hậu quả khác hẳn nhau tuỳ module — và chính vì khác nhau nên rào này có HAI lý do,
|
||||
// không phải một:
|
||||
// • PE / Hợp đồng / KHKK có **FK vật lý** ⇒ xoá trần nổ SQL 547 = 500 thô. Rào = đổi
|
||||
// 500 khó hiểu thành 409 đọc được. (UX)
|
||||
// • Proposal / Leave / Ot / Travel / Vehicle pin **LOOSE, 0-FK** ⇒ DB không đỡ gì cả,
|
||||
// xoá trần đi lọt ÊM và để lại con trỏ treo. Rào này là **hàng phòng thủ DUY NHẤT**.
|
||||
// (TOÀN VẸN DỮ LIỆU — nặng hơn vế trên)
|
||||
//
|
||||
// Thứ tự kiểm: usage TRƯỚC (i+ii), D5 SAU (iv). Cả hai đều 409; usage đứng trước vì khi cả hai
|
||||
// cùng đúng thì "5 phiếu đang gắn" là câu admin xử lý được, còn "nhóm phải còn ≥1 version" là
|
||||
// luật chính sách — nói cái hành-động-được trước.
|
||||
|
||||
public record DeleteAwDefinitionCommand(Guid Id) : IRequest;
|
||||
|
||||
@ -1195,9 +1284,90 @@ public class DeleteAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
{
|
||||
public async Task Handle(DeleteAwDefinitionCommand request, CancellationToken ct)
|
||||
{
|
||||
// (iii) `NotFoundException` thay `KeyNotFoundException`: BCL exception KHÔNG được
|
||||
// `GlobalExceptionMiddleware` map ⇒ id lạ trước nay ra 500 thô. Nay 404.
|
||||
var def = await db.ApprovalWorkflows
|
||||
.FirstOrDefaultAsync(d => d.Id == request.Id, ct)
|
||||
?? throw new KeyNotFoundException($"ApprovalWorkflow {request.Id} không tồn tại.");
|
||||
?? throw new NotFoundException("ApprovalWorkflow", request.Id);
|
||||
|
||||
// ---- (i) Đếm phiếu ĐANG PIN, quét đủ 8 module có cột `ApprovalWorkflowId`.
|
||||
//
|
||||
// 🔴 `IgnoreQueryFilters()` cho PE / Contract / KHKK — 3 entity DUY NHẤT có global
|
||||
// filter `!IsDeleted` (đã grep `HasQueryFilter` trong Configurations để chốt danh
|
||||
// sách, không suy từ trí nhớ). Phiếu XOÁ MỀM vẫn CÒN ROW ⇒ FK vẫn thật ⇒ bỏ filter
|
||||
// thì ta đếm ra 0 rồi xoá và nổ 547 đúng lúc chạy. 5 module Văn phòng số không có
|
||||
// filter nên đếm thẳng.
|
||||
var pinned = 0;
|
||||
pinned += await db.PurchaseEvaluations.IgnoreQueryFilters()
|
||||
.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.Contracts.IgnoreQueryFilters()
|
||||
.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.ContractSigningPlans.IgnoreQueryFilters()
|
||||
.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.Proposals.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.LeaveRequests.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.OtRequests.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.TravelRequests.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
pinned += await db.VehicleBookings.CountAsync(p => p.ApprovalWorkflowId == def.Id, ct);
|
||||
|
||||
// ---- (ii) Đếm CHỮ KÝ còn trỏ vào các dòng Cấp của quy trình này — 8 bảng
|
||||
// `*LevelOpinion`. Phiếu có thể đã bỏ pin mà chữ ký vẫn còn (FK Restrict về
|
||||
// `ApprovalWorkflowLevels`) ⇒ đếm pin thôi là chưa đủ để dám xoá.
|
||||
// `IgnoreQueryFilters()` cả 8 cho ĐỒNG HÌNH — bảng nào không có filter thì gọi
|
||||
// vô hại, bảng có filter mà quên gọi thì đếm hụt.
|
||||
var levelIds = await db.ApprovalWorkflowLevels.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(l => l.Step!.ApprovalWorkflowId == def.Id)
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var signatures = 0;
|
||||
if (levelIds.Count > 0)
|
||||
{
|
||||
signatures += await db.PurchaseEvaluationLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.ContractLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.ContractSigningPlanLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.ProposalLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.LeaveRequestLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.OtRequestLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.TravelRequestLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
signatures += await db.VehicleBookingLevelOpinions.IgnoreQueryFilters()
|
||||
.CountAsync(o => levelIds.Contains(o.ApprovalWorkflowLevelId), ct);
|
||||
}
|
||||
|
||||
if (pinned > 0 || signatures > 0)
|
||||
throw new ConflictException(
|
||||
$"Quy trình '{def.Code}' v{def.Version} đang được dùng: {pinned} hồ sơ đang gắn " +
|
||||
$"+ {signatures} chữ ký đã lưu — không xoá được. " +
|
||||
"Muốn ngừng dùng thì bỏ kích hoạt (hoặc bỏ ghim) thay vì xoá.");
|
||||
|
||||
// ---- (iv) [D5] Nhóm duyệt KHKK phải LUÔN còn ít nhất 1 version.
|
||||
//
|
||||
// Seeder `SeedKhkkGroupWorkflowsAsync` idempotent PER-CODE (`DbInitializer.cs:610`:
|
||||
// `if (Any(w.Code == code)) continue;`) ⇒ xoá SẠCH một mã `KHKK-N{n}` thì lần khởi
|
||||
// động sau seeder đẻ LẠI bản v01 mặc định — roster + cờ mà admin đã chỉnh biến mất
|
||||
// âm thầm, không có lỗi nào báo. Rào ở đây đóng đường UI dẫn tới ca đó.
|
||||
// (Version 0-usage mà KHÔNG phải bản cuối của mã ⇒ vẫn xoá bình thường.)
|
||||
if (ContractSigningPlanWorkflowGuards.ParseGroup(def.Code) is int group)
|
||||
{
|
||||
// [S182 MINOR-3] Lọc CẢ `ApplicableType`: UNIQUE là `(Code, Version)` chứ không kèm
|
||||
// type ⇒ một row `KHKK-N{n}` ở type KHÁC (Version khác) sẽ bị tính là "anh em" và
|
||||
// cho phép xoá bản type-10 CUỐI CÙNG — đúng ca mà D5 sinh ra để chặn.
|
||||
var siblings = await db.ApprovalWorkflows
|
||||
.CountAsync(w => w.Code == def.Code
|
||||
&& w.ApplicableType == def.ApplicableType
|
||||
&& w.Id != def.Id, ct);
|
||||
if (siblings == 0)
|
||||
throw new ConflictException(
|
||||
$"Nhóm N{group} phải còn ≥1 version — đây là version cuối cùng của quy trình " +
|
||||
$"'{def.Code}'. Tạo version mới trước, rồi mới xoá bản này.");
|
||||
}
|
||||
|
||||
db.ApprovalWorkflows.Remove(def);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
@ -317,6 +317,34 @@ internal static class ContractSigningPlanScope
|
||||
throw new ConflictException("Quy trình duyệt không thuộc loại Kế hoạch ký kết HĐ.");
|
||||
}
|
||||
|
||||
/// [S182 W1b2 — D6] Quy trình có còn được phép PIN MỚI không: `IsActive` ∧ `IsUserSelectable`.
|
||||
/// Mirror vá-3 `CreateContractFromSigningPlanFeatures.cs:141-147`.
|
||||
///
|
||||
/// 🔴 CHỈ gọi ở đường ĐẶT PIN MỚI (tạo phiếu / sửa nháp có ĐỔI quy trình). CỐ Ý KHÔNG gọi
|
||||
/// khi giữ nguyên pin cũ: phiếu lỡ pin bản đã archived vẫn phải sửa được GhiChu/HoSoLink,
|
||||
/// nếu không thì admin ngừng một quy trình là khoá cứng mọi phiếu nháp đang trỏ vào nó.
|
||||
/// Cũng KHÔNG gọi ở đường TRÌNH duyệt vì lý do đó (D6 chốt 2 site).
|
||||
///
|
||||
/// 2 cờ ném 2 message TÁCH RIÊNG (không gộp `!IsActive || !IsUserSelectable`): "đã ngừng
|
||||
/// hoạt động" và "chưa ghim" sửa ở 2 nút khác nhau trong Designer — gộp thì người đọc lỗi
|
||||
/// không biết bấm nút nào.
|
||||
internal static async Task EnsureWorkflowPinnableAsync(
|
||||
IApplicationDbContext db, Guid approvalWorkflowId, CancellationToken ct)
|
||||
{
|
||||
var wf = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == approvalWorkflowId)
|
||||
.Select(w => new { w.Code, w.IsActive, w.IsUserSelectable })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (wf is null)
|
||||
throw new NotFoundException("ApprovalWorkflow", approvalWorkflowId);
|
||||
if (!wf.IsActive)
|
||||
throw new ConflictException(
|
||||
$"Quy trình '{wf.Code}' đã ngừng hoạt động — chọn quy trình đang hoạt động.");
|
||||
if (!wf.IsUserSelectable)
|
||||
throw new ConflictException(
|
||||
$"Quy trình '{wf.Code}' chưa được ghim cho người dùng chọn — nhờ Admin ghim, hoặc chọn quy trình khác.");
|
||||
}
|
||||
|
||||
/// Precompute ID phiếu mà `userId` là approver của Cấp ĐANG CHỜ — mirror
|
||||
/// `PurchaseEvaluationFeatures.cs:817-852 ResolveV2InboxIdsAsync`.
|
||||
/// 🔴 In-memory join CÓ CHỦ ĐÍCH: `CurrentWorkflowStepIndex` là INDEX 0-based vào list
|
||||
@ -472,18 +500,20 @@ public class CreateContractSigningPlanCommandHandler(
|
||||
// ---- Rào (iv) WORKFLOW TYPE — copy trọn 2 nhánh Proposal `:258-267`.
|
||||
await ContractSigningPlanScope.EnsureWorkflowTypeAsync(db, request.ApprovalWorkflowId, ct);
|
||||
|
||||
// ---- Rào (iv-bis) [S182 W1b2 — D6] Quy trình phải CÒN DÙNG ĐƯỢC lúc pin.
|
||||
// Mirror vá-3 `CreateContractFromSigningPlanFeatures.cs:141-147` (cùng lỗ, đã bịt cho
|
||||
// type-3): FE chỉ hiện quy trình active+ghim, nhưng POST thẳng thì pin được bản đã
|
||||
// archived ⇒ phiếu trình xong treo ở quy trình không ai bảo trì.
|
||||
await ContractSigningPlanScope.EnsureWorkflowPinnableAsync(db, request.ApprovalWorkflowId, ct);
|
||||
|
||||
// ---- Rào (v) [K3 S165 — QĐ-3 "K3 chỉ ĐỌC ApprovalGroup"] Workflow họ nhóm
|
||||
// `KHKK-N{n}` ⇒ n PHẢI khớp nhóm duyệt của phiếu (phiếu nhóm 3 pin quy trình nhóm 5
|
||||
// = 409). Code NGOÀI họ (type-10 legacy/chung) → cho qua — ràng buộc chỉ áp cho
|
||||
// 8 workflow nhóm seed @K3.
|
||||
var wfCode = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == request.ApprovalWorkflowId)
|
||||
.Select(w => w.Code).FirstOrDefaultAsync(ct);
|
||||
var groupMatch = System.Text.RegularExpressions.Regex.Match(wfCode ?? string.Empty, "^KHKK-N([1-8])$");
|
||||
if (groupMatch.Success && int.Parse(groupMatch.Groups[1].Value) != approvalGroup)
|
||||
throw new ConflictException(
|
||||
$"Quy trình '{wfCode}' thuộc nhóm duyệt N{groupMatch.Groups[1].Value}, " +
|
||||
$"không khớp nhóm duyệt N{approvalGroup} của phiếu — chọn đúng quy trình của nhóm.");
|
||||
// [S182 W1b] Thân rào DỜI sang `ContractSigningPlanWorkflowGuards` — cùng luật này
|
||||
// nay chặn ở CẢ 3 cửa (tạo / sửa nháp re-pin / trình duyệt), trước chỉ có cửa này.
|
||||
await ContractSigningPlanWorkflowGuards.EnsureWorkflowGroupMatchAsync(
|
||||
db, request.ApprovalWorkflowId, approvalGroup, ct);
|
||||
|
||||
// 🔴 GEN MÃ TRƯỚC KHI `Add` — `ContractSigningPlanCodeGenerator` TỰ COMMIT
|
||||
// (`ContractSigningPlanCodeGenerator.cs:23-43`: transaction SERIALIZABLE +
|
||||
@ -600,9 +630,18 @@ public class UpdateContractSigningPlanDraftCommandHandler(
|
||||
ContractSigningPlanScope.EnsureDraftEditable(plan, currentUser);
|
||||
|
||||
// Re-pin quy trình: guard type LẠI (workflow có thể bị admin đổi loại giữa chừng).
|
||||
//
|
||||
// [S182 W1b/W1b2] Cửa này trước KHÔNG có rào nhóm ⇒ tạo phiếu đúng nhóm rồi sửa nháp
|
||||
// pin sang quy trình nhóm khác là lọt. Nay đủ 3 rào như đường TẠO.
|
||||
// 🔴 Cả 3 nằm TRONG nhánh `awId != plan.ApprovalWorkflowId` (đường ĐỔI pin) — CỐ Ý:
|
||||
// client gửi lại đúng workflow cũ (form "Sửa" echo nguyên field) không được biến
|
||||
// thành 409, nếu không thì sửa GhiChu của phiếu pin bản archived sẽ hỏng (D6).
|
||||
if (request.ApprovalWorkflowId is Guid awId && awId != plan.ApprovalWorkflowId)
|
||||
{
|
||||
await ContractSigningPlanScope.EnsureWorkflowTypeAsync(db, awId, ct);
|
||||
await ContractSigningPlanScope.EnsureWorkflowPinnableAsync(db, awId, ct);
|
||||
await ContractSigningPlanWorkflowGuards.EnsureWorkflowGroupMatchAsync(
|
||||
db, awId, plan.ApprovalGroup, ct);
|
||||
plan.ApprovalWorkflowId = awId;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
|
||||
namespace SolutionErp.Application.ContractSigningPlans;
|
||||
|
||||
// [S182 W1b — YC-013] Rào DÙNG CHUNG cho quan hệ "nhóm duyệt của phiếu" ⟷ "nhóm duyệt của
|
||||
// quy trình đã pin". Trước đợt này logic chỉ nằm ở MỘT chỗ (đường TẠO phiếu,
|
||||
// `ContractSigningPlanFeatures.cs` rào (v) — K3 S165) ⇒ hai cửa còn lại (sửa nháp re-pin,
|
||||
// và TRÌNH duyệt) đi lọt: phiếu nhóm 3 vẫn trình được bằng quy trình nhóm 5.
|
||||
//
|
||||
// 🔴 VÌ SAO `public static class` ở tầng Application chứ không nhét vào
|
||||
// `ContractSigningPlanScope` (internal, cùng file Features):
|
||||
// site thứ 3 là `ContractSigningPlanWorkflowService.SubmitAsync` nằm ở **Infrastructure**
|
||||
// ⇒ không thấy `internal` của Application. Ba lối đã cân nhắc và loại:
|
||||
// • nới `Scope` thành public → mở TOÀN BỘ helper nội bộ ra ngoài, chỉ để dùng 1 method;
|
||||
// • `InternalsVisibleTo(Infrastructure)` → mở cả assembly, cùng bệnh, còn khó soi hơn;
|
||||
// • copy-inline sang Service → 2 bản luật ĐI LỆCH NHAU theo thời gian (đúng bug-class
|
||||
// đang chữa: một chỗ có rào, chỗ kia không).
|
||||
// ⇒ tách file riêng, đúng một method public, ba site cùng gọi.
|
||||
public static class ContractSigningPlanWorkflowGuards
|
||||
{
|
||||
// Họ mã quy trình nhóm duyệt: `KHKK-N1` … `KHKK-N8`, seed ở
|
||||
// `DbInitializer.SeedKhkkGroupWorkflowsAsync` (`DbInitializer.cs:609` — `$"KHKK-N{n}"`).
|
||||
//
|
||||
// 🔴 ĐÂY LÀ NHÀ DUY NHẤT của mẫu này phía Application. Trước đợt này mẫu được gõ tay
|
||||
// trong `ContractSigningPlanFeatures.cs:482`; W1d cần lại đúng mẫu đó ở
|
||||
// `ApprovalWorkflowV2AdminFeatures` (rào xoá version cuối) ⇒ nếu chép lần nữa thì có
|
||||
// 3 bản chuỗi rời nhau, sửa 1 quên 2. Ai cần "code này thuộc nhóm mấy" thì gọi
|
||||
// `ParseGroup`, đừng viết lại regex.
|
||||
//
|
||||
// ⚠️ ĐỪNG nhầm với rào W1a bên `ApprovalWorkflowV2AdminFeatures` (deactivate-per-Code):
|
||||
// ở ĐÓ regex bị CẤM (R-6) vì phạm vi tắt-active phải phủ MỌI code type-10, kể cả code
|
||||
// ngoài họ. Ở ĐÂY regex mới là đúng việc: mục đích của rào là NHẬN DIỆN họ `KHKK-N`,
|
||||
// và code ngoài họ được cho qua CÓ CHỦ ĐÍCH (xem `EnsureWorkflowGroupMatchAsync`).
|
||||
// 🔴 [S182 MAJOR-2] `IgnoreCase` = BELT cho dữ-liệu CŨ. Đường ghi nay đã chuẩn hoá
|
||||
// `Code` về HOA (`CreateAwDefinitionCommandHandler` — `request.Code.Trim().ToUpperInvariant()`),
|
||||
// nhưng row có TRƯỚC bản vá thì chưa qua cửa đó. Thiếu cờ này, một `khkk-n1` tồn dư
|
||||
// sẽ trả `null` ⇒ rào nhóm 3-site + D5 im lặng TẮT cho cả nhóm 1, trong khi SQL Server
|
||||
// (collation `*_CI_AS`) vẫn coi nó trùng `KHKK-N1` ⇒ hai tầng nói ngược nhau.
|
||||
// Suspenders ⟂ belt: chuẩn-hoá chặn dữ-liệu MỚI, cờ này tha-thứ dữ-liệu CŨ.
|
||||
private static readonly Regex GroupCodePattern =
|
||||
new(@"^KHKK-N([1-8])$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
|
||||
/// Số nhóm duyệt suy ra từ mã quy trình, hoặc `null` nếu mã KHÔNG thuộc họ `KHKK-N[1-8]`.
|
||||
public static int? ParseGroup(string? workflowCode)
|
||||
{
|
||||
var m = GroupCodePattern.Match(workflowCode ?? string.Empty);
|
||||
return m.Success ? int.Parse(m.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
/// Quy trình thuộc họ nhóm `KHKK-N{k}` ⇒ `k` PHẢI khớp nhóm duyệt của phiếu, nếu không → 409.
|
||||
///
|
||||
/// 🔴 GIỮ NGUYÊN semantics "cho qua code NGOÀI họ" của bản gốc
|
||||
/// (`ContractSigningPlanFeatures.cs:477-478` — chủ đích, không phải sót): ràng buộc này
|
||||
/// chỉ nói về 8 workflow nhóm seed @K3. Quy trình type-10 đặt tay (vd `QT-KHKK-V2-001`)
|
||||
/// không mang thông tin nhóm trong mã ⇒ không có gì để đối chiếu, chặn nó là bịa ràng
|
||||
/// buộc. Rào loại (`EnsureWorkflowTypeAsync`) mới là chỗ chặn quy trình sai LOẠI.
|
||||
///
|
||||
/// Mã không tồn tại (`awId` rác) cũng rơi vào nhánh cho-qua: guard NotFound là việc của
|
||||
/// `EnsureWorkflowTypeAsync` (Create/UpdateDraft) — chồng thêm ở đây sẽ đẻ 2 nguồn 404
|
||||
/// cho cùng một nguyên nhân.
|
||||
public static async Task EnsureWorkflowGroupMatchAsync(
|
||||
IApplicationDbContext db, Guid awId, int approvalGroup, CancellationToken ct)
|
||||
{
|
||||
var code = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == awId)
|
||||
.Select(w => w.Code)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
var wfGroup = ParseGroup(code);
|
||||
if (wfGroup is int k && k != approvalGroup)
|
||||
throw new ConflictException(
|
||||
$"Phiếu nhóm N{approvalGroup} đang gắn quy trình N{k} — " +
|
||||
"vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình.");
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.ContractSigningPlans; // ContractSigningPlanWorkflowGuards (rào nhóm dùng chung 3 site)
|
||||
using SolutionErp.Application.ContractSigningPlans.Services;
|
||||
using SolutionErp.Application.Notifications;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
@ -186,6 +187,15 @@ public class ContractSigningPlanWorkflowService(
|
||||
throw new ConflictException(
|
||||
$"Có hạng mục không hợp lệ (đã xoá / không tồn tại) hoặc không thuộc nhóm duyệt của phiếu — mọi dòng phải cùng nhóm N{plan.ApprovalGroup}.");
|
||||
|
||||
// ── [S182 W1b] Rào NHÓM ⟷ QUY TRÌNH, site 3/3 (2 site kia ở đường tạo/sửa nháp).
|
||||
// Cửa này là chốt CUỐI và là cửa DUY NHẤT bắt được phiếu pin lệch có TỪ TRƯỚC: hai
|
||||
// rào kia chỉ chặn lúc GHI, nên phiếu tạo trước khi có rào (hoặc trước khi admin đổi
|
||||
// mã quy trình) vẫn nằm sẵn trong DB với nhóm lệch. Đứng ngay trên `LoadStepsAsync`
|
||||
// để phiếu lệch không kịp bước vào ChoDuyet rồi mới lộ.
|
||||
// Thân luật + lý do "cho qua code ngoài họ": `ContractSigningPlanWorkflowGuards`.
|
||||
await ContractSigningPlanWorkflowGuards.EnsureWorkflowGroupMatchAsync(
|
||||
db, awId, plan.ApprovalGroup, ct);
|
||||
|
||||
var steps = await LoadStepsAsync(awId, ct);
|
||||
var firstLevelGroup = steps[0].Levels.Where(l => l.Order == 1).ToList();
|
||||
if (firstLevelGroup.Count == 0)
|
||||
|
||||
@ -131,7 +131,13 @@ public class ContractSigningPlanApprovalTests
|
||||
string code,
|
||||
Guid[][][] steps,
|
||||
int stepOrderStart = 1,
|
||||
int stepOrderStride = 1)
|
||||
int stepOrderStride = 1,
|
||||
// [S182 W1 T7] 3 tham số đuôi cho rào D6 (pin-check IsActive ∧ IsUserSelectable) và
|
||||
// cho việc dựng 2 version CÙNG Code (UNIQUE là (Code, Version)). Optional trailing ⇒
|
||||
// mọi call-site cũ giữ nguyên chữ.
|
||||
int version = 1,
|
||||
bool isActive = true,
|
||||
bool isUserSelectable = true)
|
||||
{
|
||||
await using var db = f.NewDb(TestCurrentUser.System());
|
||||
|
||||
@ -139,13 +145,13 @@ public class ContractSigningPlanApprovalTests
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = code,
|
||||
Version = 1,
|
||||
Version = version,
|
||||
Name = "Quy trình duyệt Kế hoạch ký kết",
|
||||
// Loại phải là ContractSigningPlan(10) — quy trình module khác bị rào Create
|
||||
// của W2 chặn (`ContractSigningPlanFeatures.cs:218-219`).
|
||||
ApplicableType = ApprovalWorkflowApplicableType.ContractSigningPlan,
|
||||
IsActive = true,
|
||||
IsUserSelectable = true,
|
||||
IsActive = isActive,
|
||||
IsUserSelectable = isUserSelectable,
|
||||
};
|
||||
db.ApprovalWorkflows.Add(wf);
|
||||
|
||||
@ -193,21 +199,29 @@ public class ContractSigningPlanApprovalTests
|
||||
ContractSigningPlanPhase phase,
|
||||
int? stepIndex,
|
||||
int? levelOrder,
|
||||
LineSeed[] lines)
|
||||
LineSeed[] lines,
|
||||
// [S182 W1 T4-T7] Nhóm duyệt của phiếu. Hạng mục fixture ĐI THEO nhóm này (cùng giá trị)
|
||||
// ⇒ rào hạng mục `…WorkflowService.cs:182-187` luôn cho qua, và mọi 409 quan sát được
|
||||
// trong T5 chỉ có thể đến từ rào NHÓM-QUY-TRÌNH. Trailing optional ⇒ 12 call-site cũ giữ nguyên.
|
||||
int approvalGroup = 1)
|
||||
{
|
||||
// Seed dưới danh nghĩa NGƯỜI SOẠN ⇒ interceptor ghi `CreatedBy = drafter` (xem ghi chú (1)).
|
||||
await using var db = f.NewDb(drafter);
|
||||
|
||||
// [K2 S164 — vá-5] SubmitAsync nay CHẶN dòng chưa gán hạng mục + hạng mục khác nhóm.
|
||||
// Fixture gán MỌI line vào 1 hạng mục CÙNG NHÓM với phiếu (entity default ApprovalGroup=1)
|
||||
// — mirror phiếu "đã gán đủ" trước trình; Code random né unique-filtered giữa các plan.
|
||||
// Fixture gán MỌI line vào 1 hạng mục CÙNG NHÓM với phiếu — mirror phiếu "đã gán đủ"
|
||||
// trước trình; Code random né unique-filtered giữa các plan.
|
||||
// 🔴 [S182 W1] Hạng mục ĐI THEO `approvalGroup` của phiếu (không còn hằng 1): T5 seed
|
||||
// phiếu nhóm 3, nếu hạng mục vẫn nhóm 1 thì `SubmitAsync` ném 409 của rào HẠNG MỤC
|
||||
// (`ContractSigningPlanWorkflowService.cs:182-187`) và test sẽ "xanh vì nhầm cửa" —
|
||||
// đo đúng chữ 409 nhưng sai hoàn toàn cơ chế.
|
||||
var catalog = new ContractCatalogEntry
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "T-" + Guid.NewGuid().ToString("N")[..8],
|
||||
TenVi = "Hạng mục test W3",
|
||||
GroupCode = "B1",
|
||||
ApprovalGroup = 1,
|
||||
ApprovalGroup = approvalGroup,
|
||||
SignerRole = "TP.PRO",
|
||||
IsActive = true,
|
||||
SortOrder = 1,
|
||||
@ -225,6 +239,7 @@ public class ContractSigningPlanApprovalTests
|
||||
DepartmentId = Guid.NewGuid(),
|
||||
DrafterUserId = drafter.UserId!.Value,
|
||||
Phase = phase,
|
||||
ApprovalGroup = approvalGroup,
|
||||
ApprovalWorkflowId = workflowId,
|
||||
CurrentWorkflowStepIndex = stepIndex,
|
||||
CurrentApprovalLevelOrder = levelOrder,
|
||||
@ -1138,4 +1153,299 @@ public class ContractSigningPlanApprovalTests
|
||||
.Select(l => new LevelFlag(l.Order, l.ApproverUserId, l.AllowApproverFinalize))
|
||||
.ToListAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
// ###################################################################
|
||||
// [S182 W1 — YC-013, 2026-08-08] T4-T7: rào NHÓM-QUY-TRÌNH 3-site + pin-check D6.
|
||||
// SPEC: `.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/
|
||||
// spec-khkk-awf-mirror-ncc-08-08-2026.md` MỤC 2 W1 fix (b) + (b2).
|
||||
//
|
||||
// 🔴 VẤN ĐỀ: rào "phiếu nhóm N{g} chỉ pin được quy trình `KHKK-N{g}`" hôm nay chỉ đứng ở
|
||||
// ĐÚNG MỘT CỬA — `CreateContractSigningPlanCommandHandler` (`Features.cs:479-486`).
|
||||
// Hai cửa còn lại đi thẳng: UpdateDraft `:603-607` chỉ gọi `EnsureWorkflowTypeAsync`,
|
||||
// và `SubmitAsync` (`ContractSigningPlanWorkflowService.cs:176-189`) không hỏi câu này
|
||||
// bao giờ. Hệ quả không phải "409 muộn" mà là KÝ SAI DÂY CHUYỀN: phiếu nhóm 3 chạy trạm
|
||||
// của nhóm 5 ⇒ hồ sơ tới tay người không có thẩm quyền ký, im lặng, đúng luồng UI.
|
||||
// W1b gom logic vào `ContractSigningPlanWorkflowGuards.EnsureWorkflowGroupMatchAsync`
|
||||
// và cắm đủ 3 cửa.
|
||||
//
|
||||
// ⛔ KHÔNG nhân bản cửa Create ở đây. Cửa đó ĐÃ có lưới đủ răng từ K3/S164 —
|
||||
// `ContractSigningPlanGroupAndCatalogTests.Create_PinQuyTrinhKhacNhomDuyet_ThiBiChan_VaKhongDotMa`
|
||||
// (chặn sai-nhóm + cho-qua-ngoài-họ + không-đốt-mã), đang XANH ở baseline. Nó chính là
|
||||
// chứng-nhân "phép dời logic sang class dùng chung KHÔNG đổi ngữ nghĩa cửa cũ"; chép lại
|
||||
// 60 dòng seed PE vào file này chỉ đẻ bản sao đo cùng một thứ. T4-T7 chỉ viết phần nó
|
||||
// KHÔNG đo: 2 cửa mới + rào D6.
|
||||
//
|
||||
// 🔎 Chữ trong thông điệp 409: spec đưa RA HAI bản (giữ nguyên câu Create `:483-486`, hoặc
|
||||
// câu mới W1b "Phiếu nhóm N{g} đang gắn quy trình N{k}…"). Cả hai đều hợp lệ ⇒ test pin
|
||||
// cái BẤT BIẾN giữa hai bản: thông điệp phải nêu CẢ nhóm-của-phiếu LẪN nhóm-của-quy-trình,
|
||||
// vì thiếu một trong hai thì người dùng đọc xong vẫn không biết phải đổi cái gì.
|
||||
// (Nếu BE chọn câu mới nguyên văn ⇒ nó BỎ mã quy trình và làm ĐỎ assert `Contain("KHKK-N5")`
|
||||
// của test K3 nói trên — đã báo lead ở §8 artifact, KHÔNG tự nới test cũ.)
|
||||
// ###################################################################
|
||||
|
||||
private static async Task UpdateDraftAsync(
|
||||
KhkkWorkflowFixture f, TestCurrentUser actor, Guid planId,
|
||||
Guid? awId = null, string? ghiChu = null)
|
||||
{
|
||||
await using var db = f.NewDb(actor);
|
||||
var handler = new UpdateContractSigningPlanDraftCommandHandler(db, actor);
|
||||
await handler.Handle(
|
||||
new UpdateContractSigningPlanDraftCommand(planId, GhiChu: ghiChu, HoSoLink: null,
|
||||
ApprovalWorkflowId: awId),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// Đọc pin TỪ ĐĨA (context mới). Mọi vế "409 rồi thì không đổi gì" phải đo ở đây chứ không
|
||||
/// đo trên entity trong bộ nhớ — handler gán `plan.ApprovalWorkflowId` TRƯỚC khi
|
||||
/// `SaveChangesAsync` chạy, nên bản in-memory nói dối được.
|
||||
private static async Task<Guid?> ReadPinAsync(KhkkWorkflowFixture f, Guid planId)
|
||||
{
|
||||
await using var db = f.NewDb(TestCurrentUser.System());
|
||||
return await db.ContractSigningPlans.AsNoTracking()
|
||||
.Where(p => p.Id == planId)
|
||||
.Select(p => p.ApprovalWorkflowId)
|
||||
.SingleAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task<int> ChangelogCountAsync(KhkkWorkflowFixture f, Guid planId)
|
||||
{
|
||||
await using var db = f.NewDb(TestCurrentUser.System());
|
||||
return await db.ContractSigningPlanChangelogs.AsNoTracking()
|
||||
.CountAsync(c => c.ContractSigningPlanId == planId, CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task ArchiveWorkflowAsync(KhkkWorkflowFixture f, Guid workflowId)
|
||||
{
|
||||
await using var db = f.NewDb(TestCurrentUser.System());
|
||||
var wf = await db.ApprovalWorkflows.SingleAsync(w => w.Id == workflowId, CancellationToken.None);
|
||||
wf.IsActive = false;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T4) Cửa UpdateDraft — re-pin sang quy trình LỆCH NHÓM ⇒ 409.
|
||||
//
|
||||
// Đây là cửa NGUY HIỂM NHẤT trong ba: Create bị chặn thì người dùng thấy ngay và chọn lại;
|
||||
// còn ở đây phiếu ĐÃ hợp lệ rồi mới bị đổi ruột — lần sau nhìn vào chỉ thấy một phiếu Nháp
|
||||
// bình thường, không có dấu vết nào nói nó vừa bị chuyển dây chuyền ký.
|
||||
//
|
||||
// Ba vế, mỗi vế bịt một kiểu xanh-giả:
|
||||
// (1) chặn + thông điệp nêu ĐỦ hai nhóm;
|
||||
// (2) ĐĨA bất động: pin cũ còn nguyên VÀ 0 dòng changelog — handler gán pin rồi mới
|
||||
// `SaveChanges` ở cuối, nên chỉ assert "có ném" là không phân biệt được "chặn kịp"
|
||||
// với "chặn sau khi đã ghi";
|
||||
// (3) CONTROL DƯƠNG cùng lượt: re-pin sang bản v02 CÙNG NHÓM ⇒ qua, và changelog nhảy
|
||||
// 0 → 1. Thiếu vế này thì một rào chặn-tuốt (hoặc lỗi seed làm mọi UpdateDraft ném)
|
||||
// vẫn cho (1)+(2) xanh.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T4_UpdateDraft_RePinQuyTrinhLechNhom_ThiBiChan_VaGiuNguyenPinCu()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
|
||||
var n3v1 = await SeedWorkflowAsync(f, "KHKK-N3", steps: [[[approver.Id]]]);
|
||||
var n3v2 = await SeedWorkflowAsync(f, "KHKK-N3", steps: [[[approver.Id]]], version: 2);
|
||||
var n5 = await SeedWorkflowAsync(f, "KHKK-N5", steps: [[[approver.Id]]]);
|
||||
|
||||
var planId = await SeedPlanAsync(f, drafter, n3v1, "KHKK/2026/301",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 90m)],
|
||||
approvalGroup: 3);
|
||||
|
||||
// ---- (1) Re-pin sang quy trình của nhóm KHÁC.
|
||||
var act = async () => await UpdateDraftAsync(f, drafter, planId, awId: n5);
|
||||
var ex = await act.Should().ThrowAsync<ConflictException>(
|
||||
"sửa nháp là cửa thứ hai vào cùng một chỗ — rào chỉ đứng ở Create thì nó vô hiệu "
|
||||
+ "bằng đúng một request PUT hợp lệ về mọi mặt khác");
|
||||
ex.Which.Message.Should().Contain("N5").And.Contain("N3",
|
||||
"thông điệp phải nêu CẢ nhóm của quy trình lẫn nhóm của phiếu — thiếu một vế thì "
|
||||
+ "người dùng biết mình sai nhưng không biết sai ở đâu");
|
||||
|
||||
// ---- (2) Đĩa bất động.
|
||||
(await ReadPinAsync(f, planId)).Should().Be(n3v1,
|
||||
"409 mà pin đã đổi = rào ném SAU khi ghi; phiếu vẫn chạy sai dây chuyền, chỉ thêm một lỗi đỏ");
|
||||
(await ChangelogCountAsync(f, planId)).Should().Be(0,
|
||||
"lệnh bị chặn không được để lại dòng 'Cập nhật thông tin kế hoạch ký kết' — vết ma "
|
||||
+ "làm người đọc lịch sử tin rằng đã có thay đổi");
|
||||
|
||||
// ---- (3) CONTROL DƯƠNG: đổi đúng MỘT biến (nhóm của quy trình đích) ⇒ kết quả trái ngược.
|
||||
await UpdateDraftAsync(f, drafter, planId, awId: n3v2, ghiChu: "Đổi sang bản v02 cùng nhóm");
|
||||
(await ReadPinAsync(f, planId)).Should().Be(n3v2,
|
||||
"cùng cửa, cùng người, cùng phiếu — chỉ khác nhóm của quy trình đích ⇒ rào SO KHỚP "
|
||||
+ "chứ không phải chặn mọi lệnh re-pin");
|
||||
(await ChangelogCountAsync(f, planId)).Should().Be(1,
|
||||
"và phép đếm changelog ở (2) là phép đo SỐNG: nó biết tăng khi lệnh thật sự chạy");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T5) Cửa Submit — phiếu pin lệch nhóm TỪ TRƯỚC (data cũ / lách API) ⇒ 409 lúc trình.
|
||||
//
|
||||
// Vì sao cửa này bắt buộc phải có dù đã chặn ở Create + UpdateDraft: prod HÔM NAY có thể
|
||||
// đang mang phiếu pin lệch (spec W0 §0.4 chạy hẳn một query audit để đếm trước khi vá).
|
||||
// Rào ở hai cửa GHI không dọn được thứ đã nằm sẵn trong bảng — chỉ cửa TRÌNH mới chặn được
|
||||
// phiếu cũ trước khi nó bước vào dây chuyền ký sai.
|
||||
// Seed thẳng DbContext là CỐ Ý: đó là cách duy nhất dựng lại trạng thái "đã lệch từ trước"
|
||||
// sau khi rào Create đóng.
|
||||
//
|
||||
// 🔴 Điều kiện để phép đo có nghĩa: `SubmitAsync` có SẴN 3 cửa 409 khác (phase, hạng mục
|
||||
// chưa gán, hạng mục sai nhóm) ⇒ bắt được `ConflictException` là chưa nói lên gì.
|
||||
// Fixture đã kéo hạng mục theo `approvalGroup` của phiếu, nên hai rào hạng mục im;
|
||||
// và phiếu CHỨNG NHÂN pin đúng nhóm trình LỌT trong cùng một lượt chứng minh phần còn
|
||||
// lại của đường trình vẫn thông.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T5_Submit_PhieuPinLechNhomTuTruoc_ThiBiChan_ChuKhongTrinhQua()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
|
||||
var n3 = await SeedWorkflowAsync(f, "KHKK-N3", steps: [[[approver.Id]]]);
|
||||
var n5 = await SeedWorkflowAsync(f, "KHKK-N5", steps: [[[approver.Id]]]);
|
||||
|
||||
// Phiếu nhóm 3 nhưng pin quy trình nhóm 5 — dựng thẳng, mô phỏng phiếu có từ trước rào.
|
||||
var lech = await SeedPlanAsync(f, drafter, n5, "KHKK/2026/310",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 90m)],
|
||||
approvalGroup: 3);
|
||||
// Chứng nhân: y hệt, khác đúng một biến — pin ĐÚNG nhóm.
|
||||
var dung = await SeedPlanAsync(f, drafter, n3, "KHKK/2026/311",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 90m)],
|
||||
approvalGroup: 3);
|
||||
|
||||
var act = async () => await ActAsync(f, drafter, lech, "submit");
|
||||
var ex = await act.Should().ThrowAsync<ConflictException>(
|
||||
"phiếu lệch nhóm phải bị chặn Ở CỬA TRÌNH — hai rào kia không với tới dữ liệu đã nằm sẵn trong bảng");
|
||||
ex.Which.Message.Should().Contain("N5").And.Contain("N3",
|
||||
"thông điệp phải phân biệt được với 3 câu 409 khác của SubmitAsync (phase / hạng mục "
|
||||
+ "chưa gán / hạng mục sai nhóm), nếu không người dùng đi sửa nhầm chỗ");
|
||||
|
||||
var blocked = await ReadPlanAsync(f, lech);
|
||||
ShouldBeAt(blocked, ContractSigningPlanPhase.DangSoanThao, null, null,
|
||||
"bị chặn thì phiếu phải Ở NGUYÊN nháp — sang ChoDuyet nửa vời là phiếu treo trong "
|
||||
+ "hộp thư của người không có thẩm quyền ký");
|
||||
|
||||
// CHỨNG NHÂN cùng lượt: đường trình vẫn thông cho phiếu pin đúng nhóm ⇒ 409 ở trên là
|
||||
// của rào NHÓM, không phải một rào khác của SubmitAsync vô tình nổ.
|
||||
await ActAsync(f, drafter, dung, "submit");
|
||||
var ok = await ReadPlanAsync(f, dung);
|
||||
ShouldBeAt(ok, ContractSigningPlanPhase.ChoDuyet, 0, 1,
|
||||
"cùng fixture, cùng hạng mục, cùng quy trình 1-Bước-1-Cấp — chỉ khác nhóm của quy "
|
||||
+ "trình được pin ⇒ hai kết quả trái ngược");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T6) Vế ÂM — mã NGOÀI họ `KHKK-N[1-8]` (quy trình chung/legacy type-10) vẫn đi lọt ở
|
||||
// CẢ HAI cửa mới. Ngữ nghĩa này là CHỦ ĐÍCH của rào cũ (`Features.cs:477-478`) và spec W1b
|
||||
// ra lệnh giữ nguyên khi dời sang class dùng chung — siết thêm ở đây là khoá cứng mọi phiếu
|
||||
// đang pin quy trình chung, mà đường sửa duy nhất lại chính là cửa vừa bị khoá.
|
||||
//
|
||||
// 🔴 Vế âm một mình là XANH-MÙ: hôm nay rào chưa tồn tại nên "đi lọt" đúng ở cả thế giới
|
||||
// chưa-vá. Vì vậy chân (2) — CÙNG cửa, CÙNG phiếu, chỉ đổi mã đích sang trong-họ ⇒ 409 —
|
||||
// là thứ chứng minh rào ĐANG CHẠY trong chính lượt này; thiếu nó thì T6 không đo gì.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T6_MaNgoaiHoNhom_UpdateDraftVaSubmit_VanDiLot()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
|
||||
var chung01 = await SeedWorkflowAsync(f, "QT-KHKK-V2-001", steps: [[[approver.Id]]]);
|
||||
var chung02 = await SeedWorkflowAsync(f, "QT-KHKK-V2-002", steps: [[[approver.Id]]]);
|
||||
var n7 = await SeedWorkflowAsync(f, "KHKK-N7", steps: [[[approver.Id]]]);
|
||||
|
||||
var planId = await SeedPlanAsync(f, drafter, chung01, "KHKK/2026/320",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 90m)],
|
||||
approvalGroup: 4);
|
||||
|
||||
// ---- (1) Cửa UpdateDraft: mã ngoài họ ⇒ KHÔNG bị hỏi nhóm.
|
||||
await UpdateDraftAsync(f, drafter, planId, awId: chung02);
|
||||
(await ReadPinAsync(f, planId)).Should().Be(chung02,
|
||||
"quy trình chung không mang số nhóm trong mã ⇒ không có gì để so khớp; chặn ở đây là "
|
||||
+ "siết oan và phiếu mất luôn đường tự sửa");
|
||||
|
||||
// ---- (2) CHỨNG NHÂN rào đang sống: cùng cửa, cùng phiếu, chỉ đổi mã đích sang TRONG họ.
|
||||
var act = async () => await UpdateDraftAsync(f, drafter, planId, awId: n7);
|
||||
await act.Should().ThrowAsync<ConflictException>(
|
||||
"phiếu nhóm 4 pin `KHKK-N7` phải bị chặn — nếu vế này cũng đi lọt thì (1) và (3) chỉ "
|
||||
+ "đang mô tả một thế giới CHƯA CÓ rào, không chứng minh ngữ nghĩa cho-qua nào cả");
|
||||
(await ReadPinAsync(f, planId)).Should().Be(chung02, "409 thì pin giữ nguyên");
|
||||
|
||||
// ---- (3) Cửa Submit: vẫn mã ngoài họ ⇒ trình lọt.
|
||||
await ActAsync(f, drafter, planId, "submit");
|
||||
var after = await ReadPlanAsync(f, planId);
|
||||
ShouldBeAt(after, ContractSigningPlanPhase.ChoDuyet, 0, 1,
|
||||
"rào mới cắm vào SubmitAsync không được nuốt phiếu đang chạy quy trình chung");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T7) D6 — re-pin sang bản ĐÃ NGƯNG HOẠT ĐỘNG / CHƯA GHIM ⇒ 409 (mirror vá-3
|
||||
// `CreateContractFromSigningPlanFeatures.cs:141-147`, CÙNG một lỗ ở bridge type-3).
|
||||
//
|
||||
// Vì sao đáng chặn: sau W1a, "tạo phiên bản mới" hạ `IsActive` bản cũ. Phiếu pin bản đã
|
||||
// nghỉ vẫn chạy được (service resolve theo Id, không đọc cờ) nên sai KHÔNG lộ ra — nó lộ ở
|
||||
// chỗ khác: người duyệt của bản cũ đã bị thay trong bản mới.
|
||||
//
|
||||
// Bốn chân trên CÙNG một bộ seed (cả 4 bản đều mã `KHKK-N2` ⇒ rào nhóm im, mọi 409 quan
|
||||
// sát được chỉ có thể đến từ D6):
|
||||
// (1) IsActive=false ⇒ 409;
|
||||
// (2) IsUserSelectable=false ⇒ 409, và THÔNG ĐIỆP PHẢI KHÁC (1) — spec đòi 2 câu tách,
|
||||
// vì hai nguyên nhân này sửa ở hai nút khác nhau trong Designer; gộp thành một câu
|
||||
// `!IsActive || !IsUserSelectable` là admin đi bấm nhầm nút;
|
||||
// (3) CONTROL DƯƠNG: bản active + đã ghim ⇒ re-pin qua;
|
||||
// (4) 🔴 GIỮ-NGUYÊN-PIN KHÔNG BỊ HỎI: phiếu cũ đang pin bản đã nghỉ vẫn phải sửa được
|
||||
// GhiChu. Đây là vế phân biệt "check trong nhánh đổi-pin" với "check mọi lần PUT" —
|
||||
// bản vá đặt guard ngoài nhánh sẽ khoá cứng đúng những phiếu cần cứu nhất.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T7_RePinBanDaNgungHoacChuaGhim_ThiBiChan_ConGiuNguyenPinThiVanSuaDuoc()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "BCH lập kế hoạch"));
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
|
||||
var dangDung = await SeedWorkflowAsync(f, "KHKK-N2", steps: [[[approver.Id]]]);
|
||||
var daNghi = await SeedWorkflowAsync(f, "KHKK-N2", steps: [[[approver.Id]]],
|
||||
version: 2, isActive: false);
|
||||
var chuaGhim = await SeedWorkflowAsync(f, "KHKK-N2", steps: [[[approver.Id]]],
|
||||
version: 3, isUserSelectable: false);
|
||||
var banMoi = await SeedWorkflowAsync(f, "KHKK-N2", steps: [[[approver.Id]]], version: 4);
|
||||
|
||||
var planId = await SeedPlanAsync(f, drafter, dangDung, "KHKK/2026/330",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 90m)],
|
||||
approvalGroup: 2);
|
||||
|
||||
// ---- (1) Bản đã ngưng hoạt động.
|
||||
var actNghi = async () => await UpdateDraftAsync(f, drafter, planId, awId: daNghi);
|
||||
var exNghi = await actNghi.Should().ThrowAsync<ConflictException>(
|
||||
"pin bản đã nghỉ = phiếu chạy dàn người duyệt của một phiên bản không còn hiệu lực");
|
||||
(await ReadPinAsync(f, planId)).Should().Be(dangDung);
|
||||
|
||||
// ---- (2) Bản chưa ghim cho người dùng chọn.
|
||||
var actGhim = async () => await UpdateDraftAsync(f, drafter, planId, awId: chuaGhim);
|
||||
var exGhim = await actGhim.Should().ThrowAsync<ConflictException>(
|
||||
"cờ ghim là cách Admin quyết quy trình nào người dùng được chọn — bỏ qua nó ở đường "
|
||||
+ "PUT thì nút Ghim/Bỏ ghim chỉ còn là trang trí");
|
||||
(await ReadPinAsync(f, planId)).Should().Be(dangDung);
|
||||
|
||||
exGhim.Which.Message.Should().NotBe(exNghi.Which.Message,
|
||||
"hai nguyên nhân sửa ở hai nút khác nhau ⇒ phải hai câu khác nhau; một câu gộp "
|
||||
+ "`!IsActive || !IsUserSelectable` là đẩy admin đi mò");
|
||||
|
||||
// ---- (3) CONTROL DƯƠNG: bản active + đã ghim.
|
||||
await UpdateDraftAsync(f, drafter, planId, awId: banMoi, ghiChu: "Chuyển sang bản v04");
|
||||
(await ReadPinAsync(f, planId)).Should().Be(banMoi,
|
||||
"khác đúng một biến (2 cờ trạng thái của quy trình đích) ⇒ hai kết quả trái ngược");
|
||||
|
||||
// ---- (4) Bản đang pin bị cho nghỉ SAU đó ⇒ phiếu vẫn phải sửa được nội dung.
|
||||
await ArchiveWorkflowAsync(f, banMoi);
|
||||
await UpdateDraftAsync(f, drafter, planId, awId: banMoi, ghiChu: "Ghi chú sau khi quy trình nghỉ");
|
||||
|
||||
var plan = await ReadPlanAsync(f, planId);
|
||||
plan.GhiChu.Should().Be("Ghi chú sau khi quy trình nghỉ",
|
||||
"GIỮ NGUYÊN pin thì không có gì để kiểm — đặt guard ngoài nhánh `awId != pin hiện tại` "
|
||||
+ "là khoá cứng mọi phiếu đang pin bản đã nghỉ, đúng nhóm phiếu cần sửa nhất");
|
||||
plan.ApprovalWorkflowId.Should().Be(banMoi, "và pin không được tự đổi trong lượt sửa GhiChu");
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,18 +142,25 @@ public class ContractSigningPlanGroupAndCatalogTests
|
||||
KhkkGroupFixture f,
|
||||
string code,
|
||||
Guid[][][] steps,
|
||||
ApprovalWorkflowApplicableType type = ApprovalWorkflowApplicableType.ContractSigningPlan)
|
||||
ApprovalWorkflowApplicableType type = ApprovalWorkflowApplicableType.ContractSigningPlan,
|
||||
// [S182 vá MAJOR-3] 3 tham số ĐUÔI cho rào D6 (`EnsureWorkflowPinnableAsync`): cần
|
||||
// dựng nhiều version CÙNG một Code (UNIQUE là (Code, Version) —
|
||||
// `ApprovalWorkflowConfiguration.cs:23`) rồi lật TỪNG cờ một. Optional TRAILING ⇒ mọi
|
||||
// call-site cũ giữ nguyên chữ. Cùng khuôn `ContractSigningPlanApprovalTests.cs:135-140`.
|
||||
int version = 1,
|
||||
bool isActive = true,
|
||||
bool isUserSelectable = true)
|
||||
{
|
||||
await using var db = f.NewDb(TestCurrentUser.System());
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = code,
|
||||
Version = 1,
|
||||
Version = version,
|
||||
Name = "Quy trình duyệt Kế hoạch ký kết",
|
||||
ApplicableType = type,
|
||||
IsActive = true,
|
||||
IsUserSelectable = true,
|
||||
IsActive = isActive,
|
||||
IsUserSelectable = isUserSelectable,
|
||||
};
|
||||
db.ApprovalWorkflows.Add(wf);
|
||||
|
||||
@ -855,9 +862,21 @@ public class ContractSigningPlanGroupAndCatalogTests
|
||||
var wrongGroup = async () => await CreateAsync(f, bch, pe.PeId, wfN5, approvalGroup: 3);
|
||||
var ex = await wrongGroup.Should().ThrowAsync<ConflictException>(
|
||||
"quy trình họ KHKK-N{n} chỉ dùng được cho phiếu đúng nhóm n — sai nhóm là ký sai dây chuyền");
|
||||
ex.Which.Message.Should().Contain("KHKK-N5").And.Contain("N3",
|
||||
"thông báo phải chỉ ra CẢ quy trình đang pin lẫn nhóm của phiếu, nếu không người dùng "
|
||||
+ "chỉ thấy '409' và không biết đổi cái gì");
|
||||
// [S182 W1b — spec change, KHÔNG phải nới bar] Thân rào dời sang
|
||||
// `ContractSigningPlanWorkflowGuards.EnsureWorkflowGroupMatchAsync` dùng chung cho 3 cửa
|
||||
// (tạo / sửa nháp / trình), và spec chốt MỘT câu 409 duy nhất cho cả ba:
|
||||
// "Phiếu nhóm N{g} đang gắn quy trình N{k} — vào phiếu (Nháp/Trả lại) đổi lại quy trình
|
||||
// đúng nhóm trước khi trình."
|
||||
// Câu mới bỏ MÃ quy trình, giữ SỐ NHÓM. Không mất thông tin: rào chỉ nổ cho mã TRONG họ
|
||||
// `KHKK-N[1-8]` nên "N5" xác định duy nhất mã "KHKK-N5" (mã ngoài họ đi thẳng, vế (3)).
|
||||
// ⇒ pin cái BẤT BIẾN qua cả hai bản chữ: thông điệp phải nêu ĐỦ HAI nhóm.
|
||||
// [S182 MINOR-1] Pin NGUYÊN CỤM có thứ tự ở ĐÚNG 1 ca này (các ca khác giữ Contain 2 vế):
|
||||
// `Contain(N5) ∧ Contain(N3)` vẫn xanh nếu ai đó ĐẢO 2 đối số ở call-site
|
||||
// (`EnsureWorkflowGroupMatchAsync(db, awId, approvalGroup, ct)` — 2 tham số kề nhau,
|
||||
// 3 call-site). Cụm đầy đủ dưới đây chết ngay khi đảo ⇒ bất-biến thứ-tự có chứng nhân.
|
||||
ex.Which.Message.Should().Contain("Phiếu nhóm N3 đang gắn quy trình N5",
|
||||
"thông báo phải nêu ĐỦ và ĐÚNG CHIỀU: nhóm của phiếu (N3) trước, nhóm của quy trình "
|
||||
+ "đang pin (N5) sau — đảo đối số ở bất kỳ call-site nào là câu này đổi chiều");
|
||||
|
||||
// Bị chặn thì KHÔNG được để lại dấu vết nào.
|
||||
(await ReadPlanScopesAsync(f)).Should().BeEmpty("rào ném trước khi `Add` ⇒ không có phiếu nửa vời");
|
||||
@ -901,6 +920,90 @@ public class ContractSigningPlanGroupAndCatalogTests
|
||||
.ToListAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// [S182 — vá MAJOR-3 reviewer pre-commit] Rào D6 Ở ĐƯỜNG TẠO —
|
||||
// `ContractSigningPlanFeatures.cs:507` gọi `ContractSigningPlanScope
|
||||
// .EnsureWorkflowPinnableAsync` (thân `:331-346`).
|
||||
//
|
||||
// Vì sao ca này phải tồn tại RIÊNG: D6 cắm ở HAI site (Create `:507` + UpdateDraft `:642`),
|
||||
// nhưng ca duy nhất phủ D6 — T7 `ContractSigningPlanApprovalTests.cs:1402` — gọi
|
||||
// `UpdateDraftAsync` ở CẢ BỐN chân. Tức là write-path mà MỌI phiếu KHKK đi qua đang có 0
|
||||
// răng đo: ai dời/đảo/gỡ guard khỏi Create thì cả suite vẫn xanh.
|
||||
//
|
||||
// Ba chân trên CÙNG một bộ seed. Ba quy trình khác nhau ĐÚNG ở Version + MỘT cờ; Code đều
|
||||
// là `KHKK-N1` và phiếu cũng nhóm 1 ⇒ rào (v) `:515` khớp nên IM LẶNG, mọi 409 quan sát
|
||||
// được chỉ có thể đến từ D6:
|
||||
// (1) IsActive=false ⇒ 409 "… đã ngừng hoạt động";
|
||||
// (2) IsUserSelectable=false ⇒ 409 "… chưa được ghim", và THÔNG ĐIỆP PHẢI KHÁC (1) — đó
|
||||
// đúng là lý do prod tách 2 câu (`:328-330`: hai nguyên nhân sửa ở hai nút khác nhau
|
||||
// trong Designer). 🔴 Bản này BUỘC giữ IsActive=true, nếu không vế `!IsActive` nổ
|
||||
// trước và chân (2) đang đo lại chân (1) — hai cờ cùng false = phép đo rỗng.
|
||||
// (3) CONTROL DƯƠNG: bản active + đã ghim ⇒ tạo được, VÀ mã ra `001`. Hai lần 409 ở trên
|
||||
// đứng TRƯỚC `codeGen.GenerateAsync` (`:524`), mà codegen TỰ COMMIT trong transaction
|
||||
// riêng (`ContractSigningPlanCodeGenerator.cs:23-43`) ⇒ đảo thứ tự hai bước này là số
|
||||
// thứ tự bốc hơi vĩnh viễn (mã sẽ ra `003`) và dải mã KHKK thủng lỗ.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task Create_PinQuyTrinhDaNgungHoacChuaGhim_ThiBiChan_VaKhongDotMa()
|
||||
{
|
||||
using var f = new KhkkGroupFixture();
|
||||
var bch = As(await AddUserAsync(f, "BCH lập kế hoạch"));
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var pe = await SeedPeForCreateAsync(f);
|
||||
|
||||
var dangDung = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var daNghi = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]],
|
||||
version: 2, isActive: false);
|
||||
// IsActive VẪN true — xem ghi chú chân (2) ở khối trên.
|
||||
var chuaGhim = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]],
|
||||
version: 3, isUserSelectable: false);
|
||||
|
||||
// ---- (1) Pin bản ĐÃ NGƯNG HOẠT ĐỘNG.
|
||||
var actNghi = async () => await CreateAsync(f, bch, pe.PeId, daNghi, approvalGroup: 1);
|
||||
var exNghi = await actNghi.Should().ThrowAsync<ConflictException>(
|
||||
"'tạo phiên bản mới' hạ IsActive bản cũ; pin được bản đã nghỉ thì phiếu chạy dàn "
|
||||
+ "người duyệt của một phiên bản không còn hiệu lực — sai IM LẶNG, chỉ lộ khi hồ sơ "
|
||||
+ "tới tay người đã bị thay trong bản mới");
|
||||
exNghi.Which.Message.Should().Contain("ngừng hoạt động");
|
||||
|
||||
// ---- (2) Pin bản CHƯA GHIM cho người dùng chọn.
|
||||
var actGhim = async () => await CreateAsync(f, bch, pe.PeId, chuaGhim, approvalGroup: 1);
|
||||
var exGhim = await actGhim.Should().ThrowAsync<ConflictException>(
|
||||
"FE chỉ hiện quy trình active+ghim, nhưng POST thẳng thì bỏ qua FE ⇒ cờ ghim phải "
|
||||
+ "có răng ở BE, không thì nút Ghim/Bỏ ghim của Admin chỉ là trang trí");
|
||||
exGhim.Which.Message.Should().Contain("ghim");
|
||||
|
||||
exGhim.Which.Message.Should().NotBe(exNghi.Which.Message,
|
||||
"hai nguyên nhân sửa ở HAI NÚT khác nhau trong Designer ⇒ phải hai câu khác nhau; "
|
||||
+ "gộp thành một câu `!IsActive || !IsUserSelectable` là đẩy admin đi mò");
|
||||
|
||||
// ---- Bị chặn thì KHÔNG để lại gì: không phiếu nửa vời, và mã CHƯA TỪNG được cấp.
|
||||
(await ReadPlanScopesAsync(f)).Should().BeEmpty(
|
||||
"D6 ném trước `db.ContractSigningPlans.Add` (`:577`) ⇒ không phiếu nào ra đời");
|
||||
await using (var probe = f.NewDb(bch))
|
||||
{
|
||||
probe.WorkflowAppCodeSequences.Any(s => s.Prefix == "KHKK/2026").Should().BeFalse(
|
||||
"hai lần 409 xảy ra TRƯỚC codegen ⇒ hàng sequence còn chưa được tạo lần nào");
|
||||
}
|
||||
|
||||
// ---- (3) CONTROL DƯƠNG: cùng bộ seed, chỉ đổi trạng thái 2 cờ của quy trình đích.
|
||||
var ok = await CreateAsync(f, bch, pe.PeId, dangDung, approvalGroup: 1);
|
||||
ok.MaKeHoach.Should().Be("KHKK/2026/001",
|
||||
"ra 002/003 tức là hai lần bị chặn đã TIÊU số thứ tự cho phiếu chưa từng tồn tại — "
|
||||
+ "codegen tự commit nên số đó mất vĩnh viễn, không rollback theo request");
|
||||
ok.LineCount.Should().Be(1, "PE seed có đúng 1 NCC trúng thầu ⇒ 1 dòng");
|
||||
|
||||
(await ReadPlanScopesAsync(f)).Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new PlanScope("KHKK/2026/001", 1, dangDung),
|
||||
}, "đúng MỘT phiếu ra đời và pin đúng bản đang dùng — nếu D6 chặn-tuốt thì chân này đỏ, "
|
||||
+ "còn nếu D6 bị gỡ khỏi Create thì hai chân trên xanh giả; cần cả ba mới phân biệt được");
|
||||
|
||||
await using var db = f.NewDb(bch);
|
||||
db.WorkflowAppCodeSequences.Single(s => s.Prefix == "KHKK/2026").LastSeq.Should().Be(1,
|
||||
"đúng một lần cấp mã cho đúng một phiếu");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// [S168 K8-blocker] Lines-editor `UpdateContractSigningPlanLineCommand` — mảnh comment
|
||||
// auto-gen HẸN cho K4b nhưng chưa từng land, dry-run K8 bắt: submit-guard vá-5 chặn
|
||||
|
||||
@ -4,9 +4,10 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using SolutionErp.Application.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.ContractSigningPlans; // [S182 W1] KHKK plan + changelog (T1-T3, T8-T14)
|
||||
using SolutionErp.Domain.Contracts; // ApprovalDecision + ChangelogAction (shared HĐ/PE)
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Office; // Proposal + ProposalLevelOpinion (F-16 blast radius)
|
||||
using SolutionErp.Domain.Office; // Proposal + ProposalLevelOpinion (F-16 blast radius) + LeaveRequest (T12)
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
using SolutionErp.Infrastructure.Services;
|
||||
using SolutionErp.Infrastructure.Tests.Common;
|
||||
@ -67,12 +68,15 @@ public class UpdateAwDefinitionTests
|
||||
ApprovalWorkflowApplicableType type,
|
||||
(int LevelOrder, Guid User)[][] stepSpecs,
|
||||
decimal? ceoThreshold = null,
|
||||
string code = "QT-UPD-V2")
|
||||
string code = "QT-UPD-V2",
|
||||
// [S182 W1] T14 cần 2 version CÙNG Code (UNIQUE là (Code, Version) —
|
||||
// `ApprovalWorkflowConfiguration.cs:26`). Optional trailing ⇒ 14 call-site cũ không đổi.
|
||||
int version = 1)
|
||||
{
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Code = code,
|
||||
Version = 1,
|
||||
Version = version,
|
||||
ApplicableType = type,
|
||||
Name = "QT test sửa tại chỗ",
|
||||
IsActive = true,
|
||||
@ -969,4 +973,561 @@ public class UpdateAwDefinitionTests
|
||||
"vết phải đọc được bằng TÊN, không phải GUID");
|
||||
}
|
||||
}
|
||||
|
||||
// ###################################################################
|
||||
// [S182 W1 — YC-013, 2026-08-08] 8 panel "Duyệt KHKK — Nhóm 1..8" mirror trang Duyệt NCC.
|
||||
// SPEC FINAL: `.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/
|
||||
// spec-khkk-awf-mirror-ncc-08-08-2026.md` MỤC 2 W1.
|
||||
//
|
||||
// 10 lưới dưới đây (T1-T3 + T8-T14) đo 3 fix của W1 trên `ApprovalWorkflowV2AdminFeatures.cs`.
|
||||
// Viết TRƯỚC khi BE land (test-before cho a/b, test-after cho c/d) ⇒ ca nào ĐỎ hôm nay là
|
||||
// ĐỎ HỢP LỆ; hình dạng "đỏ vì chưa làm" đã ghi trong `sub-test-specialist-2.md §7`.
|
||||
//
|
||||
// 🔴 MỆNH ĐỀ TRUNG TÂM (R-6, spec `:14`): với `ApplicableType = ContractSigningPlan` (10),
|
||||
// tập bị tắt khi tạo version mới là **THEO Code**, không theo Type:
|
||||
// Where(w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)
|
||||
// Hôm nay `:340-343` lọc THEO TYPE ⇒ POST một nhóm = tắt IsActive của CẢ 8 nhóm prod.
|
||||
// T1/T3 là hai nửa của cùng một mệnh đề; T2 giữ cho bản vá KHÔNG nới sang type khác.
|
||||
// ⚠️ CẤM biến-thể regex `^KHKK-N[1-8]$` (R-6): mã type-10 ngoài họ vẫn phải per-Code —
|
||||
// vế đó chính là chân thứ hai của T3.
|
||||
// ###################################################################
|
||||
|
||||
private const ApprovalWorkflowApplicableType Khkk = ApprovalWorkflowApplicableType.ContractSigningPlan;
|
||||
|
||||
/// 8 quy trình nhóm `KHKK-N1..N8` (type-10, v01, IsActive) — hình dạng prod SAU seeder K3.
|
||||
/// Một người duyệt dùng chung cho cả 8: rào trùng chỉ áp trong CÙNG một Cấp của CÙNG một Bước.
|
||||
private static async Task<Dictionary<string, Guid>> SeedEightKhkkGroupsAsync(
|
||||
TestApplicationDbContext db, Guid approverId)
|
||||
{
|
||||
var map = new Dictionary<string, Guid>();
|
||||
for (var n = 1; n <= 8; n++)
|
||||
{
|
||||
var wf = await SeedWorkflowAsync(db, Khkk, [[(1, approverId)]], code: $"KHKK-N{n}");
|
||||
map[$"KHKK-N{n}"] = wf.Id;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// Lệnh POST "Tạo phiên bản mới" tối thiểu-nhưng-hợp-lệ (1 Bước × 1 Cấp).
|
||||
private static CreateAwDefinitionCommand NewVersionCommand(
|
||||
ApprovalWorkflowApplicableType type, string code, Guid approverId)
|
||||
=> new(
|
||||
ApplicableType: (int)type,
|
||||
Code: code,
|
||||
Name: $"Quy trình {code}",
|
||||
Description: null,
|
||||
Steps: [new CreateAwStepInput(1, "Bước 1", null, [new CreateAwLevelInput(1, "Cấp 1", approverId)])]);
|
||||
|
||||
private sealed record WfRow(string Code, int Version, bool IsActive);
|
||||
|
||||
private static Task<List<WfRow>> ReadWorkflowsAsync(
|
||||
TestApplicationDbContext db, ApprovalWorkflowApplicableType type)
|
||||
=> db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.ApplicableType == type)
|
||||
.Select(w => new WfRow(w.Code, w.Version, w.IsActive))
|
||||
.ToListAsync(CancellationToken.None);
|
||||
|
||||
/// 🔴 ĐƠN VỊ CỦA SỰ CỐ THẬT: "bao nhiêu NHÓM còn cửa tạo phiếu". FE auto-pin quy trình cho
|
||||
/// phiếu mới bằng `find(w => w.isActive && code-match)` (spec W2g) ⇒ nhóm mất bản active là
|
||||
/// nhóm KHÔNG lập được phiếu nữa. Đếm bằng đơn vị này thì thông điệp lỗi nói đúng thứ vỡ.
|
||||
private static async Task<int> KhkkGroupsWithActiveVersionAsync(TestApplicationDbContext db)
|
||||
{
|
||||
var rows = await ReadWorkflowsAsync(db, Khkk);
|
||||
return rows.Where(r => r.IsActive)
|
||||
.Select(r => r.Code)
|
||||
.Where(c => Enumerable.Range(1, 8).Any(n => c == $"KHKK-N{n}"))
|
||||
.Distinct().Count();
|
||||
}
|
||||
|
||||
private static async Task<ContractSigningPlan> SeedKhkkPlanAsync(
|
||||
TestApplicationDbContext db,
|
||||
Guid workflowId,
|
||||
Guid drafterId,
|
||||
string maKeHoach,
|
||||
ContractSigningPlanPhase phase = ContractSigningPlanPhase.ChoDuyet,
|
||||
int approvalGroup = 1,
|
||||
bool isDeleted = false)
|
||||
{
|
||||
var plan = new ContractSigningPlan
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MaKeHoach = maKeHoach,
|
||||
// 4 cột loose-Guid KHÔNG FK vật lý (`ContractSigningPlanConfiguration.cs:10-12`).
|
||||
PurchaseEvaluationId = Guid.NewGuid(),
|
||||
ProjectId = Guid.NewGuid(),
|
||||
DepartmentId = Guid.NewGuid(),
|
||||
DrafterUserId = drafterId,
|
||||
Phase = phase,
|
||||
ApprovalGroup = approvalGroup,
|
||||
ApprovalWorkflowId = workflowId,
|
||||
CurrentWorkflowStepIndex = phase == ContractSigningPlanPhase.ChoDuyet ? 0 : null,
|
||||
CurrentApprovalLevelOrder = phase == ContractSigningPlanPhase.ChoDuyet ? 1 : null,
|
||||
IsDeleted = isDeleted,
|
||||
DeletedAt = isDeleted ? new DateTime(2026, 8, 7, 0, 0, 0, DateTimeKind.Utc) : null,
|
||||
};
|
||||
db.ContractSigningPlans.Add(plan);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// Đơn nghỉ phép — module Văn phòng số pin **LOOSE, KHÔNG FK** (`WorkflowAppsConfiguration.cs:12-26`
|
||||
/// không khai `HasOne<ApprovalWorkflow>`; `LeaveTypeId` cũng chỉ là Guid trần).
|
||||
private static async Task<LeaveRequest> SeedLeaveRequestAsync(
|
||||
TestApplicationDbContext db, Guid workflowId, Guid requesterId, string maDonTu)
|
||||
{
|
||||
var leave = new LeaveRequest
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MaDonTu = maDonTu,
|
||||
RequesterUserId = requesterId,
|
||||
RequesterFullName = "Người xin nghỉ",
|
||||
LeaveTypeId = Guid.NewGuid(),
|
||||
StartDate = new DateTime(2026, 8, 10, 0, 0, 0, DateTimeKind.Utc),
|
||||
EndDate = new DateTime(2026, 8, 11, 0, 0, 0, DateTimeKind.Utc),
|
||||
NumDays = 2m,
|
||||
Reason = "Việc gia đình",
|
||||
Status = WorkflowAppStatus.DaGuiDuyet,
|
||||
ApprovalWorkflowId = workflowId,
|
||||
CurrentApprovalLevelOrder = 1,
|
||||
};
|
||||
db.LeaveRequests.Add(leave);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return leave;
|
||||
}
|
||||
|
||||
private static Task DeleteWorkflowAsync(Harness h, Guid id)
|
||||
=> new DeleteAwDefinitionCommandHandler(h.Db).Handle(
|
||||
new DeleteAwDefinitionCommand(id), CancellationToken.None);
|
||||
|
||||
private static Task<int> WorkflowRowCountAsync(Harness h, Guid id)
|
||||
=> h.Db.ApprovalWorkflows.AsNoTracking().CountAsync(w => w.Id == id, CancellationToken.None);
|
||||
|
||||
// ===================================================================
|
||||
// (T1) 🔴 BOM ĐANG ARMED — tạo v02 cho MỘT nhóm KHÔNG được tắt 7 nhóm còn lại.
|
||||
//
|
||||
// Đo bằng đơn vị của sự cố: số NHÓM còn bản active. Hôm nay `:340-343` tắt theo TYPE ⇒
|
||||
// sau một cú bấm "Tạo phiên bản mới" trên panel N1, con số đó tụt 8 → 1 và 7 folder KHKK
|
||||
// mất cửa lập phiếu — im lặng, không lỗi, không có endpoint re-activate (spec R-2).
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T1_CreateNewVersionForOneKhkkGroup_DeactivatesOnlyThatCode_OtherSevenGroupsStayActive()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await SeedEightKhkkGroupsAsync(h.Db, alice.Id);
|
||||
|
||||
// Tiền đề đo: 8 nhóm, TẤT CẢ đang có bản active. Thiếu chốt này thì một seed hỏng
|
||||
// (vd 1 nhóm đã tắt sẵn) vẫn cho assert phía dưới xanh ở thế giới sai.
|
||||
(await KhkkGroupsWithActiveVersionAsync(h.Db)).Should().Be(8, "tiền đề: seeder K3 để cả 8 nhóm active");
|
||||
|
||||
await new CreateAwDefinitionCommandHandler(h.Db).Handle(
|
||||
NewVersionCommand(Khkk, "KHKK-N1", alice.Id), CancellationToken.None);
|
||||
|
||||
var rows = await ReadWorkflowsAsync(h.Db, Khkk);
|
||||
rows.Should().HaveCount(9, "POST đẻ ĐÚNG 1 dòng mới (v02 của N1) — không xoá/không thêm dòng nào khác");
|
||||
|
||||
rows.Single(r => r is { Code: "KHKK-N1", Version: 1 }).IsActive.Should().BeFalse(
|
||||
"cùng Code thì bản cũ PHẢI nhường chỗ — đây là vế 'ratchet một chiều' của chính nhóm N1");
|
||||
rows.Single(r => r is { Code: "KHKK-N1", Version: 2 }).IsActive.Should().BeTrue(
|
||||
"version vừa tạo là bản đang dùng của nhóm N1");
|
||||
|
||||
rows.Where(r => r.Code != "KHKK-N1").Should().OnlyContain(r => r.IsActive,
|
||||
"🔴 R-6: tập bị tắt lọc theo CODE, không theo TYPE. Lọc theo type thì một cú bấm trên "
|
||||
+ "panel N1 tắt luôn N2..N8 — 7 nhóm mất bản active, và KHÔNG có endpoint bật lại");
|
||||
|
||||
(await KhkkGroupsWithActiveVersionAsync(h.Db)).Should().Be(8,
|
||||
"số NHÓM còn cửa lập phiếu phải BẤT BIẾN qua thao tác của một nhóm khác; tụt xuống 1 "
|
||||
+ "là đúng hình vụ nổ mà spec R-1 gọi là 'bom đã armed'");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T2) HỒI QUY cho các loại KHÁC: bản vá W1a chỉ được đổi hành vi của type-10.
|
||||
// Type-1 (Duyệt NCC) tạo quy trình mã MỚI vẫn phải tắt mã CŨ cùng type (1-active-per-type).
|
||||
// Kèm chứng nhân xuyên-type: 8 nhóm KHKK không được suy suyển vì một lệnh của type-1.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T2_CreateNewCodeForType1_StillDeactivatesOldCodeOfSameType_AndLeavesKhkkGroupsAlone()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await SeedWorkflowAsync(h.Db, ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
[[(1, alice.Id)]], code: "QT-DN-V2-001");
|
||||
await SeedEightKhkkGroupsAsync(h.Db, alice.Id);
|
||||
|
||||
// Mã KHÁC, cùng type-1 — đúng ca mà per-Code (nếu áp cho mọi type) sẽ để LỌT 2 bản active.
|
||||
await new CreateAwDefinitionCommandHandler(h.Db).Handle(
|
||||
NewVersionCommand(ApprovalWorkflowApplicableType.DuyetNcc, "QT-DN-V2-002", alice.Id),
|
||||
CancellationToken.None);
|
||||
|
||||
var type1 = await ReadWorkflowsAsync(h.Db, ApprovalWorkflowApplicableType.DuyetNcc);
|
||||
type1.Single(r => r.Code == "QT-DN-V2-001").IsActive.Should().BeFalse(
|
||||
"type-1 GIỮ ngữ nghĩa 1-active-per-type: mã cũ phải tắt khi có quy trình mới của cùng loại");
|
||||
type1.Single(r => r.Code == "QT-DN-V2-002").IsActive.Should().BeTrue();
|
||||
type1.Count(r => r.IsActive).Should().Be(1,
|
||||
"vá W1a mà nới per-Code cho MỌI type thì trang Duyệt NCC có 2 quy trình cùng active — "
|
||||
+ "phiếu mới không biết pin cái nào");
|
||||
|
||||
(await KhkkGroupsWithActiveVersionAsync(h.Db)).Should().Be(8,
|
||||
"chứng nhân xuyên-type: lệnh của type-1 không được chạm 8 nhóm type-10");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T3) F4 — mã type-10 NGOÀI họ `KHKK-N[1-8]`. Hai chân, mỗi chân giết một bản vá sai:
|
||||
// (a) tạo `QT-KHKK-V2-001` ⇒ 8 nhóm BẤT BIẾN → giết bản vá "giữ per-type cho code lạ"
|
||||
// (đúng cái mà công thức regex FE-invest sẽ làm — R-6 CẤM);
|
||||
// (b) tạo LẠI CHÍNH mã đó ⇒ v01 phải tắt → giết bản vá lười "type-10 thì đừng tắt gì cả"
|
||||
// (chân (a) một mình sẽ XANH với bản vá đó).
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T3_CreateType10_WithOutOfFamilyCode_LeavesEightGroupsActive_AndStillRatchetsItsOwnCode()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
await SeedEightKhkkGroupsAsync(h.Db, alice.Id);
|
||||
var handler = new CreateAwDefinitionCommandHandler(h.Db);
|
||||
|
||||
// ---- (a) mã type-10 chưa từng có ⇒ KHÔNG có bản cũ nào để tắt.
|
||||
await handler.Handle(NewVersionCommand(Khkk, "QT-KHKK-V2-001", alice.Id), CancellationToken.None);
|
||||
|
||||
(await KhkkGroupsWithActiveVersionAsync(h.Db)).Should().Be(8,
|
||||
"🔴 F4: quy trình type-10 mã ngoài họ (legacy/chung) tạo mới KHÔNG được đụng 8 nhóm. "
|
||||
+ "Công thức regex `^KHKK-N[1-8]$` cho mã này rơi về nhánh per-type ⇒ NUKE cả 8 (R-6 cấm)");
|
||||
|
||||
var afterA = await ReadWorkflowsAsync(h.Db, Khkk);
|
||||
afterA.Single(r => r.Code == "QT-KHKK-V2-001").IsActive.Should().BeTrue();
|
||||
afterA.Count(r => r.IsActive).Should().Be(9, "8 nhóm + 1 quy trình chung, cùng sống song song");
|
||||
|
||||
// ---- (b) CHÍNH mã đó, lần thứ hai ⇒ ratchet per-Code vẫn phải chạy.
|
||||
await handler.Handle(NewVersionCommand(Khkk, "QT-KHKK-V2-001", alice.Id), CancellationToken.None);
|
||||
|
||||
var afterB = await ReadWorkflowsAsync(h.Db, Khkk);
|
||||
afterB.Single(r => r is { Code: "QT-KHKK-V2-001", Version: 1 }).IsActive.Should().BeFalse(
|
||||
"per-Code áp cho MỌI mã type-10, kể cả ngoài họ — 'type-10 thì không tắt gì' là bản vá "
|
||||
+ "làm 2 version cùng mã cùng active, FE auto-pin bốc ngẫu nhiên");
|
||||
afterB.Single(r => r is { Code: "QT-KHKK-V2-001", Version: 2 }).IsActive.Should().BeTrue();
|
||||
(await KhkkGroupsWithActiveVersionAsync(h.Db)).Should().Be(8, "8 nhóm vẫn bất biến sau lượt 2");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T8) Fix (c) — PUT sửa-tại-chỗ quy trình type-10 phải ghi VẾT vào phiếu KHKK đang treo.
|
||||
// Dialog PUT đang HỨA nguyên văn "ghi vào lịch sử phiếu đang chạy" (`ApprovalWorkflowsV2Page.tsx:963-977`)
|
||||
// trong khi cổng `:863-865` chỉ mở cho DuyetNcc/DuyetNccPhuongAn ⇒ lời hứa rỗng cho KHKK.
|
||||
//
|
||||
// Đo SHAPE chứ không chỉ đếm: một dòng vết thiếu `EntityId`/`PhaseAtChange` thì màn lịch sử
|
||||
// phiếu hiện một dòng "ai đó sửa gì đó" — vô dụng đúng lúc cần nhất.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T8_UpdateType10Workflow_WithPendingKhkkPlan_WritesExactlyOneChangelogRowWithFullShape()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
var bob = await h.Fix.CreateUserAsync("bob@t.vn", "Lê Văn Bình", null, []);
|
||||
var admin = await h.Fix.CreateUserAsync("admin@t.vn", "Quản trị hệ thống", null, [AppRoles.Admin]);
|
||||
var drafter = await h.Fix.CreateUserAsync("bch@t.vn", "BCH lập kế hoạch", null, [AppRoles.Drafter]);
|
||||
|
||||
var wf = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "KHKK-N3");
|
||||
var plan = await SeedKhkkPlanAsync(h.Db, wf.Id, drafter.Id, "KHKK/2026/101",
|
||||
ContractSigningPlanPhase.ChoDuyet, approvalGroup: 3);
|
||||
|
||||
var lvlAlice = wf.Steps.Single().Levels.Single();
|
||||
var cmd = WithLevels(BuildEchoCommand(wf, reason: "Anh Chương nghỉ phép — bổ sung người ký thay"), 0,
|
||||
[EchoLevel(lvlAlice), NewLevel(1, bob.Id)]);
|
||||
|
||||
await NewHandler(h, admin).Handle(cmd, CancellationToken.None);
|
||||
|
||||
// Sanity chống rỗng-nghĩa: lệnh THẬT SỰ đổi cấu hình (⇒ `changes` khác rỗng).
|
||||
var live = await ResolveLikeRunningPeAsync(h.Db, wf.Id);
|
||||
live.Steps.Single().Levels.Should().HaveCount(2, "không có thay đổi thì không có vết để đo");
|
||||
|
||||
// Đọc bằng IgnoreQueryFilters: đọc THIẾU trông y hệt "chưa ghi" (bài S155).
|
||||
var logs = await h.Db.ContractSigningPlanChangelogs.IgnoreQueryFilters().AsNoTracking()
|
||||
.ToListAsync(CancellationToken.None);
|
||||
logs.Should().ContainSingle("đúng 1 phiếu đang treo ⇒ đúng 1 dòng vết");
|
||||
|
||||
var log = logs.Single();
|
||||
log.ContractSigningPlanId.Should().Be(plan.Id);
|
||||
log.EntityType.Should().Be(ContractSigningPlanEntityType.Workflow, "enum slot 4 đã có sẵn — không đẻ slot mới");
|
||||
log.Action.Should().Be(ChangelogAction.Update);
|
||||
log.EntityId.Should().Be(wf.Id, "trỏ về quy trình bị sửa — thiếu thì không lần ngược được");
|
||||
log.PhaseAtChange.Should().Be(ContractSigningPlanPhase.ChoDuyet);
|
||||
log.UserId.Should().Be(admin.Id);
|
||||
log.UserName.Should().Be("Quản trị hệ thống", "người đọc lịch sử cần TÊN, không phải GUID");
|
||||
log.Summary.Should().Contain("KHKK-N3").And.Contain("giữ nguyên phiên bản",
|
||||
"phân biệt 'sửa tại chỗ' với 'tạo version mới' — hai việc khác hẳn nhau về hệ quả");
|
||||
log.ContextNote.Should().Contain("Lý do: Anh Chương nghỉ phép");
|
||||
log.ContextNote.Should().Contain("THÊM NV duyệt Lê Văn Bình",
|
||||
"nội dung đổi phải đọc được bằng tên người, mirror khối PE `:874-891`");
|
||||
|
||||
(await h.Db.PurchaseEvaluationChangelogs.AsNoTracking().CountAsync(CancellationToken.None))
|
||||
.Should().Be(0, "quy trình type-10 KHÔNG được rải vết sang changelog của module PE");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T9) Vế ÂM của cùng cơ chế — và có CONTROL DƯƠNG TRONG CÙNG MỘT LƯỢT.
|
||||
// Nếu chỉ seed phiếu đã kết thúc rồi assert "0 dòng" thì ca này XANH cả trong thế giới
|
||||
// fix (c) chưa từng được viết. Vì vậy 4 phiếu cùng một quy trình:
|
||||
// • ChoDuyet SỐNG → PHẢI có vết (chứng cơ chế đang chạy)
|
||||
// • DaDuyet / TuChoi → KHÔNG (đã đóng, sửa quy trình không còn ảnh hưởng)
|
||||
// • ChoDuyet XOÁ MỀM → KHÔNG (spec `:34`: query-filter tự loại — vết cho phiếu đã xoá là rác)
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T9_UpdateType10Workflow_ChangelogGoesOnlyToLivePendingPlan_NotFinishedNorSoftDeleted()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
var bob = await h.Fix.CreateUserAsync("bob@t.vn", "Lê Văn Bình", null, []);
|
||||
var admin = await h.Fix.CreateUserAsync("admin@t.vn", "Quản trị hệ thống", null, [AppRoles.Admin]);
|
||||
var drafter = await h.Fix.CreateUserAsync("bch@t.vn", "BCH lập kế hoạch", null, [AppRoles.Drafter]);
|
||||
|
||||
var wf = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "KHKK-N5");
|
||||
var pending = await SeedKhkkPlanAsync(h.Db, wf.Id, drafter.Id, "KHKK/2026/110",
|
||||
ContractSigningPlanPhase.ChoDuyet, approvalGroup: 5);
|
||||
var approved = await SeedKhkkPlanAsync(h.Db, wf.Id, drafter.Id, "KHKK/2026/111",
|
||||
ContractSigningPlanPhase.DaDuyet, approvalGroup: 5);
|
||||
var rejected = await SeedKhkkPlanAsync(h.Db, wf.Id, drafter.Id, "KHKK/2026/112",
|
||||
ContractSigningPlanPhase.TuChoi, approvalGroup: 5);
|
||||
var softDeleted = await SeedKhkkPlanAsync(h.Db, wf.Id, drafter.Id, "KHKK/2026/113",
|
||||
ContractSigningPlanPhase.ChoDuyet, approvalGroup: 5, isDeleted: true);
|
||||
|
||||
var lvlAlice = wf.Steps.Single().Levels.Single();
|
||||
var cmd = WithLevels(BuildEchoCommand(wf, reason: "Bổ sung người ký"), 0,
|
||||
[EchoLevel(lvlAlice), NewLevel(1, bob.Id)]);
|
||||
|
||||
await NewHandler(h, admin).Handle(cmd, CancellationToken.None);
|
||||
|
||||
var logs = await h.Db.ContractSigningPlanChangelogs.IgnoreQueryFilters().AsNoTracking()
|
||||
.ToListAsync(CancellationToken.None);
|
||||
logs.Select(l => l.ContractSigningPlanId).Should().BeEquivalentTo(new[] { pending.Id },
|
||||
$"chỉ phiếu CHƯA kết thúc và CHƯA xoá mới chịu ảnh hưởng — DaDuyet {approved.MaKeHoach}, "
|
||||
+ $"TuChoi {rejected.MaKeHoach} và phiếu xoá mềm {softDeleted.MaKeHoach} đều phải bị loại; "
|
||||
+ "riêng dòng của phiếu ChoDuyet sống là CONTROL DƯƠNG chứng cơ chế đang chạy thật");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T10) Fix (d)(i) — XOÁ quy trình còn phiếu KHKK pin ⇒ 409 LỊCH SỰ, không phải 500 thô.
|
||||
// Mã dùng ở đây là NGOÀI họ `KHKK-N{n}` để rào D5 (T14) không xen vào: 409 ở đây chỉ có thể
|
||||
// do usage-check. Kèm 2 vế bắt buộc:
|
||||
// • CONTROL DƯƠNG cùng lượt: quy trình 0-usage vẫn xoá được ⇒ guard không phải "chặn tuốt";
|
||||
// • CHỨNG-NHÂN: FK Restrict là thật — không có guard thì người dùng nhận DbUpdateException (547).
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T10_DeleteWorkflow_WithPinnedKhkkPlan_Blocked409_WhileUnusedOneStillDeletes()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
var drafter = await h.Fix.CreateUserAsync("bch@t.vn", "BCH lập kế hoạch", null, [AppRoles.Drafter]);
|
||||
|
||||
var used = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "QT-KHKK-V2-001");
|
||||
var free = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "QT-KHKK-V2-002");
|
||||
await SeedKhkkPlanAsync(h.Db, used.Id, drafter.Id, "KHKK/2026/120");
|
||||
|
||||
var act = async () => await DeleteWorkflowAsync(h, used.Id);
|
||||
var ex = (await act.Should().ThrowAsync<ConflictException>(
|
||||
"còn phiếu pin thì phải chặn LỊCH SỰ — để rơi xuống FK là người dùng nhận 500 không đọc được")).Which;
|
||||
ex.Message.Should().Contain("QT-KHKK-V2-001",
|
||||
"phải gọi ĐÍCH DANH quy trình: admin bấm xoá từ một danh sách nhiều dòng, câu 409 "
|
||||
+ "không nêu tên thì không biết dòng nào vừa bị chặn");
|
||||
(await WorkflowRowCountAsync(h, used.Id)).Should().Be(1, "409 rồi thì quy trình phải còn nguyên");
|
||||
|
||||
// Thông điệp phải MANG SỐ ĐẾM THẬT (rào quét 8 module + 8 bảng chữ ký), không phải
|
||||
// một câu hằng. Đo bằng VI-SAI để không pin chữ: thêm đúng 1 hồ sơ pin nữa rồi gọi
|
||||
// LẠI CÙNG một lệnh — câu trả về phải KHÁC. Cách này miễn nhiễm với việc BE đổi
|
||||
// "phiếu"/"hồ sơ"/"bản ghi" (rào phủ cả đơn nghỉ phép lẫn hợp đồng, "phiếu" là chữ
|
||||
// của riêng KHKK), nhưng vẫn giết được bản vá trả câu cố định.
|
||||
await SeedKhkkPlanAsync(h.Db, used.Id, drafter.Id, "KHKK/2026/121");
|
||||
var ex2 = (await act.Should().ThrowAsync<ConflictException>()).Which;
|
||||
ex2.Message.Should().NotBe(ex.Message,
|
||||
"1 hồ sơ đang gắn và 2 hồ sơ đang gắn mà ra CÙNG một câu ⇒ con số trong câu là "
|
||||
+ "chữ chết; admin không biết còn bao nhiêu thứ phải gỡ trước khi xoá được");
|
||||
|
||||
// CONTROL DƯƠNG cùng lượt — cùng loại, cùng hình dạng, khác đúng một biến: có/không phiếu pin.
|
||||
await DeleteWorkflowAsync(h, free.Id);
|
||||
(await WorkflowRowCountAsync(h, free.Id)).Should().Be(0,
|
||||
"quy trình 0-usage vẫn phải xoá được — nếu ca này cũng 409 thì guard đang chặn tuốt "
|
||||
+ "và ca trên không chứng minh gì");
|
||||
|
||||
// CHỨNG-NHÂN: đây là thứ người dùng nhận nếu KHÔNG có guard (FK Restrict
|
||||
// `ContractSigningPlanConfiguration.cs:50-53`). Dọn tracker trước — bài F-14.
|
||||
h.Db.ChangeTracker.Clear();
|
||||
var stillTracked = await h.Db.ApprovalWorkflows.FirstAsync(w => w.Id == used.Id, CancellationToken.None);
|
||||
h.Db.ApprovalWorkflows.Remove(stillTracked);
|
||||
var raw = async () => await h.Db.SaveChangesAsync(CancellationToken.None);
|
||||
await raw.Should().ThrowAsync<DbUpdateException>(
|
||||
"guard 409 đang đỡ đúng quả nổ này — không có nó thì UI hiện 500 và admin đoán mò");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T11) F2 — phiếu KHKK ĐÃ XOÁ MỀM vẫn phải chặn xoá quy trình.
|
||||
// Xoá mềm = row VẪN NẰM TRONG BẢNG ⇒ FK Restrict vẫn nổ; nhưng guard đọc qua global filter
|
||||
// (`ContractSigningPlanConfiguration.cs:55`) sẽ ĐẾM ĐƯỢC 0 và cho đi tiếp — "vắng mặt đọc
|
||||
// thành sạch". Vậy nên guard BẮT BUỘC `IgnoreQueryFilters()`.
|
||||
// 🔴 Nếu ca này ĐỎ với `DbUpdateException` thay vì `ConflictException` thì đó ĐÚNG là lỗ
|
||||
// F2 — BÁO guard thiếu, KHÔNG nới test.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T11_DeleteWorkflow_PinnedOnlyBySoftDeletedKhkkPlan_StillBlocked409_NotFkCrash()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
var drafter = await h.Fix.CreateUserAsync("bch@t.vn", "BCH lập kế hoạch", null, [AppRoles.Drafter]);
|
||||
|
||||
var used = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "QT-KHKK-V2-001");
|
||||
var free = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "QT-KHKK-V2-002");
|
||||
await SeedKhkkPlanAsync(h.Db, used.Id, drafter.Id, "KHKK/2026/130", isDeleted: true);
|
||||
|
||||
// Tiền đề: phiếu KHÔNG còn nhìn thấy qua đường thường, nhưng VẪN nằm trong bảng.
|
||||
(await h.Db.ContractSigningPlans.AsNoTracking()
|
||||
.CountAsync(p => p.ApprovalWorkflowId == used.Id, CancellationToken.None))
|
||||
.Should().Be(0, "global filter che phiếu xoá mềm — đây chính là cái bẫy");
|
||||
(await h.Db.ContractSigningPlans.IgnoreQueryFilters().AsNoTracking()
|
||||
.CountAsync(p => p.ApprovalWorkflowId == used.Id, CancellationToken.None))
|
||||
.Should().Be(1, "row vẫn còn ⇒ FK Restrict vẫn sẽ nổ nếu guard bỏ qua nó");
|
||||
|
||||
var act = async () => await DeleteWorkflowAsync(h, used.Id);
|
||||
await act.Should().ThrowAsync<ConflictException>(
|
||||
"F2: phiếu xoá mềm là phiếu KHÔI PHỤC ĐƯỢC (màn 'Đã xoá') — guard phải đọc bằng "
|
||||
+ "IgnoreQueryFilters. Nhận DbUpdateException ở đây = guard đang mù trước soft-delete");
|
||||
(await WorkflowRowCountAsync(h, used.Id)).Should().Be(1);
|
||||
|
||||
await DeleteWorkflowAsync(h, free.Id);
|
||||
(await WorkflowRowCountAsync(h, free.Id)).Should().Be(0,
|
||||
"control dương: khác đúng một biến (có/không phiếu xoá-mềm pin) ⇒ hai kết quả trái ngược");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T12) F3 — phiếu Văn phòng số (nghỉ phép) pin quy trình ⇒ cũng phải 409.
|
||||
// 🔴 Đây là ca DATA-INTEGRITY, không phải UX: 5 module Office pin **LOOSE, 0 FK** ⇒ hôm nay
|
||||
// DELETE trả 200 và đơn nghỉ phép trỏ vào một quy trình KHÔNG CÒN TỒN TẠI (dangling, im lặng).
|
||||
// Guard usage-check là RÀO DUY NHẤT — chứng minh bằng chính DB ở cuối test.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T12_DeleteWorkflow_PinnedByLeaveRequest_Blocked409_BecauseGuardIsTheOnlyFence()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
var staff = await h.Fix.CreateUserAsync("nv@t.vn", "Nhân viên xin nghỉ", null, []);
|
||||
|
||||
var used = await SeedWorkflowAsync(h.Db, ApprovalWorkflowApplicableType.LeaveRequest,
|
||||
[[(1, alice.Id)]], code: "QT-NP-V2-001");
|
||||
var witness = await SeedWorkflowAsync(h.Db, ApprovalWorkflowApplicableType.LeaveRequest,
|
||||
[[(1, alice.Id)]], code: "QT-NP-V2-002");
|
||||
var leave = await SeedLeaveRequestAsync(h.Db, used.Id, staff.Id, "DT/LR/2026/001");
|
||||
var leaveWitness = await SeedLeaveRequestAsync(h.Db, witness.Id, staff.Id, "DT/LR/2026/002");
|
||||
|
||||
var act = async () => await DeleteWorkflowAsync(h, used.Id);
|
||||
await act.Should().ThrowAsync<ConflictException>(
|
||||
"đơn nghỉ phép đang pin quy trình này — xoá là đơn treo trỏ vào hư không");
|
||||
(await WorkflowRowCountAsync(h, used.Id)).Should().Be(1);
|
||||
(await h.Db.LeaveRequests.AsNoTracking().FirstAsync(x => x.Id == leave.Id, CancellationToken.None))
|
||||
.ApprovalWorkflowId.Should().Be(used.Id, "đơn vẫn pin nguyên — 409 không được đụng dữ liệu");
|
||||
|
||||
// 🔴 CHỨNG-NHÂN "rào duy nhất": xoá THẲNG quy trình thứ hai qua DbContext.
|
||||
// Với PE/HĐ/KHKK thì câu này nổ FK; với module Office nó ĐI LỌT — và đơn nghỉ phép
|
||||
// thành mồ côi. Đó là lý do guard này là DATA-INTEGRITY chứ không phải làm đẹp UX.
|
||||
h.Db.ChangeTracker.Clear();
|
||||
var tracked = await h.Db.ApprovalWorkflows.FirstAsync(w => w.Id == witness.Id, CancellationToken.None);
|
||||
h.Db.ApprovalWorkflows.Remove(tracked);
|
||||
await h.Db.SaveChangesAsync(CancellationToken.None); // KHÔNG ném — 0 FK bảo vệ
|
||||
|
||||
(await WorkflowRowCountAsync(h, witness.Id)).Should().Be(0);
|
||||
var orphan = await h.Db.LeaveRequests.AsNoTracking()
|
||||
.FirstAsync(x => x.Id == leaveWitness.Id, CancellationToken.None);
|
||||
orphan.ApprovalWorkflowId.Should().Be(witness.Id,
|
||||
"đơn vẫn giữ Id của quy trình đã biến mất = dangling pointer THẬT trong DB — "
|
||||
+ "đúng thứ mà guard 409 ở trên tồn tại để chặn");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T13) Fix (d)(iii) — id không tồn tại phải là 404, không phải 500 thô.
|
||||
// `KeyNotFoundException` là BCL, `GlobalExceptionMiddleware` KHÔNG map ⇒ rơi 500.
|
||||
// Control dương cùng lượt: id THẬT (0 usage, ngoài họ) xoá trơn ⇒ 404 là do id, không phải
|
||||
// do handler hỏng.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T13_DeleteWorkflow_UnknownId_ThrowsNotFound_NotRawBclKeyNotFound()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
var real = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "QT-KHKK-V2-001");
|
||||
|
||||
var act = async () => await DeleteWorkflowAsync(h, Guid.NewGuid());
|
||||
await act.Should().ThrowAsync<NotFoundException>(
|
||||
"middleware chỉ map NotFoundException→404; KeyNotFoundException (BCL) rơi ra 500 thô");
|
||||
|
||||
await DeleteWorkflowAsync(h, real.Id);
|
||||
(await WorkflowRowCountAsync(h, real.Id)).Should().Be(0,
|
||||
"control dương: đường xoá vẫn chạy bình thường ⇒ ngoại lệ trên là VỀ ID, không phải handler chết");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (T14) D5 — nhóm `KHKK-N{n}` phải luôn còn ≥1 version.
|
||||
// Xoá bản CUỐI của một nhóm = đóng cửa lập phiếu nhóm đó, và đường mở lại duy nhất là
|
||||
// seeder chạy khi boot (M-1) — tức người dùng không tự cứu được từ UI.
|
||||
// Ba chân trên CÙNG một bộ seed, mỗi chân bịt một kiểu vá sai:
|
||||
// (1) xoá v01 khi còn v02 ⇒ OK (vá "cấm xoá mọi bản của nhóm" sẽ ĐỎ ở đây)
|
||||
// (2) xoá v02 khi nó là bản cuối ⇒ 409
|
||||
// (3) xoá quy trình NGOÀI họ chỉ có 1 version ⇒ OK (vá "cấm xoá bản-duy-nhất" sẽ ĐỎ ở đây)
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task T14_DeleteLastVersionOfKhkkGroup_Blocked409_ButNonLastAndOutOfFamilyDeleteFine()
|
||||
{
|
||||
var h = NewHarness();
|
||||
using (h.Fix)
|
||||
{
|
||||
var alice = await h.Fix.CreateUserAsync("alice@t.vn", "An", null, []);
|
||||
|
||||
var n2v1 = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "KHKK-N2", version: 1);
|
||||
var n2v2 = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "KHKK-N2", version: 2);
|
||||
var legacy = await SeedWorkflowAsync(h.Db, Khkk, [[(1, alice.Id)]], code: "QT-KHKK-V2-001");
|
||||
|
||||
// Tiền đề: KHÔNG phiếu nào pin ⇒ 409 (nếu có) chỉ có thể đến từ D5, không phải usage-check.
|
||||
(await h.Db.ContractSigningPlans.IgnoreQueryFilters().AsNoTracking()
|
||||
.CountAsync(CancellationToken.None)).Should().Be(0, "0 usage ⇒ tách bạch D5 với rào (i)");
|
||||
|
||||
// ---- (1) còn v02 đứng sau ⇒ xoá v01 hợp lệ.
|
||||
await DeleteWorkflowAsync(h, n2v1.Id);
|
||||
(await WorkflowRowCountAsync(h, n2v1.Id)).Should().Be(0,
|
||||
"version cũ 0-usage KHÔNG phải bản cuối thì vẫn dọn được — rào D5 không được nuốt luôn ca này");
|
||||
(await WorkflowRowCountAsync(h, n2v2.Id)).Should().Be(1);
|
||||
|
||||
// ---- (2) nay v02 là bản CUỐI của nhóm N2 ⇒ chặn.
|
||||
var act = async () => await DeleteWorkflowAsync(h, n2v2.Id);
|
||||
var ex = (await act.Should().ThrowAsync<ConflictException>(
|
||||
"xoá bản cuối = nhóm N2 mất quy trình, mà đường tái sinh duy nhất là seeder lúc boot")).Which;
|
||||
ex.Message.Should().Contain("N2", "thông điệp phải chỉ đích danh nhóm đang bị đóng cửa");
|
||||
(await WorkflowRowCountAsync(h, n2v2.Id)).Should().Be(1, "409 thì bản cuối phải còn nguyên");
|
||||
|
||||
// ---- (3) ngoài họ, chỉ 1 version ⇒ KHÔNG dính D5.
|
||||
await DeleteWorkflowAsync(h, legacy.Id);
|
||||
(await WorkflowRowCountAsync(h, legacy.Id)).Should().Be(0,
|
||||
"D5 chỉ áp cho họ KHKK-N[1-8]; chặn cả 'bản duy nhất' của quy trình chung là siết oan");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user