Compare commits

...

9 Commits

Author SHA1 Message Date
88e7ecf263 [CLAUDE] Contract: K4a+K4b SP-002 sidebar 8 nhom x 6 leaf + noi ?group= end-to-end (0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
K4a (gate PASS-WITH-FLAGS 4, sub-reviewer-gate-k4a.md): 49 key Khkk_G{2-8} bo-sinh
by-construction 3-phia (All/seed/grant) MenuKeys.cs + DbInitializer 49 row + labelBackfill
G1 nhan A1 + site-2 Concat + va 7/10/11/13/15 (nhan N2 co "pha do"; comment 1893 -> mo-neo
grep). All 64->113, Policies 256->452 (test T2 khoa runtime). Test +7 KhkkGroupMenuSeedTests
(T7 revoker-khong-dung-Khkk: probe vi-phan 112->0, gotcha #84).

K4b (gate PASS-WITH-FLAGS 4M/5m 0-chan, sub-reviewer-gate-k4b.md): KhkkListPage x2 derive
phase+group tu searchParams (leaf->leaf khong F5, navKey reset) + queryKey {group,phase,
search,page,deletedView} + KhkkCreatePage preset ?group= + staticMap G1 6-leaf group=1 +
regex Khkk_G([1-8])_* fe-user (fe-admin GHI RANG BUOC isAdminHidden) + FLAG-1 lead-fix:
GET /deleted +group mirror List (Controller+Query+handler SAU-IDOR) + FE noi 2-nhanh.

2 wave 1 commit CO KHAI: Layout.tsx dinh ca 2 wave khong tach hunk phi-tuong-tac; moi dong
deu qua gate rieng. STATUS canonical 5-row dong bo (71/97/452/113/620->621) + bundle :479.
No K4c: MAJOR-A test /deleted+group filter + MAJOR-C WfView doc group. Suite 621/0
(45D+576I) + filter 41/41 + npm x2 + dotnet 0E.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 10:10:11 +07:00
bc1e7c937d wal: flush 20260801T0959 2026-08-01 09:59:59 +07:00
19eff22528 wal: flush 20260801T0952 2026-08-01 09:52:41 +07:00
14ea2eaabe wal: flush 20260801T0945 2026-08-01 09:45:05 +07:00
5b9f68080a wal: flush 20260801T0937 2026-08-01 09:37:26 +07:00
4ecc76fe6c wal: flush 20260801T0930 2026-08-01 09:30:46 +07:00
0cae24328a wal: flush 20260801T0926 2026-08-01 09:26:34 +07:00
21908b445f wal: flush 20260801T0921 2026-08-01 09:21:16 +07:00
ce15c649b3 wal: flush 20260801T0914 2026-08-01 09:14:40 +07:00
34 changed files with 2147 additions and 157 deletions

View File

@ -14,19 +14,27 @@ chain:
[x] cicd-K3 PASS 6/6 @S166 — re-spawn vai đo §1-§5 (run#436 success · gate 614 tách-phần · mig GIỮ 71 ·
seed 8-wf shape-từng-email + finalize 2+8=10 + recycle-proof 5364→1900 · bundle admin ROTATE/eoffice
FROZEN-giải-thích-trọn) rồi chết lần 2 → §6 smoke lead đo on-behalf 401/404/200/200 + END-line
[!] 🔴 ĐẦU VIỆC #1 F-1 checkbox opt-out: CODE XONG @S166 (BE DTO +AllowApproverFinalize · FE panel
actingLevel-mirror-ResolveActingLevel + checkbox + body-3-field ×2 app SHA-pair ee19ca94/eba649cc) ·
build 0W/0E + filter-test 34/34 + npm ×2 PASS · ĐANG chờ reviewer nền (sub-reviewer-f1-checkbox.md)
→ PASS thì commit+push
[x] 🔴 ĐẦU VIỆC #1 F-1 SHIPPED @S166: commit 7a903cf PUSHED (5 file đích danh — G-6). Reviewer
PASS-WITH-FLAGS 7 (3 MAJOR/4 MINOR, sub-reviewer-f1-checkbox.md): G-1 default→OPT-IN PE-live-S97
ÁP (ratify treo chờ-anh) · G-5 bỏ-(CEO) ÁP · G-6 tách-commit ÁP · nợ nhịp-K4: G-2 sơ-đồ-CEO-đã-duyệt
(badge nửa-rẻ + F-7 EndedByLevelFinalize-ra-DTO) + G-3 1-Fact-projection-cờ + G-4 mirror-drift-
Designer-trộn-cờ {D} · G-7 STATUS-rows-55/220 → commit K4a. Suite 614/0 · SHA b37cd292
[ ] K4a→K4b→K5→K4c→K6→K7→K8-prep theo build-order B3 + vá-số spec-cụm2/cụm3 (chi tiết PAUSE-3 mục (2):
K4a 49-row + vá-1 route-regex CÙNG-wave + vá-7 nhãn-N2-phá-dỡ + DELTA +49key/+196policy · K4b
searchParams + queryKey+group · K5 groups + forward-provision lines[].contractId pe.contractId ·
K4c WfView lọc Code · K6 blacklist 12-key + fe-user-0-diff · K7 hội-2-khóa + grant Contracts.Create
[câu III default Drafter+Procurement+Admin] + Line.ContractId-ghi-luôn · K8 form-14-người fail-closed
+ gói SOLUTION-PE-{n} + QĐ9 đo-2-nhánh + ZZTEST + rollback-theo-ID)
[ ] closeout-nợ: diary vai (C2) · tally #53 CHỐT (tạm ~14/17 lượt cửa-3, sub-class mới engine-process-
exit ×2 — đếm lại từ sổ) · nhãn D MIND · STATUS row S163/S164/S165 · spec-cụm1 vá-3 đính-chính
chữ PUT-Conflict (gate-K3 phán giữ-là-đúng) · lỗi-tự-bắt ship-607-khi-609 ghi bài
[ ] closeout-nợ: diary vai (C2 — checklist @sub-lead-gap khi nó trả) · tally #53 CHỐT (S166 tới giờ:
cicd×2 + reviewer×2 + stale×2 + gap×1 + laneBE×1 = 8 lượt #53-return / 0 mất công, artifact cứu hết)
· nhãn D MIND · STATUS row S163-S166 · spec-cụm1 vá-3 đính-chính PUT-Conflict · lỗi-tự-bắt 607/609
· 🆕 H24-DEEP 11-FLAG-SÀN resolve (sub-lead-stale-deep-S166.md): F-1 STATUS-canonical-5-row +
canonical-poison :465-472 (detector đòi sửa 97-đúng→96-sai!) · F-2 bundle :479 · F-7 slot(57)
_INDEX ĐÃ-VỠ 101,23% · F-8 slot(59) ring2-BÁC-một-phần: số HANDOFF:19 stale THẬT (71.492→120.633B) nhưng "180.649B
đã-đạt-dải" = stale đo SAI phạm-vi (nuốt 7 segment cũ) — đúng phạm-vi ~30-40K VẪN DƯỚI sàn ⇒
slot (59) CÒN SỐNG, chỉ cập-nhật số; bài: lead chép claim vai-đo TRƯỚC vòng-kiểm = suýt đóng
owner-decision ngược chiều (ring2 16Đ/1T cứu) · F-4 CLAUDE-mig-70/71
· F-5/F-6/F-9/F-10/F-11 · deep-đã-trả: set last_audit light=deep=40 + ring2 sau cặp
next: /tiep → đọc sub-cicd-verify-k3.md dòng-1 + re-verify 6 mục → checkbox opt-out (đầu việc #1)
→ K4a theo build-order.

View File

@ -1,5 +1,7 @@
# CI/CD Monitor Agent — Persistent Memory
- **S166 (08-01) re-verify `15349e8` K3 — PASS 6/6 `[LEAD SEED ON-BEHALF TẠI-CỬA — mày chết #53 ×2 (lượt-1 session-limit 18:50 @S165 sau §0 · lượt-2 S166 sau §1-§5, return "Smoke is clean" mà §6 chưa ghi đĩa); §6 lead đo lại 4 phép on-behalf, KHÔNG tin lời khai suông]`:** Run **#436** success 6m11s (match theo `head_sha`, control neo run kề = #435 K2). Gate **614 = 45D + 569I** khớp TỪNG VẾ (+5 khớp subject K3); control ÂM `grep -c 'Failed!'` = 0. Mig **GIỮ 71** — mục "GIỮ NGUYÊN" ít bit ⇒ răng lấy từ **control DƯƠNG cùng phiên** (§4 seed chứng startup-hook sống). **§4 = khuôn mẫu dự-đoán-trước-khi-đo**: đọc seeder → dự đoán 8wf×3step×11level + finalize 2+8=10 → sqlcmd khớp TỪNG ĐỒNG + shape element-wise từng email + 3-nguồn-độc-lập (source ⟂ sqlcmd ⟂ Serilog `Seeded 8 KHKK group workflows`) + control ÂM (OUTSIDE=2 nguyên · OUTOFSET=0). **§5 FROZEN-eoffice = hệ-quả bắt-buộc dự-đoán TRƯỚC curl** (diff fe-user 14 dòng toàn `//` comment → Vite strip → byte-identical; marker POS `KhkkN[1-8]` admin=1/eoffice=0 + NEG transition PRE-có-POST-mất; ❌ marker `earlyFinalizeLocked` LOẠI vì tên biến local bị minifier đổi = 0 bit). **Recycle-proof 2 nguồn**: PID 5364→**1900** StartTime 18:27:28 + Serilog dòng-đầu 18:27:40 (deploy xoá logs/). Ghi chú route: KHKK = kebab-case `/api/contract-signing-plans` (đề bài lead ghi nhầm không-gạch). Evidence: `runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k3.md` (§6 + END do lead). Tag `[s166, run-436, pass-6-6, du-doan-truoc-khi-do, frozen-la-he-qua, marker-bien-local-0-bit, chet-53-x2-artifact-cuu, seed-on-behalf-tai-cua]`
- **07-31 verify `0779f2d` S164 K2 SP-002 KHKK mang hạng-mục + nhóm duyệt N1-8 (23 file: 9 BE + 8 FE + 3 test + Mig 71 ×3 + 2 ignored-path):** ⚠️ **PARTIAL 5/6** — deploy KHỎE, điểm trừ = **phép đo không có răng**, không phải lỗi ship. Run **#435** (id=548) success **6m12s**. Gate **609 = 45D + 564I** (0 fail/skip) — Domain **GIỮ 45**, Infra 553→**564 = +11** khớp TỪNG VẾ kỳ-vọng "9 lane + 2 gate-fix". Mig 71 `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` TOP prod (MIGCOUNT=71), `sys.tables` **97 GIỮ NGUYÊN** khớp `grep -c CreateTable`=**0** (control dương `grep -c AddColumn`=4). 🔴 **BÀI LỚN NHẤT — ACCEPTANCE RƠI VÀO CHÂN-LÝ-RỖNG**: `ContractSigningPlans` prod **0 dòng cả PRE lẫn POST** ⇒ cả 3 acceptance mục 4 (`COUNT(group=1)==COUNT(*)``0==0`; `NOT BETWEEN 1 AND 8`=0; Σ tiền `NULL==NULL`) đều "xanh" mà **0 bit thông tin** — backfill hụt sẽ trông Y HỆT. Tác giả Mig 71 đã khai bẫy này ở comment ("đo local Σ = tập RỖNG hai phía… phép đo prod mới có răng") — nay prod cũng rỗng nốt ⇒ phép đo prod CŨNG hết răng. KHÔNG hạ FAIL (không có gì sai) nhưng CẤM đọc thành PASS. 🔑 **Đối-sách khi acceptance rỗng → thay bằng bằng-chứng KHÔNG-phụ-thuộc-số-dòng**: (a) 4 cột đúng kiểu/null/default qua `INFORMATION_SCHEMA`; (b) **hoán-đổi index chứng bằng LIỆT KÊ TRỌN HỌ 14 index** — 3-cột unique filtered `([IsDeleted]=(0))` CÓ + 2-cột cũ MẤT, vắng-mặt suy từ danh-sách-đầy-đủ nên là vắng-mặt ĐO ĐƯỢC (mạnh nhất mục 4); (c) Σ bất-biến chứng theo **CẤU TRÚC**: grep cột tiền trên Mig 71 ra 2 hit **đều là comment `//` dòng 23-24** ⇒ 0 câu lệnh thực thi, đúng kể cả khi có dữ liệu; (d) EF materialize 200-không-500. 🔑 **MỚI — chứng query-param ĐƯỢC WIRE bằng bind-error**: `?group=abc`**400** (param lạ mà bị nuốt thì ASP.NET trả 200) + khớp nguồn `Controller:42 [FromQuery(Name="group")] int? group`; `?group=1/2/8` chỉ ra `{"items":[],"total":0}` = VACUOUS. Áp bài K1: soi **thân RAW** trước khi tin parser ⇒ rỗng THẬT. Control DƯƠNG đường-ống list: `/api/catalogs/contract-catalog` **86 item** (seed K1 nguyên) ⇒ ống chạy, `items:[]` là bảng rỗng thật. Bundle **4/4 ROTATE** (admin `CKgq0YlH``DVQEwGwf`, user `CHMCU35C``CZAYiWWa`) + hash CŨ **và hash BỊA** cùng rơi `text/html` 900/876 B ⇒ `text/html` thật sự nghĩa "không tồn tại"; LM 17:10:20/17:11:20 in-window. Smoke: `/health/{ready,live}` 200 + bare `/health` **404** control · no-token **401** kèm **2 route BỊA 404**. w3wp **PID 5364 = `SolutionErp-Api`** (map PID→pool bằng `appcmd list wp` vì VPS có 6 w3wp chung VIETREPORT — CẤM đoán PID) StartTime 17:11:25 + Serilog dòng-đầu `Applying migrations...` 17:11:27 ⇒ mọi đo DB từ 17:12+ = SAU recycle. Quan sát cho K4b: `?group=999`**200** (biên API không chặn ngoài-miền 1..8, hiện vô hại); ghost-window `EndedByLevelFinalize` luôn 0 tới khi K3 land = ĐÚNG hiện trạng. Evidence: `runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k2.md`. Tag `[run-435, partial-5-6, chan-ly-rong-vacuous, liet-ke-tron-ho-index, wire-proof-bang-bind-400, gate-609, mig-71, map-PID-pool]`
- _(**07-31 `50e6d8c` S164 K1 SP-002 danh-mục-loại-HĐ +Mig 70 — PASS 6/6**: run #434 6m31s, gate 598, `sys.tables` 96→97. 🔑 **TAM-GIÁC 3 NGUỒN ĐỘC-LẬP** cho seed (source-parse `DbInitializer` == sqlcmd prod == **API materialize qua EF**, cả 3 ra 86 + phân-bố `1:5·2:10·3:5·4:23·5:14·6:15·7:13·8:1`; nguồn 3 mạnh nhất vì đi trọn Domain→Infra→Api). ⚠️ **BÀI: 0-hit là DƯƠNG-GIẢ tới khi soi format** — grep `Passed!` ra 0 chỉ vì log dùng 2-khoảng-trắng; cùng class parse seeder ra 78 (regex line-anchored, tuple xuống dòng) → vứt số, parse lại 86. ⚠️ **hash CI ≠ hash local build** ⇒ CẤM lấy hash local làm mốc kỳ-vọng prod. verbatim → `archive/2026-07.md` via `_INDEX`)_

View File

@ -1,5 +1,7 @@
# Implementer-Backend Agent — Persistent Memory
- **S166 (08-01) lane K4a — 49 row menu KHKK 8-nhóm, DONE B0-B5 `[LEAD SEED ON-BEHALF TẠI-CỬA — mày #53 sau B5 ("Now the site-4 revoker comment" trong khi comment ĐÃ land — return tụt sau đĩa 1 nhịp); B6 + END lead đóng; nguồn: sub-implbe-k4a.md + git diff]`:** Nghề mới ăn tiền: **BỘ-SINH thay 49 const tay**`KhkkGroupNumbers[7] × (KhkkGroup + 6 KhkkLeafActions)``KhkkGroupMenuKeys` làm NGUỒN CHUNG 3 phía (spread `.. ` vào `All` · vòng seed tree.Add · site-2 `.Concat`) ⇒ khớp **by-construction, 0 mirror tay 147 chuỗi** (đúng loại drift nổ FK Permission T6-K1); chệch-khuôn-so-brief KHAI RÕ + trỏ đề xuất gốc invest `:9` ⇒ lead chấp nhận không mất lượt. Anchor re-ground TRƯỚC khi sửa (số spec trượt vì DbInitializer phình sau K2/K3): dict labelBackfill `:2141-2183` · upsert THẬT `:2162-2179` (vá 2 comment stale "1893" bằng mỏ-neo GREP `existingItems.TryGetValue` thay số-dòng-trần — số dòng sẽ trượt tiếp, mỏ neo thì không). Vá-10 sửa LÝ-DO comment (group-key KHÔNG cần CanRead row — HasAccess đệ-quy theo con `:96`; invest doc viết sai lý-do, kết luận giữ-3-site vẫn đúng). labelBackfill `Khkk_G1` đổi-nhãn-không-nhân-row + fail-fast KeyNotFound khi thiếu nhãn (thà nổ lúc seed còn hơn FK nổ về sau). Suite **614/0 SAU diff** (idempotent/revoke/permission-seed giữ xanh). Tag `[s166, k4a-lane, bo-sinh-by-construction-3-phia, mo-neo-grep-thay-so-dong, fail-fast-luc-seed, return-tut-sau-dia-1-nhip, seed-on-behalf-tai-cua]`
- **S161 (07-29) ARC W1+W2+W3đ1 KHKK — 3 lượt/3 wave, chi tiết = sub-md run-folder `runs/2026-07-29-S161-khkk-{w1-schema,w2-crud,w3-duyet}/` (tổng ~47KB ghi-từng-mục; cả 3 lượt return #53 nhưng 0 mất):** **W1** Mig 69 7-bảng (UNIQUE filtered #57 · cascade-1-hop · apply 2 DB 0-drift · sys.tables 96) + seeder `SeedKeHoachKyKetAccessAsync` UPGRADE-if-exists+CanDelete + CodeGen `KHKK/{YYYY}/{Seq:D3}` WorkflowAppCodeSequences + 3 site AWV2Admin — ship `a8027c0` cicd 6/6. **W2** mega-file CRUD 15-endpoint policy-15/15 + 4 quyết D1-D4 khai-lead-duyệt; 5 bẫy né ghi sổ: codegen-TRƯỚC-Add (S88) · SUM so `w.Id` KHÔNG `w.SupplierId` · `Remove` trước `Add(changelog)` (T26) · subquery-Any-2-tầng vỡ-SQL-lúc-chạy · giao-1-grep-cùng-lớp-ra-3 (S122) — ship `6cbc6ad` cicd 6/6 (reviewer 12F: D1 "FE sẽ khớp" = giả-định-không-phải-phép-đo → 6 điểm đứt, lead vá). **W3đ1** skeleton test-before: switch-throw từng nhánh (đỏ-THẬT không đỏ-compile) + **rào-FK-547-bằng-VẮNG-MẶT-dep** (không inject IChangelogService) + **§6 tự đối-chứng chéo hợp-đồng FE 5/5 TRƯỚC bàn giao** (bài W2 → nghi thức) + §7 bàn-giao ctor-CUỐI. Tag `[s161, khkk-w1-w2-w3d1, mig69, upgrade-if-exists, rao-vang-mat-dep, doi-chung-cheo, gia-dinh-vs-phep-do]`
- **S139 (07-17) Governance wave adap-3-bản 2-lane [wf_f9981ae5-903 · L1 CLEAN · L2 #53-garble → sub-1.md ON-BEHALF em-main verify-2-nguồn] `[engine: opus-worker]`:** L1 = MASTER-CHECKLIST 2-tầng in 4 cửa (ss Phase-3 · se Phase-4 "TỔNG-HỢP CHỐT" · pause ý-6 · tiep §3-bis bold-block KHÔNG heading) + C1 2-câu đóng-băng bề-mặt-đo (§L.b(c) prepend + mirror §2.1.2) + C6i do-record ×2 + luật-số sàn-5 §L.b(j)(iv) — PASS 6/6 (heading 26/28/7/14 bất-biến · 0 checklist-file mới · BƯỚC-0 intact ×8 · 10/10 config-token preserved · add-only 26+/2). L2 = +C6 cite-2-nấc + H24-4 pending-flip vào governance-detectors.ps1 (+260/0 add-only) — fault-inject **13/13 2-chiều** (C6 7/7 lane chạy; H24-4 6/6 em-main hoàn tất — lane chết GIỮA fix harness-bug), live-run TOTAL 45 GIỮ + 2 net mới 0-flag count-riêng + exit-0 + ASCII 0-byte>127 + clean-diff 0-FOREIGN-line. Learned: (1) heading-invariant proof = block chèn toàn `>`/bullet + `grep '^+#'`=none (mạnh hơn đọc-mắt, họ insertion-only S134); (2) test-harness PS1 cần CÙNG kỷ-luật naming như production — bẫy `$h`/`$H` case-insensitive cắn chính harness dù script-đích đã né (`:813`); (3) 2 lane cùng chạm bề-mặt-đo (L1 sửa file nằm trong scan-scope L2) → số đo giữa-chừng trôi, clean-diff PHẢI cùng-snapshot. Tag `[s139, adap-3-ban, master-checklist-4-cua, c6-cite-2-nac, h24-4-pending-flip, faultinject-13-13, 53-writelane-onbehalf]`

View File

@ -1,5 +1,9 @@
# Implementer-Frontend Agent — Persistent Memory
- **S166 (08-01) K4b-FE nối `?group=` + vá-3/5/6 [artifact `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4b.md`]:** 🔴 **BÀI TO NHẤT — HARNESS ĐO NHẦM CÁI RENDER BỊ REACT VỨT, suýt kết luận NGƯỢC.** `setState` gọi **trong lúc render** ⇒ React **huỷ render đó + render lại ngay với state mới TRƯỚC khi commit**; harness bản-1 chỉ chạy 1 pass nên đọc ra `phase=3` (giá trị của render đã bị vứt) — control-âm cũng ra 3 ⇒ **2 nhánh giống nhau = phép đo mất khả năng phân biệt, trông y như PASS**. Vá = lặp tới khi state đứng yên (đúng ngữ nghĩa React) → A ra `phase=2`, control-âm giữ `3`. Khuôn giữ lại: *mô phỏng hành vi framework thì phải mô phỏng CẢ vòng đời, không chỉ 1 lượt gọi hàm.* 🔴 **Tiền-đề đề-bài sai lần 3 liên tiếp** (S164→S166-K4a→K4b): "`phase` derive từ searchParams thay useState" nguyên văn sẽ **GIẾT 6 chip lọc** (`KpiCard onClick setPhase :153`) — hỏng CÂM, build xanh. Giải = **override 3-trạng-thái** `chipPhase: T|null|undefined` (`undefined`="nghe URL" ≠ `null`="user chọn Tất cả", gộp 2 cái là chọn Tất-cả bị URL kéo ngược) + **reset trong render theo `navKey`** (KHÔNG `useEffect` — effect vẽ 1 lượt dữ-liệu-cũ rồi mới sửa = nháy). Cùng khuôn áp cho CreatePage (8 leaf chung 1 route ⇒ leaf→leaf không remount; so `lastPresetGroup` **KHÔNG** so `approvalGroup`, so nhầm = user bấm đổi nhóm bị kéo về, select thành nút chết). Đo ra BE `/deleted` **thiếu `group`** (`Controller:61-66` vs `List:42`) ⇒ không lọc client-side được (server đếm `total` trên tập chưa lọc → số trang nói dối) ⇒ xử bằng **không cho UI claim nhóm ở màn đã-xóa** (sai-thầm-lặng → thiếu-nhìn-thấy-được) + FLAG lane BE. staticMap G1: fe-user **6/6** mang `group=1`, fe-admin **5/6** (WfView admin trỏ Designer, không nhận group) — lệch có chủ đích. SHA-pair ×2: List `80ebd79d`→**`fc3944df`** · Create `677f4ab8`→**`545b4c2f`** · types `7a99b865` không đổi = **đối chứng dương**. Build ×2 PASS (user `index-C4F7kHKT` 3.6s · admin `index-kv90BBYQ` 0.9s). 4 FLAG treo (BE `/deleted` group · queryKey mang `phase` thay `filter` thô · `?group=` ở workflow-matrix chưa consumer → K5 dùng `khkkGroupWorkflowCode` · `search` cố ý không reset). Tag `[s166, khkk-k4b-group-wire, harness-do-nham-render-bi-vut, override-3-trang-thai, reset-trong-render-khong-useeffect, so-lastpreset-khong-so-state, be-deleted-thieu-group, staticmap-5-tren-6]`
- **S166 (08-01) K4a-FE regex 42 leaf KHKK + single-source nhãn nhóm [artifact `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4a.md`]:** 🔴 **Khuôn S164 "đo đĩa TRƯỚC khi gõ" ăn tiếp 2 lần trong CÙNG 1 đề bài** — cả 2 tiền-đề lead đều sai theo hướng *"tưởng chưa có / tưởng có"*: ① đề bài bảo export `KHKK_GROUP_LABELS` **mới**, đĩa đã có `KHKK_APPROVAL_GROUP_LABELS` (`types/khkk.ts:129`, K2 land, **5 call-site**) ⇒ đẻ map-2 là **phá đúng acceptance-5 (single-source) mà nó phục vụ**; K2 còn để sẵn uỷ-quyền trong comment `:125-127`. Đo thêm: nhãn BE ≡ `` `${n}. ${short}` `` cho **8/8** ⇒ giải bằng **DERIVE `khkkGroupMenuLabel(n)`, KHÔNG hardcode lần 2, 0 call-site cũ bị đụng** (hardcode lại = double-numbering "N2 - 2. …" ở select). ② comment vá-8 bảo "gỡ `KeHoachKyKet` khỏi ẩn-admin ⇒ phải nới regex trước", nhưng `isAdminHidden:194` = `key.startsWith('Ct_')` — KHKK **chưa bao giờ bị ẩn** ⇒ ràng buộc ĐANG hiệu lực chứ không phải tương lai; viết comment theo ĐĨA + FLAG "7 nhóm rỗng" (`MenuGroup` chỉ xét `children.length>0`, không xét con resolve được ⇒ header hiện, thân rỗng). **BÀI MỚI đáng giữ: chứng regex bằng `new Function()` chạy CHÍNH khối text đã ship** (`indexOf('const khkkMatch')` → slice → eval), key sinh từ `KhkkGroupNumbers`/`KhkkLeafActions` **đọc thẳng MenuKeys.cs** ⇒ 42/42 resolve, 0 drop, **+3 control-âm** (G9→null · action bogus→null · `Khkk_List` nhóm-1-không-infix→null vì chặn ở staticMap trước). Nhãn cũng kèm control-âm (chèn 'x' vào N4 → phép so FLAG) — 0-hit trần vô nghĩa. Ghi chú khuôn: `Khkk_G1` + 6 leaf **KHÔNG-infix** (S159, cấm đổi = mồ côi permission-row), 42 leaf infix chỉ là nhóm **2..8**; giữ `[1-8]` theo spec = rộng hơn tập seed 1 nấc, cố ý. `group`/`filter`/`view` ∉ `TRANSIENT_QUERY_KEYS:311` ⇒ active-state phân biệt (bài `view=approved` S155). SHA-pair types `eba649cc`→**`7a99b865`** ×2 IDENTICAL; Layout 2 app **cố ý KHÔNG mirror** (user +34 regex · admin +13 comment-only, 0 dòng xoá). Build ×2 PASS (user 1950mod `index-B2v0kn5F` 20.8s · admin 1961mod `index-84zDBHE-` 8.2s). 0 `.cs` (MenuKeys/DbInitializer M = lane BE), 0 chạm KhkkListPage/CreatePage/WorkflowPanel/DetailPage. 3 FLAG treo (tên const · `?group=` chưa có consumer → K4b quyết "vắng group = nhóm 1 hay tất cả" · 7 nhóm rỗng admin). Tag `[s166, khkk-k4a-regex-42-leaf, tien-de-lead-sai-x2, derive-khong-hardcode-lan-2, new-function-chay-text-da-ship, control-am-bat-buoc, nhom1-khong-infix, layout-co-y-khong-mirror]`
- **S164/S165 (07-31) K2-FE KHKK nhóm-duyệt N1N8 [relaunch p2 sau p1 chết #53 — chi tiết `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k2.md` §1-§5]:** 🔴 **BÀI CHÍNH — tiền-đề lead SAI, đo `git diff` TRƯỚC khi gõ:** lead khai CreatePage "mid-wire, 8 khai báo chưa vào JSX" (TS6133 sẽ đỏ) ⇒ đo đĩa thật thì **8/8 ĐÃ có call-site**; Detail cũng land đủ. Việc thật ≈ **1/4** mô tả — chỉ ListPage dở (p1 kịp thêm mỗi dòng import). Khuôn S114 "WRITE-lane garble ≠ mất code" xác nhận lần nữa: lead đọc transcript dở-dang → kết luận theo Ý ĐỊNH đang chạy, không theo đĩa. **Viết lại phần đã đúng = phá.** ⇒ p2 chỉ sửa ListPage 3 điểm: badge `N{g}` NHÉT VÀO ô "Mã kế hoạch" (không đẻ cột — bảng đã cuộn ngang @1366 do cây GĐ 19rem, ghi chú S162 nằm sẵn trong file) · +cột "Tổng duyệt" Σ`totalApprovedAmount` (vá-8 spec, BE trả **0** cho phiếu chưa duyệt ⇒ ép `—` kẻo đọc nhầm "đã duyệt 0 đ") · **`colSpan` 7→8 hai chỗ** (loading + empty row — quên = lỗi CÂM tới lúc bảng rỗng). `npm run build` = `tsc -b && vite build` + `noUnusedLocals:true` ⇒ build LÀ phép thử TS6133 thật, không cần tin IDE (#68). cp 4 file → SHA-pair **5/5 IDENTICAL** (Panel không sửa = **đối chứng dương**, hash `2B64144EF6E8` BẰNG baseline ⇒ chứng phép đo đúng thứ). diff-stat đối xứng 269+/36 ×2 app. 0 `.cs` (13 `.cs` M/?? là lane BE), 0 route/menu (K4a). Tag `[s164, khkk-nhom-duyet, do-dia-truoc-khi-go, colspan-loi-cam, badge-trong-o-khong-de-cot, panel-doi-chung-duong]`
- **S161 (07-29) ARC W1+W2+W3đ1 KHKK FE — 3 lượt, chi tiết = sub-md `runs/2026-07-29-S161-khkk-*/` (mọi lượt #53, đĩa cứu trọn):** **W1** Designer type-10: containment **2-LỚP render+payload** (`levelFlagsPayload(e, lock)` ép false 2/2 call-site — chỉ `disabled` = containment GIẢ vì `copyFromDefinition` tuồn state); 2 tiền-đề đề-bài SAI tự bẻ (FE không có bảng nhãn — nhãn từ BE `applicableTypeLabel`; khuôn type-9 không tồn tại → mirror `Contract:3`). **W2** 3 page + types ×2 SHA-pair; types-theo-ENTITY khi BE song song; header types = HỢP-ĐỒNG 12-endpoint (reviewer đối chiếu ra 6 đứt → lead vá; bài: hợp-đồng phải LEAD-định-nghĩa literal); WfView 2-app-2-đường (user matrix?type=10 whitelist+10 · admin Designer deep-link); place-3 menuKeys SKIP-có-khai (0 consumer). **W3đ1** `KhkkWorkflowPanel` mirror PE-panel 1063-dòng theo bảng **GIỮ/BỎ tường minh** (BỎ đủ 2 đường cấm + price-picker/budget/4-radio); cây workflow THÔ — **FE tự suy ✓/●/○ từ con-trỏ đôi** (StepIndex 0-based vs Level.Order giá-trị); bổ `approverUserId` opinion (W2 khai thiếu). Tag `[s161, khkk-fe-3-wave, containment-2-lop, types-theo-entity, giu-bo-tuong-minh, fe-suy-status-con-tro]`

View File

@ -1,5 +1,7 @@
# Lead-Omission-Auditor Agent — Persistent Memory
- **S166 (08-01) DEEP giữa-phiên (OVERDUE 15/15) — `6 FLAG (4H/2M), coverage 6/6` `[LEAD SEED ON-BEHALF TẠI-CỬA — mày #53 ×1 giữa chừng, artifact 28.5KB END:356; nguồn: sub-lead-gap-deep-S166.md + return CLEAN]`:** Nặng nhất = FLAG bắt **CHÍNH LEAD TRONG PHIÊN ĐANG CHẠY**: `/tiep` @S166 đọc `_mind` cắt **165/270 dòng** — nuốt đúng khối "6 ý chỉ-sống-ở-đây" (4 nhánh-LOẠI §B + 2 luật §C MIND-1) mà S165 vừa tốn công NÉN để giữ ⇒ nén-để-giữ mà reader-cửa-sau đọc cụt = công cốc; lead vá NGAY trong phiên (đọc nốt 105 dòng) — **gap bắt được khi nạn nhân còn sống là gap trả rẻ nhất**. Slot **(60)(61) thủng HANDOFF** (0-hit, control-dương cùng-lệnh ra nhà slot khác ✓; dãy CHỜ-ANH nhảy (59)→(62) = bỏ CHỌN LỌC, mồi ctx-audit ghi "chấm @close" mà S163 **không có** @close ⇒ điều-kiện chấm không bao giờ tới). Carry **không re-stamp 3 kỳ** dù HANDOFF bị sửa 3 lần/16 dòng ⇒ ratify `gap-carry-dropped` **8→9 consecutive-audit** (đơn-vị khai TRƯỚC, bài D-4). Backlog owner-verbatim **"phiếu điều chỉnh/thay thế" 0-hit cả 6 sổ** kể cả migration-todos. Checklist diary-seed closeout: **≥8 vai nợ trong khi C11 máy chỉ nêu 3** (máy đo "commit mới hơn" — mù vai chạy-mà-chưa-từng-có-diary-mtime; nặng nhất implementer-backend nợ 4 lượt/3 ngày) ⇒ lead seed 5 vai ngay tại cửa thay vì đợi closeout. 2 honest-zero (carry-aged · incident-unrecorded) kèm lập-luận. Tag `[s166, deep-6-flag, gap-bat-chinh-lead-dang-song, tiep-doc-mind-cut-165-270, slot-60-61-thung, carry-9-consecutive, diary-8-vai-vs-may-3, seed-on-behalf-tai-cua]`
- **S162 (07-30) @close bookend L8 — `4 FLAG` `[LEAD SEED ON-BEHALF @closeout M9 — nguon: sub-lead-gap-close-S162.md 18.638 B + return]`:** **FLAG-1 `gap-carry-dropped` HIGH (streak 8)** — nghi-thức re-stamp carry bị bỏ **lần 2 NGAY SAU khi bị FLAG HIGH @S159**: segment HANDOFF `1d3d167`**0 slug carry**, mà cùng segment đó lead VẪN viết `NEXT em` 1.037 B ⇒ **không phải “bỏ cả khối vì gấp”** mà bỏ chọn lọc. Detector H24-2 in **y hệt số S159** trong khi đĩa còn 219 occurrence/43 slug. **FLAG-2 `gap-decision-sunk` HIGH** — BLOCKER “3 người duyệt PRO/CCM/CEO” **0 hit trên CẢ 4 bề mặt owner** (3 hit “người duyệt” ở STATUS là **dương-giả**, mô tả tính năng PE), sống DUY NHẤT ở `.claude/WAL.md` = **đúng file §6.4 sắp reset**; bất-đối-xứng: món *không chặn* (51-bis) CÓ slot số, món **chặn cả W5→E2E** thì không — im 3 cửa-sổ/2 ngày. **FLAG-3 `gap-owner-specifics` MED** — chữ **CẢ** (“áp CẢ các trang GĐ khác”) + việc lead tự thu hẹp 3/4 trang không lên bề mặt nào; đo cứng `grep -c "Duyệt NCC" .claude/WAL.md` = **0** trong khi chính dòng đó tự khai “nhãn VERBATIM anh” ⇒ **sổ quảng cáo có verbatim mà không chứa verbatim**. **FLAG-4 `gap-underfill` MED**`tiep_reload` hẹn 40-60K tok, đo TRẦN đúng 5 nguồn đã khoanh = 71.492 B ⇒ **[20.4K..23.8K] ≈ ½ sàn**; đọc rộng nhất vẫn [35.3K..41.2K] ⇒ **tuân 100% danh sách VẪN dưới hạn**, và `/tiep` có 0 bước %-print nên **không máy nào thấy**. Honest-zero 2 mục + **trục-6 BÁC** (lead CÓ surface việc thiếu bookend-open ở `run.md:3`). JUMP khai đơn-vị trước (bài D-4): 1 class vượt = `gap-carry-dropped` **8** = **nghi-thức hỏng**, không phải sự cố lẻ. Tag `[s162, 4-flag, carry-streak-8, blocker-song-trong-file-sap-reset, so-quang-cao-verbatim-ma-khong-chua, underfill-muc-tieu-vs-nguon, seed-on-behalf]`
- **S153 (07-26) 2 LƯỢT hình-B — vòng V2 `[lead seed on-behalf @closeout M9; nguồn = sub-file đĩa]`:** @open **3 FLAG** — FLAG-1 `gap-carry-dropped` MED-HIGH (**2 khối NEXT-em cùng đánh số ①②③ song song** HANDOFF:8 @S152 vs :13 kỳ-S151; khối MỚI không nuốt mục MỞ ④⑤⑦ của khối CŨ; mục ⑦ owner-#32 "dựng máy per-item-L1-presence" = 0 hit 5 bề mặt) · FLAG-2 `gap-decision-sunk` MED-HIGH (`trio-synthesis.md:13` ra lệnh "H7 → ghi HANDOFF"; H7 = 0 hit, `convention-only` = 0 hit — **đòn bất-đối-xứng**: H6 cùng dòng cùng mệnh-lệnh ĐÃ landed 1 hit ⇒ loại được bào chữa "cả khối bị bỏ", anh chưa từng được hỏi) · FLAG-3 `gap-underfill` MED **fire lần đầu** (target 210K owner-set @S115 có nghĩa-vụ "em-main manual pour from source_order" nhưng `grep source_order|pour` trên 3 command = 0 ⇒ **không tồn tại bước rót**; khai cận-dưới trung thực: 0-hit là cận-dưới vì rót không để lại artifact).
**honest-zero có giá:** `gap-owner-specifics` = 0 — (30)(31)(32) codified 3/3 giữ trọn specifics; bảng owner-gated O-1…O-7 = **0 khoản câm****tự BÁC 2 FLAG cũ của mình** (FLAG-1@S148 + FLAG-3@S150). Tự BÁC thêm FLAG-2 HIGH @S150 (`_frozen_until_owner` vắng là ĐÚNG — owner đóng (30) frozen=False).

View File

@ -1,5 +1,7 @@
# Lead-View-Auditor Agent — Persistent Memory
- **S166 (08-01) DEEP giữa-phiên (OVERDUE 15/15 trả tại cửa /tiep) — `11 FLAG (4H/5M/2L), coverage 8/8 chạm (2 PARTIAL) ⇒ SÀN` `[LEAD SEED ON-BEHALF TẠI-CỬA — mày #53 ×2 giữa chừng, artifact 23.8KB + resume-3 chốt COVERAGE/END; nguồn: sub-lead-stale-deep-S166.md + return CLEAN cuối]`:** Sợi chỉ xuyên 4 HIGH lặp đúng câu S162: **số đo ĐÚNG nằm ở WAL/run-folder, không tới bảng canonical STATUS/HANDOFF**. Nặng mới: **F-8 slot (59) ĐẢO DẤU** — đo tươi `tiep_reload` 5-nguồn = **180.649 B ≈ 45-60K tok = ĐÃ ĐẠT dải mục tiêu** (tờ trình đang mời anh chọn trên tiền-đề "underfill ½ sàn" ĐÃ CHẾT — trả lời theo tờ trình hiện tại là CHỌN SAI); **F-7 slot (57) hết "sắp vỡ"**: `_INDEX` **101,23% / 251 B** = ĐÃ VỠ. **F-1 canonical-poison chuỗi máy**: STATUS:465-472 rows cũ làm `governance-detectors` Get-StatusValue resolve 55/220 → detector ĐỀ NGHỊ sửa `ef-core-migration/SKILL.md:93` từ **97 (đúng) xuống 96 (sai)** — view-stale không chỉ lừa người, nó **lây qua máy** thành đề-xuất phá số đúng. F-2 bundle :479 0/2 khớp (`B43R6Y17`/`NLI5umBg` chết — thật `mySTlx42`/`CZAYiWWa`; chính cicd đã đính-chính TRÊN ĐĨA @k3 §5 mà chưa ai lật sổ). 2 mục SẠCH kèm control (roster 23/23 · mirror 3-commit-K) — NEXT-em #5 hoá ra ĐÃ XONG mà chưa lật (F-6). Tag `[s166, deep-11-flag-san, dao-dau-slot-59, da-vo-slot-57, canonical-poison-lay-qua-may, so-dung-o-wal-khong-toi-status, seed-on-behalf-tai-cua]`
- **S162 (07-30) @close bookend L8 — `16 FLAG (SÀN, chưa phải TỔNG)` `[LEAD SEED ON-BEHALF @closeout M9 — nguon: sub-lead-stale-close-S162.md 22.035 B + return]`:** class view-stale-count 8 · view-stale-header 3 · view-residual-asym 2 · view-stale-status 2 · view-stale-role-desc 1. Nặng nhất **FLAG-1 `view-residual-asym`**: commit `a8027c0` vừa tạo Mig 69 + 7 bảng **vừa MỞ bảng canonical STATUS**, chỉ vá 2/5 row (policy 220, menu 55) và **bỏ đúng 3 row chính nó làm sai** ⇒ không phải “closeout chưa chạy” mà là **sửa một phía**. **FLAG-7/8 = 2 slot đang CHỜ OWNER trình bằng THƯỚC CŨ**: (51-bis) ghi 97,45%/522 B trong khi tươi **99,18%/167 B** · slot (45) vẫn “RÚT — báo động giả” (đo trên `_mind-s-7` đóng băng) trong khi file SỐNG **89,06%**. **FLAG-11** `agents/README.md:242` tự xưng “ĐỦ roster” liệt **20 tên**/thật **23** — grep-theo-số IM vì file cố ý không chép số ⇒ **phải ĐẾM PHẦN TỬ**; sống 9 nhãn phiên, tái phạm nguyên văn bài S143 cách chính dòng bài học 214 dòng. **FLAG-15** `_context-s-8` PAUSE-3 ghi menu “135→142” trong khi cicd đo **142→142, 0 row thêm** — file nằm trong `tiep_reload.sources`**sai số TÁI SINH mỗi `/tiep`**. 🔴 Câu chốt: **khoảng cách không nằm giữa lead và sự thật, mà giữa NƠI LEAD LÀM VIỆC (WAL/`_mind` — số đúng đủ cả) và NƠI ANH ĐỌC (STATUS/HANDOFF)**. + **9 mục VERIFIED-CLEAN có lệnh bác** (roster 23 · gotcha 84 · menu/policy vừa vá @S161 · ship-synthesis khớp 8/8 sau khi lead vá F-1/3/4/6/7). Coverage khai thẳng PARTIAL/CHƯA-CHẠM (HANDOFF 383/417 dòng · 20/23 persona · run-folder cây 4-folder ~47 KB) ⇒ **cấm đọc phần chưa soi thành sạch**. Tag `[s162, 16-flag-san-khong-phai-tong, sua-mot-phia, slot-owner-thuoc-cu, dem-phan-tu-khong-grep-so, sai-so-tai-sinh-moi-tiep, seed-on-behalf]`
- **S153 (07-26) 2 LƯỢT hình-B (open + close) — vòng V2 `[lead seed on-behalf @closeout M9; nguồn = sub-file đĩa + ring2 verdict]`:** @open **5 FLAG** — FLAG-1 `view-residual-asym` MED (STATUS:454 khai 3 derived-cite `:3/:37/:113`, đo lại **3/3 sai dòng**, đúng là `:5/:48/:315`) · FLAG-2 `view-stale-header` LOW (:439 "mốc gần nhất S151" trong khi :457 re-stamp @S152; `git show c2d01f0` 3 hunk, 0 chạm :439) · FLAG-3 `view-stale-count` LOW (HANDOFF carry-age 12/9/7 vs máy 13/10/8 — lệch **+1 đều 3**, nguồn = chính đoạn carry vừa append ⇒ **self-referential count**) · **FLAG-4 `view-stale-status` HIGH** (STATUS:454 còn khai trio "consent-gated + CHƯA spawn-probe" trong khi owner (32) đã AUTO **và** chính `:466` cùng file nói ngược — bác được không cần ra ngoài file) · FLAG-5 `view-stale-status` MED (H24 nhịp-`{6/15/3}` vs hình-B vô-điều-kiện; đối-chứng hành-vi: lượt này chạy @counter 27 với light 1/6 chưa tới hạn).
**ring2 chấm 8/8 ĐẠT** + **thách-CLEAN bắt M-1 MISS THẬT của mình**: persona `lead-stale-auditor.md:51-54` còn dạy "KHÔNG chạy mỗi phiên / thuần cadence" — hình-B đã vô-điều-kiện từ S149, và `398d343` chính đợt land hình-B mở file sửa mà để nguyên câu sai. 🔴 **Bài:** mình bắt ĐÚNG class đó ở STATUS (FLAG-5) nhưng **sót chính persona mình** ⇒ điểm mù **tự-quy-chiếu**; `.claude/agents/` NẰM TRONG tập-đo (đã dùng `agents/README`), chỉ 2 file persona bị bỏ ⇒ lần sau grep tập-đo PHẢI gồm persona của chính mình.

View File

@ -1,17 +1,12 @@
# Reviewer Agent — Persistent Memory
- **S166 (08-01) gate K4a 49-row menu KHKK 8-nhóm — PWF 4/0 blocker:** lõi ĐÚNG hiếm thấy (bộ-sinh 1 nguồn feed 3 phía `All`/seed-row/grant ⇒ khớp by-construction; 8/8 nhãn khớp TỪNG KÝ TỰ; suite **620/620 tôi tự chạy**). **Bài thu:** ① 🔴 **tham-số-trang-trí** — URL leaf mang `?group=n``grep "get('group')"` ×2 app = **0 hit** ⇒ 7 nhóm × 6 leaf mở ra CÙNG nội dung, `WfView` hiện toàn bộ workflow type-10; *đo "route có tồn tại" là chưa đủ, phải đo **AI ĐỌC** từng param* (đối xứng ca S164 "tham-số-chết-vì-thiếu-tầng-UI", lần này chết ở tầng ĐỌC). ② **mỏ-neo tự-vô-hiệu ngay lúc land** — 2 comment mới trỏ dòng trong CHÍNH file đang sửa, sai đúng **+34 = kích thước khối vừa chèn** (tác giả tính trên bản trước khi chèn); cùng diff đó vừa DỌN lỗi y hệt ở BE (`1893`) ⇒ **mỏ neo trỏ vào file mình đang sửa phải đo SAU khi chèn**, hoặc bỏ số dùng grep-anchor. ③ **ô canonical sai đo-được bằng 1 curl** — STATUS ghi bundle `mySTlx42`/`CZAYiWWa`, live = `1yiNV4VH`/`DoULfmdT` (2 css KHỚP ⇒ chứng đo đúng site, không phải đo nhầm). ④ **bất-biến từng-vỡ-thật lại là bất-biến 0 test** (revoker không đụng `Khkk_*`; thêm 1 chữ vào filter thì 620/620 vẫn xanh, đúng kịch bản Run #423 mà chính comment dẫn lại) — khuôn có sẵn `Revoke_DoesNotTouch_PeModule`. ⑤ test gọi seeder LẺ qua reflection ⇒ chuỗi `SeedAsync` (grant→revoke→site-3) ngoài vùng test ⇒ **restart-thật KHÔNG được coi là đã phủ**. Tag `[s166, tham-so-trang-tri, moneo-tu-vo-hieu, o-canonical-sai-1-curl]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4a.md`
- **S166 (08-01) vá F-1 ô-tích opt-out `applyLevelFinalize` KHKK — PWF 7 finding (3 MAJOR):** vá ĐÚNG kỹ thuật (mirror 2 app chứng bằng **blob-SHA in ngay trong `git diff` index-line** — rẻ hơn sha256 tay; body không rò field; cấp thường bất-biến từng bit). **Bài thu:** ① 🔴 **khuôn-nguồn có 2 tầng, chú-thích nói dối tầng dưới** — implementer trích ĐÚNG dòng JSX PE `:772-791` + nhãn "khuôn PE S96 default tick", nhưng `useState` PE `:61` = `false` (S97 owner **ĐẢO** sang opt-IN) và chính chú-thích PE `:769-771` cũng stale ⇒ **đo khuôn phải đo GIÁ-TRỊ KHỞI-TẠO, không đọc chú-thích**; tái phát S165 F-1 (đo sai tầng). ② **cây làm việc PHÌNH GIỮA LƯỢT REVIEW** — đo `git status` 2 lần cách ~40 tool-call: 5 file→7 file (+121 dòng K4a, `All` 64→113) ⇒ **verdict phải neo mốc đo + re-đo `--stat` TRƯỚC khi phát verdict**, nếu không lead commit ké code chưa qua cổng. ③ vắng-mặt-đọc-thành-sạch: `isApproved ⇒ mọi Bước 'Done'` ⇒ CEO chưa đụng vẫn "đã duyệt", dấu-vết duy nhất = THIẾU dòng "✓ ký". ④ **sợi-dây-1-argument** (projection `l.AllowApproverFinalize`) 0 test ⇒ xoá là control biến mất mà 614 test + tsc vẫn xanh. Tag `[s166, khuon-2-tang-chu-thich-noi-doi, cay-phinh-giua-review, sợi-day-1-argument]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-f1-checkbox.md`
- **[→ archive/2026-07.md @S166-curate] S164/S165 (07-31) gate K3 KHKK — PWF 8 finding/0 blocker:** **tham-số-chết-vì-thiếu-tầng-UI** (BE port đủ 3 tầng + seed cờ, panel gửi body literal 2 field ⇒ trạm CEO chết 8/8) — *claim "port khuôn X" phải đo TẦNG-CUỐI, ai GỬI tham số* · **sweep 2-cờ bất-đối-xứng** (FE gỡ 2 ô, BE gỡ 1 ⇒ tick bị nuốt im lặng) · lead cãi spec ĐÚNG khi có phép đo (`grep typeEnum` = 0 hit trong thân handler) · flake 1/3 lượt ⇒ "N/0 tươi" phải khai SỐ LƯỢT · DELTA STATUS chưa khai.
- **[→ archive/2026-07.md — verbatim ĐÃ VERIFY moved-not-cut 4/4 @S166-curate] S164/S165 gate K3 · S164 gate K1 · S162 cây-4-folder · S161-W2 KHKK-CRUD (digest):** tham-số-chết-vì-thiếu-tầng-UI (đo TẦNG-CUỐI ai GỬI) · sweep 2-cờ bất-đối-xứng · flake ⇒ "N/0 tươi" phải khai SỐ LƯỢT · giá-trị lớn nhất ở GATE QUY-TRÌNH không ở mã · **ghost-policy** (grep policy, đừng suy từ "key đã vào All") · default-lấn-ngữ-nghĩa · im-lặng-về-độ-chính-xác-của-con-số ×3/diff · **#82 ngược chiều** (FE OR nhiều key LỎNG HƠN policy khớp-CHÍNH-XÁC-1-key ⇒ tự chuốc 403) · hợp-đồng FE↔BE VỠ 6 điểm mà tsc+build+574-test XANH (2 lỗi CHE NHAU) ⇒ so ROUTE+FIELD trên ĐĨA.
- **[→ archive/2026-07.md @S165-curate] S164 (07-31) gate K1 danh mục SP-002 (Mig 70) — PWF 9 finding/0 blocker:** giá-trị lớn nhất ở **GATE QUY-TRÌNH** không ở mã (commit gộp = vượt quyền owner; seeder 0 nhánh update) · 2 quyết-định hợp-lệ ghép nhau đẻ **ghost-policy** (4 policy, 0 endpoint tiêu thụ — đo `grep policy`, đừng suy từ "key đã vào All") · **default-lấn-ngữ-nghĩa** `IncludeInactive?``IsActive?` · DELTA đo được mà STATUS chưa khai.
- **[→ archive/2026-07.md @S165-curate] S162 (07-30) cây 4-folder GĐ (FE-only) — PWF 8 flag:** im-lặng-về-độ-chính-xác-của-con-số ×3 cùng diff · **#82 ngược chiều** (FE OR nhiều menu-key LỎNG HƠN policy khớp-CHÍNH-XÁC-1-key ⇒ tự chuốc 403) · nhãn ✓ không có phép đo.
- **[→ archive/2026-07.md @S162-curate] S161-W2 (07-29) KHKK CRUD 2-lane song song:** hợp-đồng FE↔BE VỠ 6 điểm mà tsc+build+574-test XANH (2 lỗi CHE NHAU) ⇒ so ROUTE+FIELD trên ĐĨA (lời-khai-lane = GIẢ ĐỊNH); list-bịt-detail-mở; INNER-join global-filter = picker vanish.
- **[→ archive/2026-07.md @S159/@S161/@S162-curate] S159-đợt5 · S161-W1 · S155-đợt2 (digest):** chú-thích-mang-SỐ phải chạy phép đếm · cookie-cutter phải soi CẢ phần bản gốc đã CHẶN · `tsc`+`noUnusedLocals` > grep cho claim 'đã cắt' · **HIGH nằm ở `git`** (mig UNTRACKED mà snapshot tracked ⇒ gate `git status --porcelain -- src tests | grep '^??'` RỖNG trước commit) · comment 'bên kia lo' = ĐI ĐỌC BÊN KIA · grep bằng TOKEN-SONG-SINH · `hợp-đồng-đứt-2-bờ` (FE khai cờ, BE DTO không có ⇒ nút chết mà build+test xanh) · re-đo đĩa TRƯỚC verdict · đổi-chiều-assert ≠ nới-assert.
- **[→ archive/2026-07.md @S145/@S145b/@S155-curate · @S140] S145 4-lane · S143 · S139 · S134b · S133 · S146/S147/S152-D2 (digest):** bất-biến chia-đôi giữa 2 axis chỉ cross-cut sweep bắt · sweep CẢ numeric LẪN name-enum · `bằng-chứng-tự-huỷ-sau-squash` · re-measure acceptance từ git HEAD + no-self-exempt · insertion-only `grep '^-'`=0 · 'mọi nhánh set X' phải grep assignment-qua-BIẾN (#81) · `ExecuteDelete` ngoài transaction = mất dữ liệu · test đang KHOÁ chiều ngược CẤM nới assert · xanh-giả bắt bằng phép 2-thế-giới · 3 THƯỚC-HỎNG (md5 CRLF · `grep -c` citation-trap · `grep -iF` MSYS 0-hit im lặng).
- **[→ archive/2026-07.md — verbatim, verify moved 5/5 @S166-curate] S159-đợt5 · S161-W1 · S155-đợt2 · S145-4lane · S143 · S139 · S134b · S133 · S146/S147/S152-D2 (digest gộp):** chú-thích-mang-SỐ phải chạy phép đếm · cookie-cutter soi CẢ phần bản gốc đã CHẶN · `tsc`+`noUnusedLocals` > grep cho claim 'đã cắt' · **HIGH nằm ở `git`** (gate `git status --porcelain -- src tests | grep '^??'` RỖNG trước commit) · comment 'bên kia lo' = ĐI ĐỌC BÊN KIA · grep bằng TOKEN-SONG-SINH · `hợp-đồng-đứt-2-bờ` · re-đo đĩa TRƯỚC verdict · đổi-chiều-assert ≠ nới-assert · bất-biến chia-đôi 2-axis chỉ cross-cut sweep bắt · `bằng-chứng-tự-huỷ-sau-squash` · re-measure acceptance từ git HEAD + no-self-exempt · 'mọi nhánh set X' phải grep assignment-qua-BIẾN (#81) · `ExecuteDelete` ngoài transaction = mất dữ liệu · test đang KHOÁ chiều ngược CẤM nới assert · xanh-giả bắt bằng phép 2-thế-giới · 3 THƯỚC-HỎNG (md5 CRLF · `grep -c` citation-trap · `grep -iF` MSYS 0-hit im lặng).
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).

View File

@ -1,5 +1,7 @@
# ring2-audit Agent — Persistent Memory
- **S166 (08-01) KIỂM cặp H24 DEEP giữa-phiên — `16Đ/1T trên 17 FLAG` `[LEAD SEED ON-BEHALF TẠI-CỬA — mày #53 ×2 (lượt-1 sau tally-ĐẠT · lượt-2 câu "Now the falsify log…" NHƯNG đĩa đã TRỌN 31.298B END:418 — return tụt sau đĩa, 0 mất); nguồn: sub-ring2-audit-deep-S166.md]`:** stale 10Đ/1T · gap 6Đ/0T. 🔴 **TRƯỢT duy nhất = stale F-8, và đây là ca ĐẮT NHẤT lifetime của mày: quan-sát ĐÚNG (HANDOFF:19 stale thật) nhưng ĐO SAI PHẠM-VI (đọc `:1-117` nuốt 7 segment `Last updated` cũ thay vì segment sống `:1-40`) ⇒ thổi +60.016 B ⇒ KẾT-LUẬN LẬT NGƯỢC ("đã đạt dải" trong khi đúng phạm-vi ~30-40K VẪN DƯỚI sàn) — và LEAD ĐÃ CHÉP CÁI SAI VÀO WAL:33 trước khi mày chấm** ⇒ nếu closeout thi hành resolve thì owner-decision (59) bị đóng bằng tiền-đề sai NGƯỢC CHIỀU. Mày bắt = vòng-kiểm chứng minh giá trị bằng đúng 1 ca; luật rút cho lead: **claim ĐẢO-DẤU/LẬT-TIỀN-ĐỀ của vai-đo chỉ vào sổ SAU vòng-kiểm**. Nghề lặp ăn tiền lần 2: `git show HEAD:` cứu FLAG-3 stale khỏi cáo-buộc-oan (lead vá STATUS @09:21 SAU khi vai đo @09:05 — cây bẩn giữa phiên, kéo HEAD mới phán được; y bài S162). Falsify có 1 HELD tự-bắt-mình: mig-count 70 của tao = lỗi filter `grep -v Snapshot` nuốt `AddPeApprovedBudgetSnapshot` — tự khai thay vì đổ cho con-đo. Honest-zero `gap-incident-unrecorded` CÓ RĂNG (§6a); coverage PARTIAL của stale GIỮ NGUYÊN nấc, cấm làm tròn (§6c). 5 errata INFORM nhỏ (27→24 · lệnh grep không tái-lập · diary-size-drift-vì-lead-seed-giữa-lượt). Tag `[s166, 16-1, do-sai-pham-vi-lat-nguoc-ket-luan, lead-chep-truoc-vong-kiem, git-show-HEAD-cuu-oan-lan-2, falsify-tu-bat-minh, seed-on-behalf-tai-cua]`
- **S162 (07-30) KIỂM vòng soi-lead @close`19Đ/1T trên 20 FLAG` `[LEAD SEED ON-BEHALF @closeout M9 — nguon: sub-ring2-audit-close-S162.md 22.744 B, END-line 189/191, mtime 15:01:59 > 14:35 = chứng C4b tuần-tự]`:** lead-stale 15Đ/1T · lead-gap 4Đ/0T. 🔴 **Điểm TRƯỢT duy nhất = FLAG-16 ÉP-VỪA ENUM** (quan-sát ĐÚNG, class SAI): `git show a8027c0~1` cho `b.ToTable(` = **88**, HEAD = **95**, `CreateTable` = **7** ⇒ “88→95” **đúng và ĐANG đúng** trong đơn-vị EF-snapshot; định-nghĩa `view-stale-count` đòi “số cũ sau khi source đã đổi” mà source **không đổi** ⇒ vị-ngữ không thoả. Không class nào trong 11 phủ “số đúng, đơn-vị không-canonical” ⇒ **đúng luật phải escalate mở rộng enum, KHÔNG ép vừa**. Hệ-quả máy: thổi `view-stale-count` **8 thay vì 7**, chảy vào `class_repeat`/`jump_on_class_repeat=3`, và mâu-thuẫn chính dòng kết “0 finding rơi ngoài enum”. Công bằng: con-đo tự viết “Delta +7 đúng cả hai” + tự hạ LOW ⇒ **không thổi số về NỘI-DUNG, chỉ sai chỗ nhét**. 🔴 **TIME-DRIFT suýt tạo 2 cáo-buộc oan, cứu bằng `git show HEAD:`** — cây làm việc BẨN (lead vá STATUS 14:38 / HANDOFF 14:40, SAU khi cặp đo 14:33/14:35); chứng cơ-học: lead-stale trích `:649/:660/:667/:674`, cây tươi đã trôi `+7 dòng` thành `:656/…`, bản HEAD khớp cả 4 verbatim ⇒ **vá-sau KHÔNG phải flag-oan**. Falsify 6 (3H/3B) — **2 BROKE nhắm vào nghi-vấn của CHÍNH TAO**: nghi lead-gap bịa `jump_on_class_repeat=3` (key thật ở `memory-budget.json:165`) · nghi nó khai sai `"3 trạm"=0` (đo lại ở HEAD ra 0/0/0/0 tuyệt đối). Thách-CLEAN đảo chiều (rủi ro = **thổi số**, không phải im): bia-1 BẮT ĐƯỢC (F16) · bia-2 **KHÔNG bác được** (F14 — đọc NỘI DUNG dưới tiêu-đề thấy Q1-Q6 + lỗ hổng an ninh + UAT còn mở ⇒ khối đang gánh việc HIỆN TẠI mà mang nhãn phiên đã đóng) · bia-3 VERIFIED-CLEAN #7 có thật (đóng lại **M-1 của chính tao @S153**, 0 tái phát). Enum 0/20 tự chế. 2 INFORM cho lead: G2 chưa đạt thước của chính nó (`"PRO/CCM/CEO"` vẫn 0 hit trong HANDOFF lúc đo — **lead đã vá ngay: bỏ dấu cách quanh dấu gạch**) · errata offset `memory-budget.json:59` → key thật `:58`. Tag `[s162, 19-1, ep-vua-enum-f16, time-drift-cuu-bang-HEAD, thach-clean-dao-chieu-thoi-so, 2-broke-vao-nghi-van-cua-minh, seed-on-behalf]`
- **S153 (07-26) KIỂM vòng V2 (soi-lead H24) `[lead seed on-behalf @closeout M9; nguồn = sub-ring2-S153.md]`:** **8/8 FLAG ĐẠT / 0 TRƯỢT / 0 KHÔNG-CHẤM + 10 phép phụ** · falsify 6 (5 HELD, **1 BROKE là nghi-vấn của chính mình**: nghi `h24_cadence` là pointer treo → probe đường khác thấy key có thật top-level `:146-151` = 6/15/3 ⇒ con-đo **kiềm chế ĐÚNG**, ghi VERIFIED-CLEAN thay báo-động-giả).
**Tái-dựng MẠNH hơn endpoint:** kéo bản TRƯỚC bằng `git show HEAD:.session-counter.json` rồi so **delta từng key** — 7/7 class fired = **+1 chính xác**, 4/4 non-fired = **0**, 11 key cả 2 phía, mọi value `int` (bất-biến `2f39a7e` còn sống), `light_at_counter` 26→27, `signal_session` S152→S153. Ngữ-nghĩa đúng: `view-stale-status` fire 2 FLAG nhưng counter chỉ +1 (đếm **vòng-đo**, không đếm flag).

View File

@ -1,19 +1,21 @@
# Test-Specialist Agent — Persistent Memory
- **S166-K4a (08-01) 49 row menu KHKK 8-nhóm — **7 test** (6 gốc + T7 sau resume), file MỚI `Application/KhkkGroupMenuSeedTests.cs`, return SẠCH `[baseline đo tươi Infra 569 → sau 576, Δ+7, 0 hồi quy · Domain 45 · tổng 621]`:** **BƯỚC ĐẮT NHẤT = TÌM CHỒNG LẤN TRƯỚC KHI VIẾT**`ContractCatalogTests.cs:445` (T6 lane K1) đã assert `MenuKeys.All.Except(menuKeysInDb).BeEmpty()` sau `SeedMenuTreeAsync` ⇒ invariant "49 key mới đều có menu-row" TỰ ĐỘNG được phủ, và nó XANH ở baseline = chứng nhân độc lập cho claim by-construction ⇒ KHÔNG nhân bản, chỉ viết cái nó không đo. **Kỹ thuật chính (tái dụng S164 ma-trận-cùng-ruột):** T5 labelBackfill bôi hỏng nhãn **CẢ `Khkk_G1` LẪN `Khkk_G2` trong 1 lần** → seed lại → G1 tự lành / G2 giữ chữ hỏng; khác biệt duy nhất = có/không entry trong `labelBackfill` ⇒ chứng đúng cơ-chế backfill chứ không phải upsert "tiện tay" ghi Label (gotcha #11). T6 đối-chứng-cùng-lượt: cùng role, `Pe_DuyetNcc_List` CanCreate=**true** vs 49 key Khkk CanCreate=**false** ⇒ chuỗi "false" không phải do seeder no-op. T4 mirror đo **DB↔DB** (leaf nhóm 2..8 so với row nhóm 1) + chốt trước "6 nhãn nhóm 1 PHÂN BIỆT" — thiếu vế này thì mirror đúng ở mọi thế giới. T1 kỳ vọng = **mảng 49 chuỗi VIẾT TAY**, KHÔNG gọi lại `MenuKeys.KhkkGroup/KhkkLeaf` (gọi lại = tautology). 2 bẫy đo đã né: (i) đếm họ `Khkk` bằng **Ordinal in-memory** chứ không `StartsWith` → LIKE SQLite case-insensitive ASCII; (ii) kiểm KHÔNG có `HasData` cho MenuItems ⇒ 56 row đến TỪ seeder, không phải EF-seed. Số ĐO: `MenuKeys.All` runtime = **113** (Policies DERIVED 452) — trùng đếm by-source lane BE. 0 bug prod. Tag `[s166-k4a, tim-chong-lap-truoc-khi-viet, ma-tran-2-key-1-bien, doi-chung-cung-luot, ordinal-khong-LIKE]`
- **+T7 (resume, gate trả finding D MAJOR) `Revoke_DoesNotTouch_KhkkGroups` → 7/7, Infra 576, tổng 621:** bảo vệ ca ĐÃ VỠ THẬT (Run #423 447/494 row rơi, gotcha #84). **Bẫy vacuous đọc ra TỪ MÃ chứ không từ prompt: `:2546-2551` Admin-role-chưa-seed ⇒ revoker RETURN SỚM** ⇒ test không seed Admin sẽ xanh vì revoker CHƯA CHẠY → bắt buộc **control dương** (2 key `Off_*`/`Personal` của CHÍNH 2 role đó phải về false-4-cờ) + vế Admin-giữ-true + vế 112 row Khkk sống. **Chứng răng bằng PROBE VI-PHÂN** (cấm chạm prod ⇒ không fault-inject được): file tạm dựng lại đúng hình query revoke + thêm ĐÚNG 1 vế `StartsWith("Khkk")` → ĐO 112→0 ⇒ T7 phân biệt 2 thế giới; probe xoá + verify 2 chiều (git sạch ∧ suite = 569+7 chứ không +8). Đếm bằng **đơn vị của sự cố thật** (row sống sót) để thông điệp lỗi cùng ngôn ngữ với Run #423.
- **S164-K3 (07-31) KHKK level-finalize + pin-đúng-nhóm — 5 test, return SẠCH `[baseline đo tươi 609/0 = 45D+564I KHỚP đề bài · sau: 614/0, Δ+5, 0 hồi quy]`:** +4 vào `ContractSigningPlanApprovalTests.cs` (T-a/b/c finalize + T-e giữ-cờ), +1 vào `ContractSigningPlanGroupAndCatalogTests.cs` (T-d rào (v)) — chọn file thứ 3 thay "file mới" để KHÔNG nhân bản 70 LOC `SeedPeForCreateAsync`+`CreateAsync` đã có sẵn ở đó. **Kỹ thuật chính = MA TRẬN cùng-ruột-khác-1-biến thay cho fault-inject** (không được chạm prod, mà prod đang mang diff lane khác): 1 `SeedFinalizeScenarioAsync` → T-a (cờ+apply=true ⇒ DaDuyet+chốt giá) / T-b (**cùng seed, đổi ĐÚNG `applyLevelFinalize:false`** ⇒ (2,1) advance thường) / T-c (Cấp không cờ + default true ⇒ (0,2)). Bỏ qua cờ ⇒ T-a đỏ · bỏ qua param ⇒ T-b đỏ · `||` thay `&&` ⇒ T-c đỏ. **Điều kiện SỐNG CÒN: đặt cờ ở Bước-2-Cấp-2 của quy trình 3 Bước** — cờ ở cấp cuối thì "kết thúc" xảy ra ở CẢ HAI thế giới ⇒ T-a xanh mà chứng minh 0 (assert `StepCountAsync==3` để chốt). T-d: **CÙNG workflow `KHKK-N5` cho 2 kết quả trái ngược** (phiếu nhóm 3 = 409 · nhóm 5 = qua) + không-đốt-mã (rào (v) `:409-420` đứng TRƯỚC codegen `:428`) có control-dương 001/002/003 tăng thật. Kiểm dữ-liệu chứ không chỉ đọc regex: `DbInitializer.cs:609` sinh đúng `$"KHKK-N{n}"` ⇒ rào neo thật. T-e: 3 vế (tick⇒true · không-tick⇒false · **đối chứng type-1 cũng true** ⇒ ECHO chứ không phải đổi ép-false thành ép-true). **CỐ Ý KHÔNG pin `AllowApproverSkipToFinal==true`** dù mã đang echo: service KHKK 0 lần đọc cờ đó (`:47` khai "không port") ⇒ đó là ứng-viên config-lie #78 CÒN SÓT; pin nó = biến khả-năng-lỗi thành yêu cầu → REPORT lead thay vì test. 0 bug prod. Tag `[s164-k3, level-finalize, ma-tran-cung-ruot-1-bien, co-o-cap-cuoi-lam-rong-phep-do, khong-pin-config-lie]`
- **S161-W2 (07-29) KHKK CRUD 8 test `[hmw wf_7490179a-70b · chết #53 sau khi file LAND (sub-md dừng §0.1) · lead chạy suite tươi 574/0 = 566+8, cả 8 tên đúng]`:** `Application/ContractSigningPlanCrudTests.cs` (7) + extend `Api/AuthorizePolicyRegressionTests.cs` (PIN reflection). 2 PIN spec + 6 acceptance: happy-path Lines=winners + SUM IsSelected đúng NCC (bẫy `w.Id` vs `w.SupplierId`) · second-create 409 **`_AndDoesNotBurnCode`** (mã không đốt khi Conflict — phép đo sequence side-effect) · workflow-guard 2 nhánh Conflict/NotFound · no-winner · **soft-delete-then-recreate + changelog giữ** · owner/phase guard. Phát hiện đắt (sub-md §0.1): **TestApplicationDbContext KHÔNG wire AuditingInterceptor ⇒ `Remove()` = xoá CỨNG ⇒ đo "xoá-tạo-lại" trên xoá-cứng = ĐO NHẦM CƠ CHẾ** (rào 1-PE-1-KH dựa global-filter che row soft-deleted, không phải row biến mất) → dựng fixture riêng CÓ interceptor cho nhóm delete-test. Baseline đo tươi trước khi viết (không tin số đề bài). Tag `[s161-w2, khkk-8-test, khong-dot-ma-khi-409, fixture-interceptor-cho-soft-delete, do-nham-co-che-xoa-cung]`
- **S161 (07-29) W1 KHKK: 4 test schema+codegen `[hmw wf_a95c9909-14b · chết #53 LÚC ĐỢI full-suite (việc thật đã xong) → sub-md 10KB ghi-từng-mục cứu trọn; lead chạy full-suite tươi 566/0 + điền §2/§4 on-behalf]`:** File MỚI `Common/ContractSigningPlanSchemaTests.cs` — 4 test: (1) PIN-spec `BayBang_MoiBang_CoAuditFields_VaQueryFilter` (quét EF model theo NAMESPACE → `BeEquivalentTo` đúng bộ 7 tên [bảng #8 lọt = ĐỎ] + audit-fields + soft-delete assert 2-CHIỀU [BaseEntity phải KHÔNG có IsDeleted] + seed IsDeleted=true chứng filter là BIỂU-THỨC-THẬT); (2) PIN `SigningPlanCodeGenerator_Format_KhkkYearSeqD3` — regex + tuyệt-đối 001→002 + row LastSeq==2 + **đẩy đồng hồ 2027 → KHKK/2027/001 reset** (thiếu vế này `\d{4}` xanh cả khi năm hardcode); (3) #57 xoá-mềm-thêm-lại KHÔNG nổ UNIQUE + vế đối chứng 2-dòng-sống PHẢI DbUpdateException (không có = xanh cả khi index chưa từng tạo); (4) pin slot 10 enum. **Fault-inject 2 chiều trên bảng-kỳ-vọng rồi revert** — chứng `GetDeclaredQueryFilters()` phân biệt 2 thế giới. Learned: (1) **prompt lead ≠ đĩa** — LevelOpinion là AuditableEntity nhưng CỐ Ý KHÔNG filter (lý-do ghi 2 chỗ: site orphan-purge phải thấy row soft-deleted) ⇒ test theo CODE + assert cái-cố-ý thành invariant có răng; (2) EF Core 10: `GetQueryFilter()` obsolete-prone → `(IReadOnlyEntityType).GetDeclaredQueryFilters()`; (3) Serializable trên SQLite = graceful-map, tiền lệ `SqliteDbFixture.cs:36-42` + PE codegen test — KHÔNG chế fixture mới; (4) KHÔNG `Remove()` đo xoá mềm (TestCtx không wire AuditingInterceptor → xoá CỨNG, bài S155). Tag `[s161, w1-4-test, faultinject-2-chieu, test-theo-code-khong-theo-prompt, clock-2027-reset, 53-luc-doi-suite]`
- **S155 (07-27) ĐỢT 2 PE xóa-khi-đang-duyệt + màn "Đã xóa" [run 2026-07-27-S155-pe-delete-approver · sub-d2-test-4, diary 13.9KB ghi-trong-lúc-làm]:** +10 `PeDeleteByApproverTests.cs` (T21 lũy kế **4 HƯỚNG KHÁC NHAU** · T22/T22b/T22c màn Đã xóa · T23/T24/T24b cờ+lượt · T25 authz reflection · T25b chiều thuận · T26 changelog). Full **561 = 560 PASS / 1 FAIL** (45D + 516I); 0 hồi quy (Infra cũ 506 nguyên vẹn). **1 FAIL = T26 GIỮ ĐỎ = BUG PROD**: `PeSoftDeleteFeatures.cs` Add changelog (:126) → `Remove(pe)` (:142) → `CascadeDeleteTiming.Immediate` cascade **ngay tại Remove**; dependent đang `Added` bị cascade ⇒ **Detached** (không phải Deleted) ⇒ 0 row; `AuditingInterceptor` chạy ở SavingChanges = SAU điểm mất ⇒ không cứu. Đo bằng ChangeTracker diag (`Added→Detached`, `rowsInDb=0`), KHÔNG suy đoán. Grep cùng-lớp: site DUY NHẤT có hình "Add con → Remove cha → 1 SaveChanges". REPORT lead, KHÔNG tự sửa. **Hạ tầng MỚI:** `SoftDeleteFixture` (ngay trong file test, ~90 LOC) = fixture ĐẦU TIÊN của repo wire `AuditingInterceptor` — BẮT BUỘC khi đo `Remove()`=xóa-mềm, vì `TestApplicationDbContext` KHÔNG wire ⇒ mặc định **XÓA CỨNG** ⇒ màn "Đã xóa" rỗng ⇒ assert đúng ở CẢ HAI thế giới (đúng bẫy vacuous). Số học T21 nhớ kỹ: xóa phiếu ChoDuyet ⇒ PrevSubmitted↓ · PendingSelected↓ · **PrevSelected + PendingSubmitted BẤT ĐỘNG** (seed thêm peer DaDuyet + TraLai để 2 số "không đổi" ≠ 0 ở cả 2 phía). Tag `[s155, pe-delete-by-approver, cascade-detach-added-child, softdelete-fixture, 4-huong]`
- **S155 (07-27) PE xóa-khi-đang-duyệt +10 → 561 (1 FAIL GIỮ ĐỎ = BUG PROD cascade-detach-Added-child):** 2 bài SỐNG CÒN còn dùng: (i) **`TestApplicationDbContext` KHÔNG wire `AuditingInterceptor``Remove()` = XÓA CỨNG** — đo xóa-mềm phải dựng fixture riêng có interceptor, không thì assert đúng ở CẢ HAI thế giới; (ii) "Add con → Remove cha → 1 SaveChanges" = con `Added` bị cascade thành **Detached**, mất row. Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S155.
- **S153 (07-26) T4 ctx-adopt FAULT-INJECT máy `mind-check` #8-#17+#21 [run 2026-07-26-S153-ctx-adopt · lane trước đứt sau #7, file đĩa nguyên → tiếp được] `[engine: opus-5]`:** **18/18 PASS 0 FAIL**. KHÔNG phải test C# (**baseline 509 KHÔNG đổi**) — đối tượng = `scripts/session_ctx.py mind-check`, mọi mutation trong cây tạm qua seam `--root`, repo read-only. Bắt **1 GAP THẬT**: ở closed-mode **XOÁ con-trỏ → cờ/exit 0** còn **GIỮ con-trỏ đang-chờ trung thực → TRƯỢT/exit 1** ⇒ đường-rẻ-nhất-để-xanh là đường SAI (chữ luật cấm, gradient thưởng) → REPORT em main, KHÔNG tự sửa. **Bài học mới quan trọng nhất:** fixture `draft-ts` MALFORMED ra exit 0 — `inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find`=-1 ⇒ chèn GIỮA heading ⇒ `ts:` không ở đầu dòng ⇒ máy ĐÚNG mà vẫn xanh. **Fixture-hỏng cho-xanh trông Y HỆT máy-đúng cho-xanh** phải soi RUỘT fixture (`cat -A`/`repr` từng dòng), KHÔNG chỉ đọc exit code. 2 kỹ thuật xài lại: (i) **cặp CÙNG-RUỘT khác 1 biến** — cap=32 vs cap=1 trên cùng tệp = chứng đọc-config-LIVE; `_end` giả `_pause=9` vs không = chứng nguồn-đếm-là-ĐĨA, dùng `diff` 2 output để chứng dòng đo BYTE-IDENTICAL; (ii) **đo wall-clock phải gồm nhánh ĐẮT** — lệnh literal đi nhánh early-exit (repo chưa có `_mind`) = tự đo nhánh rẻ rồi gọi là chi phí thật; đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×, giao lead điền lại. Tag `[s153, t4-fault-inject, mind-check, fixture-bug-vacuous-pass, cung-ruot-1-bien]`
- **S153 (07-26) fault-inject máy `mind-check` 18/18 (KHÔNG phải test C#, baseline không đổi):** 3 bài còn dùng: (i) **fixture-HỎNG cho-xanh trông Y HỆT máy-ĐÚNG cho-xanh** ⇒ soi RUỘT fixture (`cat -A`/`repr`), đừng chỉ đọc exit code; (ii) **cặp CÙNG-RUỘT khác 1 biến** = chứng đọc-config-LIVE; (iii) đo wall-clock phải đi **nhánh ĐẮT** (early-exit = tự đo nhánh rẻ rồi gọi là chi phí thật). Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S153.
- **S134 (07-17) PE lũy-kế TẠM TÍNH TEST-C [wf_97d6a582-b7e · return sạch 0-garble] `[engine: opus-worker]`:** +8 `PePendingAccumulationTests.cs` C1-C8 (file MỚI, mirror seed PeBudgetFreezeTests :277-336): C1 ChoDuyet+winner→delta2+PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 nháp list-only/TraLai delta1 · C4 window CreatedAt · C5 delta âm signed · **C6 complementary-no-double-count: gọi CẢ ComputeAsync + ComputePendingAsync cùng seed, assert EXACT-NUMBERS 2 phía (PrevSelected 90tr/1 bất động ⟂ PendingSelected 50tr/1)** · C7 frozen→0/null qua query handler · C8 D4 changelog positive+contrast (filter `ContextNote.Contains("[S134 pending-prior]")` #48). Full **528/0** (45D+483I, +8). Learned: (1) **direct-call accumulator = synthetic curPeId + CreatedAt muộn, KHÔNG cần row phiếu-đang-xét** (chỉ filter Id!=/CreatedAt<); (2) regression "bất động" mạnh nhất = C6-style exact-numbers 2 hàm cùng seed; (3) FluentAssertions `ContainInOrder(params T[])` + because-string overload-clash khi T=Guid → bọc `new[]{...}` trước. Surprise: D4 no-SaveChanges-in-helper → row chỉ persist sau TransitionAsync. Tag `[s134, pependingaccumulation, c6-exact-numbers-complementary, 8-test]`
- **S134 (07-17) PE lũy-kế TẠM TÍNH +8 → 528:** khuôn regression "bất động" mạnh nhất = **C6 exact-numbers 2 hàm complementary cùng seed** (gọi CẢ `ComputeAsync` + `ComputePendingAsync`, assert số CHÍNH XÁC 2 phía ⇒ bắt double-count). +bẫy FluentAssertions `ContainInOrder(params T[])` overload-clash khi T=Guid → bọc `new[]{...}`. Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S134.
- **S133 INV-C T1-T9 [wf_75c6bfd3-754 · #53 return-fail lần 3 → disk + lead tự chạy suite; entry on-behalf]:** +9 test vào PeBudgetFreezeTests (`:426` T1 đủ-11-cột · `:480` T2 CCM-threshold · `:524` T3 all-steps · `:555` T4 V1-legacy assert KHÔNG-ApprovedPrice · `:638` T5 guard CCM+Admin Conflict + ChoDuyet-allow · `:687` T6 AdjustBudget DaDuyet+TuChoi · `:730` T7 live-vs-frozen flags/editability · `:783` T8 null-safe no-WorkItem + no-pair · `:821` T9 overwrite + SnapshotAt refresh). FULL suite lead-verified exit-0: **Domain 45 + Infra 474 = 519, 0 fail** (464+10 khớp). ⚠️ Lesson mới: INV-C **ghi đè** `sub-test-specialist-0.md` của INV-A — cùng role + cùng task-index qua 2 invocation chung run-folder = sub-file collision; INV-A content sống nhờ journal + diary. Multi-invocation cùng run → cần label/index phân biệt (báo lead khi gặp lại). Tag `[s133, t1-t9, subfile-collision-multi-invocation]`
- **S133 (07-17) PE budget-freeze T0 REPRODUCE [wf_d9639faa-97a INV-A, return sạch]:** RED thật `Failed 1/1` — expected ProInitial 100tr, found 999tr @`PeBudgetFreezeTests.cs:184`. Flow chuẩn: đưa phiếu tới DaDuyet QUA `svc.TransitionAsync` (V2 1-Bước-1-Cấp `AllowApproverFinalize=true` + giá chốt), KHÔNG seed thẳng Phase (post-fix snapshot CHỈ set qua transition); mutate pair qua UpdatePeBudgetPro/Ccm (2 handler KHÔNG phase-guard = đúng cơ chế bug); 2 sanity-assert chống vacuous (Phase==DaDuyet + liveRec đổi thật) TRƯỚC assert chính. Bắt được lỗi spec: query THẬT = `GetPurchaseEvaluationQuery` (bundle `.BudgetSummary`), spec ghi nhầm GetPurchaseEvaluationDetailQuery → lead sửa spec. Sau INV-B: T0 GREEN trong 44/44 filter. Tag `[s133, pe-budget-freeze, t0-red-green, anti-vacuous-sanity-assert]`
- **S133 (07-17) PE budget-freeze T0 REPRODUCE + T1-T9 → 519:** khuôn test-before bug-fix còn dùng: đưa phiếu tới trạng-thái đích **QUA service THẬT** (`TransitionAsync`), KHÔNG seed thẳng Phase (post-fix snapshot chỉ set qua transition ⇒ seed thẳng = đo nhầm); 2 **sanity-assert chống vacuous** TRƯỚC assert chính. ⚠️ 2 invocation cùng role + cùng task-index trong 1 run-folder = **sub-file GHI ĐÈ nhau**. Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S133.
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
@ -21,46 +23,17 @@
---
## 🎯 Role baseline
> Role/split-boundary/timing-rules/Pattern-10-11-12/anti-patterns: **đã có nguyên văn trong agent-definition** (auto-inject mỗi spawn) ⇒ KHÔNG chép lại ở đây. Chỉ giữ thứ agent-def KHÔNG có.
WRITE specialist độc quyền `tests/**`. xUnit + FluentAssertions 7.2 + EF SQLite TestApplicationDbContext + IdentityFixture. Tools: Read, Edit, Write, Bash, Grep, Glob + 5 RAG. Skills: `contract-workflow` + `permission-matrix`.
## 🚫 Split boundary
- ✅ MINE: `tests/SolutionErp.{Domain,Infrastructure}.Tests/**`
- ❌ NOT: production code `src/Backend/**` + `fe-*/**` → test reveal bug → REPORT em main, KHÔNG fix
- ❌ NOT: decide WHAT to test (test plan) → em main + reviewer chốt priority
## 📊 Baseline: 509 tests = 509 PASS (45 Domain + 464 Infra) as of S118b (was 505@S118; +4 Infra ReportsController company-wide-report authz-regression, filtered-run 19/19, full 509/509)
- Count history + verbose per-session test-design notes (S54→D2): [archive/baseline-history.md](archive/baseline-history.md) (L2, on-demand).
- Latest (D2): +6 PE Mig 58 multi-winner — `CreateContractFromEvaluationMultiWinnerTests.cs` (5, 1→N HĐ per IsWinner, GiaTri per-winner=quote-total / single=legacy-budget-sum) + `PeListWinnerNamesProjectionTests.cs` (1, EF collection-projection translation-smoke). No prod bug.
## 📊 Baseline ĐO TƯƠI 2026-08-01 (S166): **621 = 45 Domain + 576 Infra**, 0 FAIL
- ⚠️ Con số trong agent-definition ("130") là **STALE** — luôn đo lại `dotnet test` trước khi báo delta, đừng tin số đề bài.
- Count history + verbose per-session note (S54→S155): [archive/baseline-history.md](archive/baseline-history.md) (L2, on-demand).
> Pattern S67: private-static seed/init → invoke qua REFLECTION (`GetMethod(name, NonPublic|Static)` + `Invoke(null, [db, roleManager, NullLogger.Instance])`); seed MenuItem rows TRƯỚC Permission (FK MenuKey→MenuItem.Key Cascade, SQLite Error 19 nếu thiếu). Cycle-guard test: SqliteDbFixture đủ (no User); rollup-count test cần IdentityFixture.
Run: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1` (MSBuild OOM → serialize build)
### ⚠️ Pattern: deduction hook FK → seed LeaveType cho terminal test (S43)
LeaveBalance→LeaveType `Restrict` FK. ApproveLeaveRequest terminal (DaDuyet) insert LeaveBalance → test tới DaDuyet PHẢI seed 1 LeaveType + LeaveRequest.LeaveTypeId=type.Id (random Guid→FK fail SQLite 19). Non-terminal (advance/reject/return/OtRequest) KHÔNG cần. BuildLeave optional `leaveTypeId` default random. Year=StartDate.Year. Negative OK (no quota guard). Query lazy-synth Entitled=DaysPerYear khi 0 row.
## ⏱️ Timing rules (docs/rules.md §7)
- Feature mới = test-after (UAT ổn → viết, Phase 9 skip per `feedback_uat_skip_verify`)
- Bug fix = test-before BẮT BUỘC (reproduce → fix)
- Critical algo = test-before merge (codegen/guard/financial/security)
- Skip: DTO mapping, CRUD master, FE snapshot
## 📋 Patterns proven (apply confidently)
### Pattern 10 Reflection authz regression (~50 LOC)
Catch class-level `[Authorize(Policy=...)]` regression: `typeof(Ctrl).GetCustomAttribute<AuthorizeAttribute>().Policy.Should().Be(...)`. KHÔNG WebApplicationFactory heavy. Cho gotcha #44 silent 403.
### Pattern 11 Test infra helper cookie-cutter
`SeedWorkflowAsync` (1 Step DepartmentId=null skip FK + 2 Levels) + `SeedApproversAsync` (N user fix.CreateUserAsync). Reusable PE/Contract/Proposal workflow test.
### Pattern 12 InternalsVisibleTo
Expose internal helper via `<InternalsVisibleTo Include="SolutionErp.Infrastructure.Tests" />` csproj.
### Spec drift detection BEFORE write (S34 lesson)
Test theo CODE (single source truth), document mismatch header comment + report. Vd soft-delete UNIQUE: code chặn opt-out → test theo code, flag drift.
### gotcha #48 SQLite tie-break
`OrderByDescending(CreatedAt).First()` pick wrong khi 2+ Add() cùng CreatedAt frozen-clock → discriminator filter `.Where(Summary.Contains("Chuyển phase"))` BEFORE OrderBy.
## 🎯 Coverage gap backlog (priority — Reviewer flagged S36)
1.**DONE S45** — HrmConfig Holiday composite UNIQUE (Year,Date): 7 test (`HrmConfigHolidayTests.cs`) + surfaced Mig 43 filtered-index fix
@ -71,29 +44,12 @@ Test theo CODE (single source truth), document mismatch header comment + report.
## 📅 Recent activity (one-line FIFO — verbose in [archive/baseline-history.md](archive/baseline-history.md))
- **S118b** +4 ReportsController company-wide authz (Pattern 10 reflection; `Dashboard`+`ExportContracts` gate `Reports.Read`, `MyDashboard` CỐ Ý bare `[Authorize]`) → 509.
- **S118** +4 Procurement master-access seeder SCOPE-ISOLATION (grant KHÔNG rò sang role khác + CONTRAST assert chống vacuous-pass) → 505.
- **S117** +5 PE ProMinMax duyệt-theo-KHOẢNG (boundary min==max; ⚠️ `MethodInfo.Invoke` KHÔNG auto-bind optional param → helper phải pass đủ 5 args) → 500.
- **S116** +9 PE báo giá SỐ ÂM (guard `Sum()<=0``All(x==0)`; ranh giới net==0 mà từng-dòng≠0; CCM threshold giữ NET) → 495.
- **S113** +19 Supplier import v2 dedup precedence MST→Code (dedup ở SERVICE, không dựa DB-collation) + publish-guard; 🔴 BUG NRE `Code!.Trim()` khi Code==NULL (REPORTED em-main).
- **D4** +8 PE sign-off A1 (drafter bypass CHỈ cấp NV dưới THUẦN, pointer dừng tại minOwn) + EDGE-5 terminal-reject + opinion-history changelog.
- **D3** +10 Supplier Excel-import case-collation (dedup SERVICE `OrdinalIgnoreCase`; SQLite BINARY KHÔNG bắt CI-dup, prod SQL thì có) + header-xlsx-từ-REFLECTION pattern.
- **S97** ±0 PE EndsBeforeCeo CONFIG→RUNTIME spec-change (repro case mới: config-có-finalize NHƯNG field=false = phiếu thật lên CEO).
- **S92** +3 admin-only-modules revoke; gotcha: seed-test VẪN PASS dù prod đã TẮT call grant — vì test invoke method CÔ-LẬP qua reflection, không chạy SeedAsync chain.
- **S87** +7 PE Mig 59 CCM-budget-period setter authz (`PeCcmBudgetPeriodSetterAuthzTests.cs`, role-gate Admin|CostControl, null=clear gotcha #73). → 402.
- **S86** +3 PE drafter-notify-exclusion REGRESSION (bug-fix test-alongside; `PeApproverNotifyExcludesDrafterTests.cs`, RED-proven-via-git-stash, CapturingNotificationService spy).
- **S85b** +3 PE UpdateDraft WF-preserve ROOT-FIX (null-safe guard, ApprovalWorkflowId FK Restrict seed-real). **S85** +5 PE urgent DRAFTER-self-unset (isDrafter clear-both).
- **S77c** urgent-toggle SYMMETRIC→ASYMMETRIC correction (SET=role-đủ / UNSET=role AND DeptManager). **S77** +7 suggested-price NOTE (rides price-gate, no-arity-fix). **S76** +5 PRO-column-split (ProInitial/ProAdjust, ProAdjust ÂM-OK). **S72** +28 Mig 54 (CCM-finalize OPT-IN + ApprovedPrice private-static-reflection + isSystem-unreachable OBSERVATION).
- **S69b** +14 PE CCM-threshold-finalize (strict-`<` boundary) + urgent-toggle authz (test-before-merge). **S69** +6 Office permission-seed regression (reflection harness, 16-key Read+Create).
- **S60** +14 PE submit Section-3 guard + drafter-bypass V2-only (GUARD-FIRST; opinion-ownSlot invariant; `PeSubmitGuardAndBypassTests.cs`).
- **S54** +13 ItTicket reassign authz (2-dept matrix, Forbidden red-able BY-CONTRAST, empty-staff 0-leak). **S51-S52** (L2 `archive/activity-s51-s52.md`) filtered-unique RED→Mig45/47 + WorkflowApps codegen + Serializable-on-SQLite NON-ISSUE.
---
## ⚠️ Anti-patterns (DO NOT)
1. ❌ Touch production code → REPORT bug · 2. ❌ Skip MEMORY · 3. ❌ Test không chạy (dotnet test must PASS) · 4. ❌ `git add -A` · 5. ❌ Push remote · 6. ❌ Assertion trivial
> Danh sách FIFO S54→S118b (22 mục) ĐÃ nằm trọn trong `archive/baseline-history.md` + `archive/_INDEX.md` — gỡ khỏi index @S166 vì trùng lặp. Dưới đây CHỈ giữ mẹo còn tái dụng:
- **Bẫy đo lặp nhiều lần:** (i) `MethodInfo.Invoke` **KHÔNG auto-bind optional param** → phải pass đủ arg (S117); (ii) SQLite collation **BINARY** không bắt dup khác-hoa-thường, prod SQL Server thì CÓ → dedup phải nằm ở SERVICE, đừng dựa DB (S113/D3); (iii) test invoke method **CÔ-LẬP qua reflection VẪN PASS dù prod đã tắt call** trong chain `SeedAsync` — reflection-test không chứng chain còn sống (S92).
- **Khuôn assert mạnh:** CONTRAST/by-contrast trong CÙNG một lần chạy (S54/S118) · GUARD-FIRST (S60) · RED-proven-via-`git stash` cho bug-fix test (S86) · boundary strict-`<` (S69b) · ranh giới `net==0` mà từng-dòng≠0 (S116).
## 🔄 Curate trigger
Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): `Tests`.
Index > ~17KB → archive sang L2 (`archive/baseline-history.md` + con-trỏ trong `archive/_INDEX.md`). Commit scope (em main commits): `Tests`.
## S161-W7d2 (2026-07-30) — 3 PIN ban-cung Hdc_ (lead VERIFY-APPEND tu memoryDelta return; suite 587→590/0 CI-doc-lap xac nhan)

View File

@ -6,6 +6,11 @@
- activity-s51-s52.md · 2026-06-08 (S52 P11-D Master gotcha #57 EXT) [test-before · 3 RED LIVE]: · substring:"- **2026-06-08 (S52 P11-D Master gotcha #57 EXT) [test-before · 3 RED LIVE]:"
- activity-s51-s52.md · 2026-06-08 (S52 P11-E + P11-F WorkflowApps/Attendance test-after · substring:"- **2026-06-08 (S52 P11-E + P11-F WorkflowApps/Attendance test-after"
- activity-s51-s52.md · 2026-06-08 (S51 P11-C HMW Wave2 filtered-unique gotcha #57 · substring:"- **2026-06-08 (S51 P11-C HMW Wave2 filtered-unique gotcha #57"
- baseline-history.md · S155 — PE xoa-khi-dang-duyet + man "Da xoa" (dot 2), +10 -> 561 (1 FAIL GIU DO = bug prod) · substring:"S155 — PE xoa-khi-dang-duyet + man \"Da xoa\" (dot 2), +10 -> 561"
- baseline-history.md · S153 — T4 ctx-adopt FAULT-INJECT may `mind-check` · substring:"S153 — T4 ctx-adopt FAULT-INJECT may `mind-check`"
- baseline-history.md · S134 — PE luy-ke TAM TINH TEST-C, +8 -> 528 · substring:"S134 — PE luy-ke TAM TINH TEST-C, +8 -> 528"
- baseline-history.md · S133 INV-C — PE budget-freeze T1-T9, +10 -> 519 · substring:"S133 INV-C — PE budget-freeze T1-T9, +10 -> 519"
- baseline-history.md · S133 INV-A — PE budget-freeze T0 REPRODUCE (test-before bug-fix, RED that) · substring:"S133 INV-A — PE budget-freeze T0 REPRODUCE (test-before bug-fix, RED t"
- baseline-history.md · S118b — ReportsController company-wide-report authz REGRESSION (test-after SECURITY), +4 -> 509 · substring:"S118b — ReportsController company-wide-report authz REGRESSION (test-a"
- baseline-history.md · S118 — Procurement master-access seeder SCOPE-ISOLATION regression (test-after SECURITY), +4 -> 505 · substring:"S118 — Procurement master-access seeder SCOPE-ISOLATION regression (te"
- baseline-history.md · S117 — PE ProMinMax duyet-theo-KHOANG range-price (test-before-merge FINANCIAL), +5 -> 500 · substring:"S117 — PE ProMinMax duyet-theo-KHOANG range-price (test-before-merge F"

View File

@ -12,6 +12,21 @@ metadata:
## Count progression (one-line chain)
254 (S60) → 263 (S61 +22 PeWorkItemBudget, 14 BudgetPolicy Domain drop) → 286 (S67 +23 HRM) → 292 (S69 +6 Office) → 306 (S69b +14 PE) → 334 (S72 +28 PE Mig54) → 339 (S74 +5 CcmNote) → 344 (S76 +5 PRO-split) → 351 (S77 +7 suggested-price NOTE) → 354 (S77b symmetric, superseded) → 366 (S78-S84, MEMORY chưa ghi từng delta) → 371 (S85 +5 urgent drafter-unset) → 374 (S85b +3 UpdateDraft WF-preserve) → 377-395 (S86-S86d) → 402 (S87 +7 CCM-budget-period) → **419 (D2 +6 PE Mig58 multi-winner + translation-smoke)**.
## S155 — PE xoa-khi-dang-duyet + man "Da xoa" (dot 2), +10 -> 561 (1 FAIL GIU DO = bug prod)
**S155 (2026-07-27) ĐỢT 2 PE xóa-khi-đang-duyệt + màn "Đã xóa" [run 2026-07-27-S155-pe-delete-approver · sub-d2-test-4, diary 13.9KB ghi-trong-lúc-làm]:** +10 `PeDeleteByApproverTests.cs` (T21 lũy kế **4 HƯỚNG KHÁC NHAU** · T22/T22b/T22c màn Đã xóa · T23/T24/T24b cờ+lượt · T25 authz reflection · T25b chiều thuận · T26 changelog). Full **561 = 560 PASS / 1 FAIL** (45D + 516I); 0 hồi quy (Infra cũ 506 nguyên vẹn). **1 FAIL = T26 GIỮ ĐỎ = BUG PROD**: `PeSoftDeleteFeatures.cs` Add changelog (:126) → `Remove(pe)` (:142) → `CascadeDeleteTiming.Immediate` cascade **ngay tại Remove**; dependent đang `Added` bị cascade ⇒ **Detached** (không phải Deleted) ⇒ 0 row; `AuditingInterceptor` chạy ở SavingChanges = SAU điểm mất ⇒ không cứu. Đo bằng ChangeTracker diag (`Added→Detached`, `rowsInDb=0`), KHÔNG suy đoán. Grep cùng-lớp: site DUY NHẤT có hình "Add con → Remove cha → 1 SaveChanges". REPORT lead, KHÔNG tự sửa. **Hạ tầng MỚI:** `SoftDeleteFixture` (ngay trong file test, ~90 LOC) = fixture ĐẦU TIÊN của repo wire `AuditingInterceptor` — BẮT BUỘC khi đo `Remove()`=xóa-mềm, vì `TestApplicationDbContext` KHÔNG wire ⇒ mặc định **XÓA CỨNG** ⇒ màn "Đã xóa" rỗng ⇒ assert đúng ở CẢ HAI thế giới (đúng bẫy vacuous). Số học T21 nhớ kỹ: xóa phiếu ChoDuyet ⇒ PrevSubmitted↓ · PendingSelected↓ · **PrevSelected + PendingSubmitted BẤT ĐỘNG** (seed thêm peer DaDuyet + TraLai để 2 số "không đổi" ≠ 0 ở cả 2 phía).
## S153 — T4 ctx-adopt FAULT-INJECT may `mind-check` (KHONG phai test C#, baseline C# khong doi)
**S153 (2026-07-26) T4 ctx-adopt FAULT-INJECT máy `mind-check` #8-#17+#21 [run 2026-07-26-S153-ctx-adopt · lane trước đứt sau #7, file đĩa nguyên → tiếp được] `[engine: opus-5]`:** **18/18 PASS 0 FAIL**. KHÔNG phải test C# (**baseline 509 KHÔNG đổi**) — đối tượng = `scripts/session_ctx.py mind-check`, mọi mutation trong cây tạm qua seam `--root`, repo read-only. Bắt **1 GAP THẬT**: ở closed-mode **XOÁ con-trỏ → cờ/exit 0** còn **GIỮ con-trỏ đang-chờ trung thực → TRƯỢT/exit 1** ⇒ đường-rẻ-nhất-để-xanh là đường SAI (chữ luật cấm, gradient thưởng) → REPORT em main, KHÔNG tự sửa. **Bài học mới quan trọng nhất:** fixture `draft-ts` MALFORMED ra exit 0 — `inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find`=-1 ⇒ chèn GIỮA heading ⇒ `ts:` không ở đầu dòng ⇒ máy ĐÚNG mà vẫn xanh. **Fixture-hỏng cho-xanh trông Y HỆT máy-đúng cho-xanh** ⇒ phải soi RUỘT fixture (`cat -A`/`repr` từng dòng), KHÔNG chỉ đọc exit code. 2 kỹ thuật xài lại: (i) **cặp CÙNG-RUỘT khác 1 biến** — cap=32 vs cap=1 trên cùng tệp = chứng đọc-config-LIVE; `_end` giả `_pause=9` vs không = chứng nguồn-đếm-là-ĐĨA, dùng `diff` 2 output để chứng dòng đo BYTE-IDENTICAL; (ii) **đo wall-clock phải gồm nhánh ĐẮT** — lệnh literal đi nhánh early-exit (repo chưa có `_mind`) = tự đo nhánh rẻ rồi gọi là chi phí thật; đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×, giao lead điền lại.
## S134 — PE luy-ke TAM TINH TEST-C, +8 -> 528
**S134 (2026-07-17) PE lũy-kế TẠM TÍNH TEST-C [wf_97d6a582-b7e · return sạch 0-garble] `[engine: opus-worker]`:** +8 `PePendingAccumulationTests.cs` C1-C8 (file MỚI, mirror seed PeBudgetFreezeTests :277-336): C1 ChoDuyet+winner→delta2+PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 nháp list-only/TraLai delta1 · C4 window CreatedAt · C5 delta âm signed · **C6 complementary-no-double-count: gọi CẢ ComputeAsync + ComputePendingAsync cùng seed, assert EXACT-NUMBERS 2 phía (PrevSelected 90tr/1 bất động ⟂ PendingSelected 50tr/1)** · C7 frozen→0/null qua query handler · C8 D4 changelog positive+contrast (filter `ContextNote.Contains("[S134 pending-prior]")` #48). Full **528/0** (45D+483I, +8). Learned: (1) **direct-call accumulator = synthetic curPeId + CreatedAt muộn, KHÔNG cần row phiếu-đang-xét** (chỉ filter Id!=/CreatedAt<); (2) regression "bất động" mạnh nhất = C6-style exact-numbers 2 hàm cùng seed; (3) FluentAssertions `ContainInOrder(params T[])` + because-string overload-clash khi T=Guid → bọc `new[]{...}` trước. Surprise: D4 no-SaveChanges-in-helper → row chỉ persist sau TransitionAsync.
## S133 INV-C — PE budget-freeze T1-T9, +10 -> 519 (sub-file collision multi-invocation)
**S133 INV-C T1-T9 [wf_75c6bfd3-754 · #53 return-fail lần 3 → disk + lead tự chạy suite; entry on-behalf]:** +9 test vào PeBudgetFreezeTests (`:426` T1 đủ-11-cột · `:480` T2 CCM-threshold · `:524` T3 all-steps · `:555` T4 V1-legacy assert KHÔNG-ApprovedPrice · `:638` T5 guard CCM+Admin Conflict + ChoDuyet-allow · `:687` T6 AdjustBudget DaDuyet+TuChoi · `:730` T7 live-vs-frozen flags/editability · `:783` T8 null-safe no-WorkItem + no-pair · `:821` T9 overwrite + SnapshotAt refresh). FULL suite lead-verified exit-0: **Domain 45 + Infra 474 = 519, 0 fail** (464+10 khớp). ⚠️ Lesson mới: INV-C **ghi đè** `sub-test-specialist-0.md` của INV-A — cùng role + cùng task-index qua 2 invocation chung run-folder = sub-file collision; INV-A content sống nhờ journal + diary. Multi-invocation cùng run → cần label/index phân biệt (báo lead khi gặp lại).
## S133 INV-A — PE budget-freeze T0 REPRODUCE (test-before bug-fix, RED that)
**S133 (2026-07-17) PE budget-freeze T0 REPRODUCE [wf_d9639faa-97a INV-A, return sạch]:** RED thật `Failed 1/1` — expected ProInitial 100tr, found 999tr @`PeBudgetFreezeTests.cs:184`. Flow chuẩn: đưa phiếu tới DaDuyet QUA `svc.TransitionAsync` (V2 1-Bước-1-Cấp `AllowApproverFinalize=true` + giá chốt), KHÔNG seed thẳng Phase (post-fix snapshot CHỈ set qua transition); mutate pair qua UpdatePeBudgetPro/Ccm (2 handler KHÔNG phase-guard = đúng cơ chế bug); 2 sanity-assert chống vacuous (Phase==DaDuyet + liveRec đổi thật) TRƯỚC assert chính. Bắt được lỗi spec: query THẬT = `GetPurchaseEvaluationQuery` (bundle `.BudgetSummary`), spec ghi nhầm GetPurchaseEvaluationDetailQuery → lead sửa spec. Sau INV-B: T0 GREEN trong 44/44 filter.
## S118b — ReportsController company-wide-report authz REGRESSION (test-after SECURITY), +4 -> 509
**S118b (2026-07-14) ReportsController company-wide-report authz REGRESSION (test-after SECURITY — reviewer catch) — 505→509 PASS (+4 Infra), no-red, NO prod bug:** EXTEND `Api/AuthorizePolicyRegressionTests.cs` (Pattern 10 reflection, +4 [Fact] → file 15→19). Fix S118: 2 endpoint company-wide `Dashboard`+`ExportContracts` (lộ tài chính HĐ toàn cty: tổng giá trị, top NCC/dự án, xu-hướng-12-tháng, export-Excel) NAY gate `[Authorize(Policy="Reports.Read")]` (chỉ Admin, khớp S92); `MyDashboard` CỐ Ý giữ mở (chỉ class-level `[Authorize]` trần, scope currentUser). Trước: cả controller class-level `[Authorize]` trần → mọi user đăng nhập gọi được = lộ tài chính. **T1** class-level `[Authorize]` Policy=null+Roles=null (bare — nếu hardcode Policy sẽ khóa my-dashboard). **T2/T3** Dashboard+ExportContracts action-Policy==`Reports.Read` (gỡ→NotBeNull-fail = tái-lộ toàn cty). **T4** MyDashboard `(attr?.Policy).Should().BeNull()` — cho phép bare `[Authorize]` vô-hại NHƯNG chặn mọi Policy (thêm policy→khóa dashboard cá-nhân role không-Admin). csproj ĐÃ ref Api (line 25, từ S45) — KHÔNG vướng reference. KHÔNG touch prod.

View File

@ -0,0 +1,104 @@
IMPLFE-K4A: DONE
# Lane FE K4a — route regex 42 leaf + single-source nhãn nhóm (S166, 2026-08-01)
SPEC PIN: `spec-cum2-chi-tiet-31-07-2026.md` vá 1/8/15 + `sub-invest-fable-b2-cum2.md:37` (K4c-10 regex ship CÙNG K4a) + `:58` (acceptance-5 single-source) + `:94` (fe-admin KHÔNG nới regex).
## §0 Kế hoạch đo trước khi gõ (bài S164: đo đĩa TRƯỚC)
- [ ] Đọc route THẬT `fe-user/src/App.tsx` (grep KhkkListPage / KhkkCreatePage / WorkflowMatrixViewPage)
- [ ] Đọc query-param THẬT trong KhkkListPage (`filter` / `view`)
- [ ] Đọc khuôn Pe-regex trong `fe-user/src/components/Layout.tsx`
- [ ] Đọc nhãn nhóm gốc `DbInitializer.cs` dict `khkkGroupLabels` (Read-only, KHÔNG sửa)
- [ ] Đọc `types/khkk.ts` ×2 app (nền F-1 `7a903cf`)
## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ 1 dòng nào
### 1.1 Route THẬT (KHÔNG tin literal spec)
`fe-user/src/App.tsx` — 3 route KHỚP spec, 0 lệch:
- `:81 /khkk/list``KhkkListPage` · `:82 /khkk/create``KhkkCreatePage` · `:83 /khkk/workflow-matrix``WorkflowMatrixViewPage` (import `:21` từ `@/pages/pe/`)
- `fe-admin/src/App.tsx` `:107-110`: có `/khkk`, `/khkk/list`, `/khkk/create`, `/khkk/:id`**KHÔNG có `/khkk/workflow-matrix`**, 0 import `WorkflowMatrixViewPage` ⇒ xác nhận spec `:94` (fe-admin cố ý lệch).
### 1.2 Query-param THẬT (`KhkkListPage.tsx`)
`:61 searchParams.get('filter')``FILTER_TO_PHASE` (`:40-46`, key hợp lệ: `DangSoanThao|ChoDuyet|DaDuyet|TraLai|TuChoi`) · `:62 searchParams.get('view') === 'deleted'`. ⇒ `filter=ChoDuyet` / `filter=DaDuyet` / `view=deleted` ĐÚNG từng ký tự.
🔴 `group` **CHƯA có consumer** ở KhkkListPage lẫn KhkkCreatePage (K4b/K5 lane) — leaf trỏ đúng URL nhưng trang chưa lọc. Không phải lỗi K4a, là bàn giao (ghi ở §4 FLAG-2).
### 1.3 Active-state (Layout `queryMatches` `:315-336`)
`TRANSIENT_QUERY_KEYS = {id,q,editHeader,page,phase,awId}` `:311`**`group`/`filter`/`view` KHÔNG transient** ⇒ 6 URL/nhóm phân biệt được, không sáng nhầm (bài S155 `view=approved`). KhkkListPage KHÔNG ghi `filter` ngược lên URL (chỉ đọc lúc init) ⇒ highlight ổn định khi user đổi dropdown.
### 1.4 Bộ key BE (`MenuKeys.cs` — Read-only)
`KhkkGroupNumbers = [2..8]` `:72` · `KhkkLeafActions = [WfView,List,Create,Pending,Approved,Deleted]` `:73-74` · `KhkkLeaf(n,a) => Khkk_G{n}_{a}` `:77`.
🔴 **42 leaf infix = nhóm 2..8 (7×6)**. Nhóm 1 GIỮ 7 key CŨ **KHÔNG-infix** (`Khkk_G1` + `Khkk_WfView``Khkk_Deleted`) — đã nằm sẵn trong staticMap ×2 app. ⇒ regex `([1-8])` của spec **rộng hơn tập seed 1 nấc** (`Khkk_G1_List` không tồn tại) — giữ nguyên `[1-8]` theo spec, vô hại + phòng nếu sau này nhóm 1 đổi sang khuôn infix.
### 1.5 Nhãn nhóm BE (`DbInitializer.cs` — Read-only)
- `:1960` `Khkk_G1` = `"1. Thiết bị, máy móc, vật tư phụ (A1)"` (+ `labelBackfill :2235` cùng chuỗi)
- `:2127-2136` dict `khkkGroupLabels` nhóm 2..8:
`2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)` · `3. Hoạt động văn phòng (A4)` · `4. Vật tư (B1)` · `5. Thầu phụ xây dựng (B2)` · `6. Thầu phụ MEP (B3)` · `7. BCH công trường (C1-C3)` · `8. HĐ chưa ủy quyền — CEO (5.2)`
- Workflow code: `^KHKK-N([1-8])$``ContractSigningPlanFeatures.cs:420` (regex BE thật) ⇒ `khkkGroupWorkflowCode` khớp.
### 1.6 🔴 TIỀN-ĐỀ LEAD SAI ×2 (đo ra, không suy)
**(a) `KHKK_GROUP_LABELS` — map nhãn ĐÃ TỒN TẠI.** `types/khkk.ts:129 KHKK_APPROVAL_GROUP_LABELS: Record<number,string>` (K2 S164 land), **5 call-site đang dùng**: `KhkkListPage:243` · `KhkkDetailPage:301` · `KhkkCreatePage:232,285`. Đẻ thêm map tên `KHKK_GROUP_LABELS` = **PHÁ acceptance-5** (thành 2 nguồn). Hơn nữa comment K2 `:125-127` đã uỷ quyền sẵn cho K4a: *"nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app), đừng vá rải trong page"*.
**Quan hệ đo được:** nhãn BE ≡ `` `${n}. ${KHKK_APPROVAL_GROUP_LABELS[n]}` `` cho **8/8 nhóm** (đối chiếu từng ký tự §1.5 ↔ `:130-137`). ⇒ giải bằng **DERIVE**, không hardcode lần 2.
**(b) `isAdminHidden` KHÔNG hề ẩn `KeHoachKyKet`.** `fe-admin/Layout.tsx:194-196` = `return key.startsWith('Ct_')` — chỉ ẩn `Ct_*`. ⇒ mệnh đề "gỡ `KeHoachKyKet` khỏi ẩn-admin ⇒ phải nới regex trước" **mô tả một điều kiện tương lai KHÔNG tồn tại**; ràng buộc thật đã **ĐANG hiệu lực NGAY** (fe-admin đang hiện KHKK). Comment phải viết theo đĩa, không theo tiền-đề.
## §2 THỰC THI
### F1 — `fe-user/src/components/Layout.tsx` (+34 / 0)
Nhánh `khkkMatch` đặt **SAU** khối `peMatch`, **TRƯỚC** `return null` cuối `resolvePath`. Regex ĐÚNG literal spec `/^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$/`. 6 route (đọc từ App.tsx thật, không tin literal spec):
`WfView → /khkk/workflow-matrix?type=10&group={n}` · `List → /khkk/list?group={n}` · `Create → /khkk/create?group={n}` · `Pending → …&filter=ChoDuyet` · `Approved → …&filter=DaDuyet` · `Deleted → …&view=deleted`.
+comment khai lệch fe-admin (không có `WorkflowMatrixViewPage`, lead chốt 2026-07-27) + khai `[1-8]` rộng hơn tập seed `[2-8]` là cố ý + khai `group` không transient nên active-state phân biệt được.
### F2 — `fe-admin/src/components/Layout.tsx` (+13 / 0, COMMENT-ONLY)
Ràng buộc ngược đặt ngay trên `isAdminHidden` `:194`. **KHÔNG nới regex** (diff 0 dòng xoá, 0 dòng code — chỉ comment). Nội dung viết theo ĐĨA chứ không theo tiền-đề lead (xem §1.6b).
### F3 — `types/khkk.ts` ×2 app (+25 / +25, SHA-pair)
KHÔNG đẻ `KHKK_GROUP_LABELS` (sẽ thành nguồn thứ 2 — xem §1.6a). Thay bằng 2 export DERIVE trên map sẵn có:
- `khkkGroupMenuLabel(n)` → `` `${n}. ${KHKK_APPROVAL_GROUP_LABELS[n]}` `` , fallback `Nhóm {n}`. Comment khai TO: **không phải nguồn sidebar** (sidebar đọc `/api/menus/me` → `MenuItems.Label`, admin còn override `displayLabel` Mig 27); dùng cho cây K5 / leaf-title / WfView.
- `khkkGroupWorkflowCode(n) => \`KHKK-N${n}\`` (O-2) — khớp regex BE `^KHKK-N([1-8])$` `Features.cs:420`.
0 call-site cũ bị đụng ⇒ 5 chỗ đang dùng `KHKK_APPROVAL_GROUP_LABELS` giữ nguyên hành vi, ô select `N{g} - {label}` không bị lặp số.
## §3 CHỨNG (đo bằng lệnh, kèm control-âm)
**A. Nhãn byte-exact 8/8** — script trích chuỗi từ `DbInitializer.cs` (`Khkk_G1` tuple `:1960` + dict `:2127-2136`) và từ `types/khkk.ts`, so `"{n}. "+short` với BE:
```
OK N1..N8 -> --- byte-exact 8/8 khop, 0 lech
CONTROL(âm giả, thêm 'x' vào N4): OK-phep-do-bat-duoc-lech
```
N2 có "phá dỡ" ✅. Control-âm chứng phép so có RĂNG (không phải happy-path mù).
**B. Regex ăn đủ 42 leaf** — `new Function()` chạy CHÍNH khối text đã ship trong `Layout.tsx` (không chép tay), key sinh từ `KhkkGroupNumbers`/`KhkkLeafActions` đọc thẳng `MenuKeys.cs`:
```
BE nhom=[2,3,4,5,6,7,8] actions=[WfView,List,Create,Pending,Approved,Deleted]
--- 42 leaf, 0 tra null (drop im lang)
CONTROL Khkk_G9_List -> null
CONTROL Khkk_G2_Bogus -> null
CONTROL Khkk_List -> null (đúng: nhóm 1 không-infix đi staticMap, chặn trước regex)
```
**C. SHA-pair types (acceptance-5)**
| file | sha256 |
|---|---|
| baseline trước K4a (cả 2 app) | `eba649cc6fc7eccfeec5262f2e4d58265dbf8caaac4d53c6bc7120fc8065976d` |
| `fe-user/src/types/khkk.ts` | `7a99b865189480933c57a46a9d9aac9be525d14a78e7743f3a82293b706f22b6` |
| `fe-admin/src/types/khkk.ts` | `7a99b865189480933c57a46a9d9aac9be525d14a78e7743f3a82293b706f22b6` |
→ **IDENTICAL**. Layout.tsx 2 app KHÁC NHAU by-design (fe-user +34 có regex · fe-admin +13 comment-only) — không mirror, đúng chủ đích.
**D. Build ×2 PASS, 0 TS error** (`npm run build` = `tsc -b && vite build`, `noUnusedLocals:true` ⇒ build LÀ phép thử TS6133 thật, không tin IDE — gotcha #68)
| app | modules | bundle js | css | time |
|---|---|---|---|---|
| fe-user | 1950 | `index-B2v0kn5F.js` 1,640.21 kB (gzip 414.27) | `index-my6T4K7V.css` 92.62 kB | 20.75s |
| fe-admin | 1961 | `index-84zDBHE-.js` 1,723.20 kB (gzip 428.44) | `index-Ct6-3cYU.css` 86.80 kB | 8.17s |
Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` = PRE-EXISTING cả 2 app.
**E. Rào** — `git status` sau lượt: 4 file của tôi. `MenuKeys.cs` + `DbInitializer.cs` M nhưng là **lane BE land trước** (tôi chỉ Read/Grep, 0 Edit). `KhkkListPage.tsx` · `KhkkCreatePage.tsx` · `KhkkWorkflowPanel.tsx` · `KhkkDetailPage.tsx` **KHÔNG có trong diff**. `KhkkGroupMenuSeedTests.cs` untracked = lane test. **0 commit.**
## §4 FLAG cho lead (3 — cần quyết, KHÔNG tự quyết)
**FLAG-1 (MAJOR, đã tự bẻ — báo để lead soát lại spec):** đề bài yêu cầu export `KHKK_GROUP_LABELS` mới, nhưng đĩa đã có `KHKK_APPROVAL_GROUP_LABELS` (`types/khkk.ts:129`, K2 land, 5 call-site). Đẻ map thứ 2 là **phá đúng acceptance-5 mà nó phục vụ**. K2 còn để sẵn uỷ quyền trong comment `:125-127` ("nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app)"). Tôi đi đường DERIVE. **Nếu lead vẫn muốn đúng tên `KHKK_GROUP_LABELS`** thì đó là đổi TÊN của const sẵn có (+ sửa 5 import), không phải thêm const — 1 lượt nhỏ, nói là làm.
**FLAG-2 (MAJOR, bàn giao K4b/K5):** `?group=n` **chưa có ai đọc**. `KhkkListPage:61-62` chỉ đọc `filter` + `view`; `KhkkCreatePage` lấy nhóm từ state default, không đọc URL. ⇒ 42 leaf điều hướng đúng + sáng đúng, nhưng trang **hiện toàn bộ nhóm**. Câu hỏi treo cho K4b: **leaf "Danh sách" của NHÓM 1 (`Khkk_List` → `/khkk/list`, KHÔNG có `group=`) nghĩa là "nhóm 1" hay "tất cả"?** Nếu K4b coi vắng-`group` = nhóm 1 thì mất đường xem-tất-cả; nếu coi = tất cả thì leaf nhóm 1 nói dối tên nó. Đây là quyết định UX ⇒ tôi KHÔNG tự chọn, và cố ý KHÔNG sửa 6 dòng staticMap nhóm 1.
**FLAG-3 (MINOR, hệ quả đã biết của vá-15/`:94`):** `isAdminHidden` chỉ ẩn `Ct_*` ⇒ fe-admin ĐANG hiện root KHKK. Sau khi BE seed land, sidebar admin sẽ có **7 nhóm rỗng** (`Khkk_G2..G8` render header nhưng 42 leaf con trả null → thân rỗng), vì `MenuGroup` chỉ xét `children.length > 0` chứ không xét con có resolve được không. Nhóm 1 vẫn đủ 6 mục. Nếu lead thấy 7 nhóm rỗng là nhiễu thì có 2 đường (đều 1 lượt nhỏ): (a) nới regex admin, `WfView` trỏ Designer `AwV2_KhkkN{n}`; (b) thêm `Khkk_G` vào `isAdminHidden`. **Không tự chọn.**
IMPLFE-K4A-END

View File

@ -0,0 +1,104 @@
IMPLFE-K4B: DONE
# Lane FE K4b — nối `?group=` + vá-3/5/6 (S166, 2026-08-01)
Nối finding-A của gate K4a ("`?group=` chết-lâm-sàng" = FLAG-2 lượt trước: leaf trỏ đúng URL nhưng KHÔNG ai đọc).
SPEC PIN: spec-cụm2 vá 3 (queryKey 2 khoá) · vá 5 (`phase` derive thay mount-only) · vá 6 (Create preset từ URL) + K4b-7 (URL G1 mang `group=1`).
## §0 Kế hoạch đo trước khi gõ (khuôn S164/S166)
- [ ] Đọc `KhkkListPage.tsx` TRỌN vùng state/query ×2 app — có mirror không (SHA)
- [ ] Đọc `KhkkCreatePage.tsx` vùng state `approvalGroup` ×2 app
- [ ] Grep staticMap 6 leaf G1 ở fe-admin (đề bài dặn: có thì mirror, không thì khai)
- [ ] Xác nhận BE nhận `?group=` (K2 wire `[FromQuery(Name="group")]`) — Read-only
## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ
### 1.1 Baseline SHA (cả 2 page ĐANG mirror ×2 app ⇒ được phép `cp`)
| file | sha256 trước K4b |
|---|---|
| `KhkkListPage.tsx` ×2 | `80ebd79dba1356e8f1512f798ab105eca312d84aae2744b7ed852ff667f60f3b` |
| `KhkkCreatePage.tsx` ×2 | `677f4ab803b0b7ac73ede9b2d5871a3243f4cbe90153a218b320d642c6431268` |
### 1.2 🔴 TIỀN-ĐỀ ĐỀ-BÀI CẦN BẺ: vá-5 "derive thay useState" nguyên văn sẽ **GIẾT bộ lọc chip**
`phase` KHÔNG chỉ đến từ URL — nó còn là state do người dùng bấm: `KpiCard onClick :153-156 setPhase(c.value)` (6 chip Tất cả/Soạn/Chờ/Đã/Trả lại/Từ chối). Comment `:11-12` khai rõ chủ đích: *"Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng nên menu không sáng nhầm"* (bài S155).
⇒ Nếu làm `const phase = urlPhase` thuần thì `setPhase` mất chỗ đứng, **6 chip thành nút chết** — hỏng CÂM, build vẫn xanh.
⇒ Giải đúng = **state override + reset theo danh tính URL** (khuôn React "adjust state during render", KHÔNG `useEffect`, không nháy 2 pass):
`chipPhase: KhkkPhaseValue|null|undefined``undefined` = "chưa bấm chip, dùng URL"; `null` = "user chủ động chọn Tất cả" (2 nghĩa khác nhau, không gộp được). URL đổi ⇒ `chipPhase=undefined` + `page=1`.
### 1.3 🔴 BE `/deleted` KHÔNG nhận `group` (rào: không sửa BE)
`ContractSigningPlansController.cs:59-66 ListDeleted(page, pageSize, search, sortDesc)`**0 tham số group**, khác hẳn `List :42 [FromQuery(Name="group")] int? group`.
`Khkk_G{n}_Deleted → /khkk/list?group=n&view=deleted` sẽ hiện phiếu xóa CỦA MỌI NHÓM. Không client-side lọc được vì `total`/phân trang do server đếm trên tập chưa lọc ⇒ lọc ở FE sẽ ra số trang nói dối. ⇒ Xử: **gửi `group` chỉ ở nhánh không-xóa** + **không để tiêu đề CLAIM nhóm ở màn đã-xóa** (không nói dối) + FLAG lane BE. Xem FLAG-1.
### 1.4 fe-admin CÓ 6 leaf G1 trong staticMap, nhưng `Khkk_WfView` ĐI ĐƯỜNG KHÁC
`fe-admin/Layout.tsx:35` `Khkk_WfView → /system/approval-workflows-v2/ContractSigningPlan` (Designer), 5 leaf còn lại `:36-40` trỏ `/khkk/*` như fe-user.
⇒ mirror `?group=1` cho **5 leaf**; `Khkk_WfView` admin **KHÔNG thêm** `group` (Designer không đọc tham số này — chính ghi chú K4a đã khai). Khai lệch, không mirror mù.
### 1.5 Xác nhận đường BE cho nhánh chính
`List :42` `[FromQuery(Name="group")] int? group``ListContractSigningPlansQuery(..., group)` `:49`. ⇒ FE gửi `params.group` là ăn.
## §2 THỰC THI (6 file — 3 cặp mirror)
### F1 — `KhkkListPage.tsx` ×2 (+89 / 18 mỗi app, SHA-pair)
- **(a) vá-5:** `urlFilter`/`urlPhase`/`deletedView`/`group` đọc THẲNG từ `searchParams` mỗi render. Bộ lọc chip chuyển sang lớp override `chipPhase: KhkkPhaseValue|null|undefined` (3 trạng thái, `undefined``null` — xem §1.2), `phase = chipPhase !== undefined ? chipPhase : urlPhase`. Reset `chipPhase`+`page` khi `navKey` đổi, **điều chỉnh state trong render** (khuôn React chính thống) chứ không `useEffect``useEffect` sẽ vẽ 1 lượt bằng dữ liệu cũ rồi mới sửa (nháy).
- **(b)** `group` = `?group=` lọc `^[1-8]$`, ngoài dải/vắng ⇒ `null` = xem tất cả nhóm. Gửi `params.group` (chỉ nhánh không-xóa, §1.3).
- **(c) vá-3:** `queryKey: ['khkk-list', { group, phase, search, page, deletedView }]`.
- **(d)** `group !== null` ⇒ title `… — N{group}` + subtitle `khkkGroupMenuLabel(group)` (helper K4a). Nút "Lập kế hoạch" mang `?group=` sang màn tạo (bắt tay vá-6).
- Header file cập nhật bảng URL 6 leaf + khai lý do "8 nhóm chung 1 route ⇒ phải đọc lại mỗi render".
### F2 — `KhkkCreatePage.tsx` ×2 (+24 / 2 mỗi app, SHA-pair)
vá-6: `presetGroup` từ `?group=` (lọc `^[1-8]$`) làm giá trị khởi tạo `approvalGroup`; +đồng bộ lại khi THAM SỐ URL đổi (cùng khuôn F1 — 8 leaf "Thao tác" cũng chung 1 route). 🔴 So với `lastPresetGroup` chứ KHÔNG so với `approvalGroup`: so với `approvalGroup` thì user tự đổi nhóm sẽ bị kéo ngược về nhóm của URL, ô select thành bấm-không-được. `firstFreeGroup`/`selectPe` giữ nguyên (URL là gợi ý, không phải khoá — PE đã dùng nhóm đó vẫn tự nhảy sang nhóm trống).
### F3 — `Layout.tsx` staticMap (KHÔNG mirror, cố ý)
- fe-user: **6/6** leaf G1 mang `group=1` (`Khkk_WfView → /khkk/workflow-matrix?type=10&group=1`).
- fe-admin: **5/6**`Khkk_List/Create/Pending/Approved/Deleted` mang `group=1`; `Khkk_WfView` **KHÔNG** vì bên admin nó trỏ Designer `/system/approval-workflows-v2/ContractSigningPlan`, không phải trang danh sách (§1.4). Khai trong comment tại chỗ.
## §3 CHỨNG (mỗi phép đo kèm CONTROL-ÂM)
**A. Acceptance "leaf→leaf đổi kết quả không F5"** — mô phỏng 1 instance KHÔNG remount, chạy **CHÍNH đoạn text đã ship** (`indexOf('const navKey')` → slice → `new Function`), không chép tay:
```
R1 ?group=2 phase=null (mọi trạng thái)
[user bấm chip "Đã duyệt"]
R2 ?group=2 (URL giữ nguyên) phase=3 ⇒ CHIP CÒN SỐNG (derive không giết nó)
R3 ?group=5&filter=ChoDuyet phase=2 page=1 ⇒ RE-LỌC NGAY, không F5
CONTROL-ÂM (gỡ khối reset ra khỏi chính đoạn trên): phase=3 ⇒ kết dính bộ lọc cũ
= ĐÚNG căn bệnh vá-5 ⇒ phép đo CÓ RĂNG
```
🔴 Bẫy harness đã sập rồi bò ra, ghi lại vì dễ tái phạm: lượt chạy đầu ra `phase=3` ở R3 và tôi suýt đọc thành PASS-nhầm-hướng. Nguyên nhân: `setState` gọi TRONG lúc render thì React **vứt bỏ render đó và render lại ngay với state mới trước khi commit**; harness bản đầu chỉ đo cái render BỊ VỨT. Sửa harness thành vòng lặp tới khi state đứng yên (đúng ngữ nghĩa React) mới ra `phase=2`. **Đo nhầm cái render bị vứt = kết luận ngược.**
**B. vá-3 queryKey** (đọc thẳng dòng trong file):
```
queryKey: ['khkk-list', { group, phase, search, page, deletedView }],
CÓ group · CÓ phase · CÓ search · CÓ page · CÓ deletedView
CONTROL-ÂM khoá bịa "nhomX" → không khớp ⇒ phép đo bắt được cái thiếu
```
Ghi chú spec: đề bài nói "chứa cả `group` lẫn `filter`". Trục `?filter=` nằm trong khoá dưới dạng **đã giải mã**`phase` (`FILTER_TO_PHASE` đơn ánh, và `phase` cũng chính là tham số gửi lên server) ⇒ khoá phủ đủ 2 trục. Nhét thêm `filter` thô chỉ tách đôi ô cache cho 2 request y hệt nhau. Khai để gate soát (FLAG-2).
**C. SHA-pair (3 cặp)**
| file | trước K4b | sau K4b | ×2 app |
|---|---|---|---|
| `KhkkListPage.tsx` | `80ebd79d…` | **`fc3944df7af22bd3c2d29d191b31af7dc13c8377ad1f943e92902da1e13c3b10`** | IDENTICAL |
| `KhkkCreatePage.tsx` | `677f4ab8…` | **`545b4c2fe0ebae88c0d8ffcd4118f85e71b3e6ac2dfe6833d9e6028765f3d564`** | IDENTICAL |
| `types/khkk.ts` (K4a, không sửa lượt này) | — | `7a99b865…` | IDENTICAL (đối chứng dương) |
`Layout.tsx` 2 app KHÁC NHAU by-design (user +50 · admin +28).
**D. Build ×2 PASS, 0 TS error** (`tsc -b && vite build`, `noUnusedLocals` ⇒ build là phép thử TS6133 thật)
| app | bundle js | css | time |
|---|---|---|---|
| fe-user | `index-C4F7kHKT.js` 1,640.77 kB (gzip 414.52) | `index-my6T4K7V.css` 92.62 kB | 3.57s |
| fe-admin | `index-kv90BBYQ.js` 1,723.75 kB (gzip 428.71) | `index-Ct6-3cYU.css` 86.80 kB | 0.86s |
Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT` = PRE-EXISTING.
**E. Rào** — 0 chạm `MenuKeys.cs`/`DbInitializer.cs` (2 file M sẵn từ lane BE trước lượt tôi), 0 chạm `KhkkWorkflowPanel.tsx`/`KhkkDetailPage.tsx` (không có trong `git diff --name-only`), 0 chạm `tests/`, **0 commit**.
## §4 FLAG cho lead
**FLAG-1 (MAJOR — cần lane BE, ngoài rào của tôi):** `GET /contract-signing-plans/deleted` **không nhận `group`** (`ContractSigningPlansController.cs:61-66`, đối chiếu `List:42``[FromQuery(Name="group")]`). ⇒ 8 leaf "Đã xóa" trỏ 8 URL khác nhau nhưng trả CÙNG một tập phiếu-của-mọi-nhóm. Không vá được ở FE: lọc client-side sẽ làm `total`/số trang nói dối (server đếm trên tập chưa lọc). Xử tạm trong lượt này = **không để UI claim nhóm ở màn đã-xóa** (title/subtitle giữ nguyên bản "đã xóa", không in tên nhóm) ⇒ sai-thầm-lặng biến thành thiếu-tính-năng-nhìn-thấy-được. Fix thật: thêm `int? group` vào `ListDeletedContractSigningPlansQuery` + controller, 1 lượt nhỏ.
**FLAG-2 (MINOR — chờ gate phán):** queryKey mang `phase` (dạng đã giải mã của `filter`) chứ không mang chuỗi `filter` thô — xem §3B. Nếu gate muốn khớp LITERAL chữ `filter` trong khoá thì nói, tôi thêm (chi phí: 2 ô cache cho cùng 1 request).
**FLAG-3 (MINOR — bàn giao K5):** `?group=` trên `/khkk/workflow-matrix?type=10&group={n}` hiện **chưa có consumer**`WorkflowMatrixViewPage` chỉ đọc `type`. Leaf "Luồng duyệt" của cả 8 nhóm vì thế hiển thị cùng một ma trận. Tham số vẫn cần có (nó là danh tính để sidebar sáng đúng leaf). Nối thật = K5 hoặc lượt sau: lọc workflow theo `Code = KHKK-N{n}` — helper `khkkGroupWorkflowCode(n)` đã có sẵn trong `types/khkk.ts` từ K4a, chờ người gọi.
**FLAG-4 (MINOR — đã quyết, báo để khỏi tưởng sót):** `search` CỐ Ý không reset khi đổi leaf (đang dò một mã rồi nhảy nhóm để tìm tiếp là thao tác có thật); chỉ `chipPhase` + `page` reset. Nếu owner muốn reset cả `search` thì thêm 1 dòng.
IMPLFE-K4B-END

View File

@ -0,0 +1,238 @@
GATE-K4A: PASS-WITH-FLAGS 4
# Gate K4a — adversarial review wave K4a (49 row menu KHKK 8-nhóm)
- Spawn: S166 2026-08-01, HEAD `7a903cf`, diff CHƯA commit.
- Artifact ghi TỪNG MỤC (anti-#53). Dòng-1 = verdict.
## Mốc đo (neo — sẽ re-đo trước verdict, bài S166 cây-phình)
- HEAD THẬT lúc review = `ce15c64` ("wal: flush 20260801T0914"), KHÔNG phải `7a903cf` như đề bài.
Đã kiểm `git show --stat ce15c64`: chỉ chạm `.claude/**` (WAL + agent-memory + run-folder) ⇒
KHÔNG có code src/fe lọt vào commit đó. Đề bài lệch 1 commit governance = vô hại, ghi để neo.
- `git diff --stat -- src fe-user fe-admin` @T0 = **6 file, +213 / 5**.
- `git status --porcelain -- src fe-user fe-admin tests` @T0 = 6 M + 1 `??`
(`tests/SolutionErp.Infrastructure.Tests/Application/KhkkGroupMenuSeedTests.cs`) — khớp kỳ vọng 7 file.
- Ngoài scope code: 5 file `.claude/**` M (memory/run-folder/session-counter) — KHÔNG tính cây-phình
code, nhưng lead PHẢI `git add` có chọn lọc (đừng `git add -A` kéo `.session-counter.json` vào commit code).
## Mục 7 — thứ tự static initializer (ĐO)
`MenuKeys.cs`: `KhkkGroupNumbers` :72 → `KhkkLeafActions` :73 → `KhkkGroup/KhkkLeaf` :76-77 →
`KhkkGroupMenuKeys` :81-84 → spread `.. KhkkGroupMenuKeys` trong `All` :244. Thứ tự văn bản ĐÚNG.
Nếu ai dời khai báo xuống dưới `All`: spread mảng `null` → NRE trong static ctor →
`TypeInitializationException` ở BẤT KỲ truy cập `MenuKeys.*` ⇒ T1+T2 (và ~mọi test chạm MenuKeys) ĐỎ.
⇒ có răng, tuy răng là "nổ" chứ không phải assert-đẹp. PASS.
## Mục 6 — vá-10/11/13 (comment) — ĐO
- Vá-13 `1893`: `grep -n 1893 DbInitializer.cs` = **0 hit**; `grep -rn 1893 src/` chỉ ra file .dll nhị
phân (nhiễu, không phải mã nguồn). 2 comment đã trỏ `:2162-2179` + mỏ neo `existingItems.TryGetValue`.
Kiểm neo mới: `existingItems.TryGetValue` THẬT ở `:2164`, vòng `foreach` bắt đầu `:2162`, `added++`
kết `:2178`, đóng ngoặc `:2179` ⇒ khoảng trỏ ĐÚNG (chứa trọn nhánh "key đã tồn tại → re-set Order
→ continue" `:2164-2176`). PASS.
- Vá-11 revoker: comment `:2335-2340` khai "filter chỉ bắt prefix Hrm*/Off* + key Personal ⇒ 0 match
Khkk_*" — sẽ verify bằng đọc `RevokeTemporarilyHiddenModulesAsync` (mục dưới).
- Vá-10 lý-do site-2: comment khai `GetMyMenuTreeQuery.cs:96 HasAccess` — sẽ verify.
## Mục 4 — nhãn 8 nhóm vs transcribe (ĐO BẰNG SCRIPT, không mắt)
So khớp CHUỖI THÔ (`==`, không chỉ NFC) giữa `khkkGroupLabels[2..8]` + `("Khkk_G1", …)` trong
`DbInitializer.cs` và bảng `danh-muc-sp002-transcribe.md:22-29`: **8/8 rawEq=True**, gồm cả nhóm dài
nhất N2 `"2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)"` (CÓ vế "phá dỡ") và N8 có em-dash
`—` `"8. HĐ chưa ủy quyền — CEO (5.2)"`. Ngoài ra `tree.Add("Khkk_G1", …)` == `labelBackfill["Khkk_G1"]`
đúng từng ký tự ⇒ DB-trống và DB-đang-chạy hội tụ CÙNG một chuỗi. PASS.
## Mục 1 — routes/params fe-user (ĐO TRÊN ĐĨA)
Khớp:
- `/khkk/workflow-matrix` CÓ route fe-user (`App.tsx:83`), KHÔNG có fe-admin (`:107-110` chỉ
list/create/:id) ⇒ lời khai "fe-admin không có WorkflowMatrixViewPage" ĐÚNG.
- `filter=ChoDuyet` / `filter=DaDuyet`: `FILTER_TO_PHASE` (`KhkkListPage.tsx:40-46`) có ĐÚNG 2 khoá
`ChoDuyet`/`DaDuyet` — khớp hoa/thường từng ký tự (`Record<string,…>` ⇒ case-SENSITIVE, sai chữ là
`?? null` = list không lọc, câm).
- `view=deleted`: `:62 searchParams.get('view') === 'deleted'` — khớp chính xác, đổi endpoint sang
`/contract-signing-plans/deleted`.
- Không leaf nào rơi `if (!path) return null` Ở FE-USER: regex phủ đúng 6 action, mọi nhánh có return.
- Thứ tự query param: `queryMatches` dựng `URLSearchParams` 2 phía rồi so theo KHOÁ ⇒ thứ tự không
ảnh hưởng. OK.
🔴 **FLAG A (MAJOR) — `?group=n` KHÔNG AI ĐỌC.** Phép đo:
`grep -rn "get('group')" fe-user/src fe-admin/src` = **0 hit**; `grep -n group WorkflowMatrixViewPage.tsx`
chỉ ra `<colgroup>` (:195/:200). Hệ quả đo được, không phải suy diễn:
- `/khkk/list?group=5&filter=ChoDuyet` trả **cùng dữ liệu** với `group=2` (query key
`['khkk-list', {phase, search, page, deletedView}]` — không có `group`; `params` gửi BE cũng không có).
- `/khkk/create?group=5` KHÔNG chọn sẵn nhóm 5.
- `/khkk/workflow-matrix?type=10&group=5` bỏ qua `group` hoàn toàn (`:53` chỉ đọc `type`) ⇒ 7 leaf
"Luồng duyệt" của 7 nhóm hiện **y hệt nhau**: toàn bộ workflow type-10.
⇒ Sau K4a, sidebar có 8 nhóm nhưng **nội dung 8 nhóm giống hệt nhau** (trừ nhóm 1 vốn cũng vậy).
Comment `Layout.tsx:196-199` CÓ khai đúng bản chất ("`?group=` là DANH TÍNH điều hướng, không phải bộ
lọc") ⇒ đây là **khai-báo-thật, không phải claim sai**; nhưng nó là trạng-thái người dùng NHÌN THẤY
và chưa thấy dòng nào trong artifact 3 lane nói "K4a giao 8 nhóm rỗng-lọc". Cần lead/owner dispose
tường minh (giống FLAG-3) TRƯỚC khi commit, kèm chỉ đích wave nào nối (K4b? K5?).
🔴 **FLAG B (MAJOR) — 2 mỏ-neo dòng MỚI trong `fe-user/Layout.tsx` SAI, lệch đúng bằng chính khối vừa
chèn.** Cùng lớp lỗi mà diff này vừa VÁ ở BE (vá-13 `1893`), tái phát ở FE trong CÙNG một lượt:
| Comment mới nói | Dòng THẬT (đo `grep -n`) | Lệch |
|---|---|---|
| `TRANSIENT_QUERY_KEYS` (`:311`) — `Layout.tsx:198` | `:345` | +34 |
| `MenuLeaf :342 if (!path) return null``Layout.tsx:189` | `:376` (và `:431`) | +34 |
Khối K4a chèn vào fe-user `Layout.tsx` đúng **+34 dòng** ⇒ tác giả tính mỏ neo trên bản TRƯỚC khi chèn
chính khối của mình. Đây là mỏ-neo **tự-vô-hiệu ngay lúc land**, không phải trôi theo thời gian.
Đối chứng: 6 mỏ neo còn lại trong cùng đợt vá ĐỀU ĐÚNG (`MenuKeys.cs:77` ✔ · `DbInitializer.cs:1960` ✔ ·
`khkkGroupLabels :2127-2136` ✔ · `labelBackfill:2235` ✔ · `GetMyMenuTreeQuery.cs:96` ✔ ·
`ContractSigningPlanFeatures.cs:420` regex `^KHKK-N([1-8])$` ✔ · `KhkkListPage :61/:62` ✔) ⇒ lệch chỉ ở
đúng 2 mỏ neo TRỎ VÀO CHÍNH FILE ĐANG SỬA. Không chặn chạy, nhưng chính vá-13 vừa chứng loại lỗi này
tốn 1 lượt điều tra.
## Mục 6 (tiếp) — vá-8/10/11/15 nội dung có tự-mâu-thuẫn không
- Vá-11 (revoker) **ĐÚNG, đã đo**: `RevokeTemporarilyHiddenModulesAsync` `:2554-2567` lọc
`StartsWith("Hrm") || StartsWith("Off") || MenuKey == Personal``Khkk_G*` **0 match**. Comment khớp.
- Vá-10 (lý-do site-2) **ĐÚNG**: `GetMyMenuTreeQuery.cs:96`
`static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess);` ⇒ group KHÔNG cần
row của chính nó; đính chính lý-do sai cũ là chính xác.
- Vá-8 (fe-admin `isAdminHidden`) **ĐÚNG**: `:207-209` thân hàm đúng là `return key.startsWith('Ct_')`
⇒ lời khai "hàm này CHỈ ẩn `Ct_*`" khớp đĩa; `KeHoachKyKet` KHÔNG bị ẩn. Và `MenuGroup`
(`fe-admin/Layout.tsx:223-262`) render header + container **vô điều kiện** ⇒ 7 nhóm rỗng THẬT SỰ hiện
trên sidebar admin (khớp FLAG-3 lead đã dispose, không phải suy đoán).
- Vá-15 (2 khuôn key trong `MenuKeys.cs`) **ĐÚNG**: `Ct_*`/`Pe_*` thật sự KHÔNG có trong `All`
(chỉ factory), còn `AwV2_*`/`Khkk_G*` có ⇒ mô tả 2 khuôn không tự mâu thuẫn.
## Mục 2 — DELTA `Menu keys` 113 · `Policies` 452 (ĐO ĐỘC LẬP, không tin test cũng không tin lane)
Phép đo tự dựng (parse `MenuKeys.cs`, KHÔNG gọi lại bộ sinh — tránh tautology): tách phần tử của
`All`, phân biệt token thường vs token spread, resolve từng `const string` sang giá trị chuỗi, rồi
tự bung 7×(1+6):
- token thường = **64** · resolve được **64/64** (0 token không tra ra hằng)
- spread duy nhất = `.. KhkkGroupMenuKeys` → bung **49**
- **|All| = 113** · **trùng lặp = 0** (`duplicates: []`)
- **Policies = 113 × 4 = 452**, khớp đúng vòng sinh `Program.cs:82-89`
(`foreach menu in MenuKeys.All / foreach action in MenuKeys.Actions / AddPolicy($"{menu}.{action}")`),
`Actions` = 4 phần tử (`MenuKeys.cs:252`). Đây là chỗ DUY NHẤT sinh policy theo menu-key
(`grep AddPolicy Program.cs` = 2 hit, hit còn lại `:113` là rate-limit `auth-login`, không tính).
**452 = 113×4 ĐÚNG LOGIC SINH**, không phải số chép tay. PASS.
STATUS.md (lead vá SAU khi spawn — coi là phần của commit K4a):
- `:6` Menu keys **64→113** ✔ khớp máy · Policies **256→452** ✔ khớp máy.
- bảng canonical `:470-471` **452** + **113** ✔ khớp máy, kèm câu "test T2 đỏ nếu drift" — câu này
ĐÚNG (T2 assert `HaveCount(113)`), tức con số có phép-kiểm-làm-nó-trượt. Đạt cổng nhãn-đo-lường.
- `:6` test **614→620** (45D + 575I): file test mới có đúng **6 `[Fact]`** (`grep -c` = 6) ⇒ số học
khớp. Đang chạy `dotnet test SolutionErp.slnx` để đo tươi (kết quả ghi ở mục 8).
- bảng canonical `:465` Mig **71**: đếm đĩa `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`
(bỏ `.Designer.cs` + `ModelSnapshot`) = **71** ✔ khớp.
🔴 **FLAG C (MAJOR) — ô canonical bundle `STATUS.md:479` SAI NGAY LÚC VIẾT, đo bằng 2 lệnh curl.**
Ô mới ghi admin **`mySTlx42`** (css `Cf3iCOOv`) · user/eoffice **`CZAYiWWa`** (css `DX1FRk5n`).
Đo LIVE 2026-08-01 trong lượt review:
| Site | STATUS vừa ghi | Đo live (`curl \| grep assets/index-`) | Khớp? |
|---|---|---|---|
| admin.solutions.com.vn (js) | `mySTlx42` | **`1yiNV4VH`** | ✗ |
| admin (css) | `Cf3iCOOv` | `Cf3iCOOv` | ✓ |
| eoffice.solutions.com.vn (js) | `CZAYiWWa` | **`DoULfmdT`** | ✗ |
| eoffice (css) | `DX1FRk5n` | `DX1FRk5n` | ✓ |
2 css khớp ⇒ phép đo của tôi đang trỏ ĐÚNG site (không phải đo nhầm chỗ). Chỉ 2 hash JS lệch — đúng
như chính ô đó tiên đoán ("F-1 `7a903cf` + K4a push sau đó SẼ rotate tiếp"). Vấn đề: ô này là **hàng
canonical**, và nó đang ghi giá-trị đã chết trong khi giá-trị sống lấy được bằng 1 lệnh curl 25 giây.
Sửa rẻ: hoặc thay bằng 2 hash vừa đo, hoặc ghi thẳng "chưa đo lại sau `7a903cf`" thay vì để hash cũ
đứng ở ô canonical. (Đây là ca *claim mạnh hơn việc đã làm* — nhãn "FROZEN-hợp-lệ K3" đọc như đã-xác-
nhận, nhưng thứ được xác nhận là mốc K3, không phải trạng thái prod lúc commit.)
## Mục 3 — vá-4/12: diff có CHẶN phép đo prod không?
**Không chặn.** Đo từng vế:
- A4 (`SELECT DISTINCT MenuKey FROM Permissions WHERE MenuKey LIKE 'Khkk%'``MenuItems.Key`):
mọi key được grant đều có menu-row — site-1 cấp theo `MenuKeys.All` (113 key, invariant T6 K1 phủ),
site-2 cấp `KhkkKeys()` = 7 key G1 (có row cứng ở tree initializer) + 49 key mới (có row từ vòng
seed mới) ⇒ tập grant ⊆ tập row **by-construction**. Không có đường sinh Permission cho key
không-row.
- A5 (restart ×2 → `Khkk%` = 56 row): 7 + 49 = 56; không key nào khác bắt đầu bằng `Khkk`
(`AwV2_KhkkN*` bắt đầu bằng `AwV2`) ⇒ phép đếm `LIKE 'Khkk%'` sạch, không nhiễu.
- ⚠️ **Bẫy CHO LANE CICD, không phải lỗi của diff:** `MenuItems` có 2 cột override
(`MenuItem.cs`: `IsVisible` mặc định `true`, `DisplayLabel` nullable — Mig 27). Seeder **không bao
giờ ghi 2 cột này** (`grep DisplayLabel DbInitializer.cs` = 0 hit) ⇒ 49 row mới sinh ra `IsVisible=true`,
`DisplayLabel=null` (đúng, hiện được). NHƯNG với `Khkk_G1` trên prod: nếu admin từng bấm đổi tên qua
`PATCH /api/menus/{key}` thì `DisplayLabel` khác null và **fe-user render `DisplayLabel`, không phải
`Label`** (`GetMyMenuTreeQuery.cs:86-88`) ⇒ acceptance đo NHÃN TRÊN UI có thể thấy chữ cũ dù
`labelBackfill` đã đổi `Label` thành công. ⇒ khi nghiệm thu A5 phải đo `Label` **bằng SQL**, và nếu
đo trên UI thì kiểm `DisplayLabel IS NULL` trước. Ghi ra đây để cicd không kết luận nhầm "backfill hỏng".
## Mục 8 — idempotent + revoker-proof: test đủ răng chưa?
**Đo tươi suite (chính tôi chạy, không tin lane):** `dotnet test SolutionErp.slnx` exit-0 —
Domain **45/45**, Infrastructure **575/575**, **tổng 620 PASS / 0 FAIL**. Khớp đúng số STATUS `:6`
ghi (620 = 45D+575I). Con số này có phép-kiểm-làm-nó-trượt và tôi đã chạy phép kiểm đó.
- **Site-2 `.Concat` chạy 2 lần — KHÔNG double-grant.** 3 lớp chặn, đọc trên đĩa:
`reviewKeys` kết thúc bằng `.Distinct()` (`:2480`) ⇒ trong 1 lượt không có key lặp;
② nhánh key đã tồn tại: `existingRows.TryGetValue((role.Id, key), …)` → non-Pe `continue`
(`:2497-2508`) ⇒ lượt 2 là no-op;
③ backstop DB: `PermissionConfiguration.cs:26` `HasIndex(x => new { x.RoleId, x.MenuKey }).IsUnique()`
⇒ nếu 2 lớp trên hỏng thì NỔ chứ không âm thầm nhân đôi. PASS.
- **T3/T4 có răng cho phần chúng đo**: T3 seed ×2 rồi so `khkkAfter2.Equal(khkkAfter1)` + `totalAfter2
== totalAfter1` (đếm TOÀN bảng, không riêng họ Khkk) ⇒ bắt cả nhân-row chỗ khác. T4 đo DB↔DB lấy
nhóm 1 làm mốc + chốt trước "6 nhãn nhóm 1 phân biệt" (chống vacuous). T5 dùng ma-trận
cùng-ruột-khác-1-biến (G1 có entry backfill tự lành / G2 không có entry giữ chữ hỏng) — đúng cách
chứng CƠ CHẾ chứ không chứng kết quả. Đây là bộ test viết tốt hơn mức trung bình của repo.
🔴 **FLAG D (MAJOR) — bất-biến DUY NHẤT từng vỡ trên prod lại là bất-biến KHÔNG có test.**
`RevokeTemporarilyHiddenModulesAsync` không đụng `Khkk_*` — tôi đã verify bằng mắt trên LINQ
(`:2554-2567`, chỉ `Hrm`/`Off`/`Personal`) và comment vá-11 khai đúng. Nhưng:
- Không test nào khoá điều đó. Thêm 1 chữ `|| p.MenuKey.StartsWith("Khkk")` vào bộ lọc ⇒ **620/620
vẫn XANH**, còn thực tế 49 row CanRead vừa cấp bị lật sạch ở lần restart kế.
- Đúng kịch bản đã xảy ra THẬT và được chính comment `:2558-2565` dẫn lại: Run #423, "447/494 row rơi,
11/13 role về 0".
- Khuôn có sẵn, rẻ: `AdminOnlyModulesRevokeTests.cs:188` `Revoke_DoesNotTouch_PeModule` — mirror thành
`Revoke_DoesNotTouch_KhkkGroups` là xong.
- T3/T4/T5/T6 đều gọi **từng seeder lẻ qua reflection**, không chạy chuỗi `SeedAsync` đầy đủ ⇒ **thứ
tự** grant → revoke → site-3 hoàn toàn ngoài vùng test. Vì vậy acceptance A5 (restart API ×2 THẬT)
**chưa thể thay bằng test** — giữ nguyên là việc bắt buộc của lane cicd, đừng tick sớm.
## Mục 9 — đụng key cũ? cây phình?
- **Va key: 0.** Đo bằng chính phép parse `All`: `duplicates: []`; 7 key nhóm 1 (`Khkk_G1`,
`Khkk_WfView`…`Khkk_Deleted`) **không** nằm trong `All` (đúng chủ ý, T2 khoá bằng `NotContain`);
giao của 49 key sinh với 7 key nhóm 1 = **∅**; họ `AwV2_KhkkN1..N8` (K3) tách namespace hoàn toàn
(`AwV2_` ≠ `Khkk_`), 8/8 vẫn nguyên trong `All`.
- **Kế thừa quyền: không lây.** `GetMyMenuTreeQuery` chỉ inherit từ 4 root (`Contracts`, `Workflows`,
`PurchaseEvaluations`, `PeWorkflows`); `KeHoachKyKet` không có case ⇒ 49 key mới **không** thừa cờ
từ root, đúng thiết kế "grant từng dòng" (khớp gotcha #85 — endpoint vẫn gate `KeHoachKyKet.*`).
- **Cây phình (mốc CUỐI, re-đo sau khi lead vá STATUS):**
- HEAD trôi 2 lần trong lượt review: `ce15c64` → `21908b4` → **`0cae243`**; cả 2 commit mới là
`wal: flush`, đo `git diff --stat ce15c64..HEAD -- src fe-user fe-admin tests docs` = **RỖNG**
⇒ không có mã nào lọt vào commit governance. OK.
- `git diff --stat -- src fe-user fe-admin docs` = **7 file, +219 / 11** (6 code + `docs/STATUS.md`).
- `git status --porcelain` = 10 M + 1 `??`. Trong đó **8 file thuộc commit K4a** (6 code + STATUS +
file test untracked); **3 file KHÔNG thuộc**: `.claude/auto-memory/MEMORY.md`,
`.claude/auto-memory/feedback_return_terminates_agent.md`, `.claude/governance/.session-counter.json`.
- ⚠️ **Note E:** `git add -A` sẽ kéo 3 file governance vào commit mã. Add đích danh 8 file.
- So mốc đầu (T0: 6 file code) → mốc cuối (7 file + test): phần thêm đúng bằng `docs/STATUS.md` mà
coordinator đã báo trước ⇒ **không có phình lén**.
## VERDICT — PASS-WITH-FLAGS 4 (0 blocker)
Phần lõi K4a **đúng và đo được**: 49 key by-construction 3 phía (`All` · seed menu-row · grant site-2),
`|All| = 113` / `Policies = 452` khớp logic sinh, 8/8 nhãn khớp TỪNG KÝ TỰ với bảng owner đã soát,
0 va key, idempotent 3 lớp, revoker không đụng, suite **620/620 PASS đo tươi**. Không có gì trong diff
chặn phép đo prod A4/A5. Không kiến nghị sửa mã — dưới đây là vấn đề + tiêu chí nghiệm thu.
| # | Mức | Vấn đề | Tiêu chí đóng |
|---|---|---|---|
| A | MAJOR | `?group=n` **không code nào đọc** (`get('group')` 0 hit ×2 app) ⇒ 7 nhóm mới × 6 leaf mở ra **cùng một nội dung**; `WfView` hiện TOÀN BỘ workflow type-10 chứ không riêng `KHKK-N{n}`. Chính spec đã tiên đoán (`sub-invest-fable-b2-cum2.md:9` "silent-wrong, không lỗi build") | Lead/owner dispose tường minh như FLAG-3, ghi rõ wave nào nối (K4b đọc `group` → params API; K4c lọc WfView) và **không deploy ra người dùng trước khi nối**, hoặc chấp nhận có văn bản |
| B | MAJOR | 2 mỏ neo MỚI trong `fe-user/Layout.tsx` sai đúng +34 dòng = kích thước khối vừa chèn: `TRANSIENT_QUERY_KEYS (:311)` → thật `:345`; `MenuLeaf :342` → thật `:376`. Cùng lớp lỗi mà vá-13 vừa dọn ở BE | Sửa 2 số, hoặc bỏ số dùng mỏ neo grep (như BE đã làm: `grep existingItems.TryGetValue`) |
| C | MAJOR | `docs/STATUS.md:479` ghi bundle admin `mySTlx42` / user `CZAYiWWa`, **đo live cùng lượt = `1yiNV4VH` / `DoULfmdT`** (2 css khớp ⇒ đo đúng site). Ô canonical sai ngay lúc commit | Thay bằng 2 hash vừa đo, hoặc ghi thẳng "chưa đo lại sau `7a903cf`" thay vì để hash chết ở ô canonical |
| D | MAJOR | Bất-biến "revoker không đụng `Khkk_*`" — thứ **đã vỡ thật trên prod** (Run #423, 447/494 row rơi, chính comment `:2558-2565` dẫn lại) — **không có test**. Thêm 1 chữ vào bộ lọc revoke thì 620/620 vẫn xanh | Thêm `Revoke_DoesNotTouch_KhkkGroups` mirror khuôn sẵn có `AdminOnlyModulesRevokeTests.cs:188` |
| E | NOTE | 3 file `.claude/**` (auto-memory ×2 + `.session-counter.json`) đang dirty cùng cây | Add đích danh 8 file (6 code + `docs/STATUS.md` + file test), KHÔNG `git add -A` |
Ghi chú độc lập: A5 (restart API ×2 THẬT) **không** được coi là đã phủ bởi T3/T4 — 4 test đều gọi
từng seeder lẻ qua reflection, chuỗi `SeedAsync` (grant → revoke → site-3) chưa từng chạy trong test.
END sub-reviewer-gate-k4a — VERDICT=PASS-WITH-FLAGS 4 (A `?group=` chết-lâm-sàng · B 2 mỏ-neo tự-vô-hiệu ·
C ô bundle canonical sai đo-được · D bất-biến-từng-vỡ không test) · 0 blocker · suite 620/620 đo tươi ·
All=113/Policies=452 đo độc lập · nhãn 8/8 khớp từng ký tự

View File

@ -0,0 +1,37 @@
GATE-K4B: IN-PROGRESS
# Gate K4b — adversarial review wave K4b (nối `?group=` end-to-end), S166
**Mốc đo (neo lúc mở gate):** HEAD = `14ea2ea` (wal: flush 20260801T0945); commit code cuối = `7a903cf`.
Spec nói HEAD `7a903cf` — thực tế trên đĩa có thêm 6 commit `wal:` phía trên (chưa xác minh chỉ-WAL, sẽ đo).
`git status --porcelain` lúc mở gate: 18 M + 1 ?? (`tests/.../KhkkGroupMenuSeedTests.cs`).
`git diff --stat`: 541 insertions / 82 deletions / 18 files.
Scope chấm = K4b only: KhkkListPage ×2 · KhkkCreatePage ×2 · fe-user/Layout.tsx (staticMap-G1) · BE ContractSigningPlansController.cs (/deleted +group) · ContractSigningPlanFeatures.cs (ListDeleted).
K4a (MenuKeys/DbInitializer/Layout-regex/types/STATUS/test) ĐÃ qua gate K4a — không chấm lại.
---
## Ghi TỪNG MỤC (append khi đo xong, không soạn-gộp)
### M1 — Đo nền (tự chạy, không tin claim)
- `dotnet build SolutionErp.slnx`**Build succeeded, 0 Warning, 0 Error** (56.4s, tôi tự chạy).
- `npx tsc -b` fe-admin — **EXIT=0**; fe-user — **EXIT=0** (tôi tự chạy, không đọc log nền của lane).
- WAL 6 commit `7a903cf..HEAD` chỉ chạm `.claude/**` (đo `git diff --stat`) ⇒ toàn bộ code K4a+K4b nằm ở working-tree. Mốc so sánh dùng `7a903cf` là ĐÚNG về nội dung code dù HEAD ghi khác.
### M2 — Mirror 2 app (sha256, 16 ký tự đầu)
| file | fe-admin | fe-user | |
|---|---|---|---|
| `pages/khkk/KhkkListPage.tsx` | `fa4c7aa268f35eeb` | `fa4c7aa268f35eeb` | MATCH (khớp con số `fa4c7aa2` spec nêu) |
| `pages/khkk/KhkkCreatePage.tsx` | `545b4c2fe0ebae88` | `545b4c2fe0ebae88` | MATCH |
| `types/khkk.ts` | `7a99b86518948093` | `7a99b86518948093` | MATCH |
`Layout.tsx` lệch by-design (đã khai trong cả 2 file: admin `Khkk_WfView` → Designer, không mang `group`; admin không nới regex `Khkk_G*`).
### M3 — FLAG-1 (code LEAD tự viết) — BE `/deleted` + group
- Ký-pháp: `public record ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null) : PagedRequest, ...`. `PagedRequest``abstract record` (`Common/Models/PagedResult.cs:15`) với 4 property `init` ⇒ trộn positional-ctor + object-initializer HỢP LỆ (record kế thừa record; nếu `PagedRequest``class` thì đã không biên dịch được). Build 0E xác nhận.
- Controller mirror List **ĐÚNG TỪNG NÉT**: `[FromQuery(Name = "group")] int? group = null` đặt ở vị trí tham số đầu, `new Query(group) { Page = …, PageSize = …, Search = …, SortDesc = … }` — cùng khuôn `List` (`:42`, anchor này ĐÚNG, tôi đếm lại).
- Handler: vế lọc `if (request.ApprovalGroup is not null) q = q.Where(p => p.ApprovalGroup == request.ApprovalGroup)` nằm **SAU** khối IDOR non-admin và **TRƯỚC** `Search` — đúng thứ tự list sống (`:755-758`). `IgnoreQueryFilters().Where(x => x.IsDeleted)` giữ nguyên phía trên ⇒ lọc nhóm không đụng vế soft-delete.
- `ContractSigningPlan.ApprovalGroup``int` **không nullable**, default `= 1` (`ContractSigningPlan.cs:41`) ⇒ so sánh `== request.ApprovalGroup` (int? vs int) dịch SQL thành so sánh giá trị, không sinh ca NULL-semantics.
- **Không có validator biên 1..8** cho `/deleted` — nhưng list SỐNG cũng không có ⇒ **consistent**, ghi nhận chứ không tính lỗi. `?group=999` = lọc rỗng (200 + `items: []`, `total: 0`), không 500. `?group=abc` = model-binding fail ⇒ 400 do `[ApiController]`. Cả hai hành vi giống hệt list sống.
- FE gửi `group` cho **cả 2 nhánh** (`KhkkListPage.tsx:124`, nằm ngoài tam-nguyên `deletedView` nên áp cho cả `/contract-signing-plans` lẫn `/deleted`) ⇒ khớp BE mới. Title/subtitle màn "Đã xóa" in tên nhóm — hết là lời nói dối vì server đã lọc thật.

View File

@ -1,4 +1,4 @@
TEST-K4A: IN-PROGRESS
TEST-K4A: DONE — 7/7 PASS (6 gốc + T7 finding-D) · Infra 569→576 · Domain 45 · tổng 621 · 0 hồi quy · 0 prod-code touched
# Lane test K4a — test-after 49 row menu KHKK 8-nhóm (S166, 2026-08-01)
@ -23,11 +23,102 @@ Mục nào chưa chạy = ghi tiêu-đề + "CHƯA ĐO", KHÔNG điền số d
"49 key mới ĐỀU có menu-row" ĐÃ được test cũ phủ tự động (nó là phép thử của claim by-construction).
Lane K4a KHÔNG nhân bản; thay vào đó đo cái test cũ KHÔNG đo: hình bộ sinh · đếm 56 row họ Khkk ·
idempotent ×2 · labelBackfill · grant site-2.
- [ ] §0.4 Baseline suite Infra đo TƯƠI — ĐANG CHẠY (nền), chưa có số
- [x] §0.4 **Baseline Infra ĐO TƯƠI 2026-08-01 (trước khi thêm file): 569 PASS / 0 FAIL** (42 s).
Đo trên cây có BE K4a đã land ⇒ test T6-K1 cũ (`All.Except(menuKeysInDb)`) XANH với 49 key mới
= bằng chứng độc lập rằng vòng seed 49 row khớp đúng bộ key trong `All`.
## §1 — Test bộ sinh đúng hình — CHƯA VIẾT
## §2 — Test seed idempotent x2 — CHƯA VIẾT
## §3 — Test labelBackfill đổi-label-không-nhân-row — CHƯA VIẾT
## §4 — Test grant site-2 phủ leaf mới — CHƯA VIẾT
## §5 — Kết quả chạy filtered — CHƯA ĐO
## §6 — Kết quả full Infra — CHƯA ĐO
## §1..§4 — File test đã LAND: `tests/SolutionErp.Infrastructure.Tests/Application/KhkkGroupMenuSeedTests.cs` (6 test)
Harness: reflection `private static` + `IdentityFixture` (không fixture mới). Không đụng prod code.
| # | Test | Đo cái gì | Chống vacuous bằng |
|---|---|---|---|
| T1 | `KhkkGroupMenuKeys_Is49Keys_GroupImmediatelyFollowedBySixLeaves` | 49 key · `Equal` với **mảng 49 chuỗi VIẾT TAY** (không gọi lại factory ⇒ không tautology) · liền-kề `Khkk_G{n}` ngay trước `Khkk_G{n}_WfView` · pin 6 action | kỳ vọng là literal, sai 1 ký tự là đỏ |
| T2 | `MenuKeys_All_ContainsThe49_NoDuplicate_AndKeepsGroup1Outside` | All ⊇ 49 · `OnlyHaveUniqueItems` · **`HaveCount(113)` = số ĐO runtime** · `KhkkGroupNumbers == [2..8]` · 7 key nhóm 1 KHÔNG nằm trong All | vế cuối = guard thật: thêm số 1 vào bộ sinh ⇒ `tree.Add("Khkk_G1")` lần 2 ⇒ trùng PK lúc boot |
| T3 | `SeedMenuTree_RunTwice_Has56KhkkRows_NoDuplication_AndCorrectTreeShape` | seed ×2: **56 row họ Khkk** (7 nhóm-1 + 49) · lần 2 danh sách key Y NGUYÊN · **tổng bảng MenuItems bất động** · 8 group parent=`KeHoachKyKet` Order=n · mỗi group đúng 6 con · 8 nhãn nhóm phân biệt | đếm bằng Ordinal in-memory (không phụ thuộc collation LIKE); đã kiểm KHÔNG có `HasData` cho MenuItems ⇒ 56 row đến TỪ seeder, không phải EF seed |
| T4 | `SeedMenuTree_NewGroupLeaves_MirrorGroup1_LabelIconOrder` | 42 leaf mới khớp **nhãn + icon + Order** của leaf nhóm 1 tương ứng (đo DB↔DB, không bảng hardcode) | chốt trước "6 nhãn nhóm 1 PHÂN BIỆT" — nếu chúng trùng nhau, phép so mirror đúng ở mọi thế giới |
| T5 | `SeedMenuTree_LabelBackfill_HealsGroup1Label_WithoutAddingRows_ControlKeyStaysDirty` | **MA TRẬN cùng-ruột-khác-1-biến**: bôi hỏng nhãn CẢ `Khkk_G1` (ca thật: về chuỗi skeleton S159) LẪN `Khkk_G2` trong cùng 1 lần → seed lại → G1 **tự lành**, G2 **giữ chữ hỏng**; tổng row bất động | 2 kết quả TRÁI NGƯỢC trong 1 lần chạy; khác biệt duy nhất = có/không entry `labelBackfill` ⇒ chứng đúng cơ chế backfill, không phải upsert "tiện tay" ghi Label (gotcha #11) |
| T6 | `SeedAllRolesReviewRead_GrantsReadOnNew49KhkkKeys_ReadOnly_WithPeContrast` | site-2 phát CanRead cho cả 49 key (nêu tên `Khkk_G2_List`) · C/U/D=false · 7 key nhóm 1 GIỮ quyền (`.Concat` mở rộng chứ không thay thế) | **ĐỐI CHỨNG CÙNG LƯỢT**: cùng role đó `Pe_DuyetNcc_List` có CanCreate=true ⇒ chuỗi "false" trên Khkk không phải vì seeder no-op. Chạy `SeedMenuTreeAsync` trước = smoke FK cho TOÀN grant-set |
Đường vào duy nhất của 49 key ở site-2 = `KhkkKeys().Concat(MenuKeys.KhkkGroupMenuKeys)` (K4a) —
`All.Where(StartsWith "Pe_")` không bắt `Khkk_*` ⇒ gỡ `.Concat` là T6 ĐỎ.
## §5 — Chạy filtered — ĐÃ ĐO
`dotnet test tests/SolutionErp.Infrastructure.Tests --filter "FullyQualifiedName~KhkkGroupMenuSeed" --nologo`
**Failed: 0, Passed: 6, Total: 6** (3 s).
Ghi chú số: `MenuKeys.All` runtime = **113** ĐÚNG như đếm by-source của lane BE (assert xanh) ⇒
`Policies` DERIVED = 113 × 4 = **452**.
## §6 — Full suite — ĐÃ ĐO
- **Infra: Failed 0, Passed 575, Total 575** (30 s) — baseline 569 + 6 = 575 ⇒ **0 hồi quy**.
- **Domain: Failed 0, Passed 45** (311 ms) — đo tươi, không lấy từ trí nhớ.
- **Tổng suite = 45 + 575 = 620.**
Caveat trung thực: giữa lượt làm có commit `7a903cf` (lane F-1, FE + `ContractSigningPlanFeatures.cs`)
land vào repo. Đã kiểm: `git diff --stat -- src/` vẫn đúng 42/79 dòng của riêng K4a như lúc mở lượt
⇒ byte nguồn K4a KHÔNG đổi trong lúc đo; file lane F-1 đã nằm sẵn trên đĩa TRƯỚC lần chạy full
(nó là ` M` từ đầu, commit chỉ chuyển trạng thái index) ⇒ con số 575 đo đúng trạng thái đĩa hiện tại.
## §7 — Rào đã giữ
- KHÔNG chạm production code: `git status` cho thấy dưới `tests/` chỉ có **1 file MỚI**
`KhkkGroupMenuSeedTests.cs`; 2 file `src/` đang ` M` là diff của lane BE K4a (có từ trước lượt này).
- KHÔNG đụng `ContractSigningPlan*` / `KhkkWorkflowPanel` / `khkk.ts`.
- KHÔNG commit, KHÔNG push.
- KHÔNG sửa `SolutionErp.slnx` (project SDK-style tự glob `.cs`; test đã build + chạy).
## §8 — Quan sát gửi lead (KHÔNG phải bug, không tự sửa)
1. **Chồng lấn có lợi:** test T6 lane K1 (`ContractCatalogTests.cs:445`) nay là phép thử tự động cho
claim "49 key ∈ All đều có menu-row" — nó XANH ở baseline 569 ⇒ claim by-construction của K4a đã
có chứng nhân độc lập, lane K4a không cần lặp lại.
2. **Nhãn G2..G8 KHÔNG tự lành** (đo ở T5, đối chứng): nếu owner đổi chữ 7 nhãn nhóm sau khi prod đã
seed thì sửa trong `khkkGroupLabels` là VÔ TÁC DỤNG — phải thêm entry `labelBackfill`. Comment
BE `:2233-2234` đã khai đúng điều này; T5 nay khoá nó thành bất-biến có răng.
3. **Số canonical cần đổi kèm K4a:** `MenuKeys.All` ĐO runtime = 113 ⇒ `docs/STATUS.md` row
`Menu keys` = 113 · `Policies` = 452. (T2 sẽ ĐỎ nếu 2 số này drift khỏi mã.)
## §D — RESUME gate K4a, finding D (MAJOR): bất-biến "revoker KHÔNG đụng `Khkk_*`"
Lệnh lead: +1 Fact `Revoke_DoesNotTouch_KhkkGroups`. Lý do: ca ĐÃ VỠ THẬT trên prod
(Run #423 — 447/494 row rơi, 11/13 role về 0; gotcha #84) mà hiện 0 test ⇒ thêm 1 chữ vào
bộ lọc revoke thì suite vẫn xanh.
- [x] §D.1 Đọc PREDICATE THẬT từ mã (`DbInitializer.cs:2554-2568`), không theo prompt:
`RoleId != adminRoleId && (MenuKey.StartsWith("Hrm") || MenuKey.StartsWith("Off")
|| MenuKey == Personal) && (bất kỳ cờ nào đang true)` → set cả 4 cờ = false.
🔴 **Bẫy vacuous-pass phát hiện khi đọc mã:** `:2546-2551` — **Admin role chưa seed ⇒ revoker
RETURN SỚM, no-op**. Test không seed Admin sẽ XANH vì revoker CHƯA TỪNG CHẠY, chứ không
phải vì `Khkk_*` được tha. Bắt buộc seed Admin + có **control dương**.
- [x] §D.2 Chọn chỗ đặt: `KhkkGroupMenuSeedTests.cs` (file của lane K4a) — đã có sẵn mảng
`Expected49` + 7 key nhóm 1 + harness seeder; đặt ở `AdminOnlyModulesRevokeTests.cs` sẽ phải
chép lại danh sách 49 key (nguồn thứ 2 = nguy cơ lệch). Vẫn MIRROR khuôn 2-vế của file đó.
- [x] §D.3 ĐÃ viết `T7 Revoke_DoesNotTouch_KhkkGroups` (cuối `KhkkGroupMenuSeedTests.cs`).
Kịch bản 1 lượt, 3 chiều phân biệt:
1. seed menu THẬT + site-2 grant THẬT cho 3 role (Admin + Drafter + DeptManager);
2. cấp tay 2 key **trong** tập ẩn (`Off_DanhBa`, `Personal`) cho cả 3 role = **CONTROL DƯƠNG**;
3. tick thêm `CanCreate` cho `Khkk_G5_Create` (mô phỏng admin bật cờ ghi trên ma trận);
4. chạy `RevokeTemporarilyHiddenModulesAsync`;
5. **Vế 1** (control dương): 2 key kia của 2 role non-Admin về **false cả 4 cờ** ⇒ chứng
revoker CÓ CHẠY (nếu đỏ = phép đo hỏng, không phải Khkk hỏng);
**Admin giữ true** ⇒ chứng vế `RoleId != adminRoleId` còn sống;
**Vế 2**: 56 key Khkk × 2 role = **112 row sống sót**, `CanRead` true, row KHÔNG bị xoá,
cờ ghi đã tick vẫn true.
Đếm bằng ĐƠN VỊ của sự cố thật (row sống sót) để thông điệp lỗi cùng ngôn ngữ Run #423.
- [x] §D.4 **CHỨNG T7 CÓ RĂNG bằng PROBE VI-PHÂN** (không được chạm prod ⇒ không fault-inject
vào `DbInitializer`): file tạm `ZzProbeTeethTemp.cs` dựng lại ĐÚNG hình câu truy vấn revoke
(`:2554-2568`) + thêm **đúng 1 vế** `|| MenuKey.StartsWith("Khkk")` rồi đo trên DB thật.
Kết quả ĐO: **trước = 112 row Khkk CanRead=true · sau bộ-lọc-bị-nới = 0** ⇒ đúng tập row mà
T7 assert sẽ rơi hết ở thế giới hỏng ⇒ T7 phân biệt được 2 thế giới, KHÔNG phải assert đúng
ở mọi thế giới. Probe chạy 1/1 PASS rồi **XOÁ**; verify 2 chiều: `git status -- tests/` chỉ
còn 1 file `??` (KhkkGroupMenuSeedTests.cs) và suite = 569+**7** (không phải +8) ⇒ 0 dư lượng.
## §9 — SỐ CHỐT (đo tươi, lần cuối, sau khi đã xoá probe)
- Filtered `~KhkkGroupMenuSeed`: **7/7 PASS**.
- **Infra: 576 PASS / 0 FAIL** (baseline 569 + 7) · **Domain: 45 PASS / 0 FAIL**.
- **Tổng suite = 621.**
END sub-test-k4a — VERDICT=7/7 PASS (+7 test, suite 621 = 45 Domain + 576 Infra, 0 hồi quy, 0 prod-code touched)

View File

@ -9,6 +9,11 @@
## taskList
| # | vai | trạng thái |
|---|---|---|
| 1 | lead-stale-auditor (deep) | spawned @S166 |
| 2 | lead-gap-auditor (deep) | spawned @S166 |
| 3 | ring2-audit (SAU khi 1+2 trả) | |
| 1 | lead-stale-auditor (deep) | ✅ 11 FLAG (4H/5M/2L) coverage 8/8-chạm-2-PARTIAL = SÀN — #53 ×2, resume-3 chốt |
| 2 | lead-gap-auditor (deep) | ✅ 6 FLAG (4H/2M) coverage 6/6 — #53 ×1, resume-2 chốt; FLAG nặng nhất bắt chính lead (_mind đọc cụt 165/270, vá trong phiên) |
| 3 | ring2-audit (SAU khi 1+2 trả) | **16Đ/1T** — TRƯỢT duy nhất = stale F-8 (đo sai phạm-vi HANDOFF, kết-luận lật ngược; lead đã chép sai vào WAL:33 → đính chính @S166); khuôn C11 17/17 · enum 0-tự-chế · tally ĐẠT |
## Kết vòng (lead @S166)
- last_audit light=deep=**40** · tally 13-cặp ratified (gap-carry-dropped 9 consecutive) · detectors re-run: canonical resolve 97/71 ĐÚNG (poison STATUS:465-472 đã gỡ cùng lượt vá 5-row).
- Resolve còn treo → WAL closeout-nợ (F-4 CLAUDE-mig · F-5/F-6/F-9/F-10/F-11 · errata INFORM 2-5 của ring2 ghi tại artifact nó).
- Bài đắt nhất vòng: **lead chép claim vai-đo TRƯỚC vòng-kiểm vào sổ = suýt đóng owner-decision (59) bằng tiền-đề sai ngược chiều** — ring2 tồn tại đúng để bắt ca này; từ nay claim ĐẢO-DẤU/LẬT-TIỀN-ĐỀ của vai-đo chỉ vào WAL SAU khi vòng-kiểm chốt.

View File

@ -1,4 +1,4 @@
LEAD-GAP-DEEP: IN-PROGRESS
LEAD-GAP-DEEP: 6 FLAG (4 HIGH / 2 MED / 0 LOW) — coverage 6/6
# H24 DEEP-audit vai-GAP @S166 (2026-08-01) — soi CÁI BỊ THIẾU
@ -219,3 +219,139 @@ trong tầm đọc** ⇒ hoặc bỏ điều-kiện nghiệm-thu, hoặc phải
**resolve:** hết-flag khi (i) mỗi OG còn hiệu-lực có **1 dòng ID + 1 dòng nội-dung** trong sổ bền (WAL notes hoặc HANDOFF),
và (ii) mọi tham-chiếu `F-n` trong sổ bền mang tiền-tố cổng (`gate-K2 F-5`), và (iii) claim "N OG trọn" liệt **đủ N ID**.
---
## FLAG-6 — `gap-decision-sunk` — MED
*(scope-1: DANH-SÁCH VÁ 11 mục — con-trỏ tới nó đã CHẾT, sổ theo-dõi nằm ngoài mọi đường nạp)*
**hứa ở:** `.claude/workflows/runs/2026-07-30-S163-bookend-open/run.md:117-120`:
> `## 🔧 DANH-SÁCH VÁ — 11 mục, CHƯA CHẠM (**nhà canonical của cụm này**; `_mind` §B chỉ TRỎ về đây)`
> *"Anh gõ `/pause` trước khi lead bắt đầu vá ⇒ **0/11 đã làm**."*
> *"🔴 Liệt đích-danh vì `ctx-curator` #7 bắt đúng: cụm '11 mục' trước đó **không có nhà nào giải được** — rơi 1 dòng = mất 11 ý."*
**hiện: con-trỏ đứt ở CẢ HAI đầu.**
```
grep -c '11 mục|danh-sách vá' -> HANDOFF 0 · STATUS 0 · WAL 0 · error-ledger 0 · ACTIVE-MARKS 0 · migration-todos 0
_context 1 (PAUSE-1, entry immutable đã bị PAUSE-3 thay) · _mind 2
```
- Con-trỏ **duy nhất** nằm trong tập nạp là `_mind-s-9.md:200`: *"Hướng-tiếp cũ (chạy 11 mục vá) đã tiêu ở window-2 — **xem WAL/`_context` PAUSE-2**."*
- Đích của con-trỏ đó: `WAL` = **0 hit** · `_context` PAUSE-2 (`:108-110`) = **0 hit****con-trỏ CHẾT**.
- Và chính dòng `:200` nằm ở **dòng 200 > 165****không được đọc** trong lượt `/tiep` @S166 (xem FLAG-1). Hai lỗi chồng nhau.
- Nhà canonical (`runs/…S163…/run.md`) **không nằm trong `tiep_reload.sources`** ⇒ không nghi-thức nào mở nó.
🔴 **CHẠM ĐĨA TRƯỚC KHI PHÁN (luật S121 — bảng mô-tả Ý-ĐỊNH ≠ TRẠNG-THÁI).** Sổ vẫn ghi "0/11", đĩa nói khác:
| # | việc | đĩa nói gì (lệnh) | thật |
|---|---|---|---|
| 11 | refine A1/A2 L1→L2 `lead-gap-auditor` (23.593 B) · `harvest-curator` (23.493 B) | `wc -c .claude/agent-memory/{lead-gap-auditor,harvest-curator}/MEMORY.md`**10.777** / **18.781**; cả 2 có `archive/2026-07.md` | ✅ **XONG** |
| 1 | STATUS Gotchas 84→85 | `detectors-output.txt:3` `gotcha=85` (canonical) + disk `gotcha=85` | ✅ **XONG** |
| 2 | `CLAUDE.md` +row `ContractSigningPlans` + Mig 68/69 | có row `ContractSigningPlans … 69` + `+S157 Mig 68` trong CLAUDE.md | ✅ **XONG** |
| 5 | slot (58) viết dạng bất-biến | `HANDOFF:21` = *"Trần `_mind` CỦA PHIÊN-LOGIC HIỆN HÀNH … CẤM neo literal/tên file"* | ✅ **XONG** |
| 8 | `ctx-soft-memory-se-draft.md` header "DRAFT · CẤM wire" | header nay = *"✅ RATIFIED + ĐÃ WIRE @S153 (OWNER-VETO)"* | ✅ **XONG** |
| 7 | skills `ef-core-migration` · `permission-matrix` | `detectors-output.txt:14-25` VẪN flag `ef-core-migration/SKILL.md:93` (MED) + `permission-matrix/SKILL.md:60,:80` | ❌ **CÒN MỞ** (máy đang kêu) |
**KHÔNG được nói "0/11"** (sai) và cũng **không được nói "xong hết"** (sai). Sự-thật đo được: **≥5 xong · ≥1 còn mở · 5 CHƯA XÁC-ĐỊNH**
— và **không sổ nào ghi được điều đó**, vì sổ theo-dõi bị bỏ lại ở run-folder với con số "0/11" đóng băng từ 2026-07-30.
**im:** **2 ngày / 3 nhãn phiên** (S164 · S165 · S166) kể từ PAUSE-1 (2026-07-30T17:35).
**hệ-quả nếu không vá:** cụm 11 mục rơi vào trạng-thái tệ nhất — **không ai biết đã làm tới đâu**: nếu lead tin "0/11" thì
làm lại 5 việc đã xong; nếu lead tin "đã tiêu rồi" thì bỏ luôn mục 7 mà **máy vẫn đang kêu mỗi lượt detector**.
Đúng cảnh-báo của `ctx-curator` #7 mà run.md tự chép vào: *"rơi 1 dòng = mất 11 ý"* — nay rơi thật, chỉ khác là rơi cả cụm.
**resolve:** hết-flag khi cụm 11 mục có 1 dòng trong sổ **nằm trong đường nạp** (WAL `chain` hoặc HANDOFF NEXT-em) với
trạng-thái từng mục **đo từ đĩa** (không chép "0/11"), hoặc lead khai tường-minh "cụm này ĐÓNG, lý do X" ở sổ bền.
---
# HONEST-ZERO — mục soi mà KHÔNG ra flag (giá của việc khai sạch)
## mục 4 (một nửa): 9 slot cũ — **SẠCH, tự BÁC giả-thuyết của chính mình**
Giả-thuyết vào: *"slot cũ (39)(40)(41)(42)(43)(44)(46)(47)(48) chìm dần khi khối CHỜ-ANH bị viết đè."***BÁC.**
```
for n in 39 40 41 42 43 44 46 47 48; do grep -o "($n)" docs/HANDOFF.md | wc -l; done
(39)2 (40)3 (41)3 (42)3 (43)3 (44)3 (46)2 (47)2 (48)2 -> 9/9 CÒN SỐNG
neo: docs/HANDOFF.md:23 "**Slot cũ còn treo:** (42)(43)(44) … (46) … (47) … (48) … (39) … (40) … (41)"
CONTROL-ÂM (slot chưa từng tồn tại): (63) -> 0 hit · (64) -> 0 hit ⇒ phép đo PHÂN-BIỆT được, không phải luôn-dương
```
⇒ Khối *"Slot cũ còn treo"* đang làm **đúng việc của nó**. Lỗ duy nhất của trục này là **(60)(61)** — đã bắt ở FLAG-2,
và chính vì 9/9 kia sống mà (60)(61) mới là **bỏ CHỌN LỌC**, không phải "khối này vốn hay rơi".
## `gap-incident-unrecorded` = **0** — đã cân nhắc, TỰ BÁC
Ứng-viên: 2 ESCALATE của `harness-audit` @S163 — (i) backstop **G-015** rỗng-hoá vì Stop-hook nuốt `.claude/agent-memory/`
vào commit trước khi lead soát · (ii) stray `agent-cwd-relative-memory-misland` tái phát ×3/phiên.
Phép đo: `grep -c 'Stop-hook|nuốt .claude/agent-memory' docs/governance/error-ledger.md`**0**.
**NHƯNG** class này đòi *"0 hit trên **bất-kỳ** sổ bền nào"*, mà `_context-s-9.md:62` **CÓ** ghi cả 2, và `_context` nằm
trong `tiep_reload.sources` (đọc trọn) ⇒ **KHÔNG đủ điều-kiện****không phát flag**.
🔸 Ghi lại để lần sau khỏi soi lại: nếu tới closeout L9 mà `_context-s-9` bị nén/đóng và error-ledger vẫn 0 hit thì
**lúc đó** mới thành `gap-incident-unrecorded`. Nay chưa.
## `gap-carry-aged` = **0** — và đây là con-số PHẢI đọc kèm caveat
Detector `H24-2` in **5/5 `[ok]`** (streak cao nhất 3 < M=6). 🔴 KHÔNG được đọc thành "carry đều trẻ":
tập-đo của máy **5 slug**, đĩa **50**. Số 0 này **0-của-tập-đo-nhỏ**, không phải 0-của-thực-tế (xem FLAG-3).
Theo luật *"ĐỌC detector, KHÔNG tính lại"* tôi **không** tự dựng streak thay máy; tôi chỉ khai **mẫu-số**.
---
# mục 6 — CHECKLIST DIARY-SEED cho closeout (không phát FLAG mới)
🔸 **Vì sao KHÔNG phát flag:** cam-kết này **CÒN SỐNG** `.claude/WAL.md:27` (`[ ] closeout-nợ: **diary vai (C2)** · …`).
Cái thiếu **ĐỘ PHÂN-GIẢI** (2 chữ "diary vai" không nói VAI NÀO) cùng bệnh đã tính **FLAG-5**, không đếm 2 lần.
Nhưng lead sẽ dựa vào C11 để biết phải seed ai, **C11 chỉ thấy 3/9** dưới đây danh sách đo từ đĩa.
Phép đo: `git log -1 --format='%cI' -- .claude/agent-memory/<role>/MEMORY.md` so với **mtime artifact vai đó**.
| vai | diary commit gần nhất | artifact gần nhất | trạng-thái | C11 bắt? |
|---|---|---|---|---|
| `ctx-verifier` | **2026-08-01 08:51** | @cửa /tiep-3 | **ĐÃ SEED** | |
| `reviewer` | **2026-08-01 09:00** | `sub-reviewer-f1-checkbox.md` 08:58 | **ĐÃ SEED** | |
| `cicd-monitor` | 2026-07-31 17:20 | `sub-cicd-verify-k3.md` **2026-08-01 08:44** | **NỢ** (lượt cicd-K3 re-spawn @S166) | không |
| `lead-stale-auditor` | 2026-07-30 15:13 | `sub-lead-stale-deep-S166.md` **08:50** | **NỢ** (deep @S166) | không |
| `lead-gap-auditor` (vai này) | 2026-07-31 09:55 | `sub-lead-gap-deep-S166.md` (đang ghi) | **NỢ** (deep @S166) | không |
| `implementer-backend` | **2026-07-29 21:49** | `sub-implbe-k4a.md` **2026-08-01 08:52** | **NỢ ×4** (k1 07-31 14:35 · k2 16:20 · k3 17:33 · k4a 08-01) | không |
| `implementer-frontend` | 2026-07-31 16:47 | `sub-implfe-k3.md` 07-31 **17:42** | **NỢ ×1** (k3 sau mốc diary) | không |
| `test-specialist` | 2026-07-31 18:03 | K4a test-lane **đang chạy** | **sẽ NỢ** khi lane đẻ artifact | không |
| `ctx-audit` | 2026-07-30 15:13 | `sub-ctx-audit-open-S163.md` | **NỢ 2 ngày** | |
| `ring1-audit` | 2026-07-30 15:13 | `sub-ring1-audit-open-S163.md` | **NỢ 2 ngày** | |
| `ring2-audit` | 2026-07-30 15:13 | `sub-ring2-audit-open-S163.md` | **NỢ 2 ngày** | |
| `ctx-curator` | 2026-07-31 21:48 | verdict @/tiep-3 (nếu chạy) | kiểm lại | không |
🔴 **Số cần nhớ: nợ ≥ 8 vai, C11 chỉ nêu 3.** Nếu closeout chỉ theo C11 thì **5 vai vẫn vào phiên sau mù**,
bảng sẽ trông như đã sạch. Nặng nhất: **`implementer-backend` nợ 4 lượt / 3 ngày** vai làm nhiều code nhất chuỗi SP-002.
do C11 mù: soi run-folder `sub-<role>-*` khớp **tên vai chuẩn**, lane K1-K4a đặt tên `sub-implbe-*` / `sub-implfe-*` /
`sub-cicd-verify-*` đúng **206 prefix UNMAPPED** chính detector tự liệt `detectors-output.txt:94-95` (INFORM, không flag).
---
# TỔNG
**TOTAL: 6 FLAG** **4 HIGH · 2 MED · 0 LOW**
phân-rã theo class (cho `jump_on_class_repeat` = 3):
| class | số (ĐƠN-VỊ = **TRONG-PHIÊN**) | FLAG |
|---|---|---|
| `gap-carry-dropped` | **2** | FLAG-3 (carry không re-stamp 3 kỳ) · FLAG-4 (backlog owner 0/6 bề mặt) |
| `gap-decision-sunk` | **2** | FLAG-2 (slot 60/61 thủng) · FLAG-6 (11 mục con-trỏ chết) |
| `gap-underfill` | **1** | FLAG-1 (`_mind` cụt 38,1% + bảng CURRENT STATE 0 B) |
| `gap-owner-specifics` | **1** | FLAG-5 (OG 4/10 · F-n không tiền-tố · "9 OG trọn" không kiểm được) |
| `gap-carry-aged` | **0** | (0-của-tập-đo-nhỏ xem HONEST-ZERO) |
| `gap-incident-unrecorded` | **0** | (đã cân nhắc, tự BÁC xem HONEST-ZERO) |
🔴🔴 **KHAI ĐƠN-VỊ TRƯỚC KHI LEAD CHÉP (bài ring2 D-4 @S159 — chính vai này từng sai):**
cột số trên **TRONG-PHIÊN**. Ô tally trong sổ đếm **consecutive-audit**.
`gap-carry-dropped` đang = **8** (chốt @S162) lượt này ratify thì **9**. **CẤM ghi "2" vào ô đó** ghi 2 **xoá lịch-sử 8→2**.
Theo đơn-vị consecutive-audit, ngưỡng `jump_on_class_repeat = 3` đã **vượt từ lâu** class `gap-carry-dropped`
đây **không** phải chuỗi sự-cố lẻ, **nghi-thức hỏng** (nguyên-văn NEXT-em #3 của chính lead `HANDOFF:33`).
**COVERAGE-LINE: 6/6 mục scope đã soi** (1) work-statebacklog: FLAG-6 + FLAG-4 · (2) carry: FLAG-3 · (3) mandate specifics: FLAG-5 ·
(4) quyết-treo: FLAG-2 (60/61) + honest-zero 9/9 slot SẠCH · (5) tiep_reload: FLAG-1 · (6) diary: checklist 8 vai, không flag mới.
🔸 **Khai giới-hạn (không làm tròn thành "quét sạch"):** 11 mục mới chạm đĩa **6/11** (5 mục CHƯA xác-định trạng-thái);
27 finding gate K1/K2/K3 chỉ đo **có/không xuất-hiện trên sổ bền**, KHÔNG đọc ruột từng finding.
Vai này **read-only, propose-only** 0 Write ngoài chính file này, 0 Edit doc, 0 commit, 0 RAG-write.
END sub-lead-gap-deep-S166 VERDICT=6 FLAG (4 HIGH / 2 MED / 0 LOW) · coverage 6/6 · INFORM-only, chờ em-main VERIFYAPPEND

View File

@ -1,4 +1,4 @@
LEAD-STALE-DEEP: IN-PROGRESS
LEAD-STALE-DEEP: 11 FLAG (4 HIGH/5 MED/2 LOW) — coverage 8/8 chạm, 2/8 PARTIAL (nhóm 7 persona 2/23 · nhóm 8 chỉ cặp FE) ⇒ 11 = SÀN
# H24 DEEP-audit @S166 (2026-08-01) — vai `lead-stale-auditor`
@ -12,13 +12,13 @@ LEAD-STALE-DEEP: IN-PROGRESS
| # | Nhóm bề-mặt | Trạng thái |
|---|---|---|
| 1 | `docs/STATUS.md` CURRENT + bảng CURRENT STATE | DONE |
| 2 | `docs/HANDOFF.md` segment mới nhất | PENDING |
| 3 | `CLAUDE.md` root (bảng module/mig) | PENDING |
| 2 | `docs/HANDOFF.md` segment mới nhất | DONE |
| 3 | `CLAUDE.md` root (bảng module/mig) | DONE |
| 4 | `.claude/agents/README.md` roster | DONE — SẠCH |
| 5 | `docs/changelog/migration-todos.md` | PENDING |
| 6 | Governance surfaces `docs/governance/*.md` | PENDING |
| 7 | Mô-tả VAI `.claude/agents/*.md` | PENDING |
| 8 | Dư-lượng bất-đối-xứng (mirror pairs) | PENDING |
| 5 | `docs/changelog/migration-todos.md` | DONE |
| 6 | Governance surfaces `docs/governance/*.md` | DONE |
| 7 | Mô-tả VAI `.claude/agents/*.md` | 🔴 **PARTIAL** (chỉ soi có-mục-tiêu `cicd-monitor` + `test-specialist`; **KHÔNG** quét 23/23 persona) |
| 8 | Dư-lượng bất-đối-xứng (mirror pairs) | 🔴 **PARTIAL** (chỉ cặp `fe-admin``fe-user` trên 3 commit K; **KHÔNG** soi cặp spec-cụm⟂WAL) |
---
@ -231,3 +231,49 @@ Control: `git show --name-only --format="" <sha> | grep -c "^fe-admin/\|^fe-user
Lý do K3 lệch là **chủ-đích**: cặp mirror thật `pages/khkk/KhkkWorkflowPanel.tsx` **có mặt CẢ HAI** bên; 2 file admin dôi ra là `system/ApprovalWorkflowsV2Page.tsx` (Designer — bề mặt admin-only) và `components/Layout.tsx` (menu Designer). Đúng ghi-chú `CLAUDE.md`: duplicate giữa 2 app là CÓ CHỦ ĐÍCH, và admin có màn quản-trị mà user không có. **Đếm file thô ra "3 vs 1" trông y hệt sửa-một-phía — phải mở tên file mới phân biệt được**; ghi lại để lần sau không tạo dương-giả.
---
# 🔴 COVERAGE — đọc TRƯỚC khi dùng con số 11
**11 FLAG = SÀN, KHÔNG PHẢI TỔNG.** 8/8 nhóm đã **chạm**, nhưng **2/8 nhóm mới soi một phần** (nhóm 7 và nhóm 8) ⇒ không ai được đọc phần chưa quét thành sạch.
| # | Nhóm | Đã chạm | Kết quả | Control dùng |
|---|---|---|---|---|
| 1 | `STATUS.md` CURRENT + bảng CURRENT STATE | ✅ ĐỦ | FLAG-1, 2, 3 | đếm đĩa `Migrations/*.cs`; `CreateTable` Mig 70/71; đếm PHẦN-TỬ `MenuKeys.All`; `WAL.md:13/:37`; `.session-counter.json` |
| 2 | `HANDOFF.md` segment mới nhất (`:1-117`) | ✅ ĐỦ | FLAG-5, 6, 7, 8 | đo byte `_INDEX.md` vs `tiers.l2_index.cap_bytes`; đo 5 nguồn `tiep_reload` **theo đúng phạm-vi đã khoanh** |
| 3 | `CLAUDE.md` root (bảng module/mig) | ✅ ĐỦ | FLAG-4 (+FLAG-10 site 1) | `grep "Mig 70\|Mig 71\|AddContractCatalog\|AddKhkkApproval" CLAUDE.md` = **0 hit** |
| 4 | `.claude/agents/README.md` roster | ✅ ĐỦ | **SẠCH** | `ls .claude/agents/*.md` = 24 README = **23**; đếm PHẦN-TỬ `:242` = 23 (13 monitor + 10 worker); `:27` = 23; đối-chứng `hmw.js:22 VALID_ROLES` |
| 5 | `migration-todos.md` phase/KHKK | ✅ ĐỦ | FLAG-10 (site 2, 3) | `grep -n "KHKK\|SP-002"` (có §Phase 12 :798-812) · `grep "K1\|K2\|K3\|Mig 70\|Mig 71"` = **0 hit**/822 dòng |
| 6 | `docs/governance/*.md` header/status | ✅ ĐỦ (11 file) | FLAG-11 | quét 8 dòng đầu ×11 file lọc `Status\|updated\|version\|ngày`; `git log -1` xác nhận `README.md` đứng im từ `72bbfa5` 2026-06-02 |
| 7 | Mô-tả VAI `.claude/agents/*.md` | 🔴 **PARTIAL** | FLAG-9 | `grep "Domain 58\|Infra 23\|baseline"` **chỉ trên 2 file** (`cicd-monitor`, `test-specialist`). **CHƯA CHẠM: 21/23 persona còn lại** |
| 8 | Dư-lượng bất-đối-xứng mirror | 🔴 **PARTIAL** | **SẠCH trong phạm-vi đã soi** | `git show --name-only` ×3 commit K, đối-chiếu TÊN FILE (không chỉ đếm). **CHƯA CHẠM: cặp spec-cụm⟂WAL, cặp doc⟂skill khác** |
**Hệ-quả phải nói thẳng:** nếu quét nốt 21 persona còn lại (nhóm 7) thì gần như chắc có thêm FLAG cùng class `view-stale-role-desc` — vì ca đã tìm được (`cicd-monitor`) không phải ca hiếm mà là **trường `description` auto-inject không ai đọc lại suốt ~100 phiên**. Đề-nghị lượt sau nhắm THẲNG nhóm 7 với `grep` số-học trên cả 23 file.
**Điểm mù tự-quy-chiếu (bài M-1 @S153):** lượt này tập-đo CÓ gồm `.claude/agents/*.md`, nhưng phần persona mới soi 2/23 ⇒ **chưa loại trừ được** khả-năng chính `lead-stale-auditor.md` / `lead-gap-auditor.md` đang mang số/nhịp lệch. Không tuyên sạch.
---
# TỔNG KẾT
**11 FLAG — 4 HIGH · 5 MED · 2 LOW.** (SÀN, xem COVERAGE.)
Phân-rã theo class (cho `jump_on_class_repeat`, ngưỡng = 3):
| class (enum ĐÓNG `lead_self_audit.flag_classes`) | Số | FLAG |
|---|---|---|
| `view-stale-count` | **5** | 2 (HIGH), 3 (LOW), 4 (MED), 7 (HIGH), 8 (HIGH) |
| `view-stale-status` | **3** | 6 (MED), 10 (MED), 11 (LOW) |
| `view-residual-asym` | **1** | 1 (HIGH) |
| `view-stale-header` | **1** | 5 (MED) |
| `view-stale-role-desc` | **1** | 9 (MED) |
| **Tổng** | **11** | — |
🔴 **Một sợi chỉ xuyên 4 FLAG HIGH — đọc cái này nếu chỉ đọc một dòng:** cả 1, 2, 7, 8 đều KHÔNG phải "quên cập nhật". Cả bốn đều là **số ĐÃ ĐO ĐÚNG ở nơi lead làm việc** (`WAL.md:13`, `sub-cicd-verify-k3.md:86-94`, `STATUS.md:6`) mà **không đi tới nơi anh đọc** (bảng canonical `STATUS:465-472`, `HANDOFF:15/:19`, `STATUS:479`). Đây đúng câu chốt S162 lặp lại nguyên si sau 4 nhãn phiên. Nặng hơn lần trước ở hai chỗ: (a) **FLAG-2 có nhân-chứng tự khai** — vai cicd đã viết thẳng *"KHÔNG khớp gì đo được"* vào run-folder rồi câu đó nằm im; (b) **FLAG-7/8 là hai owner-decision đang treo, và FLAG-8 đã ĐẢO DẤU kết-luận** — trả lời theo tờ trình hiện tại sẽ gây hại thật.
🔴 **Cái máy KHÔNG thể nói, và lượt này chứng minh bằng ca cụ-thể:** `governance-detectors.ps1` resolve canonical TỪ bảng `STATUS:465-472` nhưng chỉ cross-check đĩa **2/6 trục** (`mig`, `gotcha`). Vì bảng ấy stale, máy đang **đề-nghị sửa một doc ĐÚNG thành SAI** (`detectors-output.txt:14`: `SKILL.md:93` 97 → "update to 96", trong khi đĩa = 97). ⇒ Trong TOTAL 51 của máy, các flag nhánh `derived-stale` về `table`/`test`/`menu`/`policy` phải coi là **CHƯA PHÁN ĐƯỢC** cho tới khi FLAG-1 được vá và detector chạy lại.
⚠️ **Chống META-COUNT (luật của chính vai này):** không dán nhãn `PASS`/`sạch` ở bất kỳ đâu trong file. Hai mục ghi "SẠCH" (nhóm 4, nhóm 8) đều kèm **lệnh control** và nhóm 8 khai rõ "sạch **trong phạm-vi đã soi**".
**Propose-only.** Vai không Write/Edit vào bất kỳ file nào ngoài chính artifact này; không `store_memory`. Mọi FLAG chờ em-main VERIFY → APPEND (B3, no-overwrite-unverified).
END sub-lead-stale-deep-S166 — VERDICT=11 FLAG (4 HIGH/5 MED/2 LOW) · coverage 8/8 chạm, 2/8 PARTIAL ⇒ 11 = SÀN, KHÔNG phải TỔNG

View File

@ -0,0 +1,419 @@
RING2-DEEP: IN-PROGRESS
# ring2-audit @S166 (2026-08-01) — KIỂM vòng soi-lead H24 DEEP
- Vai: KIỂM **2 con-đo H24** (`lead-stale-auditor` + `lead-gap-auditor`) — **KHÔNG** tự soi lead.
- Chế-độ: read-only, INFORM-only, propose-only. AUTO sau cặp (owner @S146 "3 → auto").
- PIN (fail-closed, đã xác-nhận TỒN-TẠI + tươi):
- `sub-lead-stale-deep-S166.md` — 29.441 B, mtime 2026-08-01 09:05
- `sub-lead-gap-deep-S166.md` — 28.466 B, mtime 2026-08-01 09:06
- `detectors-output.txt` — 26.170 B, mtime 08:49
- `.claude/governance/.session-counter.json` (tally lead vừa ratify)
- Chứng C4b tuần-tự: artifact này tạo SAU mtime cả 2 file trên.
(ghi TỪNG verdict ngay khi chấm xong — chống #53)
---
# §1 — KHUÔN C11 + ENUM ĐÓNG + FLAG-COUNT (tái-dựng số, không tin lời khai)
## 1a. FLAG-count — TÁI-DỰNG, KHỚP 17/17
```
grep -c '^## FLAG' sub-lead-stale-deep-S166.md -> 11 (return khai 11) ✅
grep -c '^## FLAG' sub-lead-gap-deep-S166.md -> 6 (return khai 6) ✅
```
Khớp tuyệt-đối. Rủi-ro **pin-bản-cắt 4-A** (tự nêu @S152, tái-khẳng @S153): cả 2 file đều có END-line
tự khai TOTAL ⇒ đối-chiếu `grep -c` == TOTAL đứng vững cả 2 phía. **Không có FLAG nào ngoài tầm pin.**
## 1b. Khuôn C11 — 17/17 ĐÚNG KHUÔN (máy đếm được)
```
grep -cE '^## FLAG-[0-9]+ — `[a-z-]+` — (HIGH|MED|LOW)$'
stale -> 11/11 gap -> 6/6
```
Em-dash (U+2014) đúng, backtick-class đúng, SEV đúng ba giá-trị. **0 dòng lệch khuôn** ⇒ gốc-lỗi S148
(`lead-stale` KHÔNG gán class ⇒ tally đứng yên) **không tái-phát**, nay là phiên thứ 6 liên-tiếp sạch khuôn.
## 1c. Enum ĐÓNG — 17/17 ∈ enum, 0 TỰ CHẾ
`memory-budget.json``lead_self_audit.flag_classes` = **list 11 phần-tử** (đếm phần-tử, không grep-theo-số):
`view-stale-count · view-stale-status · view-stale-header · view-stale-role-desc · view-residual-asym ·
gap-carry-dropped · gap-carry-aged · gap-owner-specifics · gap-decision-sunk · gap-underfill · gap-incident-unrecorded`
| vai | class dùng | ∈ enum? |
|---|---|---|
| stale | view-residual-asym(1) · view-stale-count(5) · view-stale-header(1) · view-stale-status(3) · view-stale-role-desc(1) | 5/5 ✅ |
| gap | gap-underfill(1) · gap-decision-sunk(2) · gap-carry-dropped(2) · gap-owner-specifics(1) | 4/4 ✅ |
Phân-rã class trong 2 bảng TỔNG KẾT khớp header thật **từng dòng** (không chỉ khớp tổng):
stale `view-stale-count` = {F2,F3,F4,F7,F8} = 5 ✅ · `view-stale-status` = {F6,F10,F11} = 3 ✅ · 3 class còn lại ×1 ✅.
gap `gap-carry-dropped` = {F3,F4} ✅ · `gap-decision-sunk` = {F2,F6} ✅ · 2 class còn lại ×1 ✅.
SEV: stale 4H/5M/2L = 11 ✅ · gap 4H/2M/0L = 6 ✅.
🔴 Đối-chiếu bài **S162 (FLAG-16 ÉP-VỪA ENUM)**: lượt này tôi soi đúng lỗ đó — có FLAG nào *quan-sát đúng
nhưng bị nhét vào class không thoả vị-ngữ* không? **Không.** Ca gần nhất đáng ngờ là stale FLAG-1 (`view-residual-asym`
dùng cho "một file hai bề mặt, sửa một phía") — vị-ngữ **thoả**: dư-lượng bất-đối-xứng không đòi hai FILE, đòi hai
BỀ MẶT gương nhau, và `:6` ⟂ bảng canonical đúng là cặp gương (chính `:6` trỏ sang bảng). ĐẠT.
---
# §2 — TALLY LEAD VỪA RATIFY: TÁI-DỰNG DELTA TỪNG KEY (git HEAD → cây tươi)
Phương-pháp mạnh hơn đọc endpoint: kéo bản TRƯỚC bằng `git show HEAD:.claude/governance/.session-counter.json`
rồi so **delta từng key** (khuôn S153).
| class | HEAD (trước) | cây tươi (sau) | delta | fire lượt này? | phán |
|---|---|---|---|---|---|
| view-residual-asym | 6 | **7** | +1 | có (F1) | ✅ |
| view-stale-count | 6 | **7** | +1 | có (5 FLAG) | ✅ **+1 ĐÚNG** — đơn-vị = vòng-đo, KHÔNG phải flag |
| view-stale-status | 3 | **4** | +1 | có (3 FLAG) | ✅ |
| view-stale-header | 2 | **3** | +1 | có (F5) | ✅ |
| view-stale-role-desc | 1 | **2** | +1 | có (F9) | ✅ |
| gap-carry-dropped | **8** | **9** | +1 | có (2 FLAG) | ✅ **8→9 ĐÚNG** |
| gap-decision-sunk | 2 | **3** | +1 | có (2 FLAG) | ✅ |
| gap-owner-specifics | 1 | **2** | +1 | có (F5) | ✅ |
| gap-underfill | 2 | **3** | +1 | có (F1) | ✅ |
| gap-carry-aged | 0 | **0** | 0 | KHÔNG (honest-zero) | ✅ giữ 0 |
| gap-incident-unrecorded | 0 | **0** | 0 | KHÔNG (honest-zero) | ✅ giữ 0 |
**9/9 class fired = +1 chính xác · 2/2 non-fired = 0 · 11 key cả hai phía · mọi value `int`** (bất-biến
"counts chỉ chứa số" còn sống). `counter` 39→**40** ✅ · `last_audit.light_at`36→**40** + `deep_at`25→**40**
(deep bao hàm light, khuôn S143) ✅ · `signal_session` S162→**S166** · `signal_last_kind` light→**deep** ✅.
🔴 **Bài D-4 (S159) KHÔNG tái-phát — và lần này con-đo tự chặn trước:** `gap-carry-dropped` trong-phiên = **2**,
ô tally = **consecutive-audit**. `lead-gap` viết hẳn 2 lần cảnh-báo (`:100-102``:344-348`) *"CẤM ghi 2 vào ô đó
— ghi 2 là xoá lịch-sử 8→2"*. Lead ratify **9**. ✅ Đây là ca **bài-học-đã-ăn-vào-vai**: chính vai từng sai @S159
nay tự khai đơn-vị TRƯỚC khi lead chép. Ghi nhận dương.
Số ratify khớp 11/11 với danh-sách trong đề-bài giao tôi. **Tally: ĐẠT.**
---
# §3 — VERDICT per-FLAG: `lead-stale-auditor` (11 FLAG)
## 🔴 FLAG-8 — `view-stale-count` — HIGH ⇒ **TRƯỢT** (điểm TRƯỢT DUY NHẤT của lượt)
**Quan-sát ĐÚNG, kết-luận SAI — và cái SAI nằm đúng chỗ load-bearing.**
Tôi RE-ĐO đúng 5 phạm-vi khoanh trong `memory-budget.json → tiep_reload.sources` (đọc **binary**, không qua
`Get-Content` — né bẫy E-010 no-BOM):
| nguồn (phạm-vi khoanh) | stale đo | tôi đo lại | phán |
|---|---|---|---|
| `STATUS` `:6` + bảng `:461-483` | 63.402 | **63.404** (`:6`=1.222 + `:461-483`=62.182) | ✅ lệch 2 B, vô-hại |
| `HANDOFF` **segment Last-updated MỚI NHẤT** | **69.504** (nó lấy `:1-117`) | **9.486** (`:1-40`) | 🔴 **SAI PHẠM-VI** |
| `WAL.md` trọn | 3.389 | 4.018 (đo lúc 09:4x) | ✅ time-drift, xem control dưới |
| `_mind-s-9.md` trọn | 30.573 | **30.573** | ✅ khớp tuyệt-đối |
| `_context-s-9.md` trọn | 13.781 | **13.781** | ✅ khớp tuyệt-đối |
| **TỔNG** | **180.649** | **120.633** | 🔴 **thổi +60.016 B (+50%)** |
**Biên segment là VẬT trên đĩa, không phải diễn-giải:**
```
docs/HANDOFF.md:5 **🆕 Last updated:** 2026-07-30 trưa (S161→S162 …) <- MỚI NHẤT
docs/HANDOFF.md:41 **🧊 (Last updated cũ)** 2026-07-29 chiều (S159→S160 …) <- BIÊN
```
Khoảng `:41-117` mà stale nuốt vào chứa **7 header `Last updated` CŨ** (`:41` S159→S160 · `:87` S152 ·
`:92` S151 · `:97` S149S150 · `:100` S147 · `:109` S146 · `:114` S141S143). Không cái nào là "MỚI NHẤT".
🔴 **Bác được KHÔNG CẦN RỜI FILE (đúng kỹ-thuật chính stale dùng cho `cicd-monitor` ở FLAG-9 điểm 3):**
FLAG-5 của chính nó coi `HANDOFF.md:5`**dòng đầu segment** và resolve *"bump `:5` sang segment L9"*.
Vậy nó BIẾT `:5` mở segment mới nhất — nhưng ở FLAG-8 lại lấy segment = `:1-117`, tức nuốt 7 header cùng loại.
**Hai mệnh-đề không thể cùng đúng trong một file.**
**Control chứng phép đo file-level của nó VẪN chuẩn (không vu-oan):** stale tự khai *"lượt đầu tôi đo trọn file
ra 424.634 B"*. Tôi dựng lại: `218.467 (STATUS) + 158.424 (HANDOFF) + 3.389 (WAL) + 30.573 + 13.781` =
**424.634** — khớp **TUYỆT ĐỐI**. ⇒ (a) byte-đo của nó đúng, (b) `WAL` **đúng bằng 3.389 B** lúc nó đo
(chênh của tôi = time-drift Stop-hook flush, KHÔNG phải lỗi nó). **Khuyết-tật DUY NHẤT = phạm-vi HANDOFF.**
**Hệ-quả — đây mới là chỗ chết:**
```
đúng phạm-vi: 120.633 B → dải [bytes/4 .. bytes/3.0] = ~30,2-40,2K tok
mục tiêu tiep_reload._note = ~40-60K tok
```
**VẪN DƯỚI SÀN.** Kết-luận gốc của slot (59) — *"tuân 100% danh sách vẫn dưới hạn"***ĐỨNG VỮNG**.
Cái stale gọi là **"ĐẢO DẤU"** và **"NẰM TRỌN trong dải mục-tiêu 40-60K"** là **SAI**, sinh ra từ đúng
60.016 B nó nuốt thừa. Kéo theo **cả khối "Vì sao nguy" sụp**: nó cảnh-báo *"nếu owner chọn mở-rộng thì
`/tiep` vượt trần 60K ⇒ gây hại thật"* — thực tế **mở-rộng vẫn là hướng ĐÚNG** vì đang ở ~30-40K.
🔴🔴 **KHÔNG CÒN LÀ GIẢ-ĐỊNH — lead ĐÃ NUỐT:** `.claude/WAL.md:33` (sổ closeout-nợ) đã chép nguyên:
> `F-8 slot(59) tiep_reload ĐẢO-DẤU 180.649B-đã-đạt-dải`
Nếu closeout thi hành theo resolve của FLAG-8 (*"nói rõ hiện đã ĐẠT dải mục-tiêu ⇒ câu hỏi có thể tự tiêu"*),
lead sẽ **đóng một owner-decision đang treo bằng tiền-đề sai, và đóng NGƯỢC CHIỀU**.
⚖️ **Công-bằng — phần ĐẠT của FLAG-8 phải giữ:** con số ở `HANDOFF:19` (**71.492 B**) **THẬT SỰ stale**
đúng phạm-vi là **120.633 B**, lệch **1,69×**. Vậy `view-stale-count` **có cơ-sở**, FLAG **đáng tồn tại**,
chỉ **độ lớn bị thổi 1,5×****hướng kết-luận bị lật ngược**. Tôi chấm TRƯỢT vì **mệnh-đề load-bearing
(ĐẢO DẤU) sai**, không vì FLAG vô-căn-cứ.
**Đề-nghị lead (propose-only):** giữ FLAG, sửa 3 chỗ trước khi vá `:19` — (i) TỔNG `180.649`**120.633**;
(ii) bỏ chữ *"ĐẢO DẤU"* / *"đã ĐẠT"*, ghi **"vẫn dưới sàn, khoảng cách thu hẹp còn ~0-10K"**; (iii) sửa
`WAL:33` cùng lượt, kẻo sổ bền mang số sai vào L10. Câu hỏi cho owner **KHÔNG tự tiêu** — nó vẫn sống.
---
## Verdict 10 FLAG còn lại của `lead-stale-auditor`
**FLAG-1 — `view-residual-asym` — HIGH ⇒ ĐẠT** (5/5 row tái-dựng độc-lập)
| row | view nói | tôi đo lại | khớp? |
|---|---|---|---|
| `:465` Migrations | 69 | **71** (file `^[0-9]{14}_` trừ `.Designer.cs` = 71) | ✅ |
| `:466` SQL tables | 96 | **97** (Mig 70 `Up()` = **1** `CreateTable` `ContractCatalogEntries`; Mig 71 = 0) | ✅ |
| `:470` Policies | 220 | **256** (`\|All\|`×4) | ✅ |
| `:471` Menu keys | 55 | **64** (đếm PHẦN-TỬ `MenuKeys.All` @HEAD = 64; `grep -c 'public const string'` = **65** = bẫy, stale né đúng) | ✅ |
| `:472` Tests | 590 | **614** (`WAL:13` `suite 614/0 (45D+569I)` — trích verbatim khớp) | ✅ |
🔴 **Mệnh-đề nặng nhất của FLAG-1 — "canonical-poison sinh lệnh sửa-NGƯỢC" — tôi xác-nhận bằng máy:**
`detectors-output.txt:3` = `STATUS.md canonical: mig=69 test=590 gotcha=85 table=96 menu=55 policy=220` ·
`:4` = `disk cross-check: mig=71 gotcha=85` (**chỉ 2/6 trục** chạm đĩa) · `:14` = đòi sửa
`ef-core-migration/SKILL.md:93` *"writes 97 table but canonical=96 → resolve: update to 96"* — **doc đang ĐÚNG
(97) và máy đang ra lệnh sửa thành SAI.** Nguyên văn khớp 100%.
⚠️ Đo `Up()`/`Down()` TÁCH RIÊNG mới đúng: đếm cả file thì Mig 70 ra `CreateTable=1 DropTable=1` trông như
net-0 — `DropTable` nằm trong `Down()` (khuôn EF bình thường). Ghi lại để người vá không tự tạo dương-giả.
**FLAG-2 — `view-stale-count` — HIGH ⇒ ĐẠT** (khớp verbatim CẢ HAI đầu)
`STATUS:479` = `admin **B43R6Y17** · user **NLI5umBg**` (2 hit còn sống) · nguồn
`sub-cicd-verify-k3.md:86` = `index-mySTlx42.js` **ROTATE**, `:88` = `index-CZAYiWWa.js` **FROZEN**. **0/2 khớp** ✅.
Nhân-chứng tự khai `:93-94` có thật, nguyên văn: *"Đính chính mốc PRE trong đề bài: spec giao việc ghi baseline
`admin B43R6Y17 · user NLI5umBg (@S164)` — KHÔNG khớp gì đo được"* ✅.
**FLAG-7 — `view-stale-count` — HIGH ⇒ ĐẠT** (tái-dựng khớp tới từng byte)
`_INDEX.md` = **20.731 B** / `tiers.l2_index.cap_bytes` = **20.480****101,23 % · headroom 251 B**
trùng khít con số stale đưa. `HANDOFF:15` verbatim đúng *"99,18% / headroom 167 B"* + *"còn đúng 1 dòng append"*
+ 3 lựa-chọn (a)/(b)/(c) + dòng tự-dán nhãn *"🔴 lead-stale FLAG-7 HIGH: anh đang chọn trên thước cũ"* ✅.
Lập-luận "(c) để vỡ không còn là lựa-chọn mà là mô-tả hiện-trạng" — **đúng logic**, giữ nguyên.
## §3-bis — 7 FLAG còn lại của `lead-stale-auditor`: **7/7 ĐẠT**
**FLAG-3 — `view-stale-count` — LOW ⇒ ĐẠT** 🔴 *(cứu khỏi cáo-buộc-oan bằng `git show HEAD:`)*
Đọc cây tươi: `STATUS:6` = `counter **40**`**trông như FLAG dương-giả**. Nhưng:
```
git show HEAD:docs/STATUS.md -> :6 = counter **39** (đúng cái stale trích)
docs/STATUS.md mtime 09:21:06 > sub-lead-stale mtime 09:05
git diff --stat docs/STATUS.md -> 6 insertions / 6 deletions
```
⇒ lead **đã vá SAU khi đo**. FLAG đúng tại thời-điểm đo. Lặp lại nguyên bài S162: **cây bẩn giữa phiên,
phải kéo HEAD mới phán được**. (Vá `39→40`**byte-neutral** ⇒ không ảnh hưởng phép tính §FLAG-8.)
**FLAG-4 — `view-stale-count` — MED ⇒ ĐẠT**
`CLAUDE.md:70` ô Migration KHKK = `**69**` ✓ · `grep -c 'Mig 70\|Mig 71\|AddContractCatalog\|AddKhkkApproval' CLAUDE.md`
= **0 hit** ✓ — trong khi Mig 71 `AddKhkkApprovalGroupCatalogAndFinalizeRuntime` chính là KHKK. Khớp verbatim.
**FLAG-5 — `view-stale-header` — MED ⇒ ĐẠT**
`HANDOFF:5` = `2026-07-30 trưa (S161→S162 — phiên-LOGIC L8 …)` ✓ · `STATUS:6` = `S161→S162 + S164/S165
2026-07-31 — phiên-LOGIC L9` ✓ · hôm nay **2026-08-01** · `:27` = `## 🔴 NEXT em @S163` ✓. Trễ đúng như khai.
**FLAG-6 — `view-stale-status` — MED ⇒ ĐẠT**
`HANDOFF:33` verbatim = *"…liệt **20 tên** / thật **23** … + \"10 monitor\" / thật **13**"* ✓ (treo như CHƯA LÀM).
Source `README.md:242`: dài **1.788 B** ✓, chứa **cả 3** `ctx-curator`/`ctx-verifier`/`ctx-audit` ✓, có `13` ✓.
Control roster: `ls .claude/agents/*.md` = **24** README = **23** ✓. ⇒ việc ĐÃ XONG mà sổ NEXT-em còn treo.
**FLAG-9 — `view-stale-role-desc` — MED ⇒ ĐẠT** (verbatim 4/4 site)
`:4` (description auto-inject) = `…verifies test gate pass (Domain 58 + Infra 23 tests baseline)…` ✓ ·
`:170` `(58 baseline)` ✓ · `:171` `(23 baseline)` ✓ · `:233` `111/111 PASS (58 Domain + 53 Infra)` ✓.
Mâu-thuẫn NỘI-BỘ **23 vs 53** có thật ⇒ lập-luận "bác được không cần rời file" đứng vững. Source `WAL:13`
`suite 614/0 (45D+569I)` ✓. Lệch Infra **23 vs 569****×24** — đúng cỡ nó khai.
**FLAG-10 — `view-stale-status` — MED ⇒ ĐẠT** (3/3 con-trỏ verbatim)
`CLAUDE.md:17` = `(Phase 7 PE refinement mới)` ✓ · `migration-todos.md:4` = `Active work: Phase 9` ✓ ·
`:798` = `## 🎯 Phase 12 — … (S160-S162) — 6/8 wave DONE` ✓ · file **822 dòng** ✓ ·
`grep 'K1\|K2\|K3\|SP-002\|Mig 70\|Mig 71'` = **0 hit** ✓. Ba nhãn phase chỉ ba nơi — đúng.
**FLAG-11 — `view-stale-status` — LOW ⇒ ĐẠT**
`docs/governance/README.md:5` = `> **Status:** ✅ Active trial week 1-4 (2026-05-26 → 2026-06-23)` ✓ verbatim ·
`git log -1` = `72bbfa5 2026-06-02` ✓ (2 tháng không ai chạm) · `ls docs/governance/*.md` = **11** ✓ khớp
coverage nó khai. Lập-luận điểm-mù `H24-1` (`anchors parsed: 2 doc(s)`) — có thật trong `detectors-output.txt`.
---
# §4 — VERDICT per-FLAG: `lead-gap-auditor` (6 FLAG) — **6/6 ĐẠT**
**FLAG-1 — `gap-underfill` — HIGH ⇒ ĐẠT** (tái-dựng BYTE-EXACT 6/6 số)
```
_mind-s-9.md 270 dòng / 30.573 B ✅ khớp
sed 1,165p | wc -c 18.925 ✅ khớp
sed 166,270p | wc -c 11.648 ✅ khớp
STATUS head-60 | wc -c 27.553 ✅ khớp
grep -n 'CURRENT STATE' docs/STATUS.md -> 461 ✅ head-60 KHÔNG chạm tới
HANDOFF :1-40 = 9.486 ✅ khớp
```
Khối MIND-1 `:192-217` + MIND-0 `:218-270` **toàn bộ > 165** ⇒ lập-luận *"nén-để-giữ + đọc-cụt = nén thành xoá"*
đứng vững. 🔴 **2 ERRATA (INFORM, không đổi verdict):** (i) lệnh nó ghi `grep -n 'chỉ-sống-ở-đây'` chỉ ra **1 dòng**
(`:192`) — bản trong thân dùng **khoảng trắng** (`chỉ sống ở đây`); pattern chạy được là
`grep -nE 'chỉ[- ]sống[- ]ở[- ]đây'``192, 201, 205, 206`; (ii) nó ghi `:200`, dòng thật là **`:201`**
(`:200`*"Hướng-tiếp cũ (chạy 11 mục vá)…"*). **Kết-luận không đổi** — cả 4 dòng đều > 165.
**FLAG-2 — `gap-decision-sunk` — HIGH ⇒ ĐẠT** (phép đo có RĂNG cả 2 chiều)
Hứa `_context-s-9.md:63` verbatim = *"2 mồi `ctx-audit` gieo, chờ chấm @close: MỒI-A slot (61) defect liên-danh
N-HĐ · MỒI-B slot (60) enum."* ✓. Hiện: `(60)`/`(61)` = **0** trên HANDOFF ✓ **0** trên STATUS ✓ ·
`MỒI-A|MỒI-B` = 0/0/0 trên HANDOFF/STATUS/WAL ✓ · **control DƯƠNG** `_mind`=1, `_context`=1 ✓ ·
**control ÂM** `(63)`/`(64)` = 0 ✓. Dãy slot trong segment mới nhất =
`(54)(55)(56)(57)(58)(59)(62)`**thủng đúng 60, 61** ✓. Đây là FLAG **chắc nhất lượt này**.
**FLAG-3 — `gap-carry-dropped` — HIGH ⇒ ĐẠT** (1 errata phạm-vi lệnh)
`HANDOFF:37` verbatim ✓ · `git log -3` khớp **cả 3 sha + timestamp** (`bd68f71` 21:47 · `15349e8` 18:21 ·
`50e6d8c` 15:45) ✓ · `git diff e5123ff..HEAD` = **16** dòng ✓ · carry slug **50 unique / 234 occurrence**
(khớp tuyệt-đối). 🔴 **ERRATA:** lệnh ghi `grep -o 'Carry @S[0-9]*' docs/HANDOFF.md -> chỉ 'Carry @S162'`
chạy trên **cả file** ra **3** giá-trị (`@S152` `:88`, `@S153` `:81`, `@S162` `:37`). Trong **segment mới nhất
`:1-40`** thì đúng là **chỉ `@S162`****kết-luận ĐÚNG, thiếu mệnh-đề phạm-vi**. Nên thêm `sed -n '1,40p' |`
kẻo người vá chạy lại thấy 3 rồi tưởng FLAG sai.
**FLAG-4 — `gap-carry-dropped` — HIGH ⇒ ĐẠT** (0-hit có control dương — chuẩn mực)
`spec-4gd…:54` verbatim khớp **100%**, gồm nguyên văn lời owner *"Nếu xóa thì có thể làm 1 tính năng điều chỉnh
phiếu … Cái này để giai đoạn sau."* ✓. `OG-8` / `điều chỉnh-thay thế` / `BACKLOG giai-đoạn-sau` = **0/0/0 trên
cả 6 sổ** ✓. **Control dương trên chính file nguồn: `OG-8`=2 · `authz-per-nhóm`=3 · `BACKLOG giai-đoạn-sau`=1**
⇒ regex có răng, **0 là 0 THẬT**. Phân-định *"gác là ĐÚNG, cái flag là gác-sang-ĐÂU"* — sắc và đúng luật.
**FLAG-5 — `gap-owner-specifics` — MED ⇒ ĐẠT** (1 errata số cộng)
Đĩa: `OG-1:40 OG-2:27 OG-3:25 OG-4:3 OG-5:7 OG-6:36 OG-7:22 OG-8:5 OG-9:18 OG-10:1`**khớp 10/10 tuyệt đối** ✓.
Sổ bền: HANDOFF{OG-3,OG-9} · STATUS{} · WAL{OG-5,OG-6} · _context{OG-5,OG-6} · _mind{OG-3,OG-5,OG-6}
⇒ hợp = **{3,5,6,9} = 4/10**, vắng 6/10 ✓ **khớp từng phần-tử**. OG-1 (40 hit) + OG-2 (27 hit) = 0 trên sổ bền ✓.
`_context:108` verbatim *"9 OG trọn"* + liệt **biệt-danh** ✓. 🔴 **ERRATA:** *"= **27** finding"* — cộng lại
k1(6) + k2(10) + k3(8) = **24**. Bản liệt-kê ID của nó **đúng từng cổng** (tôi tái-dựng khớp), chỉ **tổng sai +3**.
Mệnh-đề load-bearing (*F-5 sống ở CẢ 3 cổng ⇒ khoá không duy-nhất*) **đúng** ⇒ verdict giữ ĐẠT.
**FLAG-6 — `gap-decision-sunk` — MED ⇒ ĐẠT**
`runs/2026-07-30-S163-bookend-open/run.md:117-120` verbatim khớp cả 3 dòng ✓. Con-trỏ **chết ở cả hai đầu**:
`grep -ci '11 mục\|danh-sách vá'` → HANDOFF 0 · STATUS 0 · WAL 0 · error-ledger 0 · ACTIVE-MARKS 0 ·
migration-todos 0 · `_context` 1 · `_mind` 3 (nó ghi 2 — chênh do `-i`, vô-hại) ✓ · `WAL` grep `PAUSE-2` = **0**
⇒ đích con-trỏ `_mind:200` **không tồn tại** ✓ · `_context:108-110` (PAUSE-2) không chứa cụm ✓.
Bảng "chạm đĩa trước khi phán": mục 11 refine **XONG THẬT** (`lead-gap-auditor/MEMORY.md` 23.593→**12.551 B**,
`archive/2026-07.md` **22.645 B** + gist + `_INDEX`; `harvest-curator` **18.781 B** ✓ khớp) · mục 1 `gotcha=85` ✓ ·
mục 7 **CÒN MỞ** thật (`detectors-output.txt:14-25` vẫn kêu `ef-core-migration` ×9 + `permission-matrix` ×3) ✓.
Kết *"không được nói 0/11, cũng không được nói xong hết"***đúng và can-đảm**.
---
# §5 — FALSIFY-LOG (7 phép · 3 HELD · 4 BROKE)
🔴 **3/4 BROKE nhắm vào nghi-vấn của CHÍNH TÔI** — ghi đủ để không tạo báo-động-giả (bài S149/S150/S162).
**F-1 · BROKE (nghi-vấn của chính tôi) — "stale bịa mig=71, đĩa chỉ có 70"**
Phép: `ls *.cs | grep -v 'Designer\|Snapshot' | wc -l`**70** ≠ 71 ⇒ suýt buộc tội thổi số.
Phản-chứng: `ls *.cs | grep 'Snapshot'` lòi ra **`20260717032812_AddPeApprovedBudgetSnapshot.cs`** — một
**migration mà TÊN chứa chữ "Snapshot"** (Mig 67, PE freeze ngân-sách S133). Chính bộ lọc của tôi nuốt nó.
Đếm đúng: `ls *.cs | grep -E '^[0-9]{14}_' | grep -v '\.Designer\.cs$'` = **71** ✓, và
`detectors-output.txt:4 disk cross-check: mig=71` xác nhận độc-lập. ⇒ **stale ĐÚNG, tôi SAI.**
Cùng lớp bẫy `grep -c` vs `-o|wc -l` (S150) và en-dash-alternation (S149): **bộ lọc phủ-định là dao hai lưỡi.**
**F-2 · BROKE (nghi-vấn của chính tôi) — "stale trích `honest_caveats.token_estimate` = khoá TREO"**
Phép: `json['honest_caveats']`**null** ⇒ nghi nó trích công-thức không tồn tại.
Phản-chứng: probe đường khác (`grep -n 'token_estimate'`) → khoá có thật, **lồng trong `token_governor`**
tại `:105-106`, nguyên văn *"report a RANGE **[bytes/4 .. bytes/3.0]**; FIT verdict uses bytes/3.0"*.
**stale trích ĐÚNG công-thức** (chỉ rút gọn đường dẫn). Tôi giữ nguyên dải nó dùng khi tính lại F-8.
**F-3 · BROKE (nghi-vấn của chính tôi) — "gap bịa `lead-gap-auditor` có `archive/2026-07.md`"**
Phép: `ls` gộp 2 thư mục → phần `lead-gap-auditor/archive/` hiện **rỗng**.
Phản-chứng: chạy lại **tách riêng** → có `2026-07.md` **22.645 B** + `2026-07.gist.md` + `_INDEX.md`.
Lần đầu tôi cắt bằng `head -8` nên listing thứ hai bị nuốt. ⇒ **gap ĐÚNG, tôi SAI** (lần thứ 3 cùng lượt).
**F-4 · BROKE (nhắm vào CON-ĐO) — "TỔNG 180.649 B của stale F-8"** → xem §3.
Phạm-vi HANDOFF sai (`:1-117` thay `:1-40`, biên `:41` = `🧊 (Last updated cũ)`), thổi **+60.016 B**.
Đúng phạm-vi = **120.633 B** ⇒ ~30-40K tok ⇒ **vẫn dưới sàn 40K** ⇒ mệnh-đề "ĐẢO DẤU" **BROKE**.
Control chống vu-oan: tổng-trọn-file dựng lại = **424.634** khớp **tuyệt đối** con số stale tự khai ⇒
byte-đo của nó chuẩn, **chỉ phạm-vi sai**.
**F-5 · HELD — thử bác FLAG-7 (`_INDEX` 101,23%)**
Đo độc-lập từ `tiers.l2_index.cap_bytes`: `20.731 / 20.480` = **101,23 %**, headroom **251 B** — trùng
tới từng byte. Thử bác thêm bằng "cap có thể đã được nâng": `cap_bytes` ở cây tươi vẫn **20.480**. **Không bác được.**
**F-6 · HELD — thử bác FLAG-3 bằng "cây tươi nói counter 40 ⇒ FLAG oan"**
`git show HEAD:` ra **39**; `mtime STATUS.md 09:21:06` > `mtime sub-lead-stale 09:05`; `git diff --stat` = 6+/6.
**vá-SAU, không phải flag-oan.** Phép bác của tôi tự sụp. (Đúng bài S162 — lần thứ hai cứu được nhờ `HEAD:`.)
**F-7 · HELD — thử bác honest-zero `gap-incident-unrecorded`** → xem §6.
---
# §6 — THÁCH honest-zero + thách-CLEAN (bắt buộc khi con-đo tuyên 0)
## 6a. `gap-incident-unrecorded = 0` — **CÓ RĂNG, không phải 0-rỗng**
Lý-lẽ gap: class đòi *"0 hit trên **bất-kỳ** sổ bền nào"*; `_context-s-9.md:62` **CÓ** ghi cả 2 escalate S163
⇒ không đủ điều-kiện ⇒ không phát flag. **Tôi xác-nhận `:62` verbatim:**
> *"2 ESCALATE của `harness-audit` chưa xử: (i) backstop G-015 rỗng-hoá vì Stop-hook nuốt `agent-memory` vào
> commit trước khi lead soát · (ii) stray `agent-cwd-relative-memory-misland` tái phát ×3/phiên"* ✓
`error-ledger.md` = **0 hit** ✓ (đúng như nó khai — nó **không giấu** vế bất-lợi).
🔴 **Tôi thách bằng tập-đo KHÁC nó: sự-cố CỦA CHÍNH PHIÊN S166.** Nếu 8 lượt `#53` + engine-exit phiên này
không nằm ở sổ bền nào thì honest-zero vỡ. Kết quả — **`.claude/WAL.md:28-29` có ghi, đích danh và có số:**
> `[ ] closeout-nợ: … · tally #53 CHỐT (S166 tới giờ: cicd×2 + reviewer×2 + stale×2 + gap×1 + laneBE×1
> = 8 lượt #53-return / 0 mất công, artifact cứu hết)`
`WAL` nằm trong `tiep_reload.sources` (đọc **trọn**) ⇒ **sự-cố phiên này CÓ nhà**. **honest-zero HELD.**
**2 caveat tôi phải khai (không làm tròn thành sạch):**
1. `WAL` ghi `gap×1`, trong khi sổ auto-memory ghi `lead-gap ×2` (lần 1 = **skeleton-nấc-2**, 1.302 B).
Chênh **1**. Nhưng dòng WAL tự gắn nhãn *"S166 **tới giờ**"* + nằm trong mục **"tally #53 CHỐT"** của
closeout-nợ ⇒ **tự khai là tạm, chưa chốt** ⇒ trung-thực, không phải giấu. Đề-nghị lead chốt số ở closeout.
2. **Lượt chạy này của TÔI cũng chết một lần (engine-exit sau §2).** Nó xảy ra **SAU** khi cả 2 con-đo trả
(09:05 / 09:06) ⇒ **CẤM tính vào lỗi của chúng**. Time-drift bảo-vệ cả hai chiều — tôi đã dùng nó để cứu
FLAG-3 của stale thì cũng phải dùng nó để tha cho cặp ở đây.
## 6b. Thách 2 mục stale tự tuyên "SẠCH"
- **Nhóm 4 (roster) SẠCH ⇒ HELD.** Soi đúng ô nó tuyên sạch (bài S150 — *CLEAN nguy nhất ở class con-đo tự
tuyên 0*): `ls .claude/agents/*.md` = 24 README = **23** ✓ · `README:242` dài **1.788 B** ✓ chứa **đủ cả 3**
`ctx-*` ✓ có `13 monitor` ✓. **Không bới được FLAG sót.** Đây là đảo chiều so với S150 (khi đó tôi bắt được M-1
ở đúng ô này) — lần này ô đó **đã lành thật**.
- **Nhóm 8 (mirror) "SẠCH trong phạm-vi đã soi" ⇒ chấp nhận**: nó **tự dán PARTIAL**, không làm tròn. Ghi-chú
*"đếm file thô 3-vs-1 trông y hệt sửa-một-phía — phải mở TÊN file"***phòng dương-giả đúng chỗ**.
- **gap: 9/9 slot cũ SẠCH ⇒ HELD.** Nó tự dựng **control ÂM** `(63)`/`(64)` = 0 ⇒ chứng phép đo **phân-biệt
được**, không phải luôn-dương. Tôi chạy lại: 0/0 ✓. Đây là khuôn honest-zero **đúng chuẩn**.
## 6c. 🔴 Chỗ tôi KHÔNG tuyên sạch được (giữ nguyên nấc, cấm làm tròn)
`lead-stale` tự khai coverage **8/8 chạm nhưng 2/8 PARTIAL** — nhóm 7 mới soi **2/23 persona**. Tôi **giữ nguyên
nấc "chưa xác định"**, KHÔNG đọc 11 FLAG thành TỔNG. Chính nó cũng dự-báo *"quét nốt 21 persona gần như chắc
có thêm FLAG cùng class"* và tự khai **điểm mù tự-quy-chiếu** (chưa loại trừ được `lead-stale-auditor.md` /
`lead-gap-auditor.md` đang mang số/nhịp lệch — đúng M-1 tôi bắt @S153). **Khai SÀN như vậy là hành-vi ĐÚNG**,
tôi ghi nhận dương chứ không trừ điểm.
---
# §7 — TỔNG KẾT
## Verdict: **16 ĐẠT / 1 TRƯỢT** trên 17 FLAG
| vai | ĐẠT | TRƯỢT | ghi chú |
|---|---|---|---|
| `lead-stale-auditor` | **10** | **1** (FLAG-8) | 11 FLAG · 4H/5M/2L |
| `lead-gap-auditor` | **6** | 0 | 6 FLAG · 4H/2M/0L |
- **Khuôn C11:** 17/17 đúng `## FLAG-<n> — \`<class>\` — SEV` ⇒ máy đếm được.
- **Enum ĐÓNG:** 17/17 ∈ 11 class · **0 tự chế** · **0 ca ép-vừa-enum** (bài S162 không tái-phát).
- **Tally:** 9 class fired +1 · 2 non-fired giữ 0 · 11 key hai phía · counter 39→40 · light=deep=40. **ĐẠT.**
- **C4b tuần-tự:** artifact này ghi sau mtime cả hai con-đo (09:05 / 09:06). ✓
## 🔴 Câu nặng nhất
**`lead-stale` FLAG-8 quan-sát ĐÚNG (số ở `HANDOFF:19` thật sự stale, 71.492 → 120.633 B) nhưng đo SAI PHẠM-VI
HANDOFF (`:1-117` nuốt 7 header `Last updated` cũ thay vì segment `:1-40`), thổi tổng +60.016 B và **lật ngược
kết-luận**: đúng phạm-vi vẫn là **~30-40K tok, DƯỚI sàn 40K** — không hề "đã ĐẠT dải". Lead **đã chép cái sai
vào `.claude/WAL.md:33`** (`F-8 … ĐẢO-DẤU 180.649B-đã-đạt-dải`), nên nếu closeout thi hành resolve của nó
(*"câu hỏi có thể tự tiêu"*) thì **một owner-decision đang treo bị đóng bằng tiền-đề sai, và đóng ngược chiều**.**
## INFORM cho lead (không đổi verdict — nhưng nên vá cùng lượt)
1. 🔴 **`WAL.md:33`** — sửa `180.649B-đã-đạt-dải``120.633 B ~30-40K tok, VẪN DƯỚI sàn`; slot (59) **còn sống**.
2. `gap` FLAG-5: *"27 finding"***24** (6+10+8). Bản liệt-kê ID đúng, chỉ tổng sai.
3. `gap` FLAG-1: lệnh `grep -n 'chỉ-sống-ở-đây'` không tái-lập (bản thân dùng **khoảng trắng**); và `:200`**`:201`**.
4. `gap` FLAG-3: thêm `sed -n '1,40p' |` vào lệnh `Carry @S` — chạy trên cả file ra **3** giá-trị, dễ bị đọc thành FLAG sai.
5. `gap` FLAG-6: `lead-gap-auditor/MEMORY.md` nay **12.551 B** (nó ghi 10.777; mtime 09:11:25 **sau** lượt đo
09:06 — diary vừa được seed). Hướng "đã refine" **không đổi**.
6. `stale` FLAG-1/FLAG-3 **đã được lead vá một phần trong lúc tôi chấm** (`STATUS.md` 6+/6 @09:21, counter 39→40).
Nên re-chạy `governance-detectors.ps1` sau khi vá nốt 5 ô canonical — kỳ vọng FLAG `SKILL.md:93` biến mất và TOTAL đổi.
## Ranh-trục + caveat
Tôi **KIỂM 2 con-đo**, **KHÔNG tự soi lead** (chỉ chạm bề-mặt lead ở mức tái-dựng số của chúng + phép thách-CLEAN
tại §6b). Không lấn `ring1-audit` / `harness-audit` / `reviewer` / `ring4-audit`.
🔴 Caveat read-only: frontmatter khai thiếu Write/Edit **không có răng runtime** (`[carry:tools-whitelist-no-teeth]`
S143) — backstop THẬT = lead soát `git status` + commit-gate. Lượt này tôi **chỉ Write đúng artifact này**, 0 Edit
doc, 0 commit, 0 RAG-write. **Propose-only — tôi PHÁN, lead GHI.**
END sub-ring2-audit-deep-S166 — VERDICT=16 ĐẠT / 1 TRƯỢT trên 17 FLAG (stale 10Đ/1T · gap 6Đ/0T) ·
khuôn C11 17/17 · enum 17/17 · tally 11/11 key ĐÚNG · falsify 7 (3 HELD / 4 BROKE, 3 BROKE vào nghi-vấn của chính tôi)

File diff suppressed because one or more lines are too long

View File

@ -32,12 +32,17 @@ function resolvePath(key: string): string | null {
// [W2 KHKK S161] GĐ2 hết skeleton — re-point trang thật (gotcha #50: thiếu route =
// sidebar drop SILENT). WfView phía admin trỏ Designer type-10 (fe-admin CỐ Ý không có
// matrix page — chốt 2026-07-27; admin "Luồng duyệt" = dựng/sửa quy trình).
// [K4b S166 — spec K4b-7] 5 leaf trỏ `/khkk/*` mang `group=1` tường minh, mirror
// fe-user (trang coi "vắng group" = xem tất cả, nên thiếu ở đây là nhóm 1 nói dối).
// 🔴 `Khkk_WfView` CỐ Ý KHÔNG mang `group`: bên admin nó trỏ Designer quy trình,
// không phải trang danh sách — Designer không đọc tham số này (fe-user mới đi
// `/khkk/workflow-matrix?type=10&group=1`). Lệch có chủ đích, không mirror mù.
Khkk_WfView: '/system/approval-workflows-v2/ContractSigningPlan',
Khkk_List: '/khkk/list',
Khkk_Create: '/khkk/create',
Khkk_Pending: '/khkk/list?filter=ChoDuyet',
Khkk_Approved: '/khkk/list?filter=DaDuyet',
Khkk_Deleted: '/khkk/list?view=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50)
Khkk_List: '/khkk/list?group=1',
Khkk_Create: '/khkk/create?group=1',
Khkk_Pending: '/khkk/list?group=1&filter=ChoDuyet',
Khkk_Approved: '/khkk/list?group=1&filter=DaDuyet',
Khkk_Deleted: '/khkk/list?group=1&view=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50)
// [W7 KHKK S161] GĐ4 hết skeleton — 7 leaf `Hdc_*` re-point trang thật
// `/hard-copies?type=N` (N = ContractType int, mirror TYPE_CODE_TO_INT ở trên).
// gotcha #50: thiếu/để sai route ở đây = leaf DROP IM LẶNG khỏi sidebar.
@ -191,6 +196,19 @@ function resolvePath(key: string): string | null {
// [S57] BỎ ẩn "Danh mục" (Master/Catalogs + Cấu hình HRM re-parent vào đây) — gom
// master data về 1 chỗ cho CẢ admin lẫn nhân viên (đảo S29 hide). Admin nay quản
// master trực tiếp trên fe-admin; write vẫn khóa Admin/CatalogManager ở BE controller.
// 🔴 [K4a — S166, vá-8] RÀNG BUỘC NGƯỢC với `resolvePath` bên trên — đọc trước khi
// sửa bộ lọc này. fe-user có nhánh regex `^Khkk_G([1-8])_(WfView|List|Create|Pending|
// Approved|Deleted)$`; fe-admin CỐ Ý KHÔNG có (admin không có `WorkflowMatrixViewPage`
// — lead chốt 2026-07-27, giữ lệch). Hệ quả: 42 leaf `Khkk_G{2..8}_*` mà BE seed sẽ
// rơi vào `resolvePath → null` ⇒ MenuLeaf trả null ⇒ DROP IM LẶNG (gotcha #50).
// ⚠️ ĐO ĐĨA 2026-08-01: hàm này CHỈ ẩn `Ct_*` — `KeHoachKyKet` KHÔNG hề nằm trong tập
// ẩn, tức ràng buộc trên KHÔNG phải chuyện tương lai mà ĐANG hiệu lực: sidebar admin
// hiện root KHKK, 7 nhóm con, và 42 leaf của nhóm 2..8 im lặng biến mất (nhóm 1 sống
// nhờ 6 dòng `Khkk_*` không-infix trong `staticMap`). Đây là trạng thái ĐÃ KHAI, chấp
// nhận có chủ đích ở lượt K4a — KHÔNG phải bug mới phát sinh.
// ⇒ Ai muốn admin thấy đủ 8 nhóm (hoặc ngược lại, thêm `KeHoachKyKet` vào tập ẩn rồi
// sau đó gỡ ra) thì PHẢI nới regex `Khkk_G*` trong `resolvePath` TRƯỚC — và nới thì
// phải quyết luôn `WfView` bên admin trỏ đâu, vì Designer không nhận `?group=`.
function isAdminHidden(key: string): boolean {
return key.startsWith('Ct_')
}

View File

@ -14,7 +14,7 @@
// không phân biệt được "PE không đủ điều kiện" với "PE đã dùng hết nhóm".
import { useState, type FormEvent } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { CheckCircle2, FileCheck, FileText, Layers, Plus, Save, Settings2, X } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/ui/PageHeader'
@ -57,11 +57,31 @@ function firstFreeGroup(taken: number[]): number | null {
export function KhkkCreatePage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
// [K4b S166 — vá-6] Vào từ leaf "Thao tác" của một nhóm (`/khkk/create?group={n}`)
// hoặc từ nút "Lập kế hoạch" trên màn danh sách đang lọc nhóm ⇒ ô chọn nhóm mở sẵn
// đúng nhóm đó. Không có tham số (vào thẳng /khkk/create) thì giữ nguyên default cũ.
// Chỉ nhận 1..8, rác coi như không có — ô chọn không được phép mang giá trị ngoài dải.
const groupParam = searchParams.get('group')
const presetGroup = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('')
const [approvalWorkflowId, setApprovalWorkflowId] = useState('')
const [approvalGroup, setApprovalGroup] = useState<number>(KHKK_APPROVAL_GROUP_DEFAULT)
const [approvalGroup, setApprovalGroup] = useState<number>(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)
const [ghiChu, setGhiChu] = useState('')
// Cùng lý do với `KhkkListPage` (vá-5): 8 leaf "Thao tác" dùng chung route
// `/khkk/create`, nhảy leaf→leaf không dựng lại component nên giá trị khởi tạo ở
// trên KHÔNG chạy lại. Đồng bộ lại ngay trong render khi tham số nhóm đổi.
// 🔴 Chỉ đổi khi CHÍNH THAM SỐ URL đổi — nếu so với `approvalGroup` thì người dùng
// tự chọn nhóm khác sẽ bị kéo ngược về nhóm của URL, không bấm đi đâu được.
const [lastPresetGroup, setLastPresetGroup] = useState(presetGroup)
if (presetGroup !== lastPresetGroup) {
setLastPresetGroup(presetGroup)
if (presetGroup !== null) setApprovalGroup(presetGroup)
}
// Phiếu PE đủ điều kiện: Phase=DaDuyet VÀ có ≥1 NCC trúng thầu (BE lọc — khớp
// ĐÚNG 2 rào còn lại của Create nên picker không mời bấm vào thứ chắc chắn 409).
// [K2 S164] Vị ngữ "chưa có kế hoạch sống" ĐÃ BỎ (`Features.cs:1291-1295`).

View File

@ -3,13 +3,20 @@
// KpiCard làm bộ lọc + bảng + phân trang). File MIRROR SHA256 identical với
// fe-admin counterpart.
//
// URL vào từ menu (Layout.tsx staticMap — chỗ thứ 4 của Pattern 16-bis):
// Khkk_List → /khkk/list
// Khkk_Pending → /khkk/list?filter=ChoDuyet
// Khkk_Approved → /khkk/list?filter=DaDuyet
// Khkk_Deleted → /khkk/list?view=deleted
// URL vào từ menu (Layout.tsx staticMap cho nhóm 1, regex `Khkk_G{n}_*` cho nhóm
// 2..8; đây là "chỗ thứ 4" của Pattern 16-bis):
// Khkk_List / Khkk_G{n}_List → /khkk/list?group={n}
// Khkk_Pending / Khkk_G{n}_Pending → /khkk/list?group={n}&filter=ChoDuyet
// Khkk_Approved / Khkk_G{n}_Approved → /khkk/list?group={n}&filter=DaDuyet
// Khkk_Deleted / Khkk_G{n}_Deleted → /khkk/list?group={n}&view=deleted
// 🔴 Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng
// nên menu không sáng nhầm (bug UAT S155 khi URL leaf trùng URL user tự lọc).
//
// [K4b S166] 🔴 8 nhóm dùng CHUNG một route ⇒ mọi thứ lấy từ URL phải ĐỌC LẠI MỖI
// RENDER, không chụp một lần lúc mount: đi từ leaf nhóm 2 sang leaf nhóm 5 là đổi
// query chứ không đổi route, React Router không dựng lại component nên `useState(...)`
// khởi tạo một lần sẽ giữ nguyên bộ lọc cũ (trước K4b phải F5 mới đúng — vá-5).
// Bộ lọc người dùng bấm được giữ ở lớp override và tự nhả ra khi URL đổi (xem dưới).
import { useMemo, useState, type ReactNode } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useSearchParams } from 'react-router-dom'
@ -29,6 +36,7 @@ import {
KHKK_PHASE_BADGE,
KHKK_PHASE_LABELS,
KhkkPhase,
khkkGroupMenuLabel,
type KhkkListItemDto,
type KhkkPhaseValue,
type PagedResult,
@ -58,17 +66,50 @@ function formatDate(iso: string): string {
export function KhkkListPage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const initialFilter = searchParams.get('filter')
const deletedView = searchParams.get('view') === 'deleted'
const [phase, setPhase] = useState<KhkkPhaseValue | null>(
initialFilter ? (FILTER_TO_PHASE[initialFilter] ?? null) : null,
)
// ---- [K4b S166] 3 trục DANH TÍNH đọc thẳng từ URL, KHÔNG chụp một lần lúc mount ----
// Cả 8 nhóm dùng CHUNG một route `/khkk/list`; bấm leaf này sang leaf kia chỉ đổi
// query, React Router KHÔNG remount ⇒ giá trị khởi tạo của `useState` không chạy lại.
// Đó là lý do trước K4b bấm leaf→leaf phải F5 mới thấy đúng (vá-5).
const urlFilter = searchParams.get('filter')
const urlPhase: KhkkPhaseValue | null = urlFilter ? (FILTER_TO_PHASE[urlFilter] ?? null) : null
const deletedView = searchParams.get('view') === 'deleted'
// `?group=` — chỉ nhận 1..8; rác/thiếu ⇒ null = XEM TẤT CẢ NHÓM (không tự đoán nhóm 1,
// vì "vắng tham số" và "nhóm 1" là hai ý khác nhau; leaf nhóm 1 nay mang group=1 tường minh).
const groupParam = searchParams.get('group')
const group = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
// ---- Bộ lọc do NGƯỜI DÙNG bấm (6 chip KpiCard) đè lên bộ lọc của URL ----
// 🔴 3 trạng thái, KHÔNG gộp được: `undefined` = chưa bấm chip ⇒ nghe theo URL;
// `null` = user chủ động chọn "Tất cả"; số = user chọn một trạng thái.
// Gộp `undefined` với `null` thì chọn "Tất cả" sẽ bị URL kéo ngược về filter cũ.
const [chipPhase, setChipPhase] = useState<KhkkPhaseValue | null | undefined>(undefined)
const [search, setSearch] = useState('')
const [page, setPage] = useState(1)
// Đổi leaf ⇒ trả bộ lọc chip về cho URL và về trang 1 (đang đứng trang 5 của nhóm 2
// mà nhảy sang nhóm 5 thì trang 5 gần như chắc chắn rỗng). Điều chỉnh state NGAY
// TRONG RENDER theo khuôn React "reset state khi input đổi" — dùng `useEffect` sẽ
// vẽ một lượt bằng dữ liệu cũ rồi mới sửa (nháy). `search` CỐ Ý giữ lại: đang tìm
// một mã rồi đổi nhóm để dò tiếp là thao tác có thật.
const navKey = `${group ?? ''}|${urlFilter ?? ''}|${deletedView ? 'del' : ''}`
const [lastNavKey, setLastNavKey] = useState(navKey)
if (navKey !== lastNavKey) {
setLastNavKey(navKey)
setChipPhase(undefined)
setPage(1)
}
const phase: KhkkPhaseValue | null = chipPhase !== undefined ? chipPhase : urlPhase
const list = useQuery({
queryKey: ['khkk-list', { phase, search, page, deletedView }],
// 🔴 [vá-3] `group` PHẢI nằm trong khoá cache. Thiếu nó thì 8 leaf "Danh sách" của
// 8 nhóm dùng CHUNG một ô cache ⇒ mở nhóm 5 lại thấy dữ liệu nhóm 2 trong khoảnh
// khắc trước khi refetch, và tệ hơn là không refetch nếu còn fresh (sai CÂM, không
// có lỗi nào nổ). Trục `?filter=` đã nằm sẵn trong khoá dưới dạng ĐÃ GIẢI MÃ là
// `phase` (`FILTER_TO_PHASE` đơn ánh, và `phase` cũng chính là thứ gửi lên server)
// ⇒ khoá này phủ đủ cả hai trục vá-3 nhắc; thêm `filter` thô nữa chỉ tách đôi ô
// cache cho hai request y hệt nhau.
queryKey: ['khkk-list', { group, phase, search, page, deletedView }],
queryFn: async () => {
// Phiếu xóa mềm bị HasQueryFilter che ở list thường — chỉ endpoint riêng
// `/deleted` thấy (mirror PE `/purchase-evaluations/deleted`, S155).
@ -76,6 +117,11 @@ export function KhkkListPage() {
return (await api.get<PagedResult<KhkkListItemDto>>(url, {
params: {
phase: deletedView ? undefined : (phase ?? undefined),
// [K4b S166 — FLAG-1 ĐÃ NỐI CẢ 2 NHÁNH] `/deleted` nay nhận `group` mirror
// List (lead vá BE cùng lượt: Controller + `ListDeletedContractSigningPlansQuery`
// + handler lọc SAU vế IDOR). Gửi thẳng cho cả hai — lọc là việc server,
// total/số-trang vì thế nói thật ở cả màn sống lẫn màn đã-xóa.
group: group ?? undefined,
search: search.trim() || undefined,
page,
pageSize: PAGE_SIZE,
@ -115,17 +161,29 @@ export function KhkkListPage() {
<div className="space-y-5">
<PageHeader
eyebrow="Giai đoạn 2"
title={deletedView ? 'Kế hoạch ký kết HĐ — đã xóa' : 'Kế hoạch ký kết HĐ'}
subtitle={
title={
deletedView
? 'Phiếu đã xóa mềm — chỉ xem danh sách'
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
? (group !== null ? `Kế hoạch ký kết HĐ — N${group} · đã xóa` : 'Kế hoạch ký kết HĐ — đã xóa')
: group !== null
? `Kế hoạch ký kết HĐ — N${group}`
: 'Kế hoạch ký kết HĐ'
}
subtitle={
// [K4b S166] Màn "Đã xóa" nay ĐƯỢC in tên nhóm — `/deleted` đã lọc server-side
// theo `group` (FLAG-1 nối cùng lượt), tên nhóm hết là lời nói dối.
deletedView
? (group !== null ? `${khkkGroupMenuLabel(group)} — phiếu đã xóa mềm, chỉ xem` : 'Phiếu đã xóa mềm — chỉ xem danh sách')
: group !== null
? khkkGroupMenuLabel(group)
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
}
icon={deletedView ? <Trash2 className="h-5 w-5" /> : <FileCheck className="h-5 w-5" />}
accent="brand"
actions={
deletedView ? undefined : (
<Button onClick={() => navigate('/khkk/create')}>
// Mang nhóm đang xem sang màn lập phiếu — không bắt người dùng chọn lại
// đúng cái nhóm họ vừa đứng (bắt tay với vá-6 bên `KhkkCreatePage`).
<Button onClick={() => navigate(group !== null ? `/khkk/create?group=${group}` : '/khkk/create')}>
<Plus className="mr-2 h-4 w-4" />
Lập kế hoạch
</Button>
@ -151,7 +209,9 @@ export function KhkkListPage() {
accent={c.accent}
active={phase === c.value}
onClick={() => {
setPhase(c.value)
// Ghi vào lớp override, KHÔNG ghi vào URL — giữ URL của leaf đang đứng
// để sidebar không sáng nhầm mục khác (bài S155, xem ghi chú đầu file).
setChipPhase(c.value)
setPage(1)
}}
/>

View File

@ -126,6 +126,12 @@ export const KHKK_APPROVAL_GROUPS = [1, 2, 3, 4, 5, 6, 7, 8] as const
// đề xuất", **[OG-6] anh soát SAU trên UI**). Cùng bộ chữ này K4a sẽ seed làm LABEL menu
// ⇒ nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app), đừng vá rải trong page.
// Rút gọn có chủ đích: bỏ số thứ tự đầu dòng của bảng (ô select đã có "N{n}").
//
// [K4a S166 — acceptance-5 single-source] ĐÃ ĐỐI CHIẾU TỪNG KÝ TỰ với nhãn BE seed
// (`DbInitializer.cs:1960` cho nhóm 1 + dict `khkkGroupLabels` `:2127-2136` cho nhóm
// 2..8, cùng `labelBackfill:2235`): 8/8 nhóm khớp theo quan hệ `"{n}. " + chuỗi dưới đây`.
// ⇒ K4a KHÔNG đẻ map thứ hai. Bản đầy đủ (có số thứ tự) lấy qua `khkkGroupMenuLabel(n)`
// ngay bên dưới — DERIVE chứ không chép, để sửa chữ vẫn chỉ đụng đúng 1 chỗ này.
export const KHKK_APPROVAL_GROUP_LABELS: Record<number, string> = {
1: 'Thiết bị, máy móc, vật tư phụ (A1)',
2: 'Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)',
@ -137,6 +143,25 @@ export const KHKK_APPROVAL_GROUP_LABELS: Record<number, string> = {
8: 'HĐ chưa ủy quyền — CEO (5.2)',
}
// [K4a S166] Nhãn nhóm ĐẦY ĐỦ = đúng chuỗi BE ghi vào `MenuItems.Label` khi seed.
// 🔴 ĐÂY KHÔNG PHẢI NGUỒN CỦA SIDEBAR. Sidebar render nhãn từ payload `/api/menus/me`
// (bảng `MenuItems`, admin còn đổi được qua `displayLabel` — Mig 27), nên đừng ai
// tưởng sửa chuỗi ở đây thì sidebar đổi theo. Hàm này phục vụ chỗ FE tự vẽ tên nhóm
// mà không đi qua menu: cây 8 thư mục nhóm (K5), tiêu đề trang khi vào từ leaf nhóm,
// màn xem luồng duyệt theo nhóm (WfView).
// ⚠️ Ràng buộc ngầm: BE đang đặt nhãn theo khuôn "{n}. {tên}". Nếu BE bỏ tiền tố số thì
// hàm này lệch một cách CÂM (không đỏ build) — lúc đó sửa chính hàm này, đừng vá page.
export function khkkGroupMenuLabel(n: number): string {
const label = KHKK_APPROVAL_GROUP_LABELS[n]
return label ? `${n}. ${label}` : `Nhóm ${n}`
}
// [K4a S166 — O-2] Mã quy trình duyệt của nhóm n. BE khớp bằng regex `^KHKK-N([1-8])$`
// (`ContractSigningPlanFeatures.cs:420`, rào 409 "phiếu nhóm 3 pin quy trình nhóm 5");
// 8 workflow do `DbInitializer.SeedKhkkGroupWorkflowsAsync` seed, ApplicableType=10.
// Gom về 1 hàm để FE không rải chuỗi `KHKK-N` ở nhiều nơi rồi lệch nhau.
export const khkkGroupWorkflowCode = (n: number) => `KHKK-N${n}`
export interface KhkkListItemDto {
id: string
maKeHoach: string | null

View File

@ -56,10 +56,14 @@ function resolvePath(key: string): string | null {
const staticMap: Record<string, string> = {
// [W2 KHKK S161] GĐ2 hết skeleton — 6 leaf re-point trang thật (gotcha #50:
// thiếu route = sidebar drop SILENT; filter đi qua query param, page map FILTER_TO_PHASE).
Khkk_List: '/khkk/list',
Khkk_Create: '/khkk/create',
Khkk_Pending: '/khkk/list?filter=ChoDuyet',
Khkk_Approved: '/khkk/list?filter=DaDuyet',
// [K4b S166 — spec K4b-7] 6 leaf nhóm 1 mang `group=1` TƯỜNG MINH. Chúng là 6 key
// cũ KHÔNG-infix (S159, cấm đổi tên) nên không đi qua regex `Khkk_G{n}_*` bên dưới;
// thiếu `group=1` ở đây thì "Danh sách" của nhóm 1 lại hiện phiếu của cả 8 nhóm —
// đúng một mục nói dối giữa 8 mục nói thật. Trang coi "vắng group" = xem tất cả.
Khkk_List: '/khkk/list?group=1',
Khkk_Create: '/khkk/create?group=1',
Khkk_Pending: '/khkk/list?group=1&filter=ChoDuyet',
Khkk_Approved: '/khkk/list?group=1&filter=DaDuyet',
// [W7 KHKK S161] GĐ4 hết skeleton — 7 leaf `Hdc_*` re-point trang thật
// `/hard-copies?type=N` (N = ContractType int, mirror TYPE_CODE_TO_INT ở trên).
// gotcha #50: thiếu/để sai route ở đây = leaf DROP IM LẶNG khỏi sidebar.
@ -79,8 +83,8 @@ function resolvePath(key: string): string | null {
KeHoachKyKet: '/khkk/list', // [W2 S161] root GĐ2 hết placeholder — về danh sách phiếu
HopDongCung: '/dashboard',
// [W2 S161] 2 leaf còn lại — WfView đi alias matrix type=10 (App.tsx, không đẻ page mới)
Khkk_WfView: '/khkk/workflow-matrix?type=10',
Khkk_Deleted: '/khkk/list?view=deleted',
Khkk_WfView: '/khkk/workflow-matrix?type=10&group=1',
Khkk_Deleted: '/khkk/list?group=1&view=deleted',
// [Plan CA Hotfix 1 S29 2026-05-22] 4 master + 4 catalog leaf moved từ
// fe-admin → fe-user. resolvePath PHẢI có route mapping nếu không
// MenuLeaf line 238 `if (!path) return null` → sidebar drop silent.
@ -181,6 +185,41 @@ function resolvePath(key: string): string | null {
if (action === 'Approved') return `/purchase-evaluations?type=${typeInt}&phase=7&view=approved`
if (action === 'Deleted') return `/purchase-evaluations?type=${typeInt}&deleted=1`
}
// ===================== [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT ======================
// `Khkk_G{n}_{action}` — 42 leaf của nhóm 2..8 (BE `MenuKeys.KhkkLeaf` :77, sinh từ
// `KhkkGroupNumbers = [2..8]` × `KhkkLeafActions` 6 hành động). Đây là "chỗ thứ 4"
// Pattern 16-bis: BE seed 42 row mà thiếu nhánh này thì `resolvePath` trả null ⇒
// MenuLeaf nhánh `if (!path) return null` (grep chuỗi đó — số dòng trượt theo edit) ⇒ 42 leaf DROP IM LẶNG (gotcha #50).
//
// 🔴 NHÓM 1 KHÔNG đi qua đây: 7 key cũ KHÔNG-infix (`Khkk_G1` + `Khkk_WfView` …
// `Khkk_Deleted`, seed S159) nằm trong `staticMap` bên trên và CẤM đổi tên
// (đổi = mồ côi permission-row đã cấp — bài S155). Vì thế regex dưới về mặt dữ
// liệu chỉ ăn n = 2..8; để dải `[1-8]` theo spec là CỐ Ý — rộng hơn tập seed một
// nấc, vô hại hôm nay và đúng sẵn nếu sau này nhóm 1 chuyển sang khuôn infix.
//
// 🔴 `?group=n` là DANH TÍNH điều hướng, không phải bộ lọc tuỳ hứng: nó KHÔNG nằm
// trong `TRANSIENT_QUERY_KEYS` (grep tên hằng — đừng neo số dòng) nên 6 leaf cùng nhóm và cùng leaf khác nhóm
// đều phân biệt được khi so khớp active (bài `view=approved` S155 ngay khối trên).
// Tên tham số `filter` / `view` / `group` lấy ĐÚNG từ `KhkkListPage` (grep
// `get('filter')` / `get('view')` / `get('group')` trong file đó — đừng neo số dòng,
// chính wave K4b đã làm số cũ trượt) — sai một chữ ở đây là leaf trỏ tới trang mở
// được nhưng lọc SAI, hỏng câm chứ không đỏ build.
//
// 🔴 fe-admin CỐ Ý KHÔNG nới regex này (lead chốt 2026-07-27, giữ lệch): fe-admin
// không có `WorkflowMatrixViewPage` (`App.tsx` bên đó chỉ có /khkk/list|create|:id)
// nên `WfView` bên admin sẽ là link chết; "Luồng duyệt" phía admin trỏ Designer.
// Khai lệch có chủ đích — xem ghi chú ràng buộc ngược tại `isAdminHidden` fe-admin.
const khkkMatch = key.match(/^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$/)
if (khkkMatch) {
const [, group, action] = khkkMatch
if (action === 'WfView') return `/khkk/workflow-matrix?type=10&group=${group}`
if (action === 'List') return `/khkk/list?group=${group}`
if (action === 'Create') return `/khkk/create?group=${group}`
if (action === 'Pending') return `/khkk/list?group=${group}&filter=ChoDuyet`
if (action === 'Approved') return `/khkk/list?group=${group}&filter=DaDuyet`
if (action === 'Deleted') return `/khkk/list?group=${group}&view=deleted`
}
return null
}

View File

@ -14,7 +14,7 @@
// không phân biệt được "PE không đủ điều kiện" với "PE đã dùng hết nhóm".
import { useState, type FormEvent } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { CheckCircle2, FileCheck, FileText, Layers, Plus, Save, Settings2, X } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/ui/PageHeader'
@ -57,11 +57,31 @@ function firstFreeGroup(taken: number[]): number | null {
export function KhkkCreatePage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
// [K4b S166 — vá-6] Vào từ leaf "Thao tác" của một nhóm (`/khkk/create?group={n}`)
// hoặc từ nút "Lập kế hoạch" trên màn danh sách đang lọc nhóm ⇒ ô chọn nhóm mở sẵn
// đúng nhóm đó. Không có tham số (vào thẳng /khkk/create) thì giữ nguyên default cũ.
// Chỉ nhận 1..8, rác coi như không có — ô chọn không được phép mang giá trị ngoài dải.
const groupParam = searchParams.get('group')
const presetGroup = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('')
const [approvalWorkflowId, setApprovalWorkflowId] = useState('')
const [approvalGroup, setApprovalGroup] = useState<number>(KHKK_APPROVAL_GROUP_DEFAULT)
const [approvalGroup, setApprovalGroup] = useState<number>(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)
const [ghiChu, setGhiChu] = useState('')
// Cùng lý do với `KhkkListPage` (vá-5): 8 leaf "Thao tác" dùng chung route
// `/khkk/create`, nhảy leaf→leaf không dựng lại component nên giá trị khởi tạo ở
// trên KHÔNG chạy lại. Đồng bộ lại ngay trong render khi tham số nhóm đổi.
// 🔴 Chỉ đổi khi CHÍNH THAM SỐ URL đổi — nếu so với `approvalGroup` thì người dùng
// tự chọn nhóm khác sẽ bị kéo ngược về nhóm của URL, không bấm đi đâu được.
const [lastPresetGroup, setLastPresetGroup] = useState(presetGroup)
if (presetGroup !== lastPresetGroup) {
setLastPresetGroup(presetGroup)
if (presetGroup !== null) setApprovalGroup(presetGroup)
}
// Phiếu PE đủ điều kiện: Phase=DaDuyet VÀ có ≥1 NCC trúng thầu (BE lọc — khớp
// ĐÚNG 2 rào còn lại của Create nên picker không mời bấm vào thứ chắc chắn 409).
// [K2 S164] Vị ngữ "chưa có kế hoạch sống" ĐÃ BỎ (`Features.cs:1291-1295`).

View File

@ -3,13 +3,20 @@
// KpiCard làm bộ lọc + bảng + phân trang). File MIRROR SHA256 identical với
// fe-admin counterpart.
//
// URL vào từ menu (Layout.tsx staticMap — chỗ thứ 4 của Pattern 16-bis):
// Khkk_List → /khkk/list
// Khkk_Pending → /khkk/list?filter=ChoDuyet
// Khkk_Approved → /khkk/list?filter=DaDuyet
// Khkk_Deleted → /khkk/list?view=deleted
// URL vào từ menu (Layout.tsx staticMap cho nhóm 1, regex `Khkk_G{n}_*` cho nhóm
// 2..8; đây là "chỗ thứ 4" của Pattern 16-bis):
// Khkk_List / Khkk_G{n}_List → /khkk/list?group={n}
// Khkk_Pending / Khkk_G{n}_Pending → /khkk/list?group={n}&filter=ChoDuyet
// Khkk_Approved / Khkk_G{n}_Approved → /khkk/list?group={n}&filter=DaDuyet
// Khkk_Deleted / Khkk_G{n}_Deleted → /khkk/list?group={n}&view=deleted
// 🔴 Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng
// nên menu không sáng nhầm (bug UAT S155 khi URL leaf trùng URL user tự lọc).
//
// [K4b S166] 🔴 8 nhóm dùng CHUNG một route ⇒ mọi thứ lấy từ URL phải ĐỌC LẠI MỖI
// RENDER, không chụp một lần lúc mount: đi từ leaf nhóm 2 sang leaf nhóm 5 là đổi
// query chứ không đổi route, React Router không dựng lại component nên `useState(...)`
// khởi tạo một lần sẽ giữ nguyên bộ lọc cũ (trước K4b phải F5 mới đúng — vá-5).
// Bộ lọc người dùng bấm được giữ ở lớp override và tự nhả ra khi URL đổi (xem dưới).
import { useMemo, useState, type ReactNode } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useSearchParams } from 'react-router-dom'
@ -29,6 +36,7 @@ import {
KHKK_PHASE_BADGE,
KHKK_PHASE_LABELS,
KhkkPhase,
khkkGroupMenuLabel,
type KhkkListItemDto,
type KhkkPhaseValue,
type PagedResult,
@ -58,17 +66,50 @@ function formatDate(iso: string): string {
export function KhkkListPage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const initialFilter = searchParams.get('filter')
const deletedView = searchParams.get('view') === 'deleted'
const [phase, setPhase] = useState<KhkkPhaseValue | null>(
initialFilter ? (FILTER_TO_PHASE[initialFilter] ?? null) : null,
)
// ---- [K4b S166] 3 trục DANH TÍNH đọc thẳng từ URL, KHÔNG chụp một lần lúc mount ----
// Cả 8 nhóm dùng CHUNG một route `/khkk/list`; bấm leaf này sang leaf kia chỉ đổi
// query, React Router KHÔNG remount ⇒ giá trị khởi tạo của `useState` không chạy lại.
// Đó là lý do trước K4b bấm leaf→leaf phải F5 mới thấy đúng (vá-5).
const urlFilter = searchParams.get('filter')
const urlPhase: KhkkPhaseValue | null = urlFilter ? (FILTER_TO_PHASE[urlFilter] ?? null) : null
const deletedView = searchParams.get('view') === 'deleted'
// `?group=` — chỉ nhận 1..8; rác/thiếu ⇒ null = XEM TẤT CẢ NHÓM (không tự đoán nhóm 1,
// vì "vắng tham số" và "nhóm 1" là hai ý khác nhau; leaf nhóm 1 nay mang group=1 tường minh).
const groupParam = searchParams.get('group')
const group = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
// ---- Bộ lọc do NGƯỜI DÙNG bấm (6 chip KpiCard) đè lên bộ lọc của URL ----
// 🔴 3 trạng thái, KHÔNG gộp được: `undefined` = chưa bấm chip ⇒ nghe theo URL;
// `null` = user chủ động chọn "Tất cả"; số = user chọn một trạng thái.
// Gộp `undefined` với `null` thì chọn "Tất cả" sẽ bị URL kéo ngược về filter cũ.
const [chipPhase, setChipPhase] = useState<KhkkPhaseValue | null | undefined>(undefined)
const [search, setSearch] = useState('')
const [page, setPage] = useState(1)
// Đổi leaf ⇒ trả bộ lọc chip về cho URL và về trang 1 (đang đứng trang 5 của nhóm 2
// mà nhảy sang nhóm 5 thì trang 5 gần như chắc chắn rỗng). Điều chỉnh state NGAY
// TRONG RENDER theo khuôn React "reset state khi input đổi" — dùng `useEffect` sẽ
// vẽ một lượt bằng dữ liệu cũ rồi mới sửa (nháy). `search` CỐ Ý giữ lại: đang tìm
// một mã rồi đổi nhóm để dò tiếp là thao tác có thật.
const navKey = `${group ?? ''}|${urlFilter ?? ''}|${deletedView ? 'del' : ''}`
const [lastNavKey, setLastNavKey] = useState(navKey)
if (navKey !== lastNavKey) {
setLastNavKey(navKey)
setChipPhase(undefined)
setPage(1)
}
const phase: KhkkPhaseValue | null = chipPhase !== undefined ? chipPhase : urlPhase
const list = useQuery({
queryKey: ['khkk-list', { phase, search, page, deletedView }],
// 🔴 [vá-3] `group` PHẢI nằm trong khoá cache. Thiếu nó thì 8 leaf "Danh sách" của
// 8 nhóm dùng CHUNG một ô cache ⇒ mở nhóm 5 lại thấy dữ liệu nhóm 2 trong khoảnh
// khắc trước khi refetch, và tệ hơn là không refetch nếu còn fresh (sai CÂM, không
// có lỗi nào nổ). Trục `?filter=` đã nằm sẵn trong khoá dưới dạng ĐÃ GIẢI MÃ là
// `phase` (`FILTER_TO_PHASE` đơn ánh, và `phase` cũng chính là thứ gửi lên server)
// ⇒ khoá này phủ đủ cả hai trục vá-3 nhắc; thêm `filter` thô nữa chỉ tách đôi ô
// cache cho hai request y hệt nhau.
queryKey: ['khkk-list', { group, phase, search, page, deletedView }],
queryFn: async () => {
// Phiếu xóa mềm bị HasQueryFilter che ở list thường — chỉ endpoint riêng
// `/deleted` thấy (mirror PE `/purchase-evaluations/deleted`, S155).
@ -76,6 +117,11 @@ export function KhkkListPage() {
return (await api.get<PagedResult<KhkkListItemDto>>(url, {
params: {
phase: deletedView ? undefined : (phase ?? undefined),
// [K4b S166 — FLAG-1 ĐÃ NỐI CẢ 2 NHÁNH] `/deleted` nay nhận `group` mirror
// List (lead vá BE cùng lượt: Controller + `ListDeletedContractSigningPlansQuery`
// + handler lọc SAU vế IDOR). Gửi thẳng cho cả hai — lọc là việc server,
// total/số-trang vì thế nói thật ở cả màn sống lẫn màn đã-xóa.
group: group ?? undefined,
search: search.trim() || undefined,
page,
pageSize: PAGE_SIZE,
@ -115,17 +161,29 @@ export function KhkkListPage() {
<div className="space-y-5">
<PageHeader
eyebrow="Giai đoạn 2"
title={deletedView ? 'Kế hoạch ký kết HĐ — đã xóa' : 'Kế hoạch ký kết HĐ'}
subtitle={
title={
deletedView
? 'Phiếu đã xóa mềm — chỉ xem danh sách'
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
? (group !== null ? `Kế hoạch ký kết HĐ — N${group} · đã xóa` : 'Kế hoạch ký kết HĐ — đã xóa')
: group !== null
? `Kế hoạch ký kết HĐ — N${group}`
: 'Kế hoạch ký kết HĐ'
}
subtitle={
// [K4b S166] Màn "Đã xóa" nay ĐƯỢC in tên nhóm — `/deleted` đã lọc server-side
// theo `group` (FLAG-1 nối cùng lượt), tên nhóm hết là lời nói dối.
deletedView
? (group !== null ? `${khkkGroupMenuLabel(group)} — phiếu đã xóa mềm, chỉ xem` : 'Phiếu đã xóa mềm — chỉ xem danh sách')
: group !== null
? khkkGroupMenuLabel(group)
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
}
icon={deletedView ? <Trash2 className="h-5 w-5" /> : <FileCheck className="h-5 w-5" />}
accent="brand"
actions={
deletedView ? undefined : (
<Button onClick={() => navigate('/khkk/create')}>
// Mang nhóm đang xem sang màn lập phiếu — không bắt người dùng chọn lại
// đúng cái nhóm họ vừa đứng (bắt tay với vá-6 bên `KhkkCreatePage`).
<Button onClick={() => navigate(group !== null ? `/khkk/create?group=${group}` : '/khkk/create')}>
<Plus className="mr-2 h-4 w-4" />
Lập kế hoạch
</Button>
@ -151,7 +209,9 @@ export function KhkkListPage() {
accent={c.accent}
active={phase === c.value}
onClick={() => {
setPhase(c.value)
// Ghi vào lớp override, KHÔNG ghi vào URL — giữ URL của leaf đang đứng
// để sidebar không sáng nhầm mục khác (bài S155, xem ghi chú đầu file).
setChipPhase(c.value)
setPage(1)
}}
/>

View File

@ -126,6 +126,12 @@ export const KHKK_APPROVAL_GROUPS = [1, 2, 3, 4, 5, 6, 7, 8] as const
// đề xuất", **[OG-6] anh soát SAU trên UI**). Cùng bộ chữ này K4a sẽ seed làm LABEL menu
// ⇒ nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app), đừng vá rải trong page.
// Rút gọn có chủ đích: bỏ số thứ tự đầu dòng của bảng (ô select đã có "N{n}").
//
// [K4a S166 — acceptance-5 single-source] ĐÃ ĐỐI CHIẾU TỪNG KÝ TỰ với nhãn BE seed
// (`DbInitializer.cs:1960` cho nhóm 1 + dict `khkkGroupLabels` `:2127-2136` cho nhóm
// 2..8, cùng `labelBackfill:2235`): 8/8 nhóm khớp theo quan hệ `"{n}. " + chuỗi dưới đây`.
// ⇒ K4a KHÔNG đẻ map thứ hai. Bản đầy đủ (có số thứ tự) lấy qua `khkkGroupMenuLabel(n)`
// ngay bên dưới — DERIVE chứ không chép, để sửa chữ vẫn chỉ đụng đúng 1 chỗ này.
export const KHKK_APPROVAL_GROUP_LABELS: Record<number, string> = {
1: 'Thiết bị, máy móc, vật tư phụ (A1)',
2: 'Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)',
@ -137,6 +143,25 @@ export const KHKK_APPROVAL_GROUP_LABELS: Record<number, string> = {
8: 'HĐ chưa ủy quyền — CEO (5.2)',
}
// [K4a S166] Nhãn nhóm ĐẦY ĐỦ = đúng chuỗi BE ghi vào `MenuItems.Label` khi seed.
// 🔴 ĐÂY KHÔNG PHẢI NGUỒN CỦA SIDEBAR. Sidebar render nhãn từ payload `/api/menus/me`
// (bảng `MenuItems`, admin còn đổi được qua `displayLabel` — Mig 27), nên đừng ai
// tưởng sửa chuỗi ở đây thì sidebar đổi theo. Hàm này phục vụ chỗ FE tự vẽ tên nhóm
// mà không đi qua menu: cây 8 thư mục nhóm (K5), tiêu đề trang khi vào từ leaf nhóm,
// màn xem luồng duyệt theo nhóm (WfView).
// ⚠️ Ràng buộc ngầm: BE đang đặt nhãn theo khuôn "{n}. {tên}". Nếu BE bỏ tiền tố số thì
// hàm này lệch một cách CÂM (không đỏ build) — lúc đó sửa chính hàm này, đừng vá page.
export function khkkGroupMenuLabel(n: number): string {
const label = KHKK_APPROVAL_GROUP_LABELS[n]
return label ? `${n}. ${label}` : `Nhóm ${n}`
}
// [K4a S166 — O-2] Mã quy trình duyệt của nhóm n. BE khớp bằng regex `^KHKK-N([1-8])$`
// (`ContractSigningPlanFeatures.cs:420`, rào 409 "phiếu nhóm 3 pin quy trình nhóm 5");
// 8 workflow do `DbInitializer.SeedKhkkGroupWorkflowsAsync` seed, ApplicableType=10.
// Gom về 1 hàm để FE không rải chuỗi `KHKK-N` ở nhiều nơi rồi lệch nhau.
export const khkkGroupWorkflowCode = (n: number) => `KHKK-N${n}`
export interface KhkkListItemDto {
id: string
maKeHoach: string | null

View File

@ -55,16 +55,17 @@ public class ContractSigningPlansController(
public async Task<ActionResult<List<ContractSigningPlanListItemDto>>> Inbox(CancellationToken ct)
=> Ok(await mediator.Send(new GetMyContractSigningPlanInboxQuery(), ct));
/// Màn "Đã xoá" — CHỈ XEM.
/// Màn "Đã xoá" — CHỈ XEM. [K4b S166] +`group` mirror List `:42` (8 leaf Đã-xóa per-nhóm).
[HttpGet("deleted")]
[Authorize(Policy = "KeHoachKyKet.Read")]
public async Task<ActionResult<PagedResult<ContractSigningPlanListItemDto>>> ListDeleted(
[FromQuery(Name = "group")] int? group = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? search = null,
[FromQuery] bool sortDesc = true,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListDeletedContractSigningPlansQuery
=> Ok(await mediator.Send(new ListDeletedContractSigningPlansQuery(group)
{ Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
/// Picker màn tạo: phiếu Duyệt NCC đã duyệt, có NCC trúng thầu, chưa có kế hoạch sống.

View File

@ -862,7 +862,10 @@ public class GetMyContractSigningPlanInboxQueryHandler(IApplicationDbContext db,
}
/// Màn "Đã xoá" — CHỈ XEM (mirror `ListDeletedPurchaseEvaluationsQuery`).
public record ListDeletedContractSigningPlansQuery
// [K4b S166 — gate-K4b FLAG-1] +ApprovalGroup mirror list SỐNG (`:756-757`): 8 leaf "Đã xóa"
// per-nhóm trỏ `?group=n` mà endpoint này không đọc ⇒ 8 URL trả CÙNG tập mọi-nhóm; lọc client-side
// thì total/số-trang nói dối (server đếm trên tập chưa lọc) ⇒ phải lọc server-side như list sống.
public record ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null)
: PagedRequest, IRequest<PagedResult<ContractSigningPlanListItemDto>>;
public class ListDeletedContractSigningPlansQueryHandler(IApplicationDbContext db, ICurrentUser currentUser)
@ -889,6 +892,10 @@ public class ListDeletedContractSigningPlansQueryHandler(IApplicationDbContext d
&& userWfIds.Contains(p.ApprovalWorkflowId.Value)));
}
// [K4b S166] Lọc nhóm — mirror list sống `:756-757`, SAU vế IDOR (lọc chỉ thu hẹp).
if (request.ApprovalGroup is not null)
q = q.Where(p => p.ApprovalGroup == request.ApprovalGroup);
if (!string.IsNullOrWhiteSpace(request.Search))
{
var kw = request.Search.Trim();

View File

@ -46,6 +46,43 @@ public static class MenuKeys
public const string KeHoachKyKet = "KeHoachKyKet";
public const string HopDongCung = "HopDongCung";
// ============================================================
// [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT × 6 leaf. Danh mục SOL-CCM-SP-002
// (Mig 70) chia 86 dòng thành 8 nhóm duyệt; mỗi nhóm 1 workflow `KHKK-N{n}`
// type-10 (K3) ⇒ sidebar cũng tách 8 nhóm.
// - NHÓM 1 GIỮ NGUYÊN 7 key CŨ KHÔNG-infix (`Khkk_G1` + `Khkk_WfView` …
// `Khkk_Deleted`, seed từ S159 — xem SeedMenuTreeAsync). Đổi tên key =
// mồ côi permission-row đã cấp (bài S155) ⇒ CẤM đổi.
// - NHÓM 2..8 dùng khuôn CÓ infix: `Khkk_G{n}` + `Khkk_G{n}_{action}`.
// 🔴 49 key (7 group + 42 leaf) VÀO `All` ⇒ sinh thêm 196 policy
// `Khkk_G*.{Read,Create,Update,Delete}` (registered-unused: endpoint KHKK
// vẫn CHỈ dùng `KeHoachKyKet.*` — gotcha #85, đừng OR key con vào gate)
// ⇒ 2 số canonical `docs/STATUS.md` đổi: Menu keys +49 · Policies +196
// (Policies = |All| × |Actions|, DERIVED — khai DELTA, không neo mốc tuyệt đối).
// 🔴 Vào `All` thì BẮT BUỘC có menu-row tương ứng trong `SeedMenuTreeAsync`
// (thiếu row = Permission FK nổ ngay lần boot đầu — invariant T6 K1).
// `KhkkGroupMenuKeys` dưới là NGUỒN CHUNG cho cả `All` LẪN vòng seed
// menu-row + vòng grant site-2 ⇒ 3 phía khớp by-construction, KHÔNG mirror tay.
// ⚠️ 2 KHUÔN menu-key cùng sống trong file này (đọc 1 khuôn đừng tưởng khuôn kia sai):
// (a) `Ct_*` / `Pe_*` leaf CỐ Ý NGOÀI `All` — sinh động qua factory, Admin
// grant qua seeder riêng (xem chú thích tại PurchaseEvaluationApproved).
// (b) catalog / `AwV2_*` / `Khkk_G*` TRONG `All` — Admin tự có row qua
// loop `MenuKeys.All` ở SeedAdminPermissionsAsync. K4a đi khuôn (b).
// ============================================================
public static readonly int[] KhkkGroupNumbers = [2, 3, 4, 5, 6, 7, 8];
public static readonly string[] KhkkLeafActions =
["WfView", "List", "Create", "Pending", "Approved", "Deleted"];
public static string KhkkGroup(int n) => $"Khkk_G{n}";
public static string KhkkLeaf(int n, string action) => $"Khkk_G{n}_{action}";
// 49 key theo thứ tự: group n → 6 leaf của n → group n+1 → …
// (khai báo TRƯỚC `All` vì static field initializer chạy theo thứ tự văn bản).
public static readonly string[] KhkkGroupMenuKeys = KhkkGroupNumbers
.SelectMany(n => new[] { KhkkGroup(n) }
.Concat(KhkkLeafActions.Select(a => KhkkLeaf(n, a))))
.ToArray();
public static string ContractTypeGroup(string typeCode) => $"Ct_{typeCode}";
public static string ContractTypeList(string typeCode) => $"Ct_{typeCode}_List";
public static string ContractTypeCreate(string typeCode) => $"Ct_{typeCode}_Create";
@ -200,6 +237,11 @@ public static class MenuKeys
OffDatXe, OffItTicket, OffChamCong, OffAttendanceReport, HrmDashboard, // Phase 10.3-10.4 — G-O5/G-O6/G-P1/G-H3 + P11-E report
Personal, // [S57] Cá nhân (Puro grouping — Chấm công re-parent)
KeHoachKyKet, // [W1 Mig 69 — S161] GĐ2 KHKK: const :41 vào All ⇒ sinh policy KeHoachKyKet.{Read,Create,Update,Delete}
// [K4a — S166] 49 key sidebar KHKK nhóm 2..8 (7 group + 42 leaf) — nguồn sinh
// `KhkkGroupMenuKeys` (dùng chung với seed menu-row + grant site-2). 7 key nhóm 1
// KHÔNG-infix (`Khkk_G1`/`Khkk_WfView`…) CỐ Ý ngoài `All` như từ S159 — giữ nguyên
// trạng thái cũ, quyền của chúng do site-2/site-3 phát.
.. KhkkGroupMenuKeys,
System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows,
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22
// [K3 nhịp-1 — S164/S165] 8 leaf Designer cho 8 nhóm duyệt KHKK (+8 key ⇒ +32 policy).

View File

@ -1953,7 +1953,11 @@ public static class DbInitializer
// [S159-đợt3] skeleton menu GĐ2/GĐ4 (owner vẽ: "viết hoa + phân list như
// Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render
// MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app).
("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
// [K4a — S166] Nhóm 1 trong 8 NHÓM DUYỆT (danh mục SP-002): nhãn skeleton S159
// "1. Kế hoạch ký kết HĐ (NCC-TP)" → nhãn nhóm A1. Key `Khkk_G1` GIỮ NGUYÊN;
// DB đang chạy hội tụ qua `labelBackfill` bên dưới (dòng này chỉ ăn với DB trống).
// 7 nhóm còn lại (G2..G8) seed ở vòng lặp phía dưới (sau vòng `PeWf_*`).
("Khkk_G1", "1. Thiết bị, máy móc, vật tư phụ (A1)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
// [S159-đợt5 owner] đủ 6 leaf "y chang Duyệt NCC" (WfView đầu + Deleted cuối).
("Khkk_WfView", "Luồng duyệt", "Khkk_G1", 1, "Network"),
("Khkk_List", "Danh sách", "Khkk_G1", 2, "List"),
@ -2092,8 +2096,8 @@ public static class DbInitializer
tree.Add((MenuKeys.PurchaseEvaluationList(code), "Danh sách", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "List"));
tree.Add((MenuKeys.PurchaseEvaluationCreate(code), "Thao tác", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "Plus"));
// [S155 đợt 2] Nhãn ở đây CHỈ có tác dụng cho DB TRỐNG. Với DB đang chạy, upsert
// `:1893-1905` gặp key đã tồn tại thì chỉ update `Order` rồi `continue` — KHÔNG
// đụng Label ⇒ rename "Duyệt" → "Đang duyệt" phải qua `labelBackfill` bên dưới.
// `:2162-2179` (grep `existingItems.TryGetValue`) gặp key đã tồn tại thì chỉ update
// `Order` rồi `continue` ⇒ rename "Duyệt" → "Đang duyệt" phải qua `labelBackfill`.
tree.Add((MenuKeys.PurchaseEvaluationPending(code), "Đang duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCircle2"));
// 2 mục MỚI (owner chốt 2026-07-27) — key mới ⇒ đi nhánh INSERT bình thường.
tree.Add((MenuKeys.PurchaseEvaluationApproved(code), "Đã duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCheck"));
@ -2108,6 +2112,50 @@ public static class DbInitializer
tree.Add((MenuKeys.PeWorkflowTypeLeaf(code), label, MenuKeys.PeWorkflows, peWfOrder++, "FileCheck"));
}
// [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT × 6 leaf — 49 row MỚI cho nhóm 2..8
// (nhóm 1 = 7 row CŨ KHÔNG-infix nằm trong tree initializer trên: `Khkk_G1` +
// `Khkk_WfView`…`Khkk_Deleted`, key GIỮ NGUYÊN — đổi = mồ côi permission-row, S155).
// Nhãn 8 nhóm = bảng owner đã soát `danh-muc-sp002-transcribe.md:22-29` (nguồn: danh
// mục SOL-CCM-SP-002 Mig 70 chia 86 dòng thành 8 nhóm duyệt). Nhãn/icon/Order của 6
// leaf MIRROR Y nhóm 1 (Luồng duyệt/Danh sách/Thao tác/Đang duyệt/Đã duyệt/Đã xóa).
// 🔴 Key sinh bằng CHÍNH `MenuKeys.KhkkGroup/KhkkLeaf` + duyệt CHÍNH `KhkkGroupNumbers`/
// `KhkkLeafActions` ⇒ mọi key trong `MenuKeys.All` đều có menu-row by-construction
// (thiếu row = Permission FK nổ lần boot đầu — invariant T6 K1). Thêm nhóm/action mà
// quên nhãn ⇒ KeyNotFoundException NGAY lúc seed (fail-fast, hơn là FK nổ về sau).
// ⚠️ Đổi NHÃN sau khi đã seed: BẮT BUỘC thêm entry vào `labelBackfill` bên dưới +
// restart — sửa chữ tại đây VÔ TÁC DỤNG trên DB đang chạy (upsert chỉ re-set Order).
var khkkGroupLabels = new Dictionary<int, string>
{
[2] = "2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)",
[3] = "3. Hoạt động văn phòng (A4)",
[4] = "4. Vật tư (B1)",
[5] = "5. Thầu phụ xây dựng (B2)",
[6] = "6. Thầu phụ MEP (B3)",
[7] = "7. BCH công trường (C1-C3)",
[8] = "8. HĐ chưa ủy quyền — CEO (5.2)",
};
var khkkLeafMeta = new Dictionary<string, (string Label, string Icon)>
{
["WfView"] = ("Luồng duyệt", "Network"),
["List"] = ("Danh sách", "List"),
["Create"] = ("Thao tác", "Plus"),
["Pending"] = ("Đang duyệt", "CheckCircle2"),
["Approved"] = ("Đã duyệt", "CheckCheck"),
["Deleted"] = ("Đã xóa", "Trash2"),
};
foreach (var n in MenuKeys.KhkkGroupNumbers)
{
var groupKey = MenuKeys.KhkkGroup(n);
// Order nhóm = n (nhóm 1 đang Order 1 ⇒ 8 nhóm xếp 1..8 dưới root KeHoachKyKet).
tree.Add((groupKey, khkkGroupLabels[n], MenuKeys.KeHoachKyKet, n, "FileCheck"));
var khkkLeafOrder = 1;
foreach (var action in MenuKeys.KhkkLeafActions)
{
var (leafLabel, leafIcon) = khkkLeafMeta[action];
tree.Add((MenuKeys.KhkkLeaf(n, action), leafLabel, groupKey, khkkLeafOrder++, leafIcon));
}
}
var existingItems = await db.MenuItems.ToDictionaryAsync(m => m.Key);
var added = 0;
var reordered = 0;
@ -2146,7 +2194,7 @@ public static class DbInitializer
[MenuKeys.PeWorkflowTypeLeaf("DuyetNccPhuongAn")] = "Duyệt NCC và Giải pháp",
// [S155 đợt 2] 🔴 Rename "Duyệt" → "Đang duyệt" (owner, ảnh menu 2026-07-27).
// BẮT BUỘC đi qua đây: sửa chữ trong `tree.Add(...)` là VÔ TÁC DỤNG trên DB đang
// chạy vì upsert `:1893-1905` chỉ update `Order` cho key đã tồn tại.
// chạy vì upsert `:2162-2179` (grep `existingItems.TryGetValue`) chỉ re-set `Order`.
[MenuKeys.PurchaseEvaluationPending("DuyetNcc")] = "Đang duyệt",
[MenuKeys.PurchaseEvaluationPending("DuyetNccPhuongAn")] = "Đang duyệt",
// [S159] 🔴 UAT anh Kiệt: đánh số 01.-07. nhóm HĐ (verbatim "01 . HĐ thầu phụ").
@ -2180,6 +2228,11 @@ public static class DbInitializer
[MenuKeys.ContractTypeGroup("MuaBan")] = "05. HĐ Mua bán",
[MenuKeys.ContractTypeGroup("NguyenTacNcc")] = "06. HĐ Nguyên tắc NCC",
[MenuKeys.ContractTypeGroup("NguyenTacDv")] = "07. HĐ Nguyên tắc Dịch vụ",
// [K4a — S166] Nhóm 1 KHKK đổi nhãn skeleton S159 ("1. Kế hoạch ký kết HĐ
// (NCC-TP)") → nhãn nhóm duyệt A1, khớp bảng 8 nhãn owner đã soát. CHỈ đổi
// Label, KHÔNG đổi key ⇒ 0 row mới, 0 permission-row mồ côi. 7 nhóm G2..G8 là
// key MỚI nên đi nhánh INSERT của upsert, không cần entry backfill.
["Khkk_G1"] = "1. Thiết bị, máy móc, vật tư phụ (A1)",
};
var updatedLabels = 0;
foreach (var (key, expectedLabel) in labelBackfill)
@ -2282,6 +2335,9 @@ public static class DbInitializer
// Cá nhân (Personal) khỏi MỌI role TRỪ Admin. Menu tự ẩn cả 2 app
// (GetMyMenuTree lọc CanRead). Chạy SAU grant seed để revoke thắng.
// Mở lại sau golive: gỡ prefix khỏi revoke + thêm lại vào InReviewScope.
// [K4a — S166] Đã soi revoker cho 49 key KHKK mới: filter chỉ bắt prefix `Hrm*`/`Off*`
// + key `Personal` ⇒ **0 match `Khkk_*`** ⇒ 49 row CanRead vừa cấp KHÔNG bị lật. Phép
// thử của khẳng định này = restart API ×2 rồi đếm lại row `Khkk%` (acceptance A5).
await RevokeTemporarilyHiddenModulesAsync(db, roleManager, logger);
// [S118 2026-07-14 — anh Kiệt FDC] CHẠY SAU revoke để THẮNG: NGOẠI-LỆ role Procurement
@ -2370,11 +2426,24 @@ public static class DbInitializer
MenuKeys.CatalogServices, MenuKeys.CatalogWorkItems, MenuKeys.Departments,
MenuKeys.Projects, MenuKeys.Suppliers,
};
// [S159] 7 key nhóm 1 (không-infix) + [K4a S166] 49 key nhóm 2..8 (7 group + 42 leaf,
// sinh từ `MenuKeys.KhkkGroupMenuKeys` — CÙNG bộ sinh với `MenuKeys.All` và vòng seed
// menu-row ⇒ không mirror tay). CanRead mọi role, skip-existing.
// 🔴 LÝ DO GIỮ site này (đính chính lý-do sai đang lưu hành "group-key cần row để
// render"): group KHÔNG cần row CanRead của chính nó — `GetMyMenuTreeQuery.cs:96`
// `HasAccess(n) = n.CanRead || n.Children.Any(HasAccess)`. Cái BẮT BUỘC là 42 LEAF
// phải có row CanRead cho non-Admin: site-1 (loop MenuKeys.All) CHỈ cấp Admin, còn
// site-3 `SeedKeHoachKyKetAccessAsync` CHỈ đụng root + 6 leaf nhóm 1. Group vẫn liệt
// kê ở đây để có hàng tường minh trên ma trận Phân quyền (admin tự bật/tắt).
// ⚠️ CanRead là ĐỦ cho 42 leaf mới, KHÔNG cần nhét chúng vào site-3: mọi endpoint KHKK
// gate bằng policy `KeHoachKyKet.{Read,Create,Update,Delete}` (gotcha #85 — gate FE
// cũng phải dùng đúng key đó, KHÔNG OR key con), và cờ GHI của root do site-3 phát
// (13/13 role C+U, Delete = Drafter+Admin). Leaf chỉ là điểm điều hướng.
static IEnumerable<string> KhkkKeys() => new[]
{
"Khkk_G1", "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
"Khkk_Approved", "Khkk_Deleted",
};
}.Concat(MenuKeys.KhkkGroupMenuKeys);
static IEnumerable<string> HdcKeys()
=> MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);

View File

@ -0,0 +1,431 @@
using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Persistence;
using SolutionErp.Infrastructure.Tests.Common;
namespace SolutionErp.Infrastructure.Tests.Application;
// [K4a — S166 2026-08-01] Test-after cho sidebar KHKK 8 NHÓM DUYỆT (danh mục SOL-CCM-SP-002
// Mig 70 chia 86 dòng thành 8 nhóm; mỗi nhóm 1 workflow `KHKK-N{n}` type-10 — K3).
//
// BE đã land: `MenuKeys.KhkkGroupNumbers/KhkkLeafActions/KhkkGroupMenuKeys` (49 key spread vào
// `All`) + vòng seed 49 menu-row trong `SeedMenuTreeAsync` + entry `labelBackfill["Khkk_G1"]`
// + site-2 grant `KhkkKeys().Concat(MenuKeys.KhkkGroupMenuKeys)`.
//
// KHÔNG nhân bản test đã có: `ContractCatalogTests.MenuKey_ContractCatalog_...` (T6 lane K1)
// đã assert `MenuKeys.All.Except(menuKeysInDb).BeEmpty()` sau `SeedMenuTreeAsync` ⇒ invariant
// "mọi key ∈ All có menu-row" (thiếu row = Permission FK nổ lúc boot) TỰ ĐỘNG phủ 49 key mới.
// File này đo phần T6 KHÔNG đo:
// T1 hình bộ sinh (49 key, group-trước-6-leaf, đúng chuỗi ký tự `Khkk_G{n}_{action}`)
// T2 `All` — chứa đủ 49 + KHÔNG key đôi + số tổng runtime + nhóm 1 CỐ Ý đứng ngoài
// T3 seed ×2 idempotent — 56 row họ Khkk (7 nhóm-1 + 49 mới), 0 nhân đôi, hình cây đúng
// T4 6 leaf nhóm 2..8 MIRROR nhãn/icon/Order của nhóm 1 (đo DB↔DB, không bảng hardcode)
// T5 labelBackfill đổi NHÃN mà KHÔNG thêm row + đối chứng key KHÔNG có entry thì KHÔNG tự lành
// T6 site-2 phát CanRead cho 49 key mới (read-only) + đối chứng Pe_* cùng lượt được CanCreate
//
// Harness: seeder là `private static` → REFLECTION (KHÔNG sửa prod đổi visibility).
// `SeedMenuTreeAsync(ApplicationDbContext, ILogger)`
// `SeedAllRolesReviewReadPermissionsAsync(ApplicationDbContext, RoleManager<Role>, ILogger)`
// Fixture `IdentityFixture` (cần RoleManager cho site-2) — KHÔNG chế fixture mới.
public class KhkkGroupMenuSeedTests
{
// ── 49 key kỳ vọng, VIẾT TAY (KHÔNG gọi lại `MenuKeys.KhkkGroup/KhkkLeaf` — gọi lại là
// tautology: bộ sinh sai chuỗi thì kỳ vọng cũng sai theo và test xanh vô nghĩa). ────────
private static readonly string[] Expected49 =
{
"Khkk_G2", "Khkk_G2_WfView", "Khkk_G2_List", "Khkk_G2_Create", "Khkk_G2_Pending", "Khkk_G2_Approved", "Khkk_G2_Deleted",
"Khkk_G3", "Khkk_G3_WfView", "Khkk_G3_List", "Khkk_G3_Create", "Khkk_G3_Pending", "Khkk_G3_Approved", "Khkk_G3_Deleted",
"Khkk_G4", "Khkk_G4_WfView", "Khkk_G4_List", "Khkk_G4_Create", "Khkk_G4_Pending", "Khkk_G4_Approved", "Khkk_G4_Deleted",
"Khkk_G5", "Khkk_G5_WfView", "Khkk_G5_List", "Khkk_G5_Create", "Khkk_G5_Pending", "Khkk_G5_Approved", "Khkk_G5_Deleted",
"Khkk_G6", "Khkk_G6_WfView", "Khkk_G6_List", "Khkk_G6_Create", "Khkk_G6_Pending", "Khkk_G6_Approved", "Khkk_G6_Deleted",
"Khkk_G7", "Khkk_G7_WfView", "Khkk_G7_List", "Khkk_G7_Create", "Khkk_G7_Pending", "Khkk_G7_Approved", "Khkk_G7_Deleted",
"Khkk_G8", "Khkk_G8_WfView", "Khkk_G8_List", "Khkk_G8_Create", "Khkk_G8_Pending", "Khkk_G8_Approved", "Khkk_G8_Deleted",
};
// 7 key nhóm 1 KHÔNG-infix, seed từ S159 — CẤM đổi (đổi key = mồ côi permission-row, bài S155).
private const string Group1GroupKey = "Khkk_G1";
private static readonly string[] Group1LeafKeys =
{ "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending", "Khkk_Approved", "Khkk_Deleted" };
// Nhãn nhóm 1 sau K4a (entry `labelBackfill` mới) và nhãn skeleton S159 mà nó thay thế.
private const string Group1LabelAfterK4a = "1. Thiết bị, máy móc, vật tư phụ (A1)";
private const string Group1LabelSkeletonS159 = "1. Kế hoạch ký kết HĐ (NCC-TP)";
// ── Reflection helper (mirror `ContractCatalogTests.InvokePrivateSeedAsync`) ────────────
private static async Task InvokePrivateSeedAsync(
TestApplicationDbContext db, string methodName, RoleManager<Role>? rm = null)
{
var mi = typeof(DbInitializer).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
mi.Should().NotBeNull($"DbInitializer.{methodName} phải tồn tại (private static) — đổi signature thì cập nhật test");
var args = rm is null
? new object[] { db, NullLogger.Instance }
: new object[] { db, rm, NullLogger.Instance };
await (Task)mi!.Invoke(null, args)!;
}
private static Task SeedMenuAsync(TestApplicationDbContext db)
=> InvokePrivateSeedAsync(db, "SeedMenuTreeAsync");
// Đếm họ Khkk bằng so-khớp Ordinal TRONG BỘ NHỚ (LIKE của SQLite case-insensitive ASCII —
// không muốn phép đếm phụ thuộc collation của provider).
private static async Task<List<string>> KhkkKeysInDbAsync(TestApplicationDbContext db)
{
var keys = await db.MenuItems.AsNoTracking().Select(m => m.Key).ToListAsync();
return keys.Where(k => k.StartsWith("Khkk", StringComparison.Ordinal)).OrderBy(k => k, StringComparer.Ordinal).ToList();
}
private static Task<MenuItem?> RowAsync(TestApplicationDbContext db, string key)
=> db.MenuItems.AsNoTracking().FirstOrDefaultAsync(m => m.Key == key);
// =====================================================================================
// T1 — HÌNH BỘ SINH: đúng 49 key, đúng THỨ TỰ group-trước-6-leaf, đúng chuỗi ký tự.
// `Equal` (thứ tự có nghĩa) chứ không `BeEquivalentTo`: nếu ai đổi `SelectMany`
// thành "7 group trước rồi 42 leaf sau" thì vòng seed menu vẫn chạy nhưng cây menu
// đổi hình ⇒ phải ĐỎ. Kèm phép kiểm liền-kề tường minh (G{n} đứng NGAY TRƯỚC
// G{n}_WfView) để thông điệp lỗi chỉ đúng thuộc tính bị vỡ.
// =====================================================================================
[Fact]
public void KhkkGroupMenuKeys_Is49Keys_GroupImmediatelyFollowedBySixLeaves()
{
MenuKeys.KhkkGroupMenuKeys.Should().HaveCount(49,
"7 nhóm (2..8) × (1 group + 6 leaf) = 49 — số này quyết định delta `Menu keys` + `Policies` ở docs/STATUS.md");
MenuKeys.KhkkGroupMenuKeys.Should().Equal(Expected49,
"bộ sinh phải ra ĐÚNG chuỗi `Khkk_G{{n}}` / `Khkk_G{{n}}_{{action}}` theo thứ tự group → 6 leaf; " +
"sai 1 ký tự là menu-row lệch key với `MenuKeys.All` ⇒ Permission FK nổ lúc boot");
// Liền-kề tường minh: mỗi group đứng ngay trước leaf `_WfView` của chính nó.
foreach (var n in MenuKeys.KhkkGroupNumbers)
{
var idx = Array.IndexOf(MenuKeys.KhkkGroupMenuKeys, $"Khkk_G{n}");
idx.Should().BeGreaterThanOrEqualTo(0, $"phải có key nhóm Khkk_G{n}");
MenuKeys.KhkkGroupMenuKeys[idx + 1].Should().Be($"Khkk_G{n}_WfView",
$"leaf đầu của nhóm {n} phải đứng NGAY SAU key nhóm — vòng seed menu dựa vào hình này");
}
MenuKeys.KhkkLeafActions.Should().Equal(
new[] { "WfView", "List", "Create", "Pending", "Approved", "Deleted" },
"6 action khớp Y 6 leaf nhóm 1 (owner: 'y chang Duyệt NCC') — thêm/bớt action là đổi 2 số canonical");
}
// =====================================================================================
// T2 — `MenuKeys.All`: chứa đủ 49 + KHÔNG key đôi + số tổng ĐO ĐƯỢC (Policies = |All|×4,
// DERIVED ⇒ số này là nguồn của 2 row canonical docs/STATUS.md).
// ⭐ Vế cuối = guard thật: nếu ai thêm số 1 vào `KhkkGroupNumbers` thì `Khkk_G1` vào
// `All` VÀ vòng seed thêm `tree.Add("Khkk_G1")` lần thứ hai (row cứng đã có ở
// initializer) ⇒ 2 entry cùng PK trong 1 lần SaveChanges = nổ lúc boot DB trống.
// =====================================================================================
[Fact]
public void MenuKeys_All_ContainsThe49_NoDuplicate_AndKeepsGroup1Outside()
{
MenuKeys.All.Should().Contain(Expected49,
"49 key phải nằm trong All — ngoài All thì Program.cs không sinh policy và Admin không được grant qua loop All");
MenuKeys.All.Should().OnlyHaveUniqueItems(
"key đôi trong All ⇒ SeedAdminPermissionsAsync chèn Permission trùng (RoleId, MenuKey) ⇒ nổ UNIQUE");
MenuKeys.All.Should().HaveCount(113,
"số ĐO runtime tại S166 sau K4a (+49). `Policies` = |All| × |Actions| = 113 × 4 = 452 — " +
"đổi số ở đây BẮT BUỘC đổi 2 row canonical `Menu keys` / `Policies` trong docs/STATUS.md");
MenuKeys.KhkkGroupNumbers.Should().Equal(new[] { 2, 3, 4, 5, 6, 7, 8 },
"nhóm 1 CỐ Ý ngoài bộ sinh — 7 key nhóm 1 không-infix đã có row cứng trong SeedMenuTreeAsync");
foreach (var g1 in Group1LeafKeys.Append(Group1GroupKey))
MenuKeys.All.Should().NotContain(g1,
$"{g1} (nhóm 1) giữ nguyên trạng thái NGOÀI All từ S159 — kéo vào All sẽ sinh menu-row trùng PK lúc seed");
}
// =====================================================================================
// T3 — SEED ×2 IDEMPOTENT: 56 row họ Khkk (7 nhóm-1 + 49 mới), lần 2 KHÔNG nhân đôi,
// và hình cây đúng (7 group mới treo dưới root `KeHoachKyKet`, Order = n, mỗi group
// đúng 6 con). Đếm tổng MenuItems trước/sau lần 2 để bắt cả trường hợp seeder nhân
// row Ở CHỖ KHÁC (không chỉ họ Khkk).
// =====================================================================================
[Fact]
public async Task SeedMenuTree_RunTwice_Has56KhkkRows_NoDuplication_AndCorrectTreeShape()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
await SeedMenuAsync(db);
var khkkAfter1 = await KhkkKeysInDbAsync(db);
var totalAfter1 = await db.MenuItems.AsNoTracking().CountAsync();
khkkAfter1.Should().HaveCount(56,
"7 row nhóm 1 (S159, không-infix) + 49 row nhóm 2..8 (K4a) — thiếu row nào là menu mất mục / Permission FK nổ");
khkkAfter1.Should().Contain(Expected49, "49 key mới phải có menu-row thật");
khkkAfter1.Should().Contain(Group1LeafKeys.Append(Group1GroupKey),
"7 row nhóm 1 KHÔNG được mất khi thêm 8-nhóm");
await SeedMenuAsync(db); // ⭐ lần 2
var khkkAfter2 = await KhkkKeysInDbAsync(db);
var totalAfter2 = await db.MenuItems.AsNoTracking().CountAsync();
khkkAfter2.Should().Equal(khkkAfter1, "idempotent — chạy lại KHÔNG thêm/bớt row họ Khkk");
totalAfter2.Should().Be(totalAfter1, "idempotent trên TOÀN bảng menu, không riêng họ Khkk");
// Hình cây: 7 group mới dưới root KeHoachKyKet, Order = số nhóm (xếp 1..8 cùng nhóm 1).
var g1 = await RowAsync(db, Group1GroupKey);
g1!.ParentKey.Should().Be(MenuKeys.KeHoachKyKet);
g1.Order.Should().Be(1, "nhóm 1 giữ Order 1 — 8 nhóm xếp 1..8 dưới root");
foreach (var n in MenuKeys.KhkkGroupNumbers)
{
var group = await RowAsync(db, $"Khkk_G{n}");
group.Should().NotBeNull($"Khkk_G{n} phải có row");
group!.ParentKey.Should().Be(MenuKeys.KeHoachKyKet, $"nhóm {n} treo dưới root Kế hoạch ký kết HĐ");
group.Order.Should().Be(n, $"Order nhóm = {n} (8 nhóm xếp 1..8 theo danh mục SP-002)");
group.Label.Should().NotBeNullOrWhiteSpace($"nhóm {n} phải có nhãn tiếng Việt (bảng 8 nhãn owner đã soát)");
var children = await db.MenuItems.AsNoTracking()
.Where(m => m.ParentKey == group.Key).Select(m => m.Key).ToListAsync();
children.Should().HaveCount(6, $"nhóm {n} đúng 6 leaf (WfView/List/Create/Pending/Approved/Deleted)");
}
// 8 nhãn nhóm phải KHÁC NHAU — copy-paste sót 1 dòng dictionary là 2 nhóm cùng tên trên sidebar.
var groupLabels = new List<string> { g1.Label };
foreach (var n in MenuKeys.KhkkGroupNumbers)
groupLabels.Add((await RowAsync(db, $"Khkk_G{n}"))!.Label);
groupLabels.Should().OnlyHaveUniqueItems("8 nhóm duyệt = 8 nhãn phân biệt trên sidebar");
}
// =====================================================================================
// T4 — 6 leaf nhóm 2..8 MIRROR Y nhóm 1 (nhãn + icon + Order). Đo DB↔DB: lấy row nhóm 1
// làm mốc thay vì bảng hardcode ⇒ đổi nhãn leaf mà quên đồng bộ 7 nhóm kia là ĐỎ.
// Chống vacuous: chốt trước rằng 6 nhãn nhóm 1 PHÂN BIỆT (nếu chúng trùng nhau hết
// thì phép so "khớp nhãn" đúng ở mọi thế giới).
// =====================================================================================
[Fact]
public async Task SeedMenuTree_NewGroupLeaves_MirrorGroup1_LabelIconOrder()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
await SeedMenuAsync(db);
var g1Leaves = new List<MenuItem>();
foreach (var key in Group1LeafKeys)
{
var row = await RowAsync(db, key);
row.Should().NotBeNull($"{key} (leaf nhóm 1, S159) phải còn — K4a không được đụng key cũ");
g1Leaves.Add(row!);
}
g1Leaves.Select(x => x.Label).Should().OnlyHaveUniqueItems(
"6 nhãn leaf nhóm 1 phải phân biệt — nếu trùng nhau, phép so mirror bên dưới đúng vô nghĩa");
for (var i = 0; i < MenuKeys.KhkkLeafActions.Length; i++)
{
var action = MenuKeys.KhkkLeafActions[i];
var mirror = g1Leaves[i];
mirror.Key.Should().Be($"Khkk_{action}",
"thứ tự KhkkLeafActions phải khớp thứ tự 6 leaf nhóm 1 — lệch thứ tự là so nhầm cặp");
foreach (var n in MenuKeys.KhkkGroupNumbers)
{
var leaf = await RowAsync(db, $"Khkk_G{n}_{action}");
leaf.Should().NotBeNull($"Khkk_G{n}_{action} phải có row");
leaf!.ParentKey.Should().Be($"Khkk_G{n}", "leaf treo dưới đúng nhóm của nó");
leaf.Label.Should().Be(mirror.Label, $"nhãn leaf {action} nhóm {n} phải Y nhóm 1 ({mirror.Label})");
leaf.Icon.Should().Be(mirror.Icon, $"icon leaf {action} nhóm {n} phải Y nhóm 1");
leaf.Order.Should().Be(mirror.Order, $"Order leaf {action} nhóm {n} phải Y nhóm 1 (1..6)");
}
}
}
// =====================================================================================
// T5 — labelBackfill: đổi NHÃN nhóm 1 mà KHÔNG thêm row (gotcha #11 — sửa chữ trong
// `tree.Add` là vô tác dụng trên DB đang chạy vì upsert chỉ re-set Order).
// ⭐ MA TRẬN cùng-ruột-khác-1-biến: bôi hỏng nhãn của CẢ HAI key trong cùng 1 lần,
// rồi seed lại — `Khkk_G1` (CÓ entry backfill) tự lành, `Khkk_G2` (KHÔNG có entry)
// GIỮ NGUYÊN chữ hỏng. Khác biệt duy nhất = sự có mặt trong `labelBackfill` ⇒ chứng
// chính cơ chế backfill làm việc, không phải upsert chính "tiện tay" ghi đè Label.
// =====================================================================================
[Fact]
public async Task SeedMenuTree_LabelBackfill_HealsGroup1Label_WithoutAddingRows_ControlKeyStaysDirty()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
await SeedMenuAsync(db);
var countBefore = await db.MenuItems.AsNoTracking().CountAsync();
(await RowAsync(db, Group1GroupKey))!.Label.Should().Be(Group1LabelAfterK4a,
"DB trống: nhãn A1 đến thẳng từ tree initializer");
// Bôi hỏng nhãn 2 key: G1 = nhãn skeleton S159 (ca THẬT trên prod), G2 = chuỗi lạ (đối chứng).
const string dirtyG2 = "ZZZ nhãn hỏng đối chứng";
var g1 = await db.MenuItems.FirstAsync(m => m.Key == Group1GroupKey);
var g2 = await db.MenuItems.FirstAsync(m => m.Key == "Khkk_G2");
g1.Label = Group1LabelSkeletonS159;
g2.Label = dirtyG2;
await db.SaveChangesAsync(CancellationToken.None);
// Sanity: mutation ĐÃ land (nếu không, "sau seed nhãn đúng" xanh vì chưa từng hỏng).
(await RowAsync(db, Group1GroupKey))!.Label.Should().Be(Group1LabelSkeletonS159);
(await RowAsync(db, "Khkk_G2"))!.Label.Should().Be(dirtyG2);
await SeedMenuAsync(db); // ⭐ lần 2 — labelBackfill chạy
(await RowAsync(db, Group1GroupKey))!.Label.Should().Be(Group1LabelAfterK4a,
"entry labelBackfill[\"Khkk_G1\"] phải kéo nhãn skeleton S159 về nhãn nhóm A1 trên DB đang chạy");
(await RowAsync(db, "Khkk_G2"))!.Label.Should().Be(dirtyG2,
"ĐỐI CHỨNG: Khkk_G2 KHÔNG có entry backfill ⇒ upsert chính chỉ re-set Order, KHÔNG ghi đè Label " +
"(gotcha #11). Đổi nhãn G2..G8 sau khi đã seed thì BẮT BUỘC thêm entry backfill.");
var countAfter = await db.MenuItems.AsNoTracking().CountAsync();
countAfter.Should().Be(countBefore, "đổi nhãn KHÔNG được sinh row mới (đổi key mới sinh row + mồ côi permission)");
}
// =====================================================================================
// T6 — SITE-2 (`SeedAllRolesReviewReadPermissionsAsync`): role thường có CanRead trên 49
// key mới (điểm điều hướng của sidebar). Cờ ghi = false vì nhánh non-Pe là read-only.
// ⭐ Chống vacuous bằng ĐỐI CHỨNG CÙNG LƯỢT: cùng role đó, key `Pe_DuyetNcc_List`
// phải có CanCreate=true (nhánh isPe) ⇒ "C/U/D=false trên Khkk" không phải vì seeder
// không chạy, mà vì seeder PHÂN BIỆT 2 họ key.
// Chạy SeedMenuTree trước = smoke FK: mọi key trong grant-set phải có menu-row thật.
// =====================================================================================
[Fact]
public async Task SeedAllRolesReviewRead_GrantsReadOnNew49KhkkKeys_ReadOnly_WithPeContrast()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
var admin = new Role { Id = Guid.NewGuid(), Name = AppRoles.Admin };
var normal = new Role { Id = Guid.NewGuid(), Name = AppRoles.Drafter };
(await rm.CreateAsync(admin)).Succeeded.Should().BeTrue();
(await rm.CreateAsync(normal)).Succeeded.Should().BeTrue();
await SeedMenuAsync(db);
await InvokePrivateSeedAsync(db, "SeedAllRolesReviewReadPermissionsAsync", rm);
var rows = (await db.Permissions.AsNoTracking()
.Where(p => p.RoleId == normal.Id).ToListAsync())
.ToDictionary(p => p.MenuKey);
// Mục được đề bài gọi tên (leaf MỚI) — nêu riêng để thông điệp lỗi nói đúng chỗ.
rows.Should().ContainKey("Khkk_G2_List", "leaf mới phải được site-2 phát quyền cho role thường");
rows["Khkk_G2_List"].CanRead.Should().BeTrue(
"menu hiện ⟺ CanRead=true (GetMyMenuTree lọc CanRead, KHÔNG admin-bypass cho role thường)");
foreach (var key in Expected49)
{
rows.Should().ContainKey(key, $"{key} phải có Permission row cho role thường (site-2 grant)");
var row = rows[key];
row.CanRead.Should().BeTrue($"{key}: role thường phải đọc được mục sidebar");
row.CanCreate.Should().BeFalse($"{key}: nhánh non-Pe là read-only (cờ GHI của KHKK do site-3 phát trên root)");
row.CanUpdate.Should().BeFalse($"{key}: read-only");
row.CanDelete.Should().BeFalse($"{key}: read-only");
}
// 7 key nhóm 1 vẫn được phát (K4a `.Concat` mở rộng, KHÔNG thay thế).
foreach (var key in Group1LeafKeys.Append(Group1GroupKey))
rows[key].CanRead.Should().BeTrue($"{key} (nhóm 1) phải giữ CanRead sau khi K4a nối thêm 49 key");
// ⭐ ĐỐI CHỨNG cùng lượt seed: họ Pe_* đi nhánh isPe ⇒ CanCreate=true.
var peList = MenuKeys.PurchaseEvaluationList("DuyetNcc");
rows[peList].CanCreate.Should().BeTrue(
"chứng seeder CÓ chạy và CÓ phân biệt 2 họ key — nếu không, mọi assert 'false' bên trên đúng vô nghĩa");
}
// =====================================================================================
// T7 — [finding D, gate K4a @S166] ⭐ BẤT-BIẾN AN NINH-DỮ-LIỆU: `RevokeTemporarilyHidden-
// ModulesAsync` KHÔNG được đụng họ `Khkk_*`.
// Đây là ca ĐÃ VỠ THẬT trên prod, không phải giả định: 2026-07-29 grant 38 key bằng
// SQL tay bị CHÍNH revoker này lật ở app-restart kế (Run #423 — 447/494 row rơi,
// 11/13 role về 0; gotcha #84). Thêm 1 prefix vào bộ lọc revoke = 49 row Khkk vừa cấp
// biến mất Ở LẦN RESTART SAU, im lặng, không test nào đỏ ⇒ Fact này bịt đúng lỗ đó.
// Khuôn 2-vế mirror `AdminOnlyModulesRevokeTests` (StillHidden ∧ Reopened).
//
// 🔴 BẪY VACUOUS ĐÃ CHỐNG (đọc từ mã `:2546-2551`): thiếu role Admin thì revoker
// RETURN SỚM (no-op) ⇒ mọi assert "Khkk còn nguyên" xanh vì revoker CHƯA TỪNG CHẠY.
// Nên test này (a) seed Admin, (b) có CONTROL DƯƠNG: cùng lượt đó, key `Off_*` và
// `Personal` của CHÍNH 2 role kia PHẢI bị hạ hết cờ. Control dương đỏ ⇔ phép đo hỏng.
// Đo trên 2 role non-Admin (khẳng định là "mọi role", 1 role không đủ nói) + kiểm
// Admin được loại trừ.
// =====================================================================================
[Fact]
public async Task Revoke_DoesNotTouch_KhkkGroups()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
// Admin BẮT BUỘC tồn tại, nếu không revoker return sớm và test thành vô nghĩa.
var admin = new Role { Id = Guid.NewGuid(), Name = AppRoles.Admin };
var role1 = new Role { Id = Guid.NewGuid(), Name = AppRoles.Drafter };
var role2 = new Role { Id = Guid.NewGuid(), Name = AppRoles.DeptManager };
foreach (var r in new[] { admin, role1, role2 })
(await rm.CreateAsync(r)).Succeeded.Should().BeTrue();
await SeedMenuAsync(db);
await InvokePrivateSeedAsync(db, "SeedAllRolesReviewReadPermissionsAsync", rm);
// CONTROL DƯƠNG: 2 key NẰM TRONG tập revoke (Off* + Personal) — site-2 không phát
// 2 key này nên cấp tay. Có menu-row thật từ SeedMenuTreeAsync ⇒ FK ok.
var revokedSetKeys = new[] { MenuKeys.OffDanhBa, MenuKeys.Personal };
foreach (var roleId in new[] { admin.Id, role1.Id, role2.Id })
foreach (var key in revokedSetKeys)
db.Permissions.Add(new Permission
{
RoleId = roleId, MenuKey = key,
CanRead = true, CanCreate = false, CanUpdate = false, CanDelete = false,
});
// Mô phỏng admin tick thêm cờ GHI cho 1 mục Khkk trên ma trận Phân quyền — revoker
// (nếu bắt nhầm `Khkk_*`) sẽ xoá luôn cờ này, không chỉ CanRead.
const string tickedKey = "Khkk_G5_Create";
var ticked = await db.Permissions.FirstAsync(p => p.RoleId == role1.Id && p.MenuKey == tickedKey);
ticked.CanCreate = true;
await db.SaveChangesAsync(CancellationToken.None);
await InvokePrivateSeedAsync(db, "RevokeTemporarilyHiddenModulesAsync", rm);
var after = (await db.Permissions.AsNoTracking().ToListAsync())
.ToDictionary(p => (p.RoleId, p.MenuKey));
// ── VẾ 1 (CONTROL DƯƠNG): revoker THẬT SỰ đã chạy và có hiệu lực trên non-Admin ──
foreach (var roleId in new[] { role1.Id, role2.Id })
foreach (var key in revokedSetKeys)
{
var row = after[(roleId, key)];
row.CanRead.Should().BeFalse(
$"{key} thuộc tập ẩn (Hrm*/Off*/Personal) ⇒ PHẢI bị hạ cờ. Vế này ĐỎ = revoker không chạy " +
"(vd Admin chưa seed) ⇒ mọi khẳng định 'Khkk còn nguyên' bên dưới vô giá trị");
row.CanCreate.Should().BeFalse();
row.CanUpdate.Should().BeFalse();
row.CanDelete.Should().BeFalse();
}
// Admin được loại trừ khỏi revoke (chứng bộ lọc `RoleId != adminRoleId` còn sống).
after[(admin.Id, MenuKeys.OffDanhBa)].CanRead.Should().BeTrue(
"Admin KHÔNG bị revoke — nếu vế này đỏ thì revoker đang quét quá tay, kết luận về Khkk cũng đáng ngờ");
// ── VẾ 2 (BẤT-BIẾN CẦN BẢO VỆ): 56 key họ Khkk GIỮ NGUYÊN quyền trên MỌI role ──
var allKhkkKeys = Expected49.Concat(Group1LeafKeys).Append(Group1GroupKey).ToArray();
allKhkkKeys.Should().HaveCount(56);
var survivors = 0;
foreach (var roleId in new[] { role1.Id, role2.Id })
foreach (var key in allKhkkKeys)
{
after.Should().ContainKey((roleId, key),
$"{key}: revoke chỉ HẠ CỜ, KHÔNG xoá row — mất row là hỏng nặng hơn cả gotcha #84");
after[(roleId, key)].CanRead.Should().BeTrue(
$"{key} KHÔNG thuộc Hrm*/Off*/Personal ⇒ revoker phải THA. Đỏ ở đây = đúng ca prod Run #423 " +
"(grant xong bị lật ở restart kế) tái diễn cho sidebar KHKK 8 nhóm");
survivors++;
}
// Nói bằng ĐƠN VỊ của sự cố thật (đếm row sống sót) — 56 key × 2 role non-Admin.
survivors.Should().Be(112, "toàn bộ row Khkk của 2 role non-Admin phải sống qua revoke");
after[(role1.Id, tickedKey)].CanCreate.Should().BeTrue(
$"{tickedKey}: cờ GHI admin tự tick trên ma trận Phân quyền cũng không được revoker chạm");
}
}