Compare commits
9 Commits
88e7ecf263
...
e202393256
| Author | SHA1 | Date | |
|---|---|---|---|
| e202393256 | |||
| fa999d8fd7 | |||
| d163f6d3d9 | |||
| 2023f417d0 | |||
| ce35f9066b | |||
| 80b8cf5113 | |||
| 284233f2dc | |||
| 13f7f82525 | |||
| ff4d4dc9d0 |
@ -19,12 +19,20 @@ chain:
|
||||
Á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)
|
||||
[x] K4a+K4b SHIPPED @S166 — commit 88e7ecf PUSHED (14 file +876/−59, gộp-CÓ-KHAI vì Layout dính 2 wave;
|
||||
gate-K4a PWF-4 + gate-K4b PWF-4M/5m 0-chặn, cả 2 artifact + disposition từng dòng). 49-key bộ-sinh
|
||||
All=113/Policies=452 (T2 khoá) · nhãn N2 "phá dỡ" · ?group= end-to-end (list+create+deleted lead-fix
|
||||
3-chỗ BE) · staticMap G1 group=1 · test +7 (T7 revoker-#84 probe-vi-phân) suite 621/0 · STATUS
|
||||
canonical 5-row đồng bộ. cicd-K4ab **PASS 7/7** (run #438 6m23s · 621 · seed 49-menu+637-perm
|
||||
tam-giác · A1 8+48 · A5 restart×2-THẬT PID-7020→220→6636 đứng-im + revoker-control 29/377 ·
|
||||
bundle ROTATE ×2 marker-differential · smoke+bind-400; treo kế thừa: bảng 0-phiếu = chân-lý-rỗng
|
||||
lọc-nhóm, đo lại khi K8 đẻ phiếu — sub-cicd-verify-k4ab.md)
|
||||
[ ] K5→K4c→K6→K7→K8-prep theo build-order B3 + vá-số spec-cụm3 (chi tiết PAUSE-3 mục (2): K5 groups
|
||||
cây 8-sub-folder + forward-provision lines[].contractId ∪ pe.contractId · K4c WfView lọc Code
|
||||
[+nợ gate-K4b: MAJOR-A test /deleted+group · MAJOR-C WfView đọc group] · 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 — 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
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
# 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]`
|
||||
- **S166 (08-01) `88e7ecf` K4a+K4b sidebar 8-nhóm × 6-leaf + nối `?group=` (0-mig) — PASS 7/7** (#438 6m23s · gate **621=45+576** · Mig GIỮ 71 · menu 151→**200** · perm 1584→**2221**). 🔑 **PHỦ 2 RUN 1 LƯỢT ⇒ delta KHÉP KÍN**: #437 F-1 = 614 (Infra +0) ⇒ trọn **+7** quy được về K4. 🔑 **MỐC PRE ĐỀ BÀI SAI, bắt bằng ĐO**: lead giao `143⇒192`, đo LIVE lúc còn `running` ra **151** → truy phạm-vi thay vì hô drift (K3 đẻ 8 row `AwV2_KhkkN1..N8`) ⇒ đúng là **200**. **Mốc PRE lão hoá sau MỖI deploy.** 🔑 **phân rã 49+588=637 khớp cơ-chế 2-site skip-existing** (588=49×12 role còn-lại) — KHÔNG suy được từ tổng ⇒ bit thật (tam-giác source ⟂ sqlcmd ⟂ Serilog). ⚠️ **PERM row ≠ policy** (1 row/cặp 4 cờ; "49×4=196" là POLICY). 🔑 **A5 restart ×2 THẬT** (PID 7020→220→6636): 8 chỉ số ĐỨNG IM + **soi ĐIỀU KIỆN log trong source TRƯỚC khi đọc im-lặng** (`if (added>0)`) ⇒ vắng dòng ⟺ 0 added; control #84 `Hrm*/Off*` giữ **29/377**. 🔴 **MARKER LEAD GỢI Ý BỊ BÁC BẰNG ĐO** (`Kết cấu…` có sẵn từ `7a903cf` ⇒ đo **1/1** = 0 bit) → thay bằng POS `/khkk/list?group=1` 4/4 element-wise · NEG `/khkk/list?view=deleted` 0/0 · 🔑 **DIFFERENTIAL admin 0 / eoffice 2** (bất-đối-xứng CỐ Ý ship + tự chứng comment-strip). 🔑 `?group=abc` no-token=**401 (authz chặn TRƯỚC bind)** → bearer re-probe **400 bind-error**. ⚠️ Treo: bảng **0 dòng** ⇒ chứng ống+bind KHÔNG chứng lọc-đúng-nhóm; `?group=999`→200. Evidence `runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md`. Tag `[run-438, phu-2-run, moc-PRE-sai, marker-0-bit, differential-marker]`
|
||||
|
||||
- **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 `15349e8` S164 K3 8 workflow KHKK-N1-8 (0-mig) — PASS 6/6**: #436, gate 614=45+569. 🔑 **dự-đoán-TRƯỚC-khi-đo** (seeder → 8wf×3step×11level + finalize 2+8=10 → khớp từng đồng + shape element-wise từng email + control ÂM OUTSIDE=2/OUTOFSET=0). 🔑 **FROZEN = hệ-quả BẮT BUỘC** (diff toàn `//` → Vite strip); ❌ marker `earlyFinalizeLocked` LOẠI vì **tên biến local bị minifier đổi = 0 bit**. ⚠️ tao chết #53 ×2. verbatim → `archive/2026-07.md`)_
|
||||
|
||||
- _(**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`)_
|
||||
- _(**07-31 `0779f2d` S164 K2 +Mig 71 — PARTIAL 5/6**: #435, gate 609. 🔴 **ACCEPTANCE RƠI VÀO CHÂN-LÝ-RỖNG** — bảng 0 dòng cả PRE/POST ⇒ `0==0`/`NULL==NULL` "xanh" mà **0 bit**, backfill hụt trông Y HỆT ⇒ CẤM đọc thành PASS. 🔑 Đối-sách KHÔNG-phụ-thuộc-số-dòng: `INFORMATION_SCHEMA` · **LIỆT KÊ TRỌN HỌ 14 index** (vắng-mặt suy từ danh-sách-đầy-đủ = ĐO ĐƯỢC) · **wire-proof bind-error 400**. 🔑 map PID→pool bằng `appcmd list wp`. verbatim → `archive/2026-07.md`)_
|
||||
|
||||
- _(**07-30 `bfc7b79` S162 cây-4-folder-GĐ FE-ONLY — PASS 5/5**: run #432 5m35s, gate 590. 🔴 **LUẬT CẤM `HEAD~1`** — Stop-hook WAL auto-commit giữa lúc verify làm `HEAD~1` đổi nghĩa ⇒ tao kết SAI "marker vô giá trị"; đo lại bằng SHA tuyệt đối `X^..X` thì 0 hit ⇒ marker hợp lệ. 🔑 **ĐẾM TRỌN HỌ bằng 1 REGEX** `pipeline-[a-z-]{0,24}index` ra đúng 3 phần tử khớp source element-wise (vừa vế DƯƠNG vừa tự chứng regex không quét bừa). ⚠️ suýt FAIL-OAN: nhãn cũ 8/app sống hợp lệ — control ÂM phải ĐỘC-QUYỀN. verbatim → `runs/2026-07-30-S162-cay-4-folder-gd/sub-cicd-monitor-2.md`)_
|
||||
- _(**07-31 `50e6d8c` S164 K1 +Mig 70 — PASS 6/6**: #434, gate 598, `sys.tables` 96→97. 🔑 **TAM-GIÁC 3 NGUỒN** (source-parse ⟂ sqlcmd ⟂ **API materialize qua EF** — nguồn 3 mạnh nhất vì đi trọn Domain→Infra→Api). ⚠️ **0-hit là DƯƠNG-GIẢ tới khi soi format**. ⚠️ **hash CI ≠ hash local build**. verbatim → `archive/2026-07.md`)_
|
||||
|
||||
- _(**07-30 `bfc7b79` S162 FE-ONLY — PASS 5/5**: #432, gate 590. 🔴 **LUẬT CẤM `HEAD~1`** (Stop-hook WAL auto-commit đổi nghĩa ⇒ kết SAI; đo bằng SHA tuyệt đối `X^..X`). 🔑 **ĐẾM TRỌN HỌ bằng 1 REGEX**. ⚠️ suýt FAIL-OAN: **control ÂM phải ĐỘC-QUYỀN**. verbatim → `runs/2026-07-30-S162-cay-4-folder-gd/`)_
|
||||
|
||||
- _(**#427 S161 W1 KHKK `a8027c0` +Mig 69 `AddContractSigningPlans` — PASS 6/6**: gate 566, 5m40s; `sys.tables` **89→96** (+7 khớp 7 CreateTable), MenuItems 142 GIỮ. 🔑 **khuôn ĐO-CỜ không đếm ROW** (row tồn tại sẵn ⇒ đếm row = 0-bit; đo PRE/POST **cờ** 7 key 13/0/0/0→13/13/13/2) + 🔑 **control ÂM NỘI-HỌ: `Khkk_G1` bị seeder CỐ Ý bỏ khỏi tập** ⇒ phải đứng yên, và nó đứng yên ⇒ chứng 'đúng NGƯỜI, đúng TẬP'. Đọc seeder **dự đoán `0 added + 91 upgraded` TRƯỚC khi đo → log prod khớp từng đồng**. verbatim → `archive/2026-07.md` substring:"a8027c0` W1 KHKK")_
|
||||
|
||||
- _(**#426 S159 đợt-5 `ea962b1` menu 6-leaf + `GET /contracts/deleted` — PASS 9/9**: gate 562. 🔑 **LUẬT gốc control-ÂM-phải-ĐỘC-QUYỀN** (cứu false-fail: chuỗi brief bảo phải vắng vẫn sống hợp lệ ở `aria-label` ⇒ grep repo-wide TRƯỚC; chuỗi dùng-chung chỉ làm marker DƯƠNG). 🔑 **SEEDER-INSERT delta khớp TOÁN**: MenuItems **119→142 = +23 = 7+7+7+2**. Endpoint mới 401 **kèm route bịa 404** ⇒ 401 CÓ NGHĨA. ⚠️ bẫy phạm-vi query: `Ct_*` đẻ thêm 21 key ⇒ `LEFT(MenuKey,3)` không so được với đợt trước. verbatim → `archive/2026-07.md` substring:"#426 S159 đợt-5")_
|
||||
|
||||
- _(**S159 arc đợt-1→4 (#422 · #423 · #424 · #425) + S157 #419** — gate 562. 🔴 **#423 PARTIAL 8/9 = phát hiện lớn nhất arc**: `RevokeTemporarilyHiddenModulesAsync` UNGATED lật 447/494 row grant SQL tay; tao sót vì **đo TRƯỚC recycle** → đã nâng thành luật 3-CHÂN-KIỀNG ở 'Recurring bug patterns'; đóng ở #424 (guard 2-vế, 494/494 sống qua restart ×3). 🔑 #425 comment-strip **dự-đoán-trước-rồi-mới-đo**; FROZEN vẫn lành tính nếu LM in-window (#69 mode-B). 🔑 #422 **marker PHÂN-BIỆT-PHIÊN-BẢN** (cặp chữ cũ/mới ⇒ chứng prod chạy bản ĐÃ-FIX); ⚠️ **LỆCH-số phải truy PHẠM-VI-QUERY trước khi hô drift**. 🔑 #419 byte-marker **count = SỐ FILE trong diff**. ⚠️ **ĐÍNH CHÍNH: `#422`/`#425` CHƯA TỪNG vào archive** (mất lớp chưng-cất, KHÔNG mất nguyên-liệu — còn ở `runs/2026-07-29-S159-tong-quan-pipeline-menu/cicd-verify-{b5799fc,a2bbcb9}.md`); **bài: stub tự khai "đã archive" là CLAIM, phải đo mới được viết**. verbatim (#423/#424/#419) → `archive/2026-07.md`)_
|
||||
|
||||
- _(**#416 S155 `b1bae77` Workflow duyệt-TẠI-CHỖ +Mig 68 APPLIED-PROD 6/6** [gate 551; FLAG 'mig rớt khỏi commit' loại-trừ 3 lớp — mạnh nhất `GET /approval-workflows-v2` trả cột ×54 **materialize qua EF từ DB thật**] · **#407 S135 `4510cea` mã-dự-án normalize 5/5** [gate 532] · **#399 S134 `6664a61` PE lũy-kế 0-mig 5/5** [gate 528; 🔑 `ct=text/html` discriminator SẠCH HƠN size-919B vì shell-size drift] · **#398 S133 `d4fbced` +Mig 67 budget-freeze 5/5** [gate 520; backfill 18/18; python text=True crash cp1252 → dùng bytes] · verbatim → `archive/2026-07.md` via `_INDEX`)_
|
||||
- _(**Arc S155→S161 (#416 · #419 · #422-#427)** — gate 551→566. 🔑 #427 **ĐO-CỜ không đếm ROW** + **control ÂM NỘI-HỌ** (`Khkk_G1` bị seeder CỐ Ý bỏ khỏi tập ⇒ phải đứng yên, và nó đứng yên). 🔑 #426 delta khớp TOÁN +23=7+7+7+2; 401 mới **kèm route bịa 404**. 🔴 **#423 PARTIAL 8/9** revoker UNGATED lật 447/494 row, sót vì **đo TRƯỚC recycle** → luật 3-CHÂN-KIỀNG. 🔑 #422 marker **PHÂN-BIỆT-PHIÊN-BẢN**; #419 **count = SỐ FILE trong diff**. verbatim → `archive/2026-07.md` via `_INDEX`)_
|
||||
|
||||
> **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).
|
||||
@ -25,13 +20,13 @@
|
||||
|
||||
## 🎯 Role baseline
|
||||
|
||||
Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, verifies test gate + deploy ship + prod health. Tools: Read, Grep, Glob, Bash, WebFetch + 5 RAG MCP. Output: PASS/FAIL + evidence <500 words. Skills: `iis-deploy-runbook` + `dependency-audit-erp` + `ef-core-migration`. Spawn ~150K — trade-off catch fail tự động.
|
||||
Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Poll Gitea Actions API, verify test-gate + deploy-ship + prod health. Output PASS/FAIL + evidence <500 words. Skills: `iis-deploy-runbook` · `dependency-audit-erp` · `ef-core-migration`.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ cicd-lane return-garble tracker (#53)
|
||||
|
||||
cicd-lane đôi khi trả nhầm slice transcript thay verdict — **tao TÁI PHẠM dai nhất roster** (×5/6 qua S114; S117/S118 ×3; all-agents ×66-cận-dưới → [[feedback_agent_return_garble_recover]]). ⚠️ Từng **tự dán nhãn 'CLEAN-return' SAI** ⇒ **KHÔNG tự phong return sạch**. 🔒 **Đối-sách đã chứng minh (S158, lần 5): ghi-đĩa TRONG LÚC LÀM** — prompt đúng KHÔNG chặn được, artifact mới chặn (vai có-ép mất **0 B**, vai không-ép mất TOÀN BỘ). ⇒ file evidence trong run-folder là SẢN PHẨM CHÍNH, ghi từng mục ngay khi có bằng chứng, kết bằng `END … VERDICT=`; return chỉ là tóm tắt.
|
||||
cicd-lane trả nhầm slice transcript thay verdict — **tao TÁI PHẠM dai nhất roster** (×5/6 qua S114; S117/S118 ×3; → [[feedback_agent_return_garble_recover]]). ⚠️ Từng **tự dán nhãn 'CLEAN-return' SAI** ⇒ **KHÔNG tự phong return sạch**. 🔒 **Đối-sách (S158, lần 5): ghi-đĩa TRONG LÚC LÀM** — prompt đúng KHÔNG chặn được, artifact mới chặn (vai có-ép mất **0 B**, vai không-ép mất TOÀN BỘ). ⇒ file trong run-folder là SẢN PHẨM CHÍNH, ghi từng mục ngay khi có bằng chứng, kết bằng `END … VERDICT=`; return chỉ là tóm tắt.
|
||||
|
||||
---
|
||||
|
||||
@ -39,7 +34,7 @@ cicd-lane đôi khi trả nhầm slice transcript thay verdict — **tao TÁI PH
|
||||
|
||||
- **#39 act_runner github.com TCP timeout** — run hang "Set up job" 21s. Log `dial tcp github.com:443 i/o timeout`. Fix: manual checkout bypass hardcoded `.gitea/workflows/deploy.yml`. KHÔNG revert.
|
||||
- **#40 npm cache `tsc not found`** — `build_fe_admin` fail post `cache: npm`. DISABLED rolled back `a21790d`. KHÔNG re-enable.
|
||||
- **#41 paths-ignore docs-only skip** — code commit không trigger CI? Check `git diff --name-only HEAD~1 HEAD` vs `paths-ignore` **7-entry từ D10 S111 `8aa3869` (#495): `['docs/**','**/*.md','.claude/skills/**','.claude/agent-memory/**','.claude/workflows/runs/**','.gitignore','scripts/**.md']`**. Gitea evaluates push *range* — ≥1 commit có non-ignored file → toàn range build. ⚠️ **AS-6 BASELINE SHIFT: từ #495, memory-push chứa `memory-budget.json`/agent-memory-file/run-trace HẾT trigger CI (pattern #490/#492 DEAD).**
|
||||
- **#41 paths-ignore docs-only skip** — code commit không trigger CI? Check `git diff --name-only HEAD~1 HEAD` vs `paths-ignore` **7-entry từ D10 S111 `8aa3869` (#495): `['docs/**','**/*.md','.claude/skills/**','.claude/agent-memory/**','.claude/workflows/runs/**','.gitignore','scripts/**.md']`**. Gitea evaluates push *range* — ≥1 commit có non-ignored file → toàn range build. ⚠️ **AS-6: từ #495 memory-push/run-trace HẾT trigger CI.**
|
||||
- **#25 IIS WebSocket** — `notification-hub/negotiate` 401/404 prod. Fix: WebSocket module enable `web.config` site api (skill `iis-deploy-runbook`).
|
||||
- **#48 SQLite tie-break** — `OrderByDescending(CreatedAt).First()` pick wrong khi 2+ `.Add()` cùng frozen-clock. Fix: discriminator filter BEFORE OrderBy.
|
||||
- **Bundle hash unchanged = ship FAIL** — push+action success nhưng prod không đổi. Verify via INDEX.HTML ref (`curl -s https://admin.solutions.com.vn/ | grep -oE '/assets/index-[A-Za-z0-9_-]+\.(js|css)'`), NOT by GETting a hash-named asset directly. ⚠️ verify MUST sau status=success (Run #242 false-positive: check khi "running" → stale hash).
|
||||
@ -55,10 +50,10 @@ cicd-lane đôi khi trả nhầm slice transcript thay verdict — **tao TÁI PH
|
||||
- **Stage 0 RAG infra:** `Get-Service Qdrant` Running + `http://localhost:6333/healthz`. Collection `proj_solution_erp`.
|
||||
- **Stage 1 Push+filter:** `git log -1 --format='%H %s'` + `git log origin/main..HEAD` empty + diff vs paths-ignore (docs-only → SKIPPED-DOCS return).
|
||||
- **Stage 2 Gitea poll** (max 10 iter × 60s): API `.../actions/tasks?limit=5` (NOT `/runs` 404). Match `head_sha` (NOT run_number — cancelled runs shift numbering). ⚠️ `updated_at` stale ~2min (gotcha #46) → cross-check VPS mtime. Foreground-sleep BLOCKED Windows → Monitor-style busy-wait-curl-spin ~30s/iter.
|
||||
- **Stage 3 Test gate:** baseline **590 PASS** (45 Domain + 545 Infra; run #432 `bfc7b79` 07-30; climb 590/566/562/551/532/528/520/509/505/501/495/486/458/440/421). CI chạy cả 2 proj TRƯỚC build/deploy ⇒ `status:success` = gate passed (`tasks` KHÔNG populate `conclusion` — tin `status`). Local grep undercount (Theory/InlineData). Phase 9 UAT skip OK.
|
||||
- **Stage 3 Test gate:** baseline **621 PASS** (45 Domain + 576 Infra; run #438 `88e7ecf` 08-01; climb 621/614/609/598/590/566/562/551/532/528/520/509/458/440). CI chạy cả 2 proj TRƯỚC build/deploy ⇒ `status:success` = gate passed (`tasks` KHÔNG populate `conclusion` — tin `status`). Local grep undercount (Theory/InlineData). Phase 9 UAT skip OK.
|
||||
- **Stage 4 Post-deploy** (if SUCCESS): auth login bearer (admin + nv.test gotcha #44; token=`accessToken` route `/api/auth/login`) → 3-5 endpoint smoke 2XX (incl new — NEW-endpoint bodyless-POST returns **411 IIS Length-Required pre-auth NOT 404**; re-probe WITH body → 401 confirms wired) → FE bundle hash 2 app (per #69 = real-size-vs-fake not hash-delta) → SignalR negotiate (gotcha #25) → EF mig prod==repo.
|
||||
- **Stage 4.6 (S29 CRITICAL):** sqlcmd seed sample verify post-deploy (NOT chỉ schema). Seed=runtime-row-insert (no history/tables advance) → verify via DB-query (e.g. Permissions count), NOT bundle-frozen-alone.
|
||||
- Discovery: record enum cần input SỐ (SOL không có `JsonStringEnumConverter`). sqlcmd ssh Windows-auth `\\\\SQLEXPRESS` 4-backslash. INFRASTRUCTURE seed ungated / DEMO seed gated → gotcha #51.
|
||||
- **Stage 4.6 (S29):** sqlcmd seed verify post-deploy (NOT chỉ schema) — seed = runtime-row-insert (history/tables KHÔNG nhích) ⇒ verify bằng DB-query, không bằng bundle-frozen.
|
||||
- enum cần input SỐ (no `JsonStringEnumConverter`). INFRASTRUCTURE seed ungated / DEMO seed gated → #51.
|
||||
- **Stage 5 Report** PASS/FAIL + evidence + MEMORY update.
|
||||
|
||||
---
|
||||
@ -72,10 +67,11 @@ cicd-lane đôi khi trả nhầm slice transcript thay verdict — **tao TÁI PH
|
||||
|
||||
- **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore 7-entry (D10 S111 #495): `['docs/**','**/*.md','.claude/skills/**','.claude/agent-memory/**','.claude/workflows/runs/**','.gitignore','scripts/**.md']`. Anon API works (public repo) when GITEA_TOKEN absent.
|
||||
- **Prod:** api/admin/eoffice `.solutions.com.vn` · SSH `ssh vietreport-vps` (Administrator, id_ed25519) · IIS phys paths: API `C:\inetpub\solution-erp\api` · admin `\fe-admin` · user `\fe-user`. DB `.\SQLEXPRESS`/`SolutionErp`/`vrapp` SQL-auth. **Conn key = `ConnectionStrings.Default` (NOT `DefaultConnection`)**. DB pw = `buKL3TGBkD0wDDbYVw65QeX9` (User Id=`vrapp`; ⚠️ Password field has NO `vrapp/` prefix — S89 login-failed when prefix included; read verbatim from prod `appsettings.Production.json` ConnectionStrings.Default when `$env:PROD_DB_PASSWORD` empty).
|
||||
- **SSH→PS quoting (S42):** nested bash→ssh→powershell mangles `$var`/`\"` → use `iconv UTF-16LE | base64` → `powershell -EncodedCommand $B64`; OR write ps1-file + `powershell -File $(cygpath -w)`. sqlcmd over SSH direct `-W -h -1` pw-inline. sys-catalog string-concat → `COLLATE DATABASE_DEFAULT` (collation conflict).
|
||||
- **Tests baseline:** **590 PASS** (45 Domain + 545 Infra · 0 fail/skip; run #432 `bfc7b79` 07-30 đo LIVE ở web-UI log). 🔑 **Số cụ thể lấy ở web-UI log `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, KHÔNG có `/api/v1/` — api/v1 runs+tasks logs đều 404)** → mỗi project 1 dòng `Passed! - Failed: 0, Passed: N, Skipped: 0`; đối chiếu TÁCH-PHẦN (45/545) chứ đừng chỉ khớp tổng. Delta test mới = so run trước.
|
||||
- **Mig latest repo: Mig 69 `AddContractSigningPlans`** (`20260729122015`, S161 — 7 CreateTable, `sys.tables` 89→**96**, applied-prod #427). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/` (đếm `.cs` excl Designer/Snapshot). Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=96** (sau Mig 69; 89 trước đó) — AddColumn/data-only KHÔNG đổi số ⟹ no-new-table commit giữ nguyên, đừng FAIL. **NO-Mig proof = history NOT-advance + empty `git diff <base>..<head> -- '*Migrations*'`.** Bảng mig đầy đủ → skill `ef-core-migration` + `docs/STATUS.md`.
|
||||
- **Bundle hash live:** ship-baseline = run **#432** (07-30 `bfc7b79`) admin js `DXoYDy7R`/css `BZ5F-g1w` · eoffice js `o8KDluD1`/css `yNZDmVNz` (cả 4 rotate, LM in-window). ⚠️ **SNAPSHOT-per-run, KHÔNG phải mốc ổn định** (#69 TWO-MODE) → LUÔN re-snapshot LIVE sau status=success; ship-proof = LM-in-window + real-size + marker, KHÔNG phải hash-delta. ⚠️ id ≠ run_number (key theo head_sha).
|
||||
- **SSH→PS quoting (S42):** bash→ssh→powershell nuốt `$var`/`\"` ⇒ viết `.sql`/`.ps1` rồi `scp` + `sqlcmd -i` / `powershell -File` (đừng nhét inline). sqlcmd `-W -h -1` pw-inline. sys-catalog concat → `COLLATE DATABASE_DEFAULT`.
|
||||
- **Tests baseline:** **621 PASS** (45 Domain + 576 Infra · 0 fail/skip; run #438 `88e7ecf` 08-01 đo LIVE ở web-UI log). 🔑 **Số cụ thể lấy ở web-UI log `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, KHÔNG có `/api/v1/` — api/v1 runs+tasks logs đều 404)** → mỗi project 1 dòng `Passed! - Failed: 0, Passed: N, Skipped: 0`; đối chiếu TÁCH-PHẦN (45/576) chứ đừng chỉ khớp tổng. Delta test mới = so run trước.
|
||||
- **Mig latest repo: Mig 71 `AddKhkkApprovalGroupCatalogAndFinalizeRuntime`** (`20260731085624`, S164 K2 — AddColumn+index-swap, 0 CreateTable; applied-prod #435, GIỮ qua #436-#438). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/` (đếm `.cs` excl Designer/Snapshot). Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=97** (sau Mig 70; 96 trước đó) — AddColumn/data-only KHÔNG đổi số ⟹ no-new-table commit giữ nguyên, đừng FAIL. **NO-Mig proof = history NOT-advance + empty `git diff <base>..<head> -- '*Migrations*'`.** Bảng mig đầy đủ → skill `ef-core-migration`.
|
||||
- **Bundle hash live:** baseline #438 (08-01) admin `qOcoitOr`/`Cf3iCOOv` · eoffice `D9J3BbcA`/`DX1FRk5n`. ⚠️ **SNAPSHOT-per-run, KHÔNG phải mốc ổn định** (#69) → LUÔN re-snapshot LIVE sau status=success; ship-proof = LM-in-window + real-size + marker, KHÔNG phải hash-delta. ⚠️ id ≠ run_number (key theo head_sha).
|
||||
- **Prod DB anchor (POST #438, đo 08-01):** `MenuItems`=**200** · `Permissions`=**2221** · `Roles`=**13** · họ `Khkk%` = 56 menu / 728 perm (=56×13, CanRead=1 hết) · control revoker `Hrm*/Off*` CanRead=1 = **29**/377. PK `MenuItems` = **`[Key]`** (KHÔNG `MenuKey`); `Permissions(Id,RoleId,**MenuKey**,CanRead,CanCreate,CanUpdate,CanDelete)` = 1 row/cặp.
|
||||
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
|
||||
- **API Serilog:** sink THẬT `C:\inetpub\solution-erp\api\logs\solution-erp-YYYYMMDD.log` (skill-runbook `…\Logs\log-*.txt` STALE). Level=Information (seed/revoke lines CÓ ghi). ⚠️ deploy xoá luôn `logs/` ⇒ chỉ còn từ deploy cuối. Grep revoke-line + seeder-line cùng-giây để audit seed-race. *(S162: bản cũ dòng này chứa byte BEL 0x07 do `\a` bị nuốt — đã sửa.)*
|
||||
|
||||
@ -85,23 +81,24 @@ Node CI `20.x` (`feedback_node_cicd`) · MediatR `12.4.1` (gotcha #1, flag `Vers
|
||||
---
|
||||
|
||||
## 🎯 Per-NV admin opt-in wire — 10-point checklist (cumulative S22→S23)
|
||||
Cross-ref `feedback_per_nv_permission_scope` (10 điểm đầy đủ ở đó). 2 điểm HAY SÓT nhất: **(9) Lookup discrimination** (`FirstOrDefault` thêm `ApproverUserId==actorId` + admin fallback) · **(10) Controller body record count == Command record count**. Miss 9-10 ⇒ bug prod im-lặng 2-3 ngày. Sau refactor OR-of-N: `grep -n "FirstOrDefault.*Order.*==" *.cs`.
|
||||
Cross-ref `feedback_per_nv_permission_scope` (10 điểm đầy đủ ở đó). 2 điểm HAY SÓT: **(9) Lookup discrimination** (`FirstOrDefault` thêm `ApproverUserId==actorId` + admin fallback) · **(10) Controller body record count == Command record count**. Miss ⇒ bug prod im-lặng 2-3 ngày.
|
||||
|
||||
## 📊 Run stats baseline
|
||||
BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code / 0s docs-only**. >5min → escalate. Recent runs all ~4m40-5m.
|
||||
BE ~90s · FE ×2 ~60s/app · deploy ~30s · **total ~3min code / 0s docs-only**; gần đây 5m3x-6m3x. >8min → escalate.
|
||||
|
||||
---
|
||||
|
||||
## 📅 Recent runs (compressed — verbatim → archive via `_INDEX.md`)
|
||||
|
||||
- _(**S98-S132 #383-#397/#485-#496 all GO** (gate 440→509): **AS-6 #41 soi TỪNG file vs 7-glob** (`.ps1`/`.json` thoát `scripts/**.md` ⇒ vẫn trigger CI) · `conclusion:None` dù success ⇒ tin `status` · **411-refine** (endpoint MỚI bodyless-POST = 411 IIS pre-auth; re-probe CÓ body → 401/415 = đã wire) · **LM mới hơn cửa-sổ đã verify = detector 'có run bỏ sót'** · Mig 63-67 applied-prod · verbatim → `archive/2026-07.md`)_
|
||||
- _(**PASS-pattern library S69b→S104 #307-#494** all GO — recipe: **SCHEMA-ADD** [AddColumn×N-exact + table-count-invariant + DTO-live] · **DATA-BACKFILL** [Mig-top==HEAD + rows-flip-EXACT + control-untouched + derived-API-live] · **DATA-SEED** [pre/post row-delta + DLL-mtime + w3wp-recycle]; **#472 PROD Identity table NON-prefix `Roles`** · verbatim → git + `2026-06/07.md`)_
|
||||
- _(**S98-S132 #383-#397 + PASS-pattern library S69b→S104 #307-#494** all GO (gate 440→509): **AS-6 #41 soi TỪNG file vs 7-glob** · `conclusion:None` dù success ⇒ tin `status` · **411-refine** (endpoint MỚI bodyless-POST = 411 IIS pre-auth; re-probe CÓ body → 401/415 = đã wire) · **LM mới hơn cửa-sổ đã verify = detector 'có run bỏ sót'** · recipe SCHEMA-ADD / DATA-BACKFILL / DATA-SEED · **#472 PROD Identity table NON-prefix `Roles`** · verbatim → `archive/2026-07.md` + `2026-06.md`)_
|
||||
|
||||
- **Older (S83 #325 ← S29 #232) → git `d2f52ba` + `archive/2026-06.md`** (incl #291 06-16 FAIL [gotcha #65]); pre-S38 → `archive/2026-05-*`.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Curate trigger
|
||||
- >~30KB → archive recent runs → L2 `archive/<period>.md` (byte-exact append) + `_INDEX.md` substring pointer. Dup failure patterns → merge. Stale >3mo → remove.
|
||||
- **Last curate: 2026-07-30 S162 (self #432-run: 26.9KB→<17.1KB — #427+#426 verbatim→`archive/2026-07.md` moved-not-cut; #422/#423/#424/#425/#419 merged-stub; baselines gate 562→590 + bundle + Mig 69). ⚠️ **0-BYTE NEAR-MISS**: `io.open(mem,'w')` TRUNCATE trước khi `UnicodeEncodeError` (emoji viết bằng cặp surrogate `\ud83d\udd34`) ⇒ file về 0 B; cứu bằng `git checkout HEAD --`. **LUẬT: script sửa MEMORY phải build chuỗi XONG → ghi temp → `os.replace`, và verify byte>0 ngay sau.**
|
||||
- **Last curate: 2026-08-01 S166 (self #438-run: 27.3KB→~16.5KB — K2/K3/K4ab verbatim→`archive/2026-07.md` moved-not-cut + 3 `_INDEX` pointer; #416/#419/#422-#427 gộp 1 stub arc; baselines gate 621 + bundle #438 + Mig 71 + prod-DB-anchor MỚI). ⚠️ giữ luật: script sửa MEMORY phải build chuỗi XONG → temp → `os.replace` → verify byte>0.
|
||||
- **Prev:** S162 07-30 (26.9→17) · S118 · S111 20.5→17 · S107 · S104 · S98 (`2026-07.md` NEW) · S80 38.8→16 · S70 65.2→23.2 · S66 · S40.
|
||||
- **Prev:** S118 07-14 (#387/#384/#383/#496/#495→archive) · S111 20.5→17 · S107 20.4→16.9 · S104 20.5→17.9 · S98 19.9→17.06 (`2026-07.md` NEW) · S80 38.8→16 · S70 65.2→23.2 · S66 · S40.
|
||||
|
||||
|
||||
@ -76,3 +76,12 @@
|
||||
|
||||
- **07-30 verify `bfc7b79` S162 cây-4-folder-GĐ (FE-ONLY, 17 file, 12 FE = 6+6 đối xứng ×2 app):** ✅ **PASS 5/5** — Run **#432** (id=545) success **5m35s**; gate **590 = 45D+545I** (0 fail/skip). 0 mig/0 BE ⇒ không chân EF. Bundle **4/4 ROTATE** (admin js `BBQKvUMa`→`DXoYDy7R` · user js `BV5--mPf`→`o8KDluD1`, css cả 2 đổi); `index.html` LM 10:09:07 in-window + `Cache-Control:no-cache`. 🔴 **BẪY MỚI — MOVING-TARGET `HEAD~1`:** phiên có Stop-hook WAL **tự commit giữa lúc verify** ⇒ `HEAD~1` đổi nghĩa từ 'cha commit đích' thành 'chính commit đích' trong ~6 phút. Tao đã kết SAI '2 marker có sẵn ở parent ⇒ vô giá trị'; đo lại bằng SHA tuyệt đối `8eba137` thì **cả 2 = 0 hit** ⇒ marker hợp lệ. **LUẬT: verify commit X luôn neo `X^..X` bằng SHA, CẤM dùng `HEAD~1`** (mọi repo có hook auto-commit). 🔑 **Khuôn MỚI 'ĐẾM TRỌN HỌ bằng 1 REGEX'**: `pipeline-[a-z-]{0,24}index` quét bundle ra **đúng 3 phần tử** `{contract×2, khkk×1, pe×1}` khớp source element-wise ⇒ vừa là vế DƯƠNG, vừa tự chứng regex không quét bừa (mạnh hơn đếm chuỗi rời). Dự-đoán-trước-khi-đo khớp từng đồng: `contract-index`=2 (= số FILE trong diff) · VN `Kế hoạch Hợp đồng`=**1** (source 3 − **2 comment**) · raw-UTF8 không escape. ⚠️ **suýt FAIL OAN lần nữa:** `Kế hoạch ký kết HĐ` (nhãn strip cũ) = **8/app** — KHÔNG dùng làm control ÂM được vì synthesis ghi rõ nhãn folder GĐ2 **cố ý khác** nhãn strip; 1-mới + 8-cũ cùng sống = chứng phân-biệt-phiên-bản. Control ÂM: chuỗi-BỊA `pipeline-hardcopy-index`=0 (source cũng 0) + hash BỊA & **hash CŨ cả 2 app** đều `ct=text/html` **876B/900B trùng khít size index.html** ⇒ bundle cũ biến mất = ghi đè thật. Smoke 4×200 + bare `/health` **404** (control). WAL-commit `a07b942` (3 file `.md`) bị `paths-ignore` nuốt ⇒ không đẻ run rác. Evidence: `runs/2026-07-30-S162-cay-4-folder-gd/sub-cicd-monitor-2.md`. Tag `[run-432, pass-5-5, CAM-HEAD~1-neo-SHA, dem-tron-ho-1-regex, control-am-DOC-QUYEN-lan-2, gate-590, fe-only]`
|
||||
|
||||
|
||||
|
||||
### [moved from L1 @S166 curate 2026-08-01] verbatim run entries
|
||||
|
||||
- **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]`
|
||||
|
||||
- **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]`
|
||||
|
||||
- **S166 (08-01) verify `88e7ecf` K4a+K4b sidebar 8-nhóm × 6-leaf + nối `?group=` (33 file: 4 BE + 8 FE + 1 test + 20 ignored) — PASS 7/7 `[phủ LUÔN #437 F-1 `7a903cf` chưa verify riêng]`:** Run **#438** success 6m23s (#437 6m22s; chuỗi #436→#437→#438 liền mạch = control neo 2 tầng). Gate **621 = 45D + 576I**; **F-1 = 614 (Infra GIỮ 569, +0)** ⇒ **trọn +7 thuộc K4** — 🔑 **verify 2-run một lượt cho chuỗi delta KHÉP KÍN, quy được test mới về đúng commit** (1 run lẻ không làm được). Mig GIỮ 71. 🔑 **BẮT MỐC PRE ĐỀ BÀI SAI BẰNG ĐO**: lead giao `MENU_TOTAL PRE=143 ⇒ 192`; đo LIVE lúc #438 còn `running` ra **151** → truy phạm-vi thay vì hô drift: K3 đã đẻ 8 row `AwV2_KhkkN1..N8` (liệt kê đủ tên, `AwV2_ALL=10`=2 cũ+8) ⇒ 143+8=151, **kỳ vọng đúng = 200**; đo POST = **200**. ⇒ mốc PRE trong spec luôn phải re-đo, số cũ lão hoá sau MỖI deploy. 🔑 **TAM-GIÁC 3 NGUỒN cho seed**: source-parse (`KhkkGroupNumbers[2..8]×(1+6)`=49; ×13 role=637) ⟂ sqlcmd (menu 151→200, perm 1584→**2221**) ⟂ **Serilog** (`Seeded 49 menu items` + `Backfilled 1 menu labels` + `Seeded 49 admin permissions` + `all-roles: 588 added + 0 upgraded`). 🔑 **phân rã 49+588=637 khớp cơ-chế 2-site skip-existing** (588=49×12 role còn-lại) — con số KHÔNG suy được từ tổng ⇒ bit thật. ⚠️ **PERM row ≠ policy**: `Permissions(RoleId,MenuKey,4 cờ)` = **1 ROW**/cặp; "49×4=196" của đề bài là POLICY authz, cấm cộng vào PERM_TOTAL. 🔑 **A5 restart ×2 THẬT** (`appcmd recycle`, PID 7020→**220**→**6636**, 3 dòng `Applying migrations...`): 8 chỉ số ĐỨNG IM + **soi ĐIỀU KIỆN log trong source trước khi đọc im-lặng** (`if (added>0)` `:2183/:2247/:2310/:2524`) ⇒ vắng dòng ⟺ 0 added (không phải đoán từ im-lặng). Control ÂM #84: `Hrm*/Off*` giữ **29/377** qua cả 3 mốc = revoker vừa SỐNG vừa KHÔNG lố sang `Khkk_*`. 🔴 **MARKER LEAD GỢI Ý BỊ BÁC BẰNG ĐO**: `Kết cấu, hạ tầng, phá dỡ` đã có từ `7a903cf` (git show = 1 hit cả 2 app) ⇒ **đo ra 1/1 trong bundle POST = 0 bit** (sẽ "xanh" kể cả khi K4 không ship). Thay bằng 3 marker có transition: POS `/khkk/list?group=1` **4/4 khớp element-wise số lần trong source** · NEG `/khkk/list?view=deleted` 0/0 (grep repo-wide = độc quyền) · 🔑 **DIFFERENTIAL `workflow-matrix?type=10&group=` admin 0 / eoffice 2** — 2 mũi: chứng bất-đối-xứng CỐ Ý ship + tự chứng comment-strip (hit admin duy nhất nằm trong `//`). Bundle JS ROTATE ×2 (`1yiNV4VH`→`qOcoitOr` · `DoULfmdT`→`D9J3BbcA`), CSS FROZEN lành tính, 4 LM in-window; control 2 tầng hash-CŨ **và** hash-BỊA cùng text/html 900/876. A1 Procurement+Drafter **8 group + 48 leaf**; A1-bis `/api/menus/me` materialize qua EF **8 grp + 42 leaf + 6 cũ = 56** + 8 nhãn đúng dấu. Smoke 401/401 + **2 route bịa 404** + 200×4; 🔑 **gỡ giới-hạn phép đo**: `?group=abc` no-token = 401 (authz chặn trước bind) → lấy bearer re-probe ra **400 bind-error** ⇒ chứng `?group=` wire THẬT. ⚠️ Treo cho wave sau: bảng vẫn **0 dòng** ⇒ `?group=n` chứng ống+bind, KHÔNG chứng lọc-đúng-nhóm (chân-lý-rỗng kế thừa K2); `?group=999`→200 chưa chặn ngoài-miền. Evidence: `runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md`. Tag `[s166, run-438, pass-7-7, phu-2-run-delta-khep-kin, moc-PRE-de-bai-sai, marker-lead-0-bit-bac-bang-do, differential-marker, log-condition-truoc-khi-doc-im-lang, gate-621]`
|
||||
|
||||
@ -143,3 +143,6 @@
|
||||
- 2026-07-28 · S157 #419 `a52a91e` PE canh-bao amber o Dia-diem khi du-an thieu Location… · PASS 9/9 · `2026-07.md` · substring:"Run #419 `a52a91e` (2026-07-28)"
|
||||
- 2026-07-29 · S159 #423 `1a47a61` dot-2 tab strip 4GD + menu 4-GD — PARTIAL 8/9, bat regression revoker an 447/494 row · `2026-07.md` · substring:"Run #423 `1a47a61` (2026-07-29"- 2026-07-30 · S162 #432 `bfc7b79` cay-4-folder-GD FE-ONLY — PASS 5/5, LUAT cam HEAD~1 (neo SHA X^..X) + dem-tron-ho 1 regex · `2026-07.md` · substring:"07-30 verify `bfc7b79` S162"
|
||||
- 2026-07-31 · S164 #434 `50e6d8c` K1 SP-002 danh-muc loai-HD + Mig 70 — PASS 6/6, tam-giac 3 nguon doc-lap cho seed 86 + bai "0-hit la duong-gia toi khi soi format" · `2026-07.md` · substring:"07-31 verify `50e6d8c` S164 K1"
|
||||
- 2026-07-31 · S164 #435 `0779f2d` K2 KHKK mang hang-muc + nhom duyet N1-8 + Mig 71 — PARTIAL 5/6, bai CHAN-LY-RONG (acceptance 0-row hai phia) + liet-ke-tron-ho index + wire-proof bind-400 · `2026-07.md` · substring:"07-31 verify `0779f2d` S164 K2"
|
||||
- 2026-07-31 · S164 #436 `15349e8` K3 8 workflow nhom KHKK-N1-8 (0-mig) — PASS 6/6, du-doan-truoc-khi-do + FROZEN-la-he-qua + marker bien-local 0-bit · `2026-07.md` · substring:"S166 (08-01) re-verify `15349e8`"
|
||||
- 2026-08-01 · S166 #438 `88e7ecf` K4a+K4b sidebar 8-nhom x 6-leaf + noi ?group= (0-mig) — PASS 7/7, phu-2-run delta khep-kin + moc-PRE de-bai SAI + marker lead 0-bit bac-bang-do + differential-marker · `2026-07.md` · substring:"S166 (08-01) verify `88e7ecf`"
|
||||
|
||||
@ -1,114 +1,71 @@
|
||||
# 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 N1–N8 [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]`
|
||||
|
||||
- **S153 (07-26) arc ctx-adopt T2 + T5 (2 lượt hmw) `[lead seed on-behalf @closeout M9; T2 return mất schema-fail (#53 +1) → derive-from-body, template VẪN LAND 8.819B; T5 return CLEAN]`:** **T2 template** `.claude/templates/session-mind-template.md` — 3 marker (RULES-START/END · MIND-TOP) · **0 dòng key `ts:`** (ts sống trong HEADING sau em-dash ⇒ không phạm rào-2) · 6 hit `rào-` (3 rào × tên+lý-do) · chiều chèn **NGƯỢC** `_context` khai TO trong khối luật · placeholder `<k>` trong schema-comment chống parser false-match (mirror trick template `_context:32`) · kiểm `_SECRET_RES` để ví-dụ rào-3 không tự khớp mẫu (secrets-sweep rglob TRỌN session-dir, KHÔNG biết enclosure).
|
||||
**T5 `hmw.js`** 3/3 acceptance: VALID_ROLES 20→21 (sau T10/T11 = **23**) + 🔴 **writeGuard là 2 ĐIỂM không phải 1** — `:228` JS-comment doc **và** `:235` template-literal ĐI VÀO PROMPT sub; chỉ sửa `:235` ⇒ doc lệch trong CÙNG file, chỉ sửa `:228` ⇒ **prompt runtime KHÔNG đổi** ⇒ vai read-only bị hướng-dẫn như write-sub ⇒ Bash-write MD ⇒ mojibake G-009. `:235` = **điểm có RĂNG**. Vùng CẤM đo bằng diff: `WIDTH_CAP` 0 hit · `TIER2_EXPECTED_FULL_ID` 0 hit.
|
||||
**2 kẽ báo lead:** ① **naming-collision cùng-role 2-invocation** (index `i` reset mỗi invocation ⇒ đợt-2 suýt đè `sub-implementer-frontend-1.md` của đợt-1; tự né bằng `-2`, verify đợt-1 nguyên 2020B) — kẽ CÒN LẠI sau S134 (S134 chỉ vá ca 2-role-cùng-invocation) ② `node --check` file thô **luôn** SyntaxError (top-level await) ⇒ acceptance phải là **wrapped-parse + đối-chứng HEAD**, không phải check thô.
|
||||
Tag `[s153, t2-template-0-ts-key, t5-writeguard-2-diem-235-co-rang, naming-collision-cung-role-2-invocation, node-check-tho-vo-nghia]`
|
||||
|
||||
- **S134 (07-17) PE lũy kế TẠM TÍNH FE 2-app [wf_58302bd5-b9f INV-2.B · return sạch 0-garble] `[engine: opus-worker]`:** types ×2 +5 field `pending*` (DIVERGE — edit riêng từng app cùng old_string sau `budgetFrozen`; grep 1/1) · PeDetailTabs `5534addb`→**`d28eaa16`** ×2 IDENTICAL: `BudgetSharedNumCell:1184`+`BudgetColValue:1197` +**`sub?: ReactNode` prop** → sub-dòng amber "Tạm tính" row1/2 (withDetail "+X từ N phiếu chưa duyệt", N8 count-TƯƠNG-ỨNG delta KHÔNG list.length) + row5/6 (gọn) + block liệt kê `pendingPriorPes` dưới matrix (plain+Badge `PurchaseEvaluationPhaseLabel`, N9 KHÔNG link — 403 S89) + gate `showPending = !bs.budgetFrozen && (...)` `?? `-guard data cũ · PeWorkflowPanel `a83a58e1`→**`bebc0ada`** ×2: banner M2-gate `!readOnly && phase === PurchaseEvaluationPhase.ChoDuyet` (enum-constant) + reminder trong Dialog `isApproveAction` TRÊN price-picker; disabled-condition 0-diff (B3 grep). npm ×2 PASS (user `index-BVoJX7QA` 20.9s · admin `index-mcHk0pmo` 9.3s). Learned: (1) **`sub?: ReactNode` slot-prop trên cell-component dùng chung = chèn sub-dòng vào N render-site không nhân bản markup**; (2) hmw đánh index sub-file GLOBAL theo taskList (backend=0, frontend=1 → `sub-implementer-frontend-1.md`) — hết collision S133 khi 2 role KHÁC nhau cùng invocation. Tag `[s134, pe-luyke-tam-tinh, sub-slot-prop, n8-count-per-delta, global-tasklist-index]`
|
||||
|
||||
- **S133 (07-17) PE budget-freeze FE types+badge [wf_d9639faa-97a INV-A, return sạch]:** 4 file — types 2-app +`budgetFrozen: boolean` (fe-user `:333` · fe-admin `:331` — 2 file KHÔNG mirror nhưng block PeBudgetSummary nội-dung identical → 1 old_string dùng chung cả 2); PeDetailTabs 2-app byte-identical: badge amber `🔒 Ngân sách chốt tại thời điểm duyệt` @ header `PeBudgetSummaryTable` `:1350-1359` (div `bg-[#1F7DC1]` → +flex justify-between; `normal-case tracking-normal` OVERRIDE `uppercase tracking-wide` của header; non-frozen = 1 span trong flex → appearance giữ nguyên). Gotcha mới đáng nhớ: className header LẶP 2 chỗ (`:1350` budget + `:1931` winners) → Edit anchor PHẢI kèm text visible "Tổng hợp ngân sách trình ký" mới unique. Hash sau sửa `5534addb` ×2 = mirror giữ; npm build ×2 PASS. Tag `[s133, budgetfrozen-badge, anchor-by-visible-text]`
|
||||
|
||||
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
|
||||
> **Persistent diary cross-session.** Auto-injected @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).
|
||||
> **Compact @S166 (2026-08-01)** — L1 chạm trần đọc (26.9KB) ⇒ S113→S166 verbatim dời `archive/2026-H2.md`, dưới đây chỉ còn móc 1 dòng.
|
||||
> **NEW agent S39 (2026-05-29)** — split từ implementer (FE 2 app half). Backend scaffold history ở `implementer-backend`.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 S117 (2026-07-14) — PE "duyệt theo KHOẢNG" radio-5 ProMinMax ×2 app (anh Kiệt FDC; BE landed contract). Build PASS ×2, SHA-pair Panel+DetailTabs
|
||||
- **Feature:** ô "CHỌN GIÁ CHỐT" cấp cuối +radio-5 "PRO — cả Min và Max (duyệt theo khoảng)" (source=ProMinMax) CHỈ khi `proSuggestedMinPrice!=null&&proSuggestedMaxPrice!=null`. Chọn=chốt dải [Min,Max]. GIỮ radio single-select.
|
||||
- **types (DIVERGE, edit RIÊNG KHÔNG cp):** `PeDetailBundle`(L433) +`approvedPriceMaxAmount:number|null` SAU approvedPriceSource. Anchor 3-dòng approvedPriceAmount/Source/canEditPro same-string 2 file, khác line (admin488/user490).
|
||||
- **PeWorkflowPanel `011ee8cf`:** priceCandidates +`amountMax?`; ProMinMax entry qua `...(hasBothProPrices?[{amount:min,amountMax:max}]:[])` spread TRƯỚC `as {...;amountMax?:number|null}[]` cast; filter type-guard `amountMax?:number` (subtype OK). `selectedCandidate` 1-find → selectedPriceAmount+selectedPriceAmountMax. Submit +`approvedPriceMaxAmount:sendPrice?(selectedPriceAmountMax??undefined):undefined` (undefined→axios bỏ key; nguồn đơn selectedMax=null→undefined; BE reject source-đơn-kèm-Max). Render span ternary `min đ – max đ`. Gate priceMissing GIỮ (ProMinMax có amount=min). NO new state (max derived) → onSuccess reset KHÔNG đổi.
|
||||
- **PeDetailTabs `617b0aa0`:** label-map +`ProMinMax:'PRO Min–Max'`; "Giá chốt duyệt" value ternary `approvedPriceMaxAmount!=null? fmtVnd(amt) – fmtVnd(max) : fmtVnd(amt)`.
|
||||
- **LESSON: conditional array entry = spread `...(cond?[obj]:[])` TRƯỚC `as T[]` cast (né inference lỗi); `??undefined`=omit-key axios chuẩn cho "source đơn không kèm Max"; Max KHÔNG cần state riêng (derive từ selectedCandidate) → 0 reset-site.** Build ×2 PASS (user1939mod `index-DuAp5TWx` 17.8s / admin1950mod `index-D6YhgGH-` 7.2s, 0 TS err). Warn pre-existing @import+>500KB+realtime. NO BE/test touch. 6 file (3/app) diff-stat symmetric. Full precedent, NO ambiguity. Tag `[s117, pe-approve-by-range, prominmax-5th-radio, spread-conditional-before-as-cast, undefined-omit-axios-key, derived-max-no-state, sha256-2pair, type-diverge-edit-same]`
|
||||
## 🧠 Bài học SỐNG (đọc trước khi nhận việc — KHÔNG archive)
|
||||
|
||||
## 🆕 S116 (2026-07-13) — PE cho nhập SỐ ÂM báo giá "Số tiền báo giá cho hạng mục" ×2 app (owner anh Kiệt; BE+DB nhận âm sẵn 0 validator). Build PASS ×2, SHA-pair
|
||||
PeDetailTabs.tsx BYTE-IDENTICAL, cp fe-user→fe-admin. Âm = phát-sinh-giảm/hoàn tiền NCC = giá THẬT (KHÔNG lỗi). 5 change.
|
||||
- **Sign-toggle 2 dialog (AddSupplier :2379 + QuoteDialog :2948):** nút +/− pattern VndInlineEdit:1003. `neg`=useState RIÊNG (QuoteDialog `setForm({thanhTien})` thay CẢ object → nuốt neg nếu nhét vào form). 🔴 2 dialog POST commit form.thanhTien THẲNG (:2247/:2906, no nút "Lưu") → toggle PHẢI ghi giá-CÓ-DẤU NGAY `setForm({...,thanhTien:-form.thanhTien})`, chỉ flip cờ = MẤT DẤU thầm lặng. Display `formatVndInput(Math.abs(tt))`; onChange `mag=parseVnd();tt=neg?-mag:mag`. QuoteDialog init `neg=(existing?.thanhTien??0)<0`; AddSupplier `false`.
|
||||
- **GLOBAL parseVnd/formatVndInput :51-52 KHÔNG sửa** (share ngân sách) → Math.abs wrap thay vì cho helper handle âm. formatVndInput(0)='' → placeholder OK.
|
||||
- **Gate `!==0` (KHÔNG >0):** AddSupplier submit :2244 + toast :2257. `-0!==0`=false → 0/rỗng vẫn skip="chưa báo giá".
|
||||
- **2 FE gate ĐỔI (KHÔNG 1):** missingForApproval :212 + submitChecklist :243 → helper mới `hasRealSelectedQuote(ev)=ev.details.some(d=>d.quotes.some(q=>q.isSelected&&q.thanhTien!==0))`. GOTCHA quote ở `details[].quotes` KHÔNG `suppliers[].quotes` (PeSupplier no quotes field). Cũ SUM winnerQuoteTotal: net-zero +100/-100=0 chặn oan. Mirror BE selectedThanhTien WHERE IsSelected. Label→"Đã nhập giá chào thầu". computeGiaChaoThau GIỮ (Section3 display :1921 — no unused).
|
||||
- **Ngân sách :2861 GIỮ ≥0** (change 4 no toggle — không trong diff). SHA `3e16dba5…` ×2 (git-diff --no-index exit0). Build ×2 PASS (user 1939mod `index-shUGyhuw` 19.0s / admin 1950mod `index-Yk1qEFOg` 7.6s, 0 TS err). Warn pre-existing @import+>500KB+realtime. NO BE/test touch. Full precedent. Tag `[s116, pe-negative-quote, sign-toggle-immediate-commit, math-abs-no-global-mod, hasRealSelectedQuote, details-not-suppliers-quotes, gate-not-equal-zero, sha256-2pair]`
|
||||
1. **ĐO ĐĨA TRƯỚC KHI GÕ.** Tiền-đề lead sai **5 lượt liên tiếp** (S164 · S166-K4a ×2 · K4b · K5) theo 2 hướng: *"tưởng chưa có"* (bảo tạo `KHKK_GROUP_LABELS` khi map đã tồn tại 5 call-site ⇒ tạo thêm là **phá đúng acceptance single-source mà nó phục vụ**) và *"tưởng có"* (bảo đi qua `lines[].contractId` — field **không tồn tại ở cả FE lẫn BE**). 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á. Bịa field cho khớp đề bài = tệ hơn.**
|
||||
2. **Đề bài mô tả CÁCH LÀM có thể giết feature đang sống.** "`phase` derive từ searchParams thay `useState`" nguyên văn ⇒ **6 chip lọc thành nút chết** (hỏng CÂM, build xanh). Đọc kỹ *còn ai ghi vào state đó* trước khi đổi nguồn.
|
||||
3. **Phép đo phải có RĂNG — luôn kèm CONTROL-ÂM.** 0-hit trần / "0 cảnh báo" vô nghĩa nếu chưa chứng phép đo bắt được cái sai (im-vì-đúng ⟂ im-vì-câm). Chuẩn đã dùng: tiêm lỗi vào **chính khối text đã ship** rồi xác nhận FLAG nổ.
|
||||
4. 🔴 **Mô phỏng framework thì phải mô phỏng CẢ vòng đời.** Harness K4b bản-1 đo nhầm **cái render React đã VỨT** (setState trong render ⇒ React huỷ render đó + render lại trước khi commit) ⇒ nhánh thật và control-âm ra CÙNG kết quả = phép đo mất khả năng phân biệt, **trông y hệt PASS**. Lặp tới khi state đứng yên mới đúng.
|
||||
5. **Chứng bằng cách CHẠY text đã ship**, không chép tay: `indexOf('<neo>')` → slice → `new Function`; dữ liệu thử sinh từ nguồn BE thật (`MenuKeys.cs` / `DbInitializer.cs`). Mọi phép thế text phải assert-khớp trước, không khớp thì script tự chết.
|
||||
6. **Bất-biến nên là CẤU TRÚC, không phải kỷ luật.** Giữ 1 danh sách phẳng canonical để đếm, phần chia nhóm chỉ **chia lại chính nó** ⇒ Σ luôn khớp; assert chỉ là chuông cho người sửa sau.
|
||||
7. **Forward-provision = đổi HÌNH DẠNG (đọc-1-ô → gom-một-TẬP), KHÔNG bịa field.** Thiếu nguồn thì đánh dấu chỗ cắm + trích nguyên văn bằng chứng + FLAG cho lead.
|
||||
8. **Sai-thầm-lặng → đổi thành thiếu-nhìn-thấy-được.** BE `/deleted` không nhận `group` ⇒ KHÔNG cho UI claim nhóm ở màn đó. **Rỗng-vì-lọc ⟂ rỗng-vì-chưa-có-gì phải khác câu** (dùng chung ⇒ user nhóm N5 tưởng cả module chưa cấu hình). Phiếu nhóm lạ gom vào "(chưa phân nhóm)" thay vì rơi ra ngoài.
|
||||
9. **`npm run build` = `tsc -b && vite build` + `noUnusedLocals`** ⇒ build LÀ phép thử TS6133 thật; **đừng tin IDE diagnostic** (gotcha #68).
|
||||
10. **Mirror ×2 app: cái KHÔNG đổi cũng là bằng chứng.** File không sửa mà hash BẰNG baseline = **đối chứng dương** cho phép đo hash. Lệch có chủ đích (fe-admin không có `WorkflowMatrixViewPage`) phải KHAI tại chỗ, không mirror mù.
|
||||
11. **Validate query-param bằng REGEX CHUỖI, không `Number()`.** `Number("03")=3` lọt lưới; `/^[1-8]$/` chặn. Và chốt chặn kép khi param chỉ có nghĩa với 1 ngữ cảnh (`type===10 && regex`).
|
||||
12. **Lọc client-side thì KHÔNG thêm trục vào `queryKey`** nếu payload đã tải trọn — thêm = N lần gọi mạng cho cùng một payload + N ô cache trùng. Ngược lại, trục làm ĐỔI request mà thiếu = cache dùng chung sai CÂM.
|
||||
|
||||
## 🆕 S113 (2026-07-12) — Supplier import v2 FE ×2 app (anh Kiệt owner; BE done ∥ impl-backend). Build PASS ×2
|
||||
3 file×2app: 2 SHA-pair (dialog+page byte-identical) + types DIVERGED (Department.parentId only S65 → edit-identical KHÔNG cp).
|
||||
- **types/master:** Supplier +`isPublic:boolean` (anchor `// Tình trạng`+status; git-diff = CHỈ `parentId`, isPublic ∉ diff = mirror khớp).
|
||||
- **SupplierImportDialog `78403bd8…`:** +"Tải file mẫu" (Download icon, blob `GET /suppliers/import/template`→`a.download='Mau-Database-NCC.xlsx'`, pattern PeDetailTabs:3273 useMutation) dưới drop-zone · cột "Mã" `<td>{r.code}`→`<Input value={r.code??''} onChange updateRow(rowIndex,{code})>` (updateRow=setPreview map-patch) · type +`mstMissing:boolean` → amber "⚠️ Chưa có MST, có thể bị trùng" cột Thông báo · helper blue draft. **3.d disabled ĐÃ đúng SẴN** (`errorCount>0||pending`, btn hidden khi !layoutValid — KHÔNG chặn blank-code) → NO change.
|
||||
- **SuppliersPage `9947b121…`:** +cột "Trạng thái" badge emerald "Đã công bố"/slate "Nháp (ẩn)" full-class-literal(P14) · Công bố(Globe emerald)/Ẩn(EyeOff) action-btn guard Update → publish mut `POST /suppliers/{id}/publish {maNcc||undefined, publish}` (KHÔNG qua UpdateSupplier #73) · publish Dialog sm (Mã NCC Input, gate `!maNcc.trim()`) · filter Select `published`(''=omit/'true'/'false' → axios undef/true/false) queryKey+ · actions w-32→w-40 (3 icon).
|
||||
- **NO 4-place mirror** (dialog+page trên page ĐÃ routed → no route/menuKey/Layout). **NO BE/test touch** (src/Backend/** = impl-backend ∥, tôi Read-only DTO). Build: admin 1950mod `index-CWBEeKi5.js` 8.48s / user 1939mod `index-JpoDnukv.js` 1.54s, 0 TS err; pre-existing warn @import+>500KB+realtime dynamic.
|
||||
- **DEVIATION flagged:** "Tải file mẫu" đặt CHỈ trong dialog (② canonical + ③ acceptance "ImportDialog nút mẫu"; ③ SuppliersPage= badge+Công bố+filter+guard, KHÔNG list nút mẫu) — parent 2.d loosely list page-btn nữa → 1-line ask nếu muốn thêm page-level template btn. Tag `[s113, supplier-publish-draft, isPublic, template-blob-download, ma-editable-grid, mst-missing-warn, publish-not-update, sha256-2pair, type-diverge-edit-identical]`.
|
||||
---
|
||||
|
||||
## 📦 Archived detail → `archive/2026-H1.md` (read on demand)
|
||||
- **2026-07-12** Supplier import Excel FE Phase B ×2 app byte-identical — NEW SupplierImportDialog + wire SuppliersPage ("Import Excel NCC"); multipart preview→confirm (echo rows verbatim); enum serialize INT; NO 4-place (dialog trên page routed). `[supplier-import-excel, enum-int-wire, byte-identical-2app]`
|
||||
- **S97-badge** (07-01) PE chip "✅ Đã được CEO duyệt" khi `!endsBeforeCeo && phase=DaDuyet` — PeFinalizeChip +prop `phase?:number` (KHÔNG narrow-union → TS2322) wire 6 call-site. SHA-pair `da9c3ed9`. `[s97-badge, prop-number-not-union]`
|
||||
- **S97-Part2** (07-01) PE gate endsBeforeCeo runtime hook-only PeWorkflowPanel (làm-mờ Bước CEO, Mig 60). SHA-pair `b7abbc2f`. `[s97-part2, endsbeforeceo-gate]`
|
||||
- **S97** (07-01) PE finalize wording + đảo default checkbox opt-out→opt-in — sửa CẢ init `useState(false)` LẪN reset-onSuccess; bare-string 2 chỗ (comment+JSX) anchor-context. SHA-pair `bcf812fd`. `[s97, invert-default-checkbox]`
|
||||
- **S88 D2** (06-25) PE multi-winner OUTWARD — CreateContractDialog `{contractIds:string[]}`; PeListPanel `winnerSupplierNames` prefer-fallback-single. SHA-pair + type-diverge-edit-same. `[s88, multi-winner-outward, cardinality-grep-consumers]`
|
||||
- **S87+** (06-25) FROZEN PE cleanup — HoSoLink clear `.trim()` (""=CLEAR, BE null-safe); #70 stale-echo `||peFetching` 2 ô PRO; type wording DIVERGE edit-same. SHA-pair `36292924`. `[s87plus, hosolink-clear-empty, gotcha70-guard]`
|
||||
- **S79** (2026-06-19) — PE live thousand-sep (`formatVndInput(parseVnd())` on-keystroke 2 comp) + ghi chú giá PRO/CCM (`note` 3-field absolute-set) + typo "Bảng so sánh giá". SHA-pair. `[s79, type-diverge-2app]`
|
||||
- **S78** (2026-06-19) — PE "focus mode" overlay redesign mirror fe-user→admin CONVERGE. PeWorkflowPanel +`onApproved` prop; ListPage `fixed inset-0 z-50` slide overlay a11y. SHA-pair. `[s78, mirror-step, converge]`
|
||||
- **S76** (2026-06-19) — PE budget ma trận 3 cột (DỰ ÁN/PRO/CCM) → `<table border-collapse>` grid + badge `✎ NS PRO/CCM`. LESSON: flex+gap ≠ bảng, phải `<table>`. cwd-misland `fe-*/.claude/` gitignore-guard. `[s76]`
|
||||
## 📌 Nhật ký (móc 1 dòng — verbatim → `archive/2026-H2.md`, cũ hơn → `archive/2026-H1.md` + `_INDEX.md`)
|
||||
|
||||
- **S166 (08-01) K4c** WfView lọc nhóm (chốt cụm-2, fe-user ONLY 1 file, fe-admin 0-diff). Lọc `wf.code === khkkGroupWorkflowCode(n)` client-side ⇒ **không đổi queryKey** (khai rõ); rỗng-vì-lọc tách câu riêng. 21/21 proof + 2 control-âm. `[s166, khkk-k4c-wfview-loc-nhom]`
|
||||
- **S166 (08-01) K5** cây toàn-trình GĐ2 mở **8 sub-folder nhóm** + forward-provision B3-A5. `groups` chia lại `leaves` phẳng ⇒ Σ theo cấu trúc; hiện đủ 8 kể cả rỗng + "(chưa phân nhóm)"; `lines[].contractId` KHÔNG tồn tại → union 1-nguồn + FLAG. 15/15 proof (CA4 tiêm lỗi). SHA 4/4 pair (2 đổi + **2 đứng yên = đối chứng dương**). `[s166, khkk-k5-8-sub-folder]`
|
||||
- **S166 (08-01) K4b** nối `?group=` + vá-3/5/6. Override 3-trạng-thái `chipPhase` + reset-trong-render theo `navKey` (KHÔNG useEffect); queryKey +`group`; staticMap G1 user 6/6 · admin 5/6. **Harness đo nhầm render bị vứt.** `[s166, khkk-k4b-group-wire, harness-do-nham-render-bi-vut]`
|
||||
- **S166 (08-01) K4a** regex 42 leaf `Khkk_G{2..8}_*` + DERIVE `khkkGroupMenuLabel` (không đẻ map-2) + `khkkGroupWorkflowCode`. 42/42 resolve + 3 control-âm. `[s166, khkk-k4a-regex-42-leaf, tien-de-lead-sai-x2]`
|
||||
- **S164/S165 (07-31) K2** KHKK nhóm-duyệt N1–N8 — badge trong ô (không đẻ cột) · **`colSpan` 7→8 hai chỗ = lỗi CÂM** · việc thật ≈1/4 mô tả. `[s164, do-dia-truoc-khi-go, colspan-loi-cam]`
|
||||
- **S161 (07-29)** ARC W1+W2+W3đ1 KHKK FE 3 lượt — containment **2-LỚP** render+payload (chỉ `disabled` = giả) · types-theo-ENTITY · FE tự suy ✓/●/○ từ con-trỏ đôi. `[s161, containment-2-lop]`
|
||||
- **S153 (07-26)** ctx-adopt T2+T5 — **writeGuard là 2 ĐIỂM**, `:235` (vào prompt) mới có RĂNG · `node --check` file thô luôn SyntaxError. `[s153, t5-writeguard-2-diem]`
|
||||
- **S134 (07-17)** PE lũy kế TẠM TÍNH — **`sub?: ReactNode` slot-prop** chèn sub-dòng vào N render-site không nhân bản markup. `[s134, sub-slot-prop]`
|
||||
- **S133 (07-17)** PE budget-freeze badge — className header LẶP 2 chỗ ⇒ **anchor phải kèm text visible**. `[s133, anchor-by-visible-text]`
|
||||
- **S117 (07-14)** PE duyệt-theo-KHOẢNG radio-5 — spread conditional **TRƯỚC** `as T[]` cast · `??undefined` = omit-key axios. `[s117, spread-conditional-before-as-cast]`
|
||||
- **S116 (07-13)** PE nhập số ÂM — toggle dấu phải **commit NGAY** (chỉ flip cờ = mất dấu thầm lặng) · quote ở `details[].quotes` KHÔNG `suppliers[].quotes`. `[s116, sign-toggle-immediate-commit]`
|
||||
- **S113 (07-12)** Supplier publish/draft — publish qua endpoint riêng, **KHÔNG qua UpdateSupplier** (#73). `[s113, publish-not-update]`
|
||||
- **S152 (07-25)** 2 ranh auto-refuse: việc <30min/trivial = em-main solo · **KHÔNG hạ chuẩn** dù bị giục (thiếu mirror-SHA hoặc build ×2 = trả lại). `[s152, no-lower-bar]`
|
||||
- Cũ hơn (S65→S97: office-dashboard · dept-parent · capability-flag · hosolink…) → `archive/2026-H1.md`.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Role baseline
|
||||
|
||||
WRITE specialist FE 2 app (fe-admin + fe-user). Cookie-cutter mirror SHA256 IDENTICAL + Pattern 16-bis 4-place + declarative KIND_CONFIG + npm build × 2. Case 1+2 only. Tools: Read, Edit, Write, Bash, Skill, Grep, Glob + 5 RAG. Skill: `permission-matrix`.
|
||||
WRITE specialist FE 2 app (fe-admin + fe-user). Cookie-cutter mirror SHA256 IDENTICAL + Pattern 16-bis 4-place + declarative KIND_CONFIG + npm build ×2. Case 1+2 only. Tools: Read, Edit, Write, Bash, Skill, Grep, Glob + 5 RAG. Skill: `permission-matrix`.
|
||||
|
||||
## 🚫 Split boundary
|
||||
### 🚫 Split boundary
|
||||
- ✅ MINE: `fe-admin/src/**` + `fe-user/src/**`
|
||||
- ❌ NOT: `src/Backend/**` → `implementer-backend` (chỉ Read DTO shape) · tests/ → `test-specialist`
|
||||
- ❌ NOT: UX flow decision (drawer/tab/modal) → em main solo
|
||||
- ❌ NOT: `src/Backend/**` → `implementer-backend` (chỉ Read DTO shape) · `tests/` → `test-specialist`
|
||||
- ❌ NOT: UX flow decision (drawer/tab/modal, mở-sẵn hay đóng) → em main solo
|
||||
|
||||
## 📋 Patterns proven (apply confidently)
|
||||
### 📋 Patterns proven
|
||||
- **Pattern 16-bis 4-place mirror (9× cumulative):** page/types · `App.tsx` Route · `lib/menuKeys.ts` · ⚠️ `Layout.tsx` `resolvePath` staticMap (**DỄ MISS** → silent sidebar drop, gotcha #50). Spec MUST list 4 places explicit.
|
||||
- **SHA256 IDENTICAL ×2 app:** viết 1 bên → `cp` → `sha256sum`. Khác UX thì mirror tay + `diff` + KHAI lý do tại chỗ.
|
||||
- **Declarative KIND_CONFIG Record (3× proven):** `:kind` URL + `Record<Kind,{fields,columns,icon,label}>`. ADD-KIND = union + entry + KINDS + renderCells branch + icon; **0 route mới**.
|
||||
- **Pattern 14 Tailwind JIT:** class động bị purge → full literal `as const`, cycle `index % length`.
|
||||
- **TS6:** `erasableSyntaxOnly` cấm `enum` → `const X = {...} as const` + `type X = typeof X[keyof typeof X]`. Tuple `as const` cần `as readonly number[]` mới `.includes(n: number)` được.
|
||||
- Named export only (trừ `App`). UI 100% tiếng Việt. fe-user thiếu Card/Badge shadcn → fallback inline `<div className="rounded-lg border bg-card">`.
|
||||
- ⚠️ **PageHeader KHÔNG một chuẩn:** bản `@/components/PageHeader` = `title/description/actions`; bản `@/components/ui/PageHeader` (pipeline/KHKK) thêm `eyebrow/subtitle/icon/accent`. **Đọc bản đang import trước khi truyền prop** (bài S37, lặp lại @K4c).
|
||||
|
||||
### Pattern 16-bis 4-place mirror (9× cumulative — BLESSED FOUNDATION)
|
||||
Add/move page cross-app MUST mirror 4 places:
|
||||
1. Page/types file · 2. `App.tsx` Route · 3. `lib/menuKeys.ts` const · 4. ⚠️ `components/Layout.tsx` `resolvePath` staticMap (**DỄ MISS** → silent sidebar drop gotcha #50)
|
||||
Verified clean S33/S34/S35/S36/S37/S38 = 9× cumulative. Spec MUST list 4 places explicit.
|
||||
### ✅ Verify protocol
|
||||
`npm run build` ×2 app BOTH 0 TS error + `sha256sum` mirror proof + proof-script có control-âm cho mọi khẳng định "đủ/đúng/không sót". Bundle >500KB + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts` = warning PRE-EXISTING, OK.
|
||||
|
||||
### SHA256 IDENTICAL × 2 app
|
||||
Viết fe-admin → `cp` fe-user → `sha256sum` verify. Khác UX (admin full sidebar vs user filter) → mirror tay + `diff`. Proven S36 (3 pair) + S37 (4 pair) + S38 (5 pair).
|
||||
### ⚠️ Anti-patterns (DO NOT)
|
||||
1. ❌ Touch BE files · 2. ❌ Miss 4th Layout staticMap · 3. ❌ Skip npm build ×2 · 4. ❌ `git add -A` · 5. ❌ Push/commit — em main commits · 6. ❌ UX decision autonomous → REFUSE · 7. ❌ Bịa field cho khớp đề bài → FLAG.
|
||||
|
||||
### Declarative KIND_CONFIG Record (S35, 3× proven)
|
||||
Single-page multi-kind CRUD URL `:kind` param + `Record<Kind, {fields, columns, icon, label}>` + renderField switch FieldType. Reuse: HrmConfigs (S35) + WorkflowApps (S38) + HrmConfigs +2 kind (S51). ADD-KIND playbook: union type + KIND_CONFIG entry + KINDS array + renderCells branch (before fallback) + import icon — `:kind`-driven page = NO new App.tsx route.
|
||||
|
||||
### Pattern 14 Tailwind JIT palette
|
||||
Dynamic class purged. PALETTE array full literal `as const` cycle `index % length`.
|
||||
|
||||
### TS6 / convention
|
||||
- `erasableSyntaxOnly` cấm enum → `const X = {...} as const` + `type X = typeof X[keyof typeof X]`
|
||||
- Named export only (trừ App). UI 100% tiếng Việt.
|
||||
- **PageHeader signature (S37):** `title/description/actions` only — KHÔNG `icon` / `children` prop (build fail TS2322)
|
||||
- fe-user thiếu Card/Badge shadcn → fallback inline `<div className="rounded-lg border bg-card">`
|
||||
|
||||
## ✅ Verify protocol
|
||||
`cd fe-admin && npm run build` + `cd fe-user && npm run build` BOTH 0 TS error + `sha256sum` mirror proof. Bundle >500KB warning OK pre-existing.
|
||||
|
||||
## 📅 Recent activity (FIFO — one-line hooks; full detail → `archive/2026-H1.md`)
|
||||
- **2026-06-17** Văn phòng số "Bảng điều khiển" — mirror fe-user→admin 4 file (PageHeader/KpiCard/WidgetCard/OfficeDashboardPage) + 4-place wiring `/office/` prefix route. SHA-5file. `[office-dashboard, 4-place]`
|
||||
- **2026-06-16 (S65)** PE mục "e. Link hồ sơ" NAS hyperlink + rename "Dự trù PRO"→"Ngân sách PRO". SHA-mirror 4 file. #53 empty-return (render landed OK); em-main bắt sót badge rename. `[s65, hosolink]`
|
||||
- **2026-06-16** Department parentId cây tổ chức, **fe-admin ONLY** (no SHA). Select "Phòng cha" + load-all pageSize:200 + exclude-self khi Edit + cột "Thuộc"; BE cycle-guard 409. `[dept-parent, admin-only]`
|
||||
- **2026-06-11 (S57bis)** PE WorkItem FE ×2 app (select "c. Hạng mục" + PeHeaderForm/PeDetailTabs +3 field). Truncated #53 GIỮA mirror → em-main solo. LESSON: per-app stage khi slice lớn. `[s57bis, truncated-53]`
|
||||
- **2026-06-09 (S55)** Project +4 optional master fields (year/investor/location/package). String-form+convert-on-submit (S42 pattern). `package`=valid TS key. SHA-pair. `[s55, master-enrich]`
|
||||
- **2026-06-08 (S54)** ItTicket reassign → CONVERGE 2 app (reverse S53). Pattern: BE capability-flag gate (`GET /it-tickets/assignable-staff {canReassign,staff}`) thay FE đoán role. SHA `4bcaf2f`. `[s54, capability-flag]`
|
||||
- **2026-06-08 (S52)** ItTicket admin reassign + AttendanceReport menu, **fe-admin ONLY**. Menu = 2/4-place (menuKeys+Layout staticMap; types/menu.ts key:string no-union). `[s52, admin-only, menu-2place]`
|
||||
---
|
||||
|
||||
## ⚠️ Anti-patterns (DO NOT)
|
||||
1. ❌ Touch BE files · 2. ❌ Miss 4th Layout staticMap · 3. ❌ Skip npm build × 2 · 4. ❌ `git add -A` · 5. ❌ Push remote · 6. ❌ UX decision autonomous → REFUSE
|
||||
|
||||
## 🔄 Curate trigger
|
||||
Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): `FE-Admin` · `FE-User`.
|
||||
|
||||
## S152 (2026-07-25) — R1 retention-note 2 ranh auto-refuse [M9 on-behalf lead — queue memory-ops]
|
||||
- **(a) Việc < 30 min / trivial:** fix 1-dòng CSS / label-polish (lớp Run #303/#304-era) = em-main solo nhanh hơn spawn-cost — mình REFUSE nhận đúng role-file; nhận việc trivial = đốt spawn vô ích.
|
||||
- **(b) KHÔNG hạ chuẩn "lower bar" kiểu Smart-Friend nể-nang:** bị giục "làm nhanh" vẫn giữ sàn mirror-SHA ×2-app + npm build ×2 PASS; thiếu 1 trong 2 = trả lại chứ không ship. Tag `[s152, r1-retention, auto-refuse-trivial-30min, no-lower-bar-smart-friend]`
|
||||
### 🔄 Curate trigger
|
||||
L1 > ~30KB **hoặc chạm trần đọc** → archive verbatim sang `archive/<period>.md`, L1 giữ móc 1 dòng. Commit scope (em main commits): `FE-Admin` · `FE-User`.
|
||||
|
||||
74
.claude/agent-memory/implementer-frontend/archive/2026-H2.md
Normal file
74
.claude/agent-memory/implementer-frontend/archive/2026-H2.md
Normal file
@ -0,0 +1,74 @@
|
||||
# archive/2026-H2 — implementer-frontend (verbatim L2, dựng @S166 2026-08-01)
|
||||
|
||||
> Chuyển verbatim từ `MEMORY.md` khi L1 chạm trần đọc. Con-trỏ 1 dòng ở `MEMORY.md`; Ctrl-F tag để nhảy.
|
||||
> Thứ tự: mới → cũ.
|
||||
|
||||
---
|
||||
|
||||
## S166 (2026-08-01) — K4c-FE WfView lọc theo nhóm (chốt cụm-2) `[s166, khkk-k4c-wfview-loc-nhom, rong-vi-loc-khac-rong-that]`
|
||||
Artifact `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4c.md`. fe-user ONLY, **1 file** (+68/−5), `git diff -- fe-admin` = RỖNG.
|
||||
Đóng FLAG-3 lượt K4b (`?group=` trên workflow-matrix chưa có consumer ⇒ 8 leaf mở ra 8 trang hệt nhau).
|
||||
**Đo trước khi gõ (4 thứ, không đoán cái nào):** ① `AwDefinitionDto.code` là tên field THẬT (`types/approvalWorkflowV2.ts:41`, đang render ở `:165`) ② trang **tải trọn bộ theo type** rồi `summary.history` ⇒ lọc CLIENT-side, **KHÔNG thêm trục `group` vào queryKey** (thêm = 8 lần gọi mạng cho cùng payload + 8 ô cache trùng) — khai rõ theo yêu cầu ③ `DbInitializer.cs:609` `code = $"KHKK-N{n}"` + `:623` **`IsUserSelectable = true`** ⇒ 8 wf THỰC SỰ lọt bộ lọc `isUserSelectable:true` của trang (seed `false` thì lọc kiểu gì cũng trang trắng) ④ `PageHeader` bản `@/components/PageHeader` CÓ `actions` — repo có ≥2 bản PageHeader khác chữ ký, **đọc bản đang import** (lặp bài S37).
|
||||
**Khuôn giữ lại:** chốt chặn KÉP `typeInt === 10 && /^[1-8]$/` — `group` là khái niệm riêng KHKK, type khác/rác thì BỎ QUA hoàn toàn (thà hiện đủ còn hơn lọc theo số vô nghĩa ra trang trắng); regex chuỗi bắt được `"03"` mà `Number()` sẽ nuốt. Lọc giữ **đủ VERSION cùng code** (nghĩa là "quy trình của nhóm", không phải "1 bản ghi"). Link "xem tất cả" bám `useLocation().pathname` vì trang gắn **3 route**. 🔴 **Rỗng-vì-lọc ⟂ rỗng-vì-chưa-có-gì phải khác câu** — dùng chung thì user nhóm N5 tưởng cả module chưa cấu hình trong khi 7 nhóm kia đang chạy (cùng họ bài "sai-thầm-lặng → thiếu-nhìn-thấy-được").
|
||||
**Chứng 21/21** chạy chính 2 khối đã ship + helper cắt từ types (assert khuôn trước khi bỏ annotation), code sinh theo khuôn đọc thẳng `DbInitializer.cs`: 8 URL → mỗi cái đúng 1 wf khớp số · trần → cả 8 · `group=0/9/abc/rỗng/"03"` → không lọc · `type=1|2|3` kèm group → bỏ qua · 2 version cùng code → giữ đủ · **2 CONTROL-ÂM** (gỡ vị-ngữ lọc ⇒ ra 8 = phép đo phân biệt được · helper sai khuôn `KHKK_N3` ⇒ 0 wf, không âm thầm khớp). Build fe-user PASS 1950mod `index-DBtbmn5s` 531ms.
|
||||
|
||||
## S166 (2026-08-01) — K5-FE cây toàn-trình: GĐ2 mở 8 sub-folder NHÓM `[s166, khkk-k5-8-sub-folder]`
|
||||
Artifact `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k5.md`.
|
||||
🔴 **BÀI: forward-provision đúng nghĩa = đổi HÌNH DẠNG, KHÔNG bịa field.** Đề bài đòi GĐ3/4 đi qua `lines[].contractId ∪ pe.contractId`. Đo 4 nơi → **cả 4 âm**: `PeListItem` 0 hit chữ `lines` · `PurchaseEvaluation.cs:31` chỉ `Guid? ContractId` đơn · `ContractListItem` không có `purchaseEvaluationId` · `Contract.cs` 0 khoá ngược. Bằng chứng đắt nhất là BE **tự khai**: `CreateContractFromEvaluationFeatures.cs:143-145` `pe.ContractId = contracts[0].Id` + comment "N HĐ liên-danh truy qua changelog" — endpoint TRẢ `contractIds[]` (`PeDetailTabs.tsx:2186`) nhưng chỉ HĐ ĐẦU được ghi ngược ⇒ HĐ 2+ vô hình. ⇒ Wire vế làm được: `contractIdsOfPackage = new Set<string>()` (union) thay vì đọc 1 ô, chỗ cắm nguồn-2 đánh dấu sẵn + trích nguyên văn BE; FLAG 2 đường cho lead (PE list DTO trả `contractIds[]` — rẻ, dữ liệu đã có; hoặc K7 `Line.ContractId`).
|
||||
**Khuôn giữ lại:** `stage.leaves` GIỮ vai danh sách PHẲNG canonical (nguồn đếm duy nhất), `groups` chỉ **chia lại chính nó** ⇒ bất-biến Σ là chuyện **CẤU TRÚC**, không phải kỷ luật nhớ-cộng-cho-khớp; `stageCount`/`hideEmpty`/chip số 0 phải sửa. Tách `LeafList` dùng chung thân-folder ⟂ thân-ngăn (0 nhân bản markup). Nút tên ngăn trong `<summary>` phải `preventDefault`+`stopPropagation`, không thì mỗi lần đi trang kèm 1 nhịp đóng/mở nhấp nháy. Ngăn mặc định ĐÓNG (8 ngăn × N gói trên cột trái 19rem = tường chữ). Hiện đủ 8 kể cả rỗng = `.map` trên hằng, KHÔNG `.filter` — mirror tiền lệ `SoonChip`; ngăn "(chưa phân nhóm)" CHỈ mọc khi thật có phiếu lạc (8 nhóm = khung nghiệp vụ cố định ⟂ đây = chỗ chứa bất thường).
|
||||
**Chứng:** chạy chính khối đã ship qua `new Function`, **15/15 OK** — CA1 rải đều (8 ngăn, Σ 5=5, nhóm-3 rỗng vẫn hiện, route `?group=1..8` khớp URL leaf K4b) · CA2 phiếu lạc (9 ngăn, gom đủ 4 ca null/9/undefined/thiếu-field) · CA3 gói rỗng (vẫn 8, không đẻ ngăn lạc) · **CA4 CONTROL-ÂM tiêm lỗi nhân-đôi ⇒ `console.warn` NỔ** (không có CA4 thì "0 cảnh báo" ở CA1-3 vô nghĩa: im-vì-đúng ⟂ im-vì-câm). 4 phép thế text đều assert-khớp trước khi chạy, không khớp thì script tự chết.
|
||||
**SHA 4/4 pair ×2 app:** `usePipelineStages` `7144eeb7`→**`edff9931`** · `PipelineStageFolders` `d449cc8d`→**`bed52f39`** · `PipelineTreePanel` `d70ac315` KHÔNG ĐỔI · `PePipelineStrip` `6fb7337e` KHÔNG ĐỔI ⇒ **2 hash đứng yên = đối chứng dương** cho phép đo. Build ×2 PASS (user `index-CZUcTUUg` 19.8s · admin 1961mod `index-BZWvx0Y8` 10.9s). 0 BE, 0 K4b file, 0 commit. 3 FLAG (lines[].contractId · ngăn mặc-định-đóng · `truncated` không lặp ở từng ngăn).
|
||||
|
||||
## S166 (2026-08-01) — K4b-FE nối `?group=` + vá-3/5/6 `[s166, khkk-k4b-group-wire, harness-do-nham-render-bi-vut]`
|
||||
Artifact `…/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: *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→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 cho CreatePage (8 leaf chung 1 route ⇒ leaf→leaf không remount; so `lastPresetGroup` **KHÔNG** so `approvalGroup` — so nhầm thì 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.
|
||||
|
||||
## S166 (2026-08-01) — K4a-FE regex 42 leaf KHKK + single-source nhãn nhóm `[s166, khkk-k4a-regex-42-leaf, tien-de-lead-sai-x2]`
|
||||
Artifact `…/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 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: 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.
|
||||
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; Layout 2 app **cố ý KHÔNG mirror** (user +34 regex · admin +13 comment-only). Build ×2 PASS (user 1950mod `index-B2v0kn5F` 20.8s · admin 1961mod `index-84zDBHE-` 8.2s).
|
||||
|
||||
## S164/S165 (2026-07-31) — K2-FE KHKK nhóm-duyệt N1–N8 `[s164, khkk-nhom-duyet, do-dia-truoc-khi-go, colspan-loi-cam]`
|
||||
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) · +cột "Tổng duyệt" Σ`totalApprovedAmount` (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). diff-stat đối xứng 269+/36− ×2 app.
|
||||
|
||||
## S161 (2026-07-29) — ARC W1+W2+W3đ1 KHKK FE, 3 lượt `[s161, khkk-fe-3-wave, containment-2-lop, types-theo-entity]`
|
||||
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).
|
||||
|
||||
## S153 (2026-07-26) — arc ctx-adopt T2 + T5 `[s153, t5-writeguard-2-diem-235-co-rang]`
|
||||
Lead seed on-behalf @closeout M9; T2 return mất schema-fail (#53 +1) → derive-from-body, template VẪN LAND 8.819B; T5 return CLEAN.
|
||||
**T2 template** `.claude/templates/session-mind-template.md` — 3 marker (RULES-START/END · MIND-TOP) · **0 dòng key `ts:`** (ts sống trong HEADING sau em-dash ⇒ không phạm rào-2) · 6 hit `rào-` (3 rào × tên+lý-do) · chiều chèn **NGƯỢC** `_context` khai TO trong khối luật · placeholder `<k>` trong schema-comment chống parser false-match · kiểm `_SECRET_RES` để ví-dụ rào-3 không tự khớp mẫu.
|
||||
**T5 `hmw.js`** 3/3 acceptance: VALID_ROLES 20→21 (sau T10/T11 = **23**) + 🔴 **writeGuard là 2 ĐIỂM không phải 1** — `:228` JS-comment doc **và** `:235` template-literal ĐI VÀO PROMPT sub; chỉ sửa `:235` ⇒ doc lệch trong CÙNG file, chỉ sửa `:228` ⇒ **prompt runtime KHÔNG đổi** ⇒ vai read-only bị hướng-dẫn như write-sub ⇒ Bash-write MD ⇒ mojibake G-009. `:235` = **điểm có RĂNG**. Vùng CẤM đo bằng diff: `WIDTH_CAP` 0 hit · `TIER2_EXPECTED_FULL_ID` 0 hit.
|
||||
**2 kẽ báo lead:** ① **naming-collision cùng-role 2-invocation** (index `i` reset mỗi invocation ⇒ đợt-2 suýt đè `sub-implementer-frontend-1.md` của đợt-1) — kẽ CÒN LẠI sau S134 ② `node --check` file thô **luôn** SyntaxError (top-level await) ⇒ acceptance phải là **wrapped-parse + đối-chứng HEAD**.
|
||||
|
||||
## S134 (2026-07-17) — PE lũy kế TẠM TÍNH FE 2-app `[s134, pe-luyke-tam-tinh, sub-slot-prop]`
|
||||
wf_58302bd5-b9f INV-2.B · return sạch 0-garble · engine opus-worker.
|
||||
types ×2 +5 field `pending*` (DIVERGE — edit riêng từng app cùng old_string sau `budgetFrozen`) · PeDetailTabs `5534addb`→**`d28eaa16`** ×2: `BudgetSharedNumCell:1184`+`BudgetColValue:1197` +**`sub?: ReactNode` prop** → sub-dòng amber "Tạm tính" row1/2 (withDetail "+X từ N phiếu chưa duyệt", N8 count-TƯƠNG-ỨNG delta KHÔNG list.length) + row5/6 + block liệt kê `pendingPriorPes` dưới matrix (N9 KHÔNG link — 403 S89) + gate `showPending = !bs.budgetFrozen && (...)` · PeWorkflowPanel `a83a58e1`→**`bebc0ada`** ×2: banner M2-gate `!readOnly && phase === ChoDuyet` + reminder trong Dialog TRÊN price-picker. npm ×2 PASS.
|
||||
Learned: (1) **`sub?: ReactNode` slot-prop trên cell-component dùng chung = chèn sub-dòng vào N render-site không nhân bản markup**; (2) hmw đánh index sub-file GLOBAL theo taskList — hết collision S133 khi 2 role KHÁC nhau cùng invocation.
|
||||
|
||||
## S133 (2026-07-17) — PE budget-freeze FE types+badge `[s133, budgetfrozen-badge, anchor-by-visible-text]`
|
||||
4 file — types 2-app +`budgetFrozen: boolean` (fe-user `:333` · fe-admin `:331`, 2 file KHÔNG mirror nhưng block PeBudgetSummary nội-dung identical → 1 old_string dùng chung); PeDetailTabs 2-app byte-identical: badge amber `🔒 Ngân sách chốt tại thời điểm duyệt` @ header `PeBudgetSummaryTable` `:1350-1359` (`normal-case tracking-normal` OVERRIDE `uppercase tracking-wide`). 🔴 Gotcha: className header LẶP 2 chỗ (`:1350` budget + `:1931` winners) → Edit anchor PHẢI kèm text visible "Tổng hợp ngân sách trình ký" mới unique. Hash `5534addb` ×2; build ×2 PASS.
|
||||
|
||||
## S117 (2026-07-14) — PE "duyệt theo KHOẢNG" radio-5 ProMinMax ×2 app `[s117, prominmax-5th-radio, spread-conditional-before-as-cast]`
|
||||
Ô "CHỌN GIÁ CHỐT" cấp cuối +radio-5 "PRO — cả Min và Max" (source=ProMinMax) CHỈ khi `proSuggestedMinPrice!=null&&proSuggestedMaxPrice!=null`. types DIVERGE (`PeDetailBundle` L433 +`approvedPriceMaxAmount`, anchor same-string khác line admin488/user490). PeWorkflowPanel `011ee8cf`: `priceCandidates` +`amountMax?`; entry qua `...(hasBothProPrices?[{...}]:[])` **spread TRƯỚC `as T[]` cast**; submit `??undefined` (axios bỏ key). PeDetailTabs `617b0aa0`: label-map + value ternary `min đ – max đ`.
|
||||
**LESSON:** conditional array entry = spread trước cast (né inference lỗi); `??undefined` = omit-key axios chuẩn; Max KHÔNG cần state riêng (derive từ selectedCandidate) → 0 reset-site. Build ×2 PASS.
|
||||
|
||||
## S116 (2026-07-13) — PE cho nhập SỐ ÂM báo giá ×2 app `[s116, sign-toggle-immediate-commit, hasRealSelectedQuote]`
|
||||
PeDetailTabs BYTE-IDENTICAL, cp fe-user→fe-admin. Âm = phát-sinh-giảm/hoàn tiền NCC = giá THẬT. 5 change.
|
||||
Sign-toggle 2 dialog (AddSupplier :2379 + QuoteDialog :2948); `neg` = useState RIÊNG (QuoteDialog `setForm({thanhTien})` thay CẢ object → nuốt neg). 🔴 2 dialog POST commit `form.thanhTien` THẲNG (:2247/:2906, no nút "Lưu") → toggle PHẢI ghi giá-CÓ-DẤU NGAY, chỉ flip cờ = **MẤT DẤU thầm lặng**. GLOBAL `parseVnd/formatVndInput :51-52` KHÔNG sửa (share ngân sách) → `Math.abs` wrap. Gate `!==0` (KHÔNG `>0`). **2 FE gate ĐỔI (KHÔNG 1):** `missingForApproval:212` + `submitChecklist:243` → helper `hasRealSelectedQuote`. 🔴 GOTCHA quote ở `details[].quotes` KHÔNG `suppliers[].quotes`. Cũ SUM winnerQuoteTotal: net-zero +100/−100=0 chặn oan. SHA `3e16dba5` ×2; build ×2 PASS.
|
||||
|
||||
## S113 (2026-07-12) — Supplier import v2 FE ×2 app `[s113, supplier-publish-draft, publish-not-update]`
|
||||
3 file×2app: 2 SHA-pair + types DIVERGED (edit-identical KHÔNG cp). types/master: Supplier +`isPublic`. SupplierImportDialog `78403bd8`: +"Tải file mẫu" (blob `GET /suppliers/import/template`) · cột "Mã" `<td>`→`<Input>` editable (updateRow map-patch) · +`mstMissing` amber warn. 3.d disabled ĐÃ đúng SẴN → NO change. SuppliersPage `9947b121`: +cột "Trạng thái" badge full-class-literal(P14) · Công bố/Ẩn → `POST /suppliers/{id}/publish` (**KHÔNG qua UpdateSupplier** #73) · filter Select `published` · actions w-32→w-40. NO 4-place mirror (page ĐÃ routed). Build ×2 PASS.
|
||||
DEVIATION flagged: "Tải file mẫu" đặt CHỈ trong dialog (② canonical + ③ acceptance) — parent 2.d loosely list page-btn nữa.
|
||||
|
||||
## S152 (2026-07-25) — R1 retention-note 2 ranh auto-refuse `[s152, auto-refuse-trivial-30min, no-lower-bar-smart-friend]`
|
||||
M9 on-behalf lead — queue memory-ops.
|
||||
**(a) Việc < 30 min / trivial:** fix 1-dòng CSS / label-polish = em-main solo nhanh hơn spawn-cost — REFUSE nhận đúng role-file; nhận việc trivial = đốt spawn vô ích.
|
||||
**(b) KHÔNG hạ chuẩn "lower bar" kiểu Smart-Friend nể-nang:** bị giục "làm nhanh" vẫn giữ sàn mirror-SHA ×2-app + npm build ×2 PASS; thiếu 1 trong 2 = trả lại chứ không ship.
|
||||
@ -1,13 +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` mà `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`
|
||||
- **[→ archive/2026-07.md — verbatim ĐÃ VERIFY moved-not-cut 3/3 @S166-K5-curate] S166 gate K4b · K4a · vá F-1 (digest):** mỏ-neo XUYÊN FILE (quét file BỊ DỜI, không chỉ file đang sửa) · **tham-số-trang-trí** (đo AI ĐỌC từng param, đừng đo "route có tồn tại") · mỏ-neo tự-vô-hiệu ngay lúc land (đo SAU khi chèn) · **ô canonical sai đo được bằng 1 curl** · khuôn-nguồn 2 tầng ⇒ đo **GIÁ-TRỊ KHỞI-TẠO** không đọc chú-thích · **cây phình giữa review** ⇒ neo mốc + re-đo `--stat` TRƯỚC verdict · HIGH-nằm-ở-git (`git status --porcelain -- src tests | grep '^??'`) · khoá-cache định-danh REQUEST không định-danh URL · sợi-dây-1-argument 0 test.
|
||||
|
||||
- **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`
|
||||
- **S166 (08-01) gate K5 cây GĐ2 mở 8 sub-folder nhóm — PASS 0 blocker (1 CLARIFY + 5 MINOR):** lõi hiếm-đúng: bất-biến Σ do **CẤU TRÚC** (1 mảng nguồn `khkkItems` → 1 phép phân hoạch if/else TOÀN PHẦN → `stage.leaves` vẫn là chỗ đếm duy nhất `stageCount():96`) nên không phụ thuộc ai nhớ cộng; route `?group=` nối đủ 3 chặng (FE `:79-80` → param `:124` → BE `Where(:757)`), **khác hẳn tham-số-trang-trí K4a**. **Bài thu:** ① 🔴 **phép-đo-rẻ-nhất nằm ở file RÁC của lane khác** — `menus.json` untracked ở gốc repo (sản phẩm curl của cicd-monitor) chính là payload `/api/menus/me` LIVE ⇒ đối chiếu nhãn cây suy-ra ⟂ nhãn sidebar thật **8/8 khớp từng ký tự + `displayLabel` đều null**, thứ mà đọc code không bao giờ chứng được. Cùng file đó là **rào commit** (`git add -A` sẽ nuốt 44KB rác) ⇒ 1 vật vừa là bằng-chứng vừa là rủi-ro, đừng chỉ báo 1 vế. ② **chân-lý-rỗng phải khai là rỗng**: curl prod `total=0` phiếu KHKK ⇒ phân bố `approvalGroup` = `{}`, KHÔNG dùng để kết luận gì; nhưng cùng con số ấy đẻ ra sự-thật dùng được cho UAT (ngày đầu mọi gói hiện **8 dòng "SẮP" rỗng** — đúng spec, dễ bị đọc thành hỏng). ③ **tương-đương-từng-vế > "cùng tập cùng thứ tự"**: vòng lặp GĐ3/4 bị viết lại ⇒ lập bảng 4 vế (tập id · thứ tự = `Set` giữ thứ-tự-chèn · khử trùng · dòng-phụ `find` = phiếu ĐẦU) mới dám phán 0-regression; thân stage4 **0 tham chiếu `p`** là lý do cấu-trúc khiến rewrite an toàn. ④ **tiền-lệ phải đo ở BASE, không đo ở cây đã sửa**: `git show 88e7ecf:<file> | grep -A6 '<summary'` cho thấy 6/6 summary cũ chỉ chứa `span`/`svg` ⇒ `<button>` lồng trong `<summary>` là **khuôn MỚI** (nested-interactive a11y), lane tưởng đang bám tiền lệ. ⑤ **"15/15 OK" chết theo phiên**: script `new Function` không commit + FE **0 hạ tầng test** (`package.json` chỉ `dev|build|lint|preview`) + chuông Σ là `import.meta.env.DEV` ⇒ prod bị strip ⇒ 0 lưới an toàn; đừng đọc số-test-ad-hoc thành đã-phủ. ⑥ 2 nghĩa của 1 dòng checklist ("GĐ3/4 by c.type") — lead và lane **cùng chọn 1 nghĩa không chứng minh nghĩa đó đúng ý owner** ⇒ ghim cách đọc vào commit. Tag `[s166, bang-chung-o-file-rac, chan-ly-rong, tuong-duong-tung-ve, tien-le-do-o-base]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k5.md`
|
||||
|
||||
- **[→ 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 — 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).
|
||||
|
||||
- **[→ 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' · comment 'bên kia lo' = ĐI ĐỌC BÊN KIA · grep bằng TOKEN-SONG-SINH · `hợp-đồng-đứt-2-bờ` · đổ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 · 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).
|
||||
> Update BEFORE every stop. Tiered Memory v1: L1 HOT cap ~17KB (hook 24.4KB read-limit) · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
|
||||
@ -18,7 +17,7 @@
|
||||
## 📁 Area memory (L2 on-demand — Read khi review vùng tương ứng)
|
||||
- [S62 PE budget soft-warning](project_s62_pe_budget_soft_warning.md) — PASS: hard-block→soft-warning; submit-guard intact + validator giữ `BudgetPeriodAmount>0`, row8 negative-safe (additive-only). Validator class `PurchaseEvaluationFeatures.cs:317`.
|
||||
- [Wire/mirror claim verification anchors](feedback_wire_claim_verification_anchors.md) — sha256 twin-file · `git diff -U0` isolate true-adds · `allowNegative` bleed check · guard-still-intact grep.
|
||||
- **[→ archive/2026-07.md @S161-curate] S160 (07-29) review SPEC-BỘ dry-run KHKK:** review SPEC ≠ review CODE (lỗ ở acceptance + default-lấn-quyền) · seeder-grant kiểu insert = NO-OP 403 im lặng ⇒ acceptance phải đo CỜ không đếm ROW · acceptance 'PASS SẴN' = 0-bit · choke-point 'DUY NHẤT' sai vì gán QUA BIẾN (#81).
|
||||
- **[→ archive @S161-curate] S160 review SPEC-BỘ KHKK:** review SPEC ≠ review CODE · seeder-grant kiểu insert = NO-OP 403 im lặng ⇒ acceptance đo CỜ không đếm ROW · acceptance 'PASS SẴN' = 0-bit · choke-point 'DUY NHẤT' sai vì gán QUA BIẾN (#81).
|
||||
- **S89 trio** ([sectionB](project_s89_pe_sectionb_review.md) · [endsBeforeCeo](project_s89_pe_endsbeforeceo_plan_review.md) · [finalize-note](project_s89_finalize_note_plan_review.md)) — adjustMut share race + authz-lens ≠ concurrency-lens · FE 4 render-sites plan-missed-2 · ground-truth-accurate-nhưng-gaps.
|
||||
- **S101 H18/H19 WF2 + LANE-C:** content-hash ≠ commit-SHA (hex strip/no-strip) · exact-token sweep miss paraphrase → widen concept-phrase. → `archive/2026-07.md`.
|
||||
- [S103 H20+crystallized-backfill review](project_s103_h20_crystallized_backfill_review.md) — WF2 PASS; 6/6 hash recompute MATCH + 4 already-met verified. Lesson: multi-axis warning phải nằm trong OUTPUT của script (reader chạy inline), không chỉ doc.
|
||||
@ -81,7 +80,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` + `archive/2026-07.md` via `archive/_INDEX.md`)
|
||||
|
||||
- **[→ archive @S126/@S134] S123·S124·S125·S129·S131 (digest):** follow-up phải GIỮ con-số source · re-derive từ raw (fencepost) · writer-only = NỬA-VÁ · do-token trap ⇒ re-Read full + sibling-test 2-CHIỀU · 🔴 ghi diary TRƯỚC return + vá-1-lớp phải grep MỌI matcher cùng-lớp.
|
||||
- **Digest S93→S117 + June-2026 (verbatim → `archive/2026-07.md`):** code-gate re-derived denom · default-flip byte-mirror SHA ×2 · HELD stale run-id · persist-claim cần reader-side + tracked + 0-drift · outward-claim 'đã sửa X' phải cat X trước gate · gist meta-count ≠ đĩa · claim-về-code grep lại kể cả reword · nâng-nấc-quên-sync cùng-diff · verify EACH regex-alt by-hand · merge-distill đẻ false-specificity ⇒ QUALITY gate SAU presence · sha = self-declared NOT recompute · TRACKED = check-ignore ≠ ls-files · menu-hide ≠ API-lock.
|
||||
- **Digest S93→S117 + June-2026 (verbatim → `archive/2026-07.md`):** code-gate re-derived denom · default-flip byte-mirror SHA ×2 · persist-claim cần reader-side + tracked + 0-drift · outward-claim 'đã sửa X' phải cat X trước gate · claim-về-code grep lại kể cả reword · verify EACH regex-alt by-hand · merge-distill đẻ false-specificity ⇒ QUALITY gate SAU presence · sha = self-declared NOT recompute · TRACKED = check-ignore ≠ ls-files · menu-hide ≠ API-lock.
|
||||
|
||||
- **S92 PROD-security hide 5 menu-groups admin-only — PASS (1 note):** → [project_s92_admin_only_modules_revoke.md](project_s92_admin_only_modules_revoke.md).
|
||||
|
||||
@ -90,9 +89,9 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
---
|
||||
|
||||
## 🔄 Curate trigger
|
||||
- Hook-cap **>17.1KB** (24.4KB read-limit — đổi từ ~30KB cũ, S109) → archive recent → L2 `archive/<period>.md` (append additive) + `_INDEX.md` substring pointer. Stale >3mo → remove.
|
||||
- **S162-curate (self, 2026-07-30, 23.9KB→<17.1KB):** move VERBATIM 2 entry S161-W2 + S161-W1 → `archive/2026-07.md` @S162-curate (probe moved-not-cut 2/2 TRƯỚC khi xoá khỏi L1, bài S102) + nén 10 dòng digest đã-archive-sẵn. KHÔNG cut-without-move.
|
||||
- **Prev curates:** S162 (moved-not-cut probe 2/2) · S145 · S140 · S134 · S128 · **S102 recovery: cut-not-moved ×5 `f229b07` ⇒ mọi L1-shrink kèm '→archive' BẮT BUỘC grep moved-not-cut TRƯỚC khi trust** · S116·S113·S109·S80·S71·S70·S40.
|
||||
- ✅ **NỢ CURATE @S166-K4b ĐÃ TRẢ @S166-K5** (23.4KB→~17KB): move VERBATIM 3 entry đã hẹn (K4b·K4a·vá-F-1) → `archive/2026-07.md` §`@S166-K5-curate`. Trình tự bài S102: **append archive TRƯỚC → probe 3/3 kèm control-âm → assert `line in arc` NGAY TRONG script cắt → mới cắt → hậu-kiểm L1=0/archive=1**. Bài mới: digest sinh bằng escape `\uXXXX` **đẻ chữ hỏng câm** (byte-count không bắt được) ⇒ **script sinh chữ phải ĐỌC LẠI bằng mắt**.
|
||||
- Hook-cap **>17.1KB** (24.4KB read-limit, S109) → archive recent → L2 `archive/<period>.md` (append additive) + `_INDEX.md` pointer. Stale >3mo → remove.
|
||||
- **Prev curates:** S166-K5 (probe 3/3) · S162 (2/2) · S145 · S140 · S134 · S128 · **S102 recovery: cut-not-moved ×5 `f229b07` ⇒ mọi L1-shrink kèm '→archive' BẮT BUỘC grep moved-not-cut TRƯỚC khi trust** · S116·S113·S109·S80·S71·S70·S40.
|
||||
|
||||
## S152 (07-25) — R1 ranh NEVER (on-behalf lead)
|
||||
- **NEVER Edit/Write code-tree · NEVER commit · NEVER push** — vai = verdict-only; sửa là việc em-main/implementer. Khai thật 2 nấc: ranh commit/push/code-tree **chưa từng vỡ**; Write/Edit runtime CÓ cấp và dùng hợp lệ cho sub-file run-folder + diary-self (AS-10). "NEVER" áp cho ĐÍCH (code/commit/push), không phải cho tool-tồn-tại; containment thật = git-diff lead. Tag `[s152, r1-retention, never-target-scoped]`
|
||||
|
||||
@ -202,3 +202,12 @@
|
||||
## @S166-curate — moved verbatim từ L1 (S164/S165 gate K3)
|
||||
|
||||
- **S164/S165 (07-31) gate K3 KHKK (8 wf nhóm + port level-finalize + gỡ khoá Designer) — PWF 8 finding/0 blocker:** build 0W/0E · 614/0 (45D+569I) · npm ×2 sạch; 3/3 quyết-định lead ĐÚNG kỹ-thuật. **Bài thu:** ① 🔴 **tham-số-chết-vì-thiếu-tầng-UI** — BE port opt-out `applyLevelFinalize` đủ 3 tầng + seed cờ finalize cho TP.CCM ở CẢ 8 wf, nhưng panel KHKK POST **body literal đúng 2 field** `{action,comment}` ⇒ luôn finalize ⇒ **trạm CEO chết 8/8**, mất đúng quyền owner vừa chốt. Bắt bằng **đối-chứng khuôn-nguồn**: PE panel `useState(false)` + luôn gửi tường minh ⇒ PE opt-IN, KHKK opt-OUT ⇒ *"mirror khuôn PE"* SAI ở chiều mặc-định. **Luật: claim 'port khuôn X' phải đo TẦNG-CUỐI (ai GỬI tham số), đừng đếm tầng-BE.** ② **sweep 2-cờ bất-đối-xứng** — FE gỡ khoá CẢ 2 ô, BE chỉ gỡ 1 ⇒ tick bị NUỐT IM LẶNG (chính class comment vừa xoá đã cảnh báo, đảo chiều); **vá 1 cờ ⇒ grep cờ ANH-EM ở CẢ 2 bờ**. ③ spec liệt SAI site (bảo gỡ PUT-Conflict) — bác bằng `grep typeEnum` trong thân handler = 0 hit ⇒ luật CHUNG mọi type; **lead cãi spec ĐÚNG khi có phép đo**. ④ suite **flake 1/3 lượt** ⇒ 'N/0 tươi' phải khai SỐ LƯỢT. ⑤ DELTA STATUS chưa khai — tái phát y hệt K1 cùng phiên. Tag `[s165, tham-so-chet-thieu-tang-ui, doi-chung-khuon-nguon, sweep-2-co-bat-doi-xung, spec-liet-sai-site]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k3.md`
|
||||
|
||||
|
||||
## @S166-K5-curate — moved verbatim tu L1 (S166 gate K4b · K4a · va F-1)
|
||||
|
||||
- **S166 (08-01) gate K4b nối `?group=` end-to-end — PWF 4 MAJOR/5 MINOR, 0 blocker:** lõi ĐÚNG (derive-thuần + reset-trong-render `navKey`, 6/6 ca chuyển leaf hết cần F5; BE `/deleted` mirror list-sống ĐÚNG TỪNG NÉT, vế lọc SAU IDOR; sha mirror ×3 khớp; build 0E + `tsc -b` ×2 EXIT=0 **tôi tự chạy**). **Bài thu:** ① 🔴 **mỏ-neo XUYÊN FILE** — wave-B sửa file A làm sai chú-thích-neo-số-dòng trong file B (`Layout.tsx:204` trỏ `KhkkListPage :61/:62`, đĩa = `:74/:76`), 2 wave vào **1 commit** ⇒ sai từ **byte-0** chứ không trôi dần; đo 2 đầu rẻ: `git show <base>:<file> | grep -n` vs đĩa. Tái phát bài K4a ② sau ĐÚNG 1 wave, lần này khác file ⇒ quét mỏ-neo phải quét **file BỊ dời**, không chỉ file đang sửa. ② **phán NGƯỢC 1 FLAG của lane** (queryKey mang giá-trị-ĐÃ-GIẢI-MÃ): ĐÚNG, vì khoá cache định danh **REQUEST** không định danh URL — kèm **điều-kiện-phản-chứng** ("sai nếu raw `filter` chảy vào JSX", đã grep hết đường đi) thay vì gật "OK". ③ **HIGH-nằm-ở-git tái xuất** — sắp commit gộp mà `git status --porcelain -- src tests | grep '^??'` vẫn ra test K4a **chưa track** ⇒ mã seed lên repo, test chứng minh nó thì không. ④ **cây phình giữa review** lại xảy ra (HEAD +2 commit, +123 dòng) nhưng phân lập được bằng `git diff --stat -- <đường-code>` + sha lại file mirror ⇒ **.claude-only, verdict giữ**; đây là cách rẻ để không phải soi lại từ đầu. ⑤ tham-số-trang-trí **không tự khỏi**: K4b đẻ instance thứ 8 (`Khkk_WfView?group=1`) khi consumer vẫn 0 hit. ⑥ positional-record + object-initializer: điều-kiện ẨN = base phải là **record** (record không kế thừa class) — kiểm base rồi hãy phán "compile được". Tag `[s166, moneo-xuyen-file, khoa-cache-dinh-danh-request, high-o-git, cay-phinh]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4b.md`
|
||||
|
||||
- **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` mà `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`
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# 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]`
|
||||
- **+1 (resume-2, nợ gate-K4b MAJOR-A) lọc nhóm màn "Đã xoá" → Infra 577, tổng 622:** đặt ở `ContractSigningPlanCrudTests.cs` CHỨ KHÔNG file K4a — **chọn file theo FIXTURE, không theo chủ đề**: chỉ fixture đó có `AuditingInterceptor` (màn "Đã xoá" đọc row `IsDeleted=1`; fixture thường xoá CỨNG ⇒ màn rỗng ⇒ đo nhầm cơ chế). 2 phiếu CÙNG 1 PE khác nhóm là hợp lệ vì rào khoá theo CẶP (PeId, ApprovalGroup); workflow `QT-KHKK-V2` ngoài họ `KHKK-N{n}` nên rào (v) cho qua. Mọi vế assert **`Total`** (dấu vân tay lọc-SERVER: `CountAsync` sau `Where`; lọc client-side cho `Items` y hệt mà `Total` = cả thế giới) + đo **cả nhánh Admin** vì vế lọc nằm NGOÀI khối IDOR. 🔴 **Mẹo chứng-răng RẺ NHẤT gặp tới giờ:** khi vế cần bảo vệ là "handler CÓ đọc param", thế-giới-hỏng **tương đương chính xác** với việc gọi handler bằng `param=null` ⇒ RED→GREEN đo được bằng cách đổi ĐÚNG 1 chữ TRONG TEST, chạy trên MÃ PROD THẬT (mạnh hơn probe mimic), 0 rủi ro chạm prod. RED thật: `Expected g1.Items to contain 1 item(s), but found 2`.
|
||||
- **+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]`
|
||||
@ -25,7 +26,7 @@
|
||||
|
||||
> 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ó.
|
||||
|
||||
## 📊 Baseline ĐO TƯƠI 2026-08-01 (S166): **621 = 45 Domain + 576 Infra**, 0 FAIL
|
||||
## 📊 Baseline ĐO TƯƠI 2026-08-01 (S166): **622 = 45 Domain + 577 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.
|
||||
|
||||
@ -0,0 +1,323 @@
|
||||
CICD-K4AB: PASS — 7/7 (run #438 success 6m23s · gate 621=45+576 · Mig GIỮ 71 · seed 49 menu+637 perm tam-giác-3-nguồn · A1 8+48 & A5 restart×2 đứng im · bundle JS ROTATE ×2 marker 3/3 · smoke 401/404/200 + bind-400)
|
||||
|
||||
# cicd-monitor — verify deploy K4a+K4b SP-002 (`88e7ecf`) + phủ luôn F-1 (`7a903cf`)
|
||||
|
||||
> Artifact chống #53: ghi TỪNG MỤC ngay khi có bằng chứng. Kết bằng `END ... VERDICT=`.
|
||||
> Repo `D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP` · push `7a903cf..88e7ecf main` · phiên S166 01/08.
|
||||
> Khuôn kế thừa k1/k2/k3 cùng folder: tam-giác 3-nguồn ⟂ control ÂM ⟂ recycle-proof ⟂ dự-đoán-TRƯỚC-khi-đo.
|
||||
|
||||
## §0 — Tiền đề (git, đo LIVE)
|
||||
|
||||
```
|
||||
HEAD = 88e7ecf263f019f4b31d4defb9520704568042a9 2026-08-01 10:10:11 +0700
|
||||
[CLAUDE] Contract: K4a+K4b SP-002 sidebar 8 nhom x 6 leaf + noi ?group= end-to-end (0-mig)
|
||||
git log origin/main..HEAD -> (rỗng — đã push)
|
||||
```
|
||||
|
||||
`git diff --name-only 7a903cf..88e7ecf` = 33 file. Phân loại vs `paths-ignore` 7-glob:
|
||||
- **CODE (13, trigger CI):**
|
||||
- BE 4: `ContractSigningPlansController.cs` · `ContractSigningPlanFeatures.cs` · `MenuKeys.cs` · `DbInitializer.cs`
|
||||
- FE-admin 4: `components/Layout.tsx` · `pages/khkk/KhkkCreatePage.tsx` · `pages/khkk/KhkkListPage.tsx` · `types/khkk.ts`
|
||||
- FE-user 4: y hệt 4 file trên (cặp song sinh)
|
||||
- test 1: `tests/…/Application/KhkkGroupMenuSeedTests.cs`
|
||||
- **IGNORED (20):** `docs/STATUS.md` (`**/*.md`) + `.claude/WAL.md` + `.claude/agent-memory/**` ×10 + `.claude/workflows/runs/**` ×8
|
||||
- **0 file `*Migrations*`** ⇒ K4a+K4b = 0-mig (khẳng định này đo lại độc lập ở §3).
|
||||
|
||||
**F-1 `7a903cf` (run #437, chưa verify riêng — lượt này phủ luôn):**
|
||||
`git diff --name-only 15349e8..7a903cf` = 41 file; CODE 5 (`fe-admin/khkk/KhkkWorkflowPanel.tsx` + `fe-admin/types/khkk.ts` + `fe-user/khkk/KhkkWorkflowPanel.tsx` + `fe-user/types/khkk.ts` + `ContractSigningPlanFeatures.cs`) — khớp mô tả "FE+DTO nhỏ"; phần còn lại ignored-path (`CLAUDE.md`, `docs/**`, `.claude/**`).
|
||||
|
||||
### §0.1 — Snapshot PRE đo LIVE lúc run #438 CÒN `running` (10:12-10:14 +07)
|
||||
|
||||
Đây là mốc PRE MẠNH NHẤT có thể có: đo trong lúc CI đang chạy ⇒ chắc chắn chưa bị K4a/K4b ghi đè.
|
||||
|
||||
**Bundle PRE (curl index.html, 10:12:09):**
|
||||
```
|
||||
admin /assets/index-1yiNV4VH.js + /assets/index-Cf3iCOOv.css
|
||||
eoffice /assets/index-DoULfmdT.js + /assets/index-DX1FRk5n.css
|
||||
```
|
||||
✔ Khớp ĐÚNG mốc đề bài lead giao (`1yiNV4VH` / `DoULfmdT`) và khớp nguồn gốc của mốc đó
|
||||
(`sub-reviewer-gate-k4a.md:131,133` — gate-K4a đo post-#437). **2 nguồn độc lập ⇒ mốc PRE hợp lệ.**
|
||||
|
||||
**Prod DB PRE (sqlcmd `localhost\SQLEXPRESS`):**
|
||||
```
|
||||
MENU_TOTAL = 151
|
||||
MENU_KHKK_PCT = 7 (đúng 7 key nhóm-1 không-infix: Khkk_G1/WfView/List/Create/Pending/Approved/Deleted)
|
||||
MENU_KHKK_G1_LABEL = "1. Kế hoạch ký kết HĐ (NCC-TP)" <- nhãn SKELETON S159, chưa backfill
|
||||
PERM_TOTAL = 1584
|
||||
PERM_KHKK_PCT = 91 (= 7 × 13, và CanRead=1 cả 91 ⇒ họ Khkk đang mở đủ)
|
||||
ROLE_TOTAL = 13 (Accounting/Admin/AuthorizedSigner/CatalogManager/CostControl/DeptManager/
|
||||
Director/Drafter/Equipment/Finance/HrAdmin/Procurement/ProjectManager)
|
||||
MIGTOP = 20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime · MIGCOUNT = 71
|
||||
CONTROL ÂM nền #84 : Hrm*/Off* rows=377 nhưng CanRead=1 chỉ **29** (revoker đang SỐNG, giữ 348 row tắt)
|
||||
```
|
||||
⚠️ Cột PK `MenuItems` = **`[Key]`**; bảng quyền `Permissions(Id,RoleId,**MenuKey**,CanRead,CanCreate,CanUpdate,CanDelete)`
|
||||
— 1 ROW / cặp (role,key) với 4 cột cờ, **KHÔNG phải 4 row**. Bảng role = `Roles` (non-prefix).
|
||||
|
||||
🔴 **ĐÍNH CHÍNH MỐC PRE TRONG ĐỀ BÀI (đo, không đoán):** lead giao `MENU_TOTAL PRE=143 ⇒ kỳ vọng 192`.
|
||||
Đo LIVE ra **151**. Truy PHẠM-VI-QUERY trước khi hô drift (bài #422) → giải thích TRỌN, element-wise:
|
||||
K3 (#436) đẻ thêm **8 menu-row `AwV2_KhkkN1..N8`** (đo: `MENU_AwV2_ALL=10` = 2 cũ `AwV2_DuyetNcc`/
|
||||
`AwV2_DuyetNccPhuongAn` + 8 mới, liệt kê đủ tên). **143 + 8 = 151 khít** ⇒ 143 là mốc PRE-K3 đã lão hoá,
|
||||
không phải drift. **Kỳ vọng đúng cho lượt này = 200, KHÔNG phải 192.**
|
||||
|
||||
**w3wp PRE:** `appcmd list wp` → `WP "4876" (applicationPool:SolutionErp-Api)`, StartTime **01/08 09:11:51**
|
||||
= đúng cửa-sổ recycle của #437 (09:05:59→09:12:21). ⇒ mốc-so-sánh recycle-proof cho #438.
|
||||
(VPS có 6 w3wp chung VIETREPORT ⇒ map PID→pool bằng `appcmd`, CẤM đoán PID.)
|
||||
|
||||
### §0.2 — DỰ ĐOÁN TỪ SOURCE, ghi ĐĨA TRƯỚC KHI ĐO POST
|
||||
|
||||
Đọc `MenuKeys.cs:72-84` + `DbInitializer.cs` (vòng seed menu K4a + `labelBackfill` + grant site-2):
|
||||
- `KhkkGroupNumbers=[2..8]` (7) × (`KhkkGroup(n)` 1 + `KhkkLeafActions` 6 = `WfView,List,Create,Pending,Approved,Deleted`)
|
||||
⇒ `KhkkGroupMenuKeys` = **7 × 7 = 49 key MỚI** (7 group + 42 leaf).
|
||||
- Vòng seed menu-row dùng CHÍNH bộ sinh đó ⇒ **MENU_TOTAL 151 → 200** · **MENU_KHKK_PCT 7 → 56**.
|
||||
- `labelBackfill["Khkk_G1"]` ⇒ Label đổi → **"1. Thiết bị, máy móc, vật tư phụ (A1)"** (key GIỮ, 0 row mới).
|
||||
- Grant: site-1 loop `MenuKeys.All` cấp **Admin**; site-2 `KhkkKeys().Concat(KhkkGroupMenuKeys)` cấp
|
||||
**CanRead mọi role, skip-existing** ⇒ hợp lại = 49 key × **13 role** = **+637 ROW**.
|
||||
⇒ **PERM_TOTAL 1584 → 2221** · **PERM_KHKK_PCT 91 → 728** (chéo: 728 = 56 × 13, và 728−91 = 637 — hai lối tính khớp).
|
||||
- ⚠️ Số **196** trong đề bài (`49×4`) là **POLICY** (|All|×4 action, tầng authz — `MenuKeys.cs:57-62` khai đúng vậy),
|
||||
KHÔNG phải row DB. Hai đại lượng khác nhau, đừng cộng nhầm vào PERM_TOTAL.
|
||||
- Mig: 0 file `*Migrations*` trong diff ⇒ **GIỮ 71 / top `20260731085624`**.
|
||||
|
||||
---
|
||||
|
||||
## §1 — Run Gitea Actions — ✅ PASS (2 run: #438 K4a+K4b · #437 F-1)
|
||||
|
||||
API `.../actions/tasks?limit=8` (anon, public repo). Match theo **`head_sha`**, KHÔNG theo run_number:
|
||||
```
|
||||
id=551 | run# 438 | 88e7ecf263 | status=success | concl=None | 10:10:19 -> 10:16:42 +07 <- K4a+K4b
|
||||
id=550 | run# 437 | 7a903cf108 | status=success | concl=None | 09:05:59 -> 09:12:21 +07 <- F-1 (phủ lượt này)
|
||||
id=549 | run# 436 | 15349e8ee0 | status=success | concl=None | 31/07 18:21:48 -> 18:27:59 <- K3 (neo thứ tự)
|
||||
```
|
||||
- **#438 duration 6m23s** · **#437 duration 6m22s** — in-band (#436 6m11s · #435 6m12s · #434 6m31s).
|
||||
- **Control neo thứ tự 2 tầng:** run kề dưới #438 = `7a903cf` (F-1), kề dưới nữa = `15349e8` (K3, đã verify ở
|
||||
`sub-cicd-verify-k3.md`) ⇒ chuỗi #436→#437→#438 liền mạch, **không có run lạ chen giữa**, không nhầm run.
|
||||
- Tao **đo LIVE lúc #438 còn `running`** (poll 10:12→10:16:51, 3 nhịp `running` rồi `success`) ⇒ không dính bẫy
|
||||
false-positive "verify khi còn running" (Run #242).
|
||||
- `concl=None` là bình thường với endpoint `tasks` ⇒ tin `status` (gate test chạy TRƯỚC build/deploy).
|
||||
- URL: `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/438` (và `/437`).
|
||||
|
||||
## §2 — Gate CI **621 = 45 + 576** — ✅ PASS (khớp TỪNG VẾ + đo luôn F-1)
|
||||
|
||||
Log web-UI `.../actions/runs/{438,437}/jobs/0/logs` (HTTP 200; 18.665 B và 18.689 B). Đọc bằng **bytes**
|
||||
(text-mode crash cp1252); regex nới khoảng-trắng vì log dùng **2 space** sau `Passed!`:
|
||||
```
|
||||
#438 Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 — SolutionErp.Domain.Tests.dll
|
||||
#438 Passed! - Failed: 0, Passed: 576, Skipped: 0, Total: 576 — SolutionErp.Infrastructure.Tests.dll
|
||||
#437 Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 — SolutionErp.Domain.Tests.dll
|
||||
#437 Passed! - Failed: 0, Passed: 569, Skipped: 0, Total: 569 — SolutionErp.Infrastructure.Tests.dll
|
||||
```
|
||||
- **#438 = 45 + 576 = 621** ✔ khớp kỳ vọng TÁCH-PHẦN (45D/576I), 0 fail / 0 skip.
|
||||
- **Chuỗi delta khép kín 3 mốc** (đây là chỗ việc phủ-2-run trả giá trị): #436 K3 = 45+569 = 614 →
|
||||
**#437 F-1 = 45+569 = 614 (Infra GIỮ 569, +0)** → **#438 = 45+576 = +7**.
|
||||
⇒ F-1 đúng là "FE+DTO nhỏ, 0 test mới"; **toàn bộ +7 thuộc về K4a+K4b**, khớp `KhkkGroupMenuSeedTests.cs`
|
||||
(file test DUY NHẤT trong diff #438). Domain **GIỮ 45** suốt 3 run.
|
||||
- **Control ÂM:** `grep -c 'Failed!'` = **0** trên CẢ HAI log ⇒ 2 dòng `Passed!` là TOÀN BỘ project,
|
||||
không có project thứ 3 bị nuốt (nếu fail, dotnet in `Failed! - Failed: N…`).
|
||||
|
||||
## §3 — Mig **GIỮ 71** — ✅ PASS (2 nguồn ĐỘC LẬP repo ⟂ prod)
|
||||
|
||||
```
|
||||
prod: SELECT MAX(MigrationId) -> 20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime ; COUNT(*) = 71
|
||||
repo: git diff --name-only 7a903cf..88e7ecf -- '*Migrations*' -> (rỗng)
|
||||
```
|
||||
- MIGCOUNT 71 (§0.1 PRE) == 71 (POST) ⇒ **không có mig lén chen vào**; top y nguyên mốc K2.
|
||||
- ⚠️ Mục "GIỮ NGUYÊN" tự nó ít bit ⇒ răng lấy từ **control DƯƠNG cùng phiên**: §4 chứng minh DbInitializer
|
||||
ĐÃ chạy (seed 49 row + backfill 1 label) ⇒ startup-hook SỐNG. "Mig không nhích" là vì **không có mig**,
|
||||
chứ không phải vì app không khởi động lại.
|
||||
|
||||
## §4 — Seed 49 menu-row + 637 permission-row + backfill label — ✅ PASS (TAM-GIÁC 3 NGUỒN, khớp TỪNG ĐỒNG)
|
||||
|
||||
Mọi số dưới đo **SAU recycle** (recycle-proof ở §4.1). Đối chiếu với dự đoán §0.2 **đã ghi đĩa TRƯỚC khi đo**:
|
||||
|
||||
| Chỉ số | PRE (§0.1) | DỰ ĐOÁN §0.2 | ĐO POST | |
|
||||
|---|---|---|---|---|
|
||||
| `MENU_TOTAL` | 151 | **200** | **200** | ✔ |
|
||||
| `MenuItems` LIKE `Khkk%` | 7 | **56** | **56** | ✔ |
|
||||
| `Khkk_G1`.Label | "1. Kế hoạch ký kết HĐ (NCC-TP)" | "1. Thiết bị, máy móc, vật tư phụ (A1)" | **đúng chuỗi đó** | ✔ |
|
||||
| `PERM_TOTAL` | 1584 | **2221** | **2221** | ✔ |
|
||||
| `Permissions` LIKE `Khkk%` | 91 | **728** (=56×13) | **728** | ✔ |
|
||||
| … trong đó `CanRead=1` | 91 | 728 | **728** (100%) | ✔ |
|
||||
| `MIGCOUNT` | 71 | 71 | **71** | ✔ |
|
||||
|
||||
**3 NGUỒN ĐỘC LẬP cùng ra 49 / 637** (khuôn tam-giác K1):
|
||||
- (a) **source-parse**: `KhkkGroupNumbers`[2..8] × (1 group + `KhkkLeafActions` 6) = 7×7 = 49; grant 49 × 13 role = 637.
|
||||
- (b) **sqlcmd prod**: menu 151→200 (+49) · perm 1584→2221 (**+637**).
|
||||
- (c) **Serilog prod** (nguồn thứ 3, độc lập hoàn toàn với 2 nguồn trên):
|
||||
```
|
||||
10:16:30.458 [INF] Seeded 49 menu items
|
||||
10:16:30.531 [INF] Backfilled 1 menu labels
|
||||
10:16:30.711 [INF] Seeded 49 admin permissions
|
||||
10:16:31.298 [INF] Seeded all-roles review perms: 588 added + 0 upgraded (139 keys × 13 roles)
|
||||
```
|
||||
🔑 **Phân rã 637 khớp CHÍNH XÁC cơ chế 2-site**: site-1 (loop `MenuKeys.All`, chỉ Admin) = **49**;
|
||||
site-2 (`KhkkKeys().Concat(KhkkGroupMenuKeys)`, mọi role, **skip-existing** nên Admin không đếm lại) =
|
||||
**588 = 49 × 12 role còn lại**. **49 + 588 = 637** ✔. Con số 588 này KHÔNG suy ra được từ tổng —
|
||||
nó chỉ khớp nếu đúng cơ chế skip-existing ⇒ đây là bit thông tin thật, không phải trùng khớp số học rỗng.
|
||||
`0 upgraded` cũng đúng: 49 key MỚI toàn đi nhánh INSERT.
|
||||
|
||||
**SHAPE element-wise 8 nhóm** (không chỉ đếm — soi từng dòng, khớp bảng nhãn owner `danh-muc-sp002-transcribe.md`):
|
||||
```
|
||||
Khkk_G1|ord=1|kids=6| 1. Thiết bị, máy móc, vật tư phụ (A1) <- nhãn ĐÃ backfill
|
||||
Khkk_G2|ord=2|kids=6| 2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)
|
||||
Khkk_G3|ord=3|kids=6| 3. Hoạt động văn phòng (A4)
|
||||
Khkk_G4|ord=4|kids=6| 4. Vật tư (B1)
|
||||
Khkk_G5|ord=5|kids=6| 5. Thầu phụ xây dựng (B2)
|
||||
Khkk_G6|ord=6|kids=6| 6. Thầu phụ MEP (B3)
|
||||
Khkk_G7|ord=7|kids=6| 7. BCH công trường (C1-C3)
|
||||
Khkk_G8|ord=8|kids=6| 8. HĐ chưa ủy quyền — CEO (5.2)
|
||||
```
|
||||
8 nhóm × đúng **6 kids** mỗi nhóm · Order 1..8 liền không trùng · 8/8 nhãn khớp bảng owner từng ký tự.
|
||||
|
||||
**Control ÂM (chứng "đúng NGƯỜI, đúng TẬP", không quét bừa):**
|
||||
- `CTRL_OUTOFSET_G0_G9plus = 0` — không đẻ `Khkk_G0`/`Khkk_G9+` ngoài miền 1..8.
|
||||
- **Revoker #84 còn SỐNG mà KHÔNG đụng Khkk**: `Hrm*`/`Off*` có 377 row nhưng `CanRead=1` chỉ **29** —
|
||||
**y hệt PRE (29)**. Nếu revoker chết thì số này bung lên 377; nếu revoker quét lố sang `Khkk_*` thì 728
|
||||
đã tụt. Cả hai đều không xảy ra ⇒ checker còn răng VÀ phạm vi đúng (chân-kiềng iii).
|
||||
|
||||
## §4.1 — Recycle-proof (2 nguồn) — mọi số §4 đo SAU khởi động lại
|
||||
|
||||
- `appcmd list wp` → `WP "7020" (applicationPool:SolutionErp-Api)`; `Get-Process w3wp` → PID 7020
|
||||
StartTime **01/08 10:16:22**. **KHÁC PRE (PID 4876 @ 09:11:51)** ⇒ pool ĐÃ recycle, đúng cửa-sổ deploy #438.
|
||||
(VPS có 6 w3wp chung VIETREPORT ⇒ map PID→pool bằng `appcmd`, CẤM đoán PID.)
|
||||
- Nguồn 2: dòng **ĐẦU TIÊN** của `solution-erp-20260801.log` = `10:16:23.961 [INF] Applying migrations...`
|
||||
(deploy xoá `logs/` ⇒ dòng đầu = boot). Seed log 10:16:30 nằm sau đó ⇒ đúng lần boot này.
|
||||
|
||||
## §6 — Bundle ×2 — DỰ ĐOÁN TỪ SOURCE **trước khi curl** (ghi đĩa trước, đo sau)
|
||||
|
||||
🔴 **LOẠI marker lead gợi ý — nó KHÔNG CÓ RĂNG.** Đề bài đề xuất `Kết cấu, hạ tầng, phá dỡ` (từ
|
||||
`KHKK_APPROVAL_GROUP_LABELS`) với kỳ vọng "0-hit PRE / 1-hit POST". Đo nguồn:
|
||||
`git show 7a903cf:fe-user/src/types/khkk.ts | grep -c` = **1** (và fe-admin cũng **1**) ⇒ chuỗi này ĐÃ CÓ
|
||||
từ trước K4 (nó vào repo ở wave trước, K4 chỉ thêm comment quanh nó) ⇒ **đã nằm sẵn trong bundle PRE**
|
||||
⇒ 0 bit. Cùng lớp sai lầm với `earlyFinalizeLocked` (§5 k3). Thay bằng 3 marker sau, đều là **string
|
||||
literal** (minifier giữ nguyên) và đều có **transition PRE→POST đo được**:
|
||||
|
||||
| Marker | Loại | Nguồn PRE `7a903cf` | Nguồn POST `88e7ecf` | Kỳ vọng bundle LIVE |
|
||||
|---|---|---|---|---|
|
||||
| `/khkk/list?group=1` | POS | admin 0 · user 0 | admin 4 · user 4 | **>0 CẢ HAI** |
|
||||
| `/khkk/list?view=deleted` | NEG | admin 1 · user 1 | admin 0 · user 0 | **0 CẢ HAI** |
|
||||
| `workflow-matrix?type=10&group=` | DIFFERENTIAL | admin 0 · user 0 | admin 1 (**trong `//` comment**) · user 2 (**code**) | **admin 0 · user 2** |
|
||||
|
||||
- NEG **ĐỘC QUYỀN** (bài "control ÂM phải độc quyền"): `grep -rn` toàn `fe-admin/src`+`fe-user/src` bản POST
|
||||
= **0 hit** ⇒ vắng mặt trong bundle là vắng mặt THẬT, không phải chuỗi còn sống ở chỗ khác.
|
||||
- DIFFERENTIAL là marker mạnh nhất lượt này, **2 mũi cùng lúc**: (1) chứng bất-đối-xứng CỐ Ý đã ship
|
||||
(fe-admin cố ý KHÔNG nới regex `Khkk_G*` — lead chốt 27/07), (2) **tự chứng comment bị strip** —
|
||||
admin có đúng 1 lần xuất hiện và nó nằm trong `//` (dòng 39) nên phải ra 0; nếu admin ra 1 thì hoặc
|
||||
comment lọt vào bundle hoặc tao grep nhầm file.
|
||||
|
||||
### §6.1 — ĐO BUNDLE (sau khi dự đoán trên đã ghi đĩa) — ✅ PASS, khớp 3/3 marker
|
||||
|
||||
| App | PRE (§0.1) | POST (đo 10:23) | CT | Size | Last-Modified |
|
||||
|---|---|---|---|---|---|
|
||||
| admin js | `index-1yiNV4VH.js` | **`index-qOcoitOr.js` ROTATE** | application/javascript | 1.756.651 | 01/08 03:15:14 GMT = **10:15:14 +07** |
|
||||
| admin css | `index-Cf3iCOOv.css` | `index-Cf3iCOOv.css` FROZEN | text/css | 86.364 | **10:15:14 +07** |
|
||||
| eoffice js | `index-DoULfmdT.js` | **`index-D9J3BbcA.js` ROTATE** | application/javascript | 1.673.668 | 01/08 03:16:16 GMT = **10:16:16 +07** |
|
||||
| eoffice css | `index-DX1FRk5n.css` | `index-DX1FRk5n.css` FROZEN | text/css | 92.148 | **10:16:16 +07** |
|
||||
|
||||
- **JS ROTATE ×2 = ĐÚNG YÊU CẦU** (cả 2 app đổi code thật: Layout + ListPage + CreatePage + types).
|
||||
- Cả 4 LM **trong cửa-sổ deploy** 10:10:19→10:16:42 ⇒ deploy CÓ chạm cả 4 file (#69: LM-in-window mới là
|
||||
ship-proof, hash-delta thì không). **CSS FROZEN lành tính**: K4a/K4b không thêm class Tailwind mới
|
||||
(chỉ đổi chuỗi route + logic), LM vẫn in-window ⇒ re-ship byte-identical (#69 mode-B), KHÔNG phải kẹt bundle cũ.
|
||||
- **Control ÂM 2 tầng** — hash CŨ **và** hash BỊA cùng rơi `text/html` 900 B / 876 B:
|
||||
`admin/index-1yiNV4VH.js`→text/html 900 · `eoffice/index-DoULfmdT.js`→text/html 876 · `admin/index-ZZfakeZZ.js`→text/html 900.
|
||||
⇒ `text/html` thật sự nghĩa "không tồn tại" (SPA-fallback), và **bundle PRE đã biến mất khỏi đĩa = chứng ghi-đè**.
|
||||
|
||||
**Byte-marker đo trên file tải về (`adm.js` 1.738.624 ký tự · `eof.js` 1.658.019 ký tự):**
|
||||
|
||||
| Marker | Kỳ vọng §6 | admin | eoffice | |
|
||||
|---|---|---|---|---|
|
||||
| POS `/khkk/list?group=1` | >0 cả hai | **4** | **4** | ✔ (khớp ĐÚNG 4 = số lần trong source mỗi app — element-wise, không chỉ ">0") |
|
||||
| NEG `/khkk/list?view=deleted` | 0 cả hai | **0** | **0** | ✔ (chuỗi độc quyền, PRE source =1 ⇒ transition đo được) |
|
||||
| DIFF `workflow-matrix?type=10&group=` | admin 0 · user 2 | **0** | **2** | ✔ (bất-đối-xứng cố ý ship + comment bị strip đúng như dự đoán) |
|
||||
| _(marker lead gợi ý)_ `Kết cấu, hạ tầng, phá dỡ` | — | **1** | **1** | 🔴 **=1 ở CẢ HAI ⇒ xác nhận bằng ĐO rằng nó 0-bit**: có mặt bất kể K4 ship hay không |
|
||||
|
||||
## §5 — Acceptance A1 (quyền user-thường) + A5 (idempotent qua restart ×2) — ✅ PASS
|
||||
|
||||
**A1 — SQL JOIN `Permissions × Roles × MenuItems`, role NON-admin:**
|
||||
```
|
||||
Procurement : GROUP = 8 LEAF = 48 (CanRead=1)
|
||||
Drafter : GROUP = 8 LEAF = 48 (CanRead=1)
|
||||
```
|
||||
- Khớp ĐÚNG kỳ vọng đề bài "8 group + 48 leaf". Toán khép: group 8 = `Khkk_G1` + `Khkk_G2..G8`;
|
||||
leaf 48 = 6 (nhóm-1 không-infix) + 42 (nhóm 2..8) ⇒ tổng 56 = `MENU_KHKK_PCT`.
|
||||
- Toàn họ: `PERM_KHKK_CANREAD1 = 728 = 56 × 13` ⇒ **KHÔNG role nào bị hụt** (nếu 1 role sót thì ≤ 715).
|
||||
|
||||
**A1-bis — chứng ĐI TRỌN STACK (nguồn thứ 3, mạnh nhất): `GET /api/menus/me` materialize qua EF**
|
||||
(http 200, 44.656 B; parse cây theo `key`, không grep thô):
|
||||
```
|
||||
GROUP distinct = 8 LEAF distinct = 42 per-group = {G2..G8 : 6 mỗi nhóm}
|
||||
OLD nhóm-1 leaf non-infix = 6 [Khkk_Approved, Khkk_Create, Khkk_Deleted, Khkk_List, Khkk_Pending, Khkk_WfView]
|
||||
TOTAL Khkk* distinct = 56
|
||||
Khkk_G1 -> 1. Thiết bị, máy móc, vật tư phụ (A1) Khkk_G5 -> 5. Thầu phụ xây dựng (B2)
|
||||
Khkk_G2 -> 2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3) Khkk_G6 -> 6. Thầu phụ MEP (B3)
|
||||
Khkk_G3 -> 3. Hoạt động văn phòng (A4) Khkk_G7 -> 7. BCH công trường (C1-C3)
|
||||
Khkk_G4 -> 4. Vật tư (B1) Khkk_G8 -> 8. HĐ chưa ủy quyền — CEO (5.2)
|
||||
```
|
||||
⇒ 8 nhãn ra ĐÚNG tới dấu tiếng Việt qua đường Domain→Infra→Api (không phải chỉ có row trong bảng);
|
||||
`per-group = 6 đều` chứng không nhóm nào thiếu leaf. **Đây là cái sqlcmd không nói được.**
|
||||
|
||||
**A5 — RESTART APP-POOL ×2 THẬT** (`appcmd recycle apppool /apppool.name:SolutionErp-Api`, KHÔNG gọi seeder lẻ
|
||||
qua reflection — chuỗi `SeedAsync` đủ chỉ chạy lúc boot):
|
||||
|
||||
| Mốc | w3wp PID | StartTime | MENU_TOTAL | Khkk menu | PERM_TOTAL | Khkk perm | CanRead=1 | A1 Proc leaf | CTRL Hrm/Off CanRead=1 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| sau deploy | 7020 | 10:16:22 | 200 | 56 | 2221 | 728 | 728 | 48 | **29** |
|
||||
| **restart #1** | **220** | **10:23:55** | 200 | 56 | 2221 | 728 | 728 | 48 | **29** |
|
||||
| **restart #2** | **6636** | **10:24:38** | 200 | 56 | 2221 | 728 | 728 | 48 | **29** |
|
||||
|
||||
- **Restart là THẬT, không no-op**: PID đổi 3 lần (7020→220→6636) + `appcmd` xác nhận đúng pool
|
||||
`SolutionErp-Api` + Serilog có **3 dòng `Applying migrations...`** (10:16:23 · 10:23:57 · 10:24:39).
|
||||
- **Idempotent**: sau boot 2 và 3, Serilog **KHÔNG có** `Seeded … menu items` / `Seeded … admin permissions` /
|
||||
`Seeded all-roles review perms`. Đây là suy luận CÓ CƠ SỞ, không phải đoán từ im-lặng — đã soi điều kiện
|
||||
log trong source: `DbInitializer.cs:2180-2183 if (added > 0)` · `:2247 if (updatedLabels > 0)` ·
|
||||
`:2310 if (added > 0)` · `:2524 if (added > 0 || upgraded > 0)`. ⇒ vắng dòng ⟺ **0 added / 0 upgraded**.
|
||||
Cộng với 8 con số DB đứng im hoàn toàn ⇒ seeder hội tụ, không nhân bản row.
|
||||
- **Revoker-proof (#84)**: `Hrm*`/`Off*` giữ **29/377** CanRead=1 qua cả 3 mốc. Đây là control ÂM có RĂNG
|
||||
2 chiều: revoker vẫn CHẠY mỗi boot (nếu chết → bung lên 377) và vẫn KHÔNG chạm `Khkk_*` (nếu quét lố →
|
||||
728 tụt). Đúng như comment K4a khai ở `DbInitializer.cs` ("filter chỉ bắt prefix `Hrm*`/`Off*` + `Personal`").
|
||||
- Nhãn `Khkk_G1` giữ nguyên bản backfill qua cả 2 restart ⇒ `labelBackfill` không lật ngược về skeleton.
|
||||
|
||||
## §7 — Smoke prod — ✅ PASS
|
||||
|
||||
```
|
||||
GET /api/contract-signing-plans (no token) -> 401 authz đóng đúng
|
||||
GET /api/contract-signing-plans/deleted?group=1 (no token) -> 401 route MỚI cũng đóng
|
||||
GET /api/contract-signing-plans-khong-ton-tai-xyz -> 404 \ 2 route BỊA
|
||||
GET /api/route-rac-k4ab-999 -> 404 / ⇒ 401 ở trên CÓ NGHĨA
|
||||
GET /health/ready -> 200
|
||||
GET /health/live -> 200
|
||||
GET https://admin.solutions.com.vn/ -> 200
|
||||
GET https://eoffice.solutions.com.vn/ -> 200
|
||||
```
|
||||
🔑 **401 chỉ có nghĩa vì đi kèm 404 của route bịa** — nếu server nuốt mọi path thành 401 thì phép đo rỗng.
|
||||
|
||||
**Giới hạn phép đo đã gỡ được (đề bài dự phòng đúng):** `?group=abc` **không token → 401**, tức authz chặn
|
||||
TRƯỚC model-binding ⇒ không đọc được bind-error. Đã lấy bearer admin re-probe để đóng lỗ này:
|
||||
```
|
||||
(có token) ?group=abc -> 400 <- BIND-ERROR: param ĐƯỢC WIRE thật (nếu bị nuốt, ASP.NET trả 200)
|
||||
(có token) ?group=1 -> 200 ?group=8 -> 200 (không group) -> 200
|
||||
(có token) /deleted?group=1 -> 200
|
||||
body ?group=1 = {"items":[],"total":0,"page":1,"pageSize":20,"totalPages":0,"hasNext":false,"hasPrev":false}
|
||||
```
|
||||
⇒ `?group=` **nối end-to-end THẬT** (chứng bằng 400 bind-error, không bằng lời khai).
|
||||
|
||||
⚠️ **2 quan sát KHÔNG hạ verdict, chuyển tiếp cho wave sau:**
|
||||
1. **Chân-lý-rỗng còn nguyên (kế thừa K2):** `ContractSigningPlans` prod vẫn **0 dòng** ⇒ `items:[]` với MỌI
|
||||
group. Phép đo `?group=n` chứng được **đường ống + binding**, KHÔNG chứng được **lọc đúng nhóm** —
|
||||
backfill/lọc sai sẽ trông y hệt. Muốn có răng phải có ≥1 phiếu thật ở ≥2 nhóm khác nhau.
|
||||
2. `?group=999` → **200** (không phải 400/404): biên API vẫn không chặn ngoài-miền 1..8 — **y hệt K2 ghi
|
||||
nhận**, chưa hồi quy nhưng cũng chưa đóng. Vô hại hôm nay vì bảng rỗng.
|
||||
|
||||
---
|
||||
|
||||
END sub-cicd-verify-k4ab — VERDICT=PASS 7/7
|
||||
(1: run #438 success 6m23s match head_sha + #437 F-1 success 6m22s, chuỗi #436→#437→#438 liền mạch ·
|
||||
2: gate 621 = 45D + 576I tách-phần, F-1 = 614 (+0) ⇒ trọn +7 thuộc K4, control ÂM Failed!=0 cả 2 log ·
|
||||
3: Mig GIỮ 71 / top 20260731085624, 2 nguồn repo ⟂ prod ·
|
||||
4: seed 49 menu + 637 perm + backfill 1 label — TAM-GIÁC 3 nguồn (source-parse ⟂ sqlcmd ⟂ Serilog
|
||||
49+588=637), shape 8 nhóm × 6 kids element-wise, recycle-proof PID 4876→7020 + Serilog dòng đầu ·
|
||||
5: A1 Procurement/Drafter 8-group + 48-leaf + menus/me materialize 8+42+6=56 qua EF; A5 restart ×2 THẬT
|
||||
(PID 7020→220→6636) 8 chỉ số ĐỨNG IM + log-condition đã soi source ⇒ 0 added; revoker Hrm/Off giữ 29 ·
|
||||
6: bundle JS ROTATE ×2 (1yiNV4VH→qOcoitOr · DoULfmdT→D9J3BbcA), 4 LM in-window, marker 3/3 khớp
|
||||
(POS 4/4 · NEG 0/0 · DIFF admin 0 / user 2); marker lead gợi ý ĐO RA 1/1 ⇒ bác vì 0-bit ·
|
||||
7: smoke 401/401 + 404 route bịa ×2 + 200 ×4; bind-error 400 chứng ?group= wire thật)
|
||||
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
IMPLFE-K4C: DONE
|
||||
|
||||
# Lane FE K4c — WfView lọc theo nhóm (S166, 2026-08-01) — chốt cụm-2
|
||||
|
||||
Nền: K5 commit `ce35f90`. SPEC PIN: spec-cụm2 vá K4c-10 (phần WfView) + gate-K4b MAJOR-C (FLAG-3 lượt K4b của tôi: `?group=` trên workflow-matrix chưa có consumer).
|
||||
Phạm vi: **fe-user ONLY** — fe-admin không có `WorkflowMatrixViewPage` (lead chốt 2026-07-27 giữ lệch; K4a đã comment tại `isAdminHidden`).
|
||||
|
||||
## §0 Kế hoạch đo trước khi gõ
|
||||
- [ ] Đọc `WorkflowMatrixViewPage.tsx`: nguồn data, tên field `code` THẬT của DTO workflow, cách đọc `?type=`
|
||||
- [ ] Xác nhận fetch toàn bộ hay fetch-theo-param (quyết queryKey có cần trục `group` không)
|
||||
- [ ] Đối chiếu `khkkGroupWorkflowCode(n)` (K4a) với code seed BE thật
|
||||
|
||||
## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ
|
||||
|
||||
### 1.1 Tên field DTO — ĐỌC, không đoán
|
||||
`types/approvalWorkflowV2.ts:39-52` `AwDefinitionDto` có **`code: string`** (`:41`) — đúng tên đề bài dặn phải kiểm. Trang đang render nó ở `:165` (`{wf.code} v{version}`). ⇒ `wf.code === khkkGroupWorkflowCode(n)` khớp field thật.
|
||||
|
||||
### 1.2 Nguồn dữ liệu ⇒ quyết định queryKey (mục 2 đề bài)
|
||||
`:59-67` `useQuery({ queryKey: ['workflow-matrix', typeInt], … api.get('/approval-workflows-v2', { params: { applicableType: typeInt, isUserSelectable: true } }) })`, rồi `:70-71` `summary?.history ?? []`.
|
||||
⇒ Trang **tải TRỌN bộ quy trình của type**, nhóm chỉ là **lát cắt của chính payload đó** ⇒ lọc CLIENT-side, **KHÔNG thêm trục `group` vào queryKey**. Thêm vào sẽ đẻ 8 lần gọi mạng cho cùng một payload + 8 ô cache trùng nội dung. **Khai rõ theo đúng yêu cầu mục 2.**
|
||||
|
||||
### 1.3 Đối chiếu helper K4a ↔ seed BE thật
|
||||
`DbInitializer.cs:609` `var code = $"KHKK-N{n}"` · `:623` **`IsUserSelectable = true`** ⇒ 8 workflow nhóm THỰC SỰ lọt qua bộ lọc `isUserSelectable: true` của trang (nếu seed để `false` thì lọc kiểu gì cũng ra trang trắng — đo trước, không giả định). Helper `khkkGroupWorkflowCode` (types K4a) sinh đúng khuôn đó; cùng chuỗi BE dùng gác phiếu-pin-đúng-nhóm (`ContractSigningPlanFeatures.cs:420` regex `^KHKK-N([1-8])$`).
|
||||
|
||||
### 1.4 `PageHeader` (bản `@/components/PageHeader`)
|
||||
Props = `{ title: ReactNode; description?: ReactNode; actions?: ReactNode }` — **có `actions`** ⇒ đặt đường "Xem tất cả nhóm" ở đó, không phải chế thêm thanh công cụ. (Ghi để khỏi lặp bài S37: repo có ≥2 bản PageHeader khác chữ ký, phải đọc bản đang import.)
|
||||
|
||||
### 1.5 Trang gắn 3 route
|
||||
`/purchase-evaluations/workflow-matrix` · `/contracts/workflow-matrix` · `/khkk/workflow-matrix` (App.tsx `:71/:73/:83`) ⇒ link "xem tất cả" phải bám `useLocation().pathname`, hardcode 1 đường là gãy 2 đường kia.
|
||||
|
||||
## §2 THỰC THI — 1 file, fe-user ONLY (+68 / −5)
|
||||
|
||||
`fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx`:
|
||||
1. `rawGroup = searchParams.get('group')`; `khkkGroup = typeInt === 10 && /^[1-8]$/.test(rawGroup) ? Number : null`. **Chốt chặn kép**: sai type HOẶC group rác ⇒ `null` ⇒ 0 đổi hành vi.
|
||||
2. `allWorkflows` (đổi tên từ `workflows`) → `workflows` = lọc `wf.code === khkkGroupWorkflowCode(khkkGroup)` khi có nhóm. Nhiều VERSION cùng `code` thì giữ đủ (đúng nghĩa "quy trình của nhóm n", không phải "1 bản ghi").
|
||||
3. Header: badge tím `Nhóm N{n}` cạnh tiêu đề + `description` mở đầu bằng `khkkGroupMenuLabel(n)` ⇒ 8 leaf không còn mở ra 8 trang trông hệt nhau.
|
||||
4. `actions`: `<Link to={`${pathname}?type=${typeInt}`}>Xem tất cả nhóm</Link>` — bỏ `group`, giữ đường dẫn đang đứng.
|
||||
5. **Trạng thái rỗng tách 2 nghĩa** (bài "sai-thầm-lặng → thiếu-nhìn-thấy-được"): rỗng-vì-lọc in "…cho nhóm N{n} (loại phiếu này có {N} quy trình ở nhóm khác)"; rỗng-thật giữ nguyên câu cũ. Dùng chung 1 câu thì người dùng nhóm N5 tưởng cả module chưa cấu hình trong khi 7 nhóm kia đang chạy.
|
||||
|
||||
## §3 CHỨNG
|
||||
|
||||
**A. Acceptance 8 URL + các ca bỏ qua** — chạy CHÍNH 2 khối đã ship (`const rawGroup…` và `const workflows =…` cắt từ file bằng `indexOf`) + helper cắt từ `types/khkk.ts` (chỉ bỏ annotation `n: number`, có assert khuôn trước khi thế); dữ liệu thử là 8 code sinh theo **khuôn đọc thẳng `DbInitializer.cs`**. **21/21 OK:**
|
||||
```
|
||||
A ?type=10&group=1..8 → mỗi URL ĐÚNG 1 wf, code KHKK-N1..N8 khớp từng số
|
||||
B ?type=10 trần → hiện CẢ 8
|
||||
group=0 · 9 · abc · rỗng · "03" → KHÔNG lọc (hiện 8) ← "03" là ca dễ lọt nếu dùng Number()
|
||||
type=1|2|3 kèm group=5 → BỎ QUA, 0 đổi hành vi
|
||||
C 2 version cùng code KHKK-N3 → giữ đủ 2
|
||||
D CONTROL-ÂM ① gỡ vị-ngữ lọc (`=== code` → `true`) ⇒ ra 8 wf (phép đo PHÂN BIỆT được)
|
||||
CONTROL-ÂM ② helper sai khuôn (`KHKK_N3`) ⇒ 0 wf, KHÔNG âm thầm khớp
|
||||
```
|
||||
|
||||
**B. Build** — `npx tsc -b` exit 0; `npm run build` fe-user PASS: **1950 modules**, `dist/assets/index-DBtbmn5s.js` 1,643.96 kB (gzip 415.22) · `index-CHYZ6n3Q.css` 92.71 kB · 531ms. Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT` = PRE-EXISTING.
|
||||
|
||||
**C. Rào** — `git diff --stat`: **đúng 1 file** `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` (68+/5−). `git diff --stat -- fe-admin` = **RỖNG (0-diff)** ✅ đúng lệnh fe-user-ONLY. 0 BE, 0 file khác, **0 commit**.
|
||||
|
||||
## §4 FLAG cho lead
|
||||
|
||||
**FLAG-1 (MINOR — đóng lại FLAG-3 của K4b):** gate-K4b MAJOR-C xử lý xong, `?group=` trên workflow-matrix nay có consumer. Còn lại **1 lệch cố ý đã khai từ K4a**: leaf `Khkk_WfView` của **nhóm 1 phía fe-admin** trỏ Designer (`/system/approval-workflows-v2/ContractSigningPlan`) chứ không phải matrix ⇒ admin không có đường xem-luồng-theo-nhóm. Đúng chủ trương "fe-admin không có `WorkflowMatrixViewPage`" (lead chốt 2026-07-27), nêu lại để cụm-2 đóng sổ mà không ai tưởng là sót.
|
||||
|
||||
**FLAG-2 (MINOR — quan sát, chưa cần làm):** trang lọc theo `code`, nên nếu Admin đổi `Code` của một workflow nhóm trong Designer (ví dụ `KHKK-N5` → `KHKK-N05`) thì leaf nhóm đó ra trang rỗng — **có báo rõ** nhờ câu rỗng-vì-lọc mới, không im lặng. Rào cứng đã nằm ở BE (`Features.cs:420` cũng khớp `^KHKK-N([1-8])$` nên phiếu sẽ không pin được vào workflow đổi mã). Nếu muốn chặt hơn thì khoá sửa `Code` cho 8 workflow nhóm ở Designer — lane admin, không phải lượt này.
|
||||
|
||||
IMPLFE-K4C-END
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
IMPLFE-K5: DONE
|
||||
|
||||
# Lane FE K5 — cây toàn-trình per-gói: GĐ2 mở 8 sub-folder NHÓM (S166, 2026-08-01)
|
||||
|
||||
Nền: K4a+K4b đã commit `88e7ecf`. SPEC PIN: spec-cụm2 vá-9 + checklist K5 + forward-provision B3-A5.
|
||||
|
||||
## §0 Kế hoạch đo trước khi gõ
|
||||
- [ ] Grep CHÍNH XÁC bộ file pipeline trong `fe-user/src` (spec nói "4 file" — đừng tin, đếm)
|
||||
- [ ] Đọc `usePipelineStages.ts`: shape DTO builder đang ăn, có `lines[].contractId` không
|
||||
- [ ] Đọc component render folder: tiền lệ folder GĐ "SẮP" (hiện-cả-khi-rỗng)
|
||||
- [ ] SHA baseline từng file ×2 app
|
||||
|
||||
## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ
|
||||
|
||||
### 1.1 Bộ file pipeline = ĐÚNG 4, và cả 4 ĐANG SHA-pair ×2 app
|
||||
| file | sha256 baseline (×2 app IDENTICAL) |
|
||||
|---|---|
|
||||
| `hooks/usePipelineStages.ts` | `7144eeb787a8ff8b5ed861d7278fe31450162c23da51dafb6266a3a9196ab148` |
|
||||
| `components/pipeline/PipelineStageFolders.tsx` | `d449cc8dae55c55aab4a8bd4521e113361e0859de93bdac4a4e7099f8cecdc2d` |
|
||||
| `components/pipeline/PipelineTreePanel.tsx` | `d70ac315bae485e6e0f1fad8327caf063b4b22cabde53c59d9847b0d407e73d3` |
|
||||
| `components/pe/PePipelineStrip.tsx` | `6fb7337e4bb9c60a579e1dc561f6742785e9526cd8fd4b4bce593eec1c8dda65` |
|
||||
2 call-site của `buildStages`: `PipelineTreePanel.tsx:303` và `PurchaseEvaluationsListPage.tsx:524` (cái sau truyền `stage1Content`).
|
||||
|
||||
### 1.2 Tiền lệ "hiện cả khi rỗng" CÓ SẴN — bám đúng nó, không đẻ khuôn mới
|
||||
`PipelineStageFolders.tsx:149-153`: `count > 0 ? <badge số> : <SoonChip/>`; `:166-167` thân rỗng in *"Chưa có dữ liệu ở giai đoạn này."*; `:95` `muted` làm xám nhãn. Sub-folder nhóm sẽ dùng LẠI đúng 3 thứ này (chip "Sắp" + dòng rỗng + muted), không thêm token màu mới.
|
||||
|
||||
### 1.3 🔴 FORWARD-PROVISION B3-A5: `lines[].contractId` **KHÔNG TỒN TẠI** — không bịa
|
||||
Đo 4 nơi, cả 4 đều âm:
|
||||
1. `types/purchaseEvaluation.ts` — `PeListItem:112-…` có **`contractId: string | null`** (`:126`) và **0 hit chữ `lines`** trong TOÀN file.
|
||||
2. `Domain/PurchaseEvaluations/PurchaseEvaluation.cs:31` — `public Guid? ContractId` (1 FK đơn, không phải tập).
|
||||
3. `types/contracts.ts` `ContractListItem:84-101` — **KHÔNG có `purchaseEvaluationId`** ⇒ không thể đi ngược từ index HĐ về phiếu.
|
||||
4. `Domain/Contracts/Contract.cs` — **0 hit** `PurchaseEvaluationId`/`SourcePe`.
|
||||
|
||||
🔴 **Bằng chứng đắt nhất, chính BE tự khai** (`CreateContractFromEvaluationFeatures.cs:143-145`):
|
||||
```
|
||||
// Idempotency: pe.ContractId = HĐ ĐẦU (chặn tạo lại). N HĐ liên-danh truy qua changelog +
|
||||
// Contract (FE nhận list contractIds). 1 phiếu = 1 link chính + N hợp đồng.
|
||||
pe.ContractId = contracts[0].Id;
|
||||
```
|
||||
Endpoint create TRẢ `contractIds: string[]` (FE nhận ở `PeDetailTabs.tsx:2186`) nhưng **chỉ HĐ ĐẦU được ghi ngược vào phiếu**; HĐ 2..N chỉ còn dấu vết trong changelog. ⇒ Đúng như đề bài lo: **HĐ thứ 2+ VÔ HÌNH trên cây** — và đó là giới hạn DỮ LIỆU, không phải lỗi builder.
|
||||
⇒ Vế wire được lượt này: **đổi HÌNH DẠNG builder từ "đọc 1 id" sang "gom một TẬP id"** (union), nguồn thứ hai để sẵn chỗ cắm 1 dòng. Vế KHÔNG wire được: chính nguồn thứ hai (không có field nào để đọc). FLAG-1.
|
||||
|
||||
### 1.4 GĐ3/GĐ4 "by `c.type`" — kiểm, KHÔNG đụng
|
||||
Builder hiện KHÔNG gom GĐ3/4 theo loại HĐ; `c.type` chỉ dùng ở `onOpen` GĐ4 → `/hard-copies?type=${c.type}` (`:240`, khớp `HardCopiesPage.tsx:64-67`). Đang đúng ⇒ giữ nguyên, chỉ đổi phần lấy id sang tập (1.3).
|
||||
|
||||
### 1.5 Ghi chú kiểu dữ liệu
|
||||
`KHKK_APPROVAL_GROUPS` là tuple `as const` ⇒ `.includes(g: number)` sẽ đỏ TS; phải ép `as readonly number[]`. `k.approvalGroup` khai `number` nhưng payload cũ trong cache React Query có thể thiếu (K2 để `!= null` guard ở 2 page) ⇒ lọc bằng `typeof g === 'number'`. `import.meta.env` có sẵn (5 call-site), dùng `.DEV` cho assert Σ.
|
||||
|
||||
## §2 THỰC THI — 2/4 file pipeline đổi (2 file kia KHÔNG cần đụng, xem §3C)
|
||||
|
||||
### F1 — `components/pipeline/PipelineStageFolders.tsx` ×2 (+165 / −54)
|
||||
- Type mới `PipelineGroup {key,label,leaves,onOpenAll?}` + `PipelineStage.groups?`.
|
||||
🔴 `stage.leaves` GIỮ vai trò danh sách PHẲNG canonical (nguồn duy nhất để đếm — `stageCount`, `hideEmpty`, chip số đều không phải sửa); `groups` chỉ **chia lại chính nó**. Nhờ vậy bất-biến Σ là chuyện CẤU TRÚC chứ không phải kỷ luật.
|
||||
- Tách `LeafList` (dùng chung thân folder ⟂ thân ngăn — 0 nhân bản markup) + thêm `GroupFolder`.
|
||||
- `GroupFolder`: `<details>` mặc định ĐÓNG (8 ngăn mở sẵn = tường chữ ở cột trái 19rem); summary = chevron + **nút tên ngăn** (`preventDefault`+`stopPropagation` ⇒ đi trang mà không kèm một nhịp đóng/mở nhấp nháy) + chip số **hoặc `SoonChip` khi rỗng**; thân rỗng in "Chưa có phiếu ở nhóm này."
|
||||
- Tái dùng 100% token cũ (`SoonChip`, `style.head/count/code`, `muted`→`slate-500` giữ tương phản AA) — **0 màu mới, 0 icon mới**.
|
||||
|
||||
### F2 — `hooks/usePipelineStages.ts` ×2 (+122 / −54)
|
||||
- **GĐ2 chia ngăn:** `toKhkkLeaf` tách ra dùng 2 nơi; phân hoạch theo `k.approvalGroup`; 8 ngăn dựng từ `KHKK_APPROVAL_GROUPS` + nhãn `khkkGroupMenuLabel(n)` (types, K4a); `onOpenAll → /khkk/list?group=${n}` khớp ĐÚNG URL leaf sidebar K4b ⇒ bấm ngăn hay bấm menu ra cùng trang, và trang tự sáng đúng mục.
|
||||
- **Hiện đủ 8 kể cả rỗng** (`.map` trên hằng, không `.filter`) — mirror tiền lệ folder GĐ chưa có gì.
|
||||
- **Ngăn "(chưa phân nhóm)"** chỉ mọc khi THẬT SỰ có phiếu lạc (nhóm null/undefined/thiếu field/ngoài 1..8). Khác 8 ngăn kia: 8 nhóm là khung nghiệp vụ cố định, đây là chỗ chứa bất thường — hiện thường trực một ngăn luôn rỗng là nhiễu.
|
||||
- **Bất-biến Σ:** bảo đảm bằng vòng phân hoạch; thêm chuông `import.meta.env.DEV && console.warn` phòng người sau sửa vòng lặp thành lọc-trùng/lọc-sót.
|
||||
- **Forward-provision B3-A5:** `contractIdsOfPackage = new Set<string>()` gom id (union) rồi mới dựng GĐ3/GĐ4; chỗ cắm nguồn thứ hai để sẵn kèm ghi chú vì-sao + trích nguyên văn BE. `seen` cũ bỏ (Set tự khử trùng). Hành vi GĐ3/4 **0 đổi** (cùng tập HĐ, cùng thứ tự, cùng dedup).
|
||||
- **GĐ3/GĐ4 theo `c.type`:** kiểm — builder KHÔNG gom theo loại; `c.type` chỉ dùng ở `onOpen` GĐ4 → `/hard-copies?type=` (khớp `HardCopiesPage.tsx:64-67`). Đang đúng ⇒ **KHÔNG đụng** (đúng lệnh mục 2).
|
||||
|
||||
## §3 CHỨNG (mọi phép đo kèm CONTROL)
|
||||
|
||||
**A. Phân hoạch + Σ + "hiện đủ 8"** — chạy CHÍNH khối text đã ship (`indexOf('const khkkByGroup')` → slice → `new Function`; 4 phép thế khai minh bạch: 3 bỏ type-annotation + `import.meta.env.DEV`→cờ tiêm, mỗi phép thế đều assert khớp trước khi chạy — không khớp thì script tự chết chứ không âm thầm chạy tiếp). **15/15 OK:**
|
||||
```
|
||||
CA1 rải đều 8 ngăn · Σ 5=5 · nhóm-2 có 2 · nhóm-3 RỖNG VẪN HIỆN · 0 cảnh báo
|
||||
route /khkk/list?group=1 … ?group=8 · nhãn "4. nhan-N4" đúng khuôn helper
|
||||
CA2 phiếu lạc 9 ngăn (8 + "(chưa phân nhóm)") · gom đủ 4 ca lạc (null/9/undefined/thiếu field) · Σ 5=5
|
||||
CA3 gói rỗng VẪN 8 ngăn rỗng · KHÔNG đẻ ngăn lạc thừa
|
||||
CA4 CONTROL-ÂM tiêm lỗi nhân-đôi vào chính đoạn ⇒ console.warn NỔ:
|
||||
"Σ ngăn (4) ≠ Σ folder (2) — có phiếu bị nhân đôi hoặc rơi mất khi chia nhóm."
|
||||
```
|
||||
CA4 là thứ chứng chuông Σ **có răng** — không có nó thì "0 cảnh báo" ở CA1-3 vô nghĩa (im lặng vì đúng ⟂ im lặng vì câm).
|
||||
|
||||
**B. Build ×2 PASS, 0 TS error** (`tsc -b` chạy riêng trước khi mirror cũng exit 0)
|
||||
| app | modules | bundle js | css | time |
|
||||
|---|---|---|---|---|
|
||||
| fe-user | — | `index-CZUcTUUg.js` | `index-CHYZ6n3Q.css` | 19.81s |
|
||||
| fe-admin | 1961 | `index-BZWvx0Y8.js` 1,725.90 kB (gzip 429.20) | `index-AgG72YRZ.css` 86.93 kB | 10.88s |
|
||||
Warning >500KB + `INEFFECTIVE_DYNAMIC_IMPORT` = PRE-EXISTING.
|
||||
|
||||
**C. SHA-pair 4/4 file pipeline ×2 app**
|
||||
| file | trước K5 | sau K5 | ×2 |
|
||||
|---|---|---|---|
|
||||
| `hooks/usePipelineStages.ts` | `7144eeb7…` | **`edff99314e8b75de39b0123bcdad5d98d8081a5bd294e2e05b1ef784094a19ac`** | IDENTICAL |
|
||||
| `pipeline/PipelineStageFolders.tsx` | `d449cc8d…` | **`bed52f39d595c86a24afd27180146871d52e29943731ffb52f7caf23e66de936`** | IDENTICAL |
|
||||
| `pipeline/PipelineTreePanel.tsx` | `d70ac315…` | `d70ac315…` KHÔNG ĐỔI | IDENTICAL |
|
||||
| `pe/PePipelineStrip.tsx` | `6fb7337e…` | `6fb7337e…` KHÔNG ĐỔI | IDENTICAL |
|
||||
🔴 2 file cuối **cố ý không đổi**: `PipelineTreePanel` chỉ gọi `buildStages(wg.items)` rồi đưa cho `PipelineStageFolders` — ngăn nhóm chảy qua nó mà không cần biết; `PePipelineStrip` là DẢI 4 giai đoạn, không phải cây. Hai hash bằng baseline = **đối chứng dương** cho phép đo (nếu phép so hash sai thì 2 dòng này cũng phải đổi). diff-stat đối xứng 165+/122+ ×2 app.
|
||||
|
||||
**D. Rào** — `git status`: đúng 4 file (2 cặp). 0 `src/Backend/**`, 0 `KhkkListPage`/`KhkkCreatePage`/`Layout.tsx` (K4b đã chốt), **0 commit**.
|
||||
|
||||
## §4 FLAG cho lead
|
||||
|
||||
**FLAG-1 (MAJOR — chặn hoàn thiện B3-A5, cần lead quyết + lane BE):** `lines[].contractId` **không tồn tại ở bất cứ đâu** (4 phép đo §1.3, cả FE lẫn BE). Union hôm nay chỉ có 1 nguồn ⇒ với phiếu liên danh đẻ N hợp đồng, **HĐ thứ 2+ vẫn vô hình trên cây** — tôi đã dựng đúng hình dạng để cắm, nhưng KHÔNG bịa field. Bằng chứng BE tự khai: `CreateContractFromEvaluationFeatures.cs:143-145` `pe.ContractId = contracts[0].Id` + "N HĐ liên-danh truy qua changelog". Hai đường lead chọn: **(a)** PE list DTO trả `contractIds: string[]` (rẻ — dữ liệu đã có, chỉ là không được ghi ngược) · **(b)** K7 `Line.ContractId` ghi-luôn (đắt hơn, nhưng đúng hạt "1 dòng phiếu → 1 HĐ" mà OG-1 UI-1-1 mô tả). Cắm vào builder = **1 dòng**, chỗ đã đánh dấu.
|
||||
|
||||
**FLAG-2 (MINOR — đã quyết, báo để khỏi tưởng sót):** ngăn nhóm mặc định **ĐÓNG**. 8 ngăn mở sẵn dưới mỗi gói thầu, nhân với N gói trên cây cột trái = không đọc được gì. Nếu owner muốn mở sẵn ngăn có phiếu thì 1 dòng (`defaultOpen = leaves.length > 0`) — không tự quyết vì đó là UX.
|
||||
|
||||
**FLAG-3 (MINOR — giới hạn kế thừa, không phải mới):** đếm ngăn vẫn nằm trong cửa sổ index 200 (`INDEX_PAGE_SIZE`) — gói có nhiều phiếu KHKK thì `truncated` đã cảnh báo sẵn ở chân folder cha, nhưng **cảnh báo đó không lặp lại ở từng ngăn**. Nếu thấy cần thì nhắc, tôi thêm.
|
||||
|
||||
IMPLFE-K5-END
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
GATE-K4B: IN-PROGRESS
|
||||
GATE-K4B: PASS-WITH-FLAGS (4 MAJOR, 5 MINOR — 0 CHẶN; vá MAJOR-B + chạy cổng MAJOR-D trước khi gõ commit)
|
||||
|
||||
# Gate K4b — adversarial review wave K4b (nối `?group=` end-to-end), S166
|
||||
|
||||
@ -35,3 +35,232 @@ K4a (MenuKeys/DbInitializer/Layout-regex/types/STATUS/test) ĐÃ qua gate K4a
|
||||
- `ContractSigningPlan.ApprovalGroup` là `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.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 1 — Vá-5: derive `phase`/`group` từ URL + `navKey` reset (`KhkkListPage.tsx:70-102`)
|
||||
|
||||
**Cơ chế lane dùng:** KHÔNG phải `useEffect`. Là hai lớp:
|
||||
1. **Derive thuần** — `urlFilter`/`urlPhase`/`deletedView`/`group` đọc thẳng `searchParams` mỗi render (`:74-80`). `useSearchParams` bám `location` nên đổi query (cùng route) VẪN re-render ⇒ 3 trục danh tính luôn tươi. Trước K4b `phase` là `useState(initialFilter…)` — khởi tạo 1 lần, đây đúng là nguồn của lỗi "phải F5".
|
||||
2. **Lớp override 3-trạng-thái** — `chipPhase: KhkkPhaseValue | null | undefined`, `undefined` = chưa bấm chip (nghe URL), `null` = user chọn "Tất cả", số = user chọn trạng thái. Tách `undefined` khỏi `null` là ĐÚNG và cần: gộp lại thì bấm "Tất cả" trên leaf `?filter=ChoDuyet` sẽ bị URL kéo ngược.
|
||||
3. **Reset trong render** (`:95-101`) — `navKey = ${group}|${urlFilter}|${deletedView}`, so với `lastNavKey`, khác thì `setLastNavKey` + `setChipPhase(undefined)` + `setPage(1)`. Đây là khuôn React chính thống "điều chỉnh state khi prop/input đổi", KHÔNG phải `useEffect` ⇒ không có lượt vẽ bằng dữ liệu cũ.
|
||||
|
||||
**Bảng ca chuyển tôi soi từng ca:**
|
||||
|
||||
| Ca | navKey trước → sau | reset `chipPhase` | reset `page` | `phase` cuối |
|
||||
|---|---|---|---|---|
|
||||
| leaf→leaf cùng nhóm khác filter (`?group=2` → `?group=2&filter=DaDuyet`) | `2\|\|` → `2\|DaDuyet\|` | CÓ | CÓ | `DaDuyet` |
|
||||
| leaf→leaf khác nhóm cùng filter (`?group=2&filter=ChoDuyet` → `?group=5&filter=ChoDuyet`) | `2\|ChoDuyet\|` → `5\|ChoDuyet\|` | CÓ | CÓ | `ChoDuyet` (đúng, chỉ đổi `group`) |
|
||||
| vào deleted (`?group=3&filter=DaDuyet` → `?group=3&view=deleted`) | `3\|DaDuyet\|` → `3\|\|del` | CÓ | CÓ | `null` |
|
||||
| ra khỏi deleted | `3\|\|del` → `3\|\|` | CÓ | CÓ | `null` |
|
||||
| root trần `/khkk/list` ↔ leaf nhóm 1 `?group=1` | `\|\|` ↔ `1\|\|` | CÓ | CÓ | `null` |
|
||||
| mount lần đầu | `lastNavKey` khởi tạo = `navKey` | KHÔNG (đúng) | KHÔNG | theo URL |
|
||||
|
||||
⇒ **Vá-5 ĐẠT.** Cả 6 ca đều re-render đúng, không cần F5.
|
||||
|
||||
**2 ghi chú kỹ thuật (không phải lỗi):**
|
||||
- Lượt render bị React huỷ (lượt phát hiện `navKey` đổi) vẫn chạy tới `useQuery` với `page`/`chipPhase` CŨ. TanStack v5 `getOptimisticResult` sẽ `build()` một ô cache rỗng cho khoá cũ đó; effect không chạy trên render bị huỷ nên **không có fetch thừa**, ô rỗng bị gc. Vô hại.
|
||||
- `search` CỐ Ý không reset (lane FLAG-4, lead giữ). Nó nằm trong `queryKey` nên không đẻ sai dữ liệu; chỉ là lựa chọn UX.
|
||||
|
||||
**MINOR-1 (không phải hồi quy):** bấm LẠI đúng leaf đang đứng ⇒ `navKey` không đổi ⇒ chip override KHÔNG nhả. Ví dụ: đứng ở "Đang duyệt" nhóm 1, bấm chip "Tất cả", rồi bấm lại mục "Đang duyệt" trong sidebar → vẫn hiện tất cả. Hình dạng này ĐÃ có trước K4b (khi đó `phase` là `useState` khởi tạo 1 lần) nên K4b không làm xấu đi. Muốn chữa thì so bằng `location.key` thay vì `navKey`.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 2 — Vá-3 cache-key + phán FLAG-2 (phase-decoded thay filter-thô)
|
||||
|
||||
Khoá hiện tại: `['khkk-list', { group, phase, search, page, deletedView }]` (`KhkkListPage.tsx:112`).
|
||||
|
||||
**Đủ trục chưa — đối chiếu với ĐÚNG tập tham số gửi lên server** (`:117-128`): `phase` (bỏ khi deleted), `group`, `search`, `page`, `pageSize` (hằng), + chính `url` (`/contract-signing-plans` vs `/deleted`).
|
||||
|
||||
| Thứ quyết định response | Có trong khoá? |
|
||||
|---|---|
|
||||
| endpoint (`deletedView`) | CÓ |
|
||||
| `group` | CÓ (vá-3, trục mới) |
|
||||
| `phase` | CÓ |
|
||||
| `search` | CÓ |
|
||||
| `page` | CÓ |
|
||||
| `pageSize` | hằng `PAGE_SIZE` — không cần |
|
||||
|
||||
⇒ **Không thiếu trục nào.**
|
||||
|
||||
**PHÁN FLAG-2 (lane nêu, lead nhận, tôi phán lại — ĐỘC LẬP): dùng `phase` đã giải mã là ĐÚNG, không phải nhân nhượng.**
|
||||
- Lý do quyết định: khoá cache phải định danh **REQUEST**, không định danh **URL**. `filter` thô không bao giờ được gửi đi; thứ gửi đi là `phase`. Hai chuỗi `filter` khác nhau mà cùng giải mã ra một `phase` sẽ sinh **hai request byte-identical** ⇒ gộp một ô cache là đúng, tách ra mới là lỗi (đôi cache, đôi lượt mạng cho cùng một câu hỏi).
|
||||
- Ca lệch mà spec hỏi (`?filter=rác` → `phase=null` → trùng khoá với "tất cả"): **hành vi ĐÚNG**. `?filter=rác` khiến trang hiển thị mọi trạng thái, y hệt "tất cả" — cùng màn hình, cùng dữ liệu, nên cùng ô cache. Không có gì để phân biệt.
|
||||
- Chỗ raw `filter` VẪN cần thì nó CÓ mặt: `navKey` (`:95`) dùng `urlFilter` THÔ, nên hai leaf `?filter=rác1` / `?filter=rác2` vẫn được coi là hai lượt điều hướng khác nhau và vẫn reset chip/page. Hai vai trò tách bạch đúng chỗ.
|
||||
- Điều kiện để phán này SAI (nêu ra để sau này ai đổi thì biết): nếu trang bắt đầu render khác nhau theo chuỗi `filter` thô (ví dụ in tên bộ lọc lấy từ URL). Hiện `urlFilter` chỉ chảy vào `urlPhase` + `navKey`, không chảy vào JSX — tôi đã grep hết đường đi của nó trong file.
|
||||
|
||||
**MINOR-5:** trong `deletedView`, `phase` vẫn nằm trong khoá nhưng KHÔNG được gửi (`:119`). Chip bị ẩn ở màn xoá (`:201 {!deletedView && …}`) và không leaf nào sinh URL vừa `filter` vừa `view=deleted`, nên thực tế `phase` luôn `null` ở đó. Chỉ URL gõ tay mới đẻ được 2 ô cache cho cùng một response. Vô hại, có sẵn từ trước K4b.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 3 — FLAG-1: bản vá LEAD TỰ VIẾT (BE 3 chỗ + FE 2 nhánh) — soi kỹ nhất
|
||||
|
||||
*(số đo chi tiết đã ghi ở M3 phía trên; đây là phần phán)*
|
||||
|
||||
**3 chỗ BE — đối chiếu từng nét với list SỐNG:**
|
||||
|
||||
| | List sống (K2, `:756-758` + Controller `:42,49`) | `/deleted` (K4b, lead viết) | Khớp? |
|
||||
|---|---|---|---|
|
||||
| record | `ListContractSigningPlansQuery(…, int? ApprovalGroup = null) : PagedRequest` | `ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null) : PagedRequest` | KHỚP |
|
||||
| controller param | `[FromQuery(Name = "group")] int? group = null` | y hệt | KHỚP |
|
||||
| truyền | `new Query(phase, …, group) { Page = … }` | `new Query(group) { Page = … }` | KHỚP khuôn |
|
||||
| vị trí lọc | SAU khối IDOR, TRƯỚC `Search` | SAU khối IDOR, TRƯỚC `Search` | KHỚP |
|
||||
| vị ngữ | `p.ApprovalGroup == request.ApprovalGroup` | y hệt | KHỚP |
|
||||
|
||||
**Positional-record trộn object-initializer có compile đúng thứ tự không** (spec hỏi thẳng): CÓ, và không có bẫy thứ tự. C# chạy constructor positional TRƯỚC, rồi mới gán object-initializer; hai tập không giao nhau (`ApprovalGroup` chỉ ở ctor; `Page/PageSize/Search/SortDesc` chỉ ở initializer, đều là `init` accessor của `PagedRequest`). Điều kiện ngầm khiến nó hợp lệ: `PagedRequest` phải là **record** (record không kế thừa được class) — đã kiểm: `public abstract record PagedRequest` (`Common/Models/PagedResult.cs:15`). `dotnet build` 0E do tôi tự chạy xác nhận.
|
||||
|
||||
**Validator biên 1..8:** KHÔNG có, ở CẢ HAI endpoint ⇒ **consistent, ghi nhận chứ không trừ điểm** (đúng như spec dự liệu). Hành vi đo được bằng suy luận kiểu:
|
||||
- `?group=999` → `int?` bind được → `Where(ApprovalGroup == 999)` → **200 + `items: []`, `total: 0`** (không 500, không lộ dữ liệu).
|
||||
- `?group=abc` → bind fail → **400** tự động do `[ApiController]`.
|
||||
- `?group=` (rỗng) → `null` → xem tất cả.
|
||||
- Không có ca nào NỚI phạm vi thấy được, vì vế lọc nằm SAU IDOR và chỉ thu hẹp.
|
||||
|
||||
**FE 2 nhánh:** `group: group ?? undefined` (`:124`) đặt NGOÀI biểu thức tam nguyên `deletedView`, nên áp cho cả `/contract-signing-plans` lẫn `/deleted` — đúng "cả 2 nhánh". FE chỉ gửi khi `group` đã qua `/^[1-8]$/` (`:80`) nên rác không tới server. Title/subtitle màn xoá in tên nhóm (`:166,175`) — hợp lệ VÌ server đã lọc thật; nếu thiếu vá BE thì đúng là "tên nhóm nói dối" như chú thích lead viết.
|
||||
|
||||
🔴 **MAJOR-A — `sợi-dây-1-argument`, 0 test.** Toàn bộ tính năng này treo trên 2 mẩu: `(group)` ở Controller và 3 dòng `if` ở handler. Tôi grep `tests/` cho `ListDeletedContractSigningPlansQuery` → **0 hit**; không test nào chạm handler này. Phép thử phản chứng: xoá `(group)` khỏi `new ListDeletedContractSigningPlansQuery(group)` ⇒ tính năng chết câm, mà `dotnet build` vẫn 0E, bộ test lead báo 41/41 vẫn xanh, `tsc` ×2 vẫn EXIT=0 — **không phép kiểm nào TRƯỢT**. §7 cho phép test-after với feature mới nên đây không phải điều kiện chặn commit, nhưng phải vào sổ nợ K4c: 1 test "hai phiếu xoá khác nhóm, `?group=n` chỉ trả 1" là đủ. (Khuôn có sẵn: `ContractSigningPlanApprovalTests`.)
|
||||
|
||||
---
|
||||
|
||||
## MỤC 4 — staticMap G1 vs regex nhóm 2..8: có key nào trùng-match 2 đường không?
|
||||
|
||||
**Đo:** `staticMap` (fe-user `:63-87`) dùng 6 key **KHÔNG-infix**: `Khkk_List`, `Khkk_Create`, `Khkk_Pending`, `Khkk_Approved`, `Khkk_WfView`, `Khkk_Deleted`. Regex (`:212`) là `^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$` — **bắt buộc có khúc `G{n}_`**. Hai tập **rời nhau theo cấu trúc**: không chuỗi nào vừa có vừa không có infix. Thêm nữa `if (staticMap[key]) return staticMap[key]` (`:139`) chặn TRƯỚC regex, nên kể cả có trùng thì staticMap thắng — thứ tự an toàn.
|
||||
|
||||
**Đo phía dữ liệu (không suy từ code FE):** `MenuKeys.KhkkGroupNumbers = [2,3,4,5,6,7,8]` (`MenuKeys.cs:72`) ⇒ BE **không hề sinh** key `Khkk_G1_*`; `grep -rn "Khkk_G1_" src/Backend fe-admin/src fe-user/src` = **0 hit**. Nhánh `1` trong dải `[1-8]` của regex là nhánh CHẾT — lane đã khai rõ là cố ý, tôi xác nhận vô hại hôm nay.
|
||||
|
||||
⚠️ **Bẫy ngủ (ghi để người sau biết, KHÔNG tính lỗi hôm nay):** nếu sau này ai seed `Khkk_G1_List`, sẽ có **hai leaf khác key nhưng cùng URL** `/khkk/list?group=1` (một từ staticMap, một từ regex). `queryMatches` (`:353-374`) so khớp active bằng **URL chứ không bằng key** ⇒ tái hiện đúng lớp bug UAT S155 "mục render sau thắng". Ai chuyển nhóm 1 sang khuôn infix thì phải **bỏ 6 dòng staticMap cùng lượt**.
|
||||
|
||||
**6 leaf nhóm 1 — kiểm từng URL (fe-user):**
|
||||
|
||||
| key | URL | đúng? |
|
||||
|---|---|---|
|
||||
| `Khkk_List` | `/khkk/list?group=1` | ĐÚNG |
|
||||
| `Khkk_Create` | `/khkk/create?group=1` | ĐÚNG |
|
||||
| `Khkk_Pending` | `/khkk/list?group=1&filter=ChoDuyet` | ĐÚNG (`FILTER_TO_PHASE.ChoDuyet` tồn tại) |
|
||||
| `Khkk_Approved` | `/khkk/list?group=1&filter=DaDuyet` | ĐÚNG |
|
||||
| `Khkk_Deleted` | `/khkk/list?group=1&view=deleted` | ĐÚNG (`get('view') === 'deleted'`) |
|
||||
| `Khkk_WfView` | `/khkk/workflow-matrix?type=10&group=1` | route sống, **`group` KHÔNG có người đọc** — xem MAJOR-C |
|
||||
|
||||
`group` không nằm trong `TRANSIENT_QUERY_KEYS` (`:349` = `id, q, editHeader, page, phase, awId`) ⇒ so khớp active NGHIÊM theo `group`; 8 leaf cùng hành động khác nhóm phân biệt được, và root trần `/khkk/list` (0 key query) không khớp leaf `?group=1` (1 key) vì `queryMatches` so cả SỐ LƯỢNG khoá. Đúng ý đồ.
|
||||
|
||||
🔴 **MAJOR-C — tham-số-trang-trí lan sang staticMap.** `grep "searchParams.get('group')"` trên cả 2 app = **4 hit, TẤT CẢ nằm ở `KhkkListPage` + `KhkkCreatePage`**; `WorkflowMatrixViewPage` **0 hit**. Tức `Khkk_WfView` (mới thêm `&group=1` trong ĐÚNG diff K4b này) và 7 leaf `Khkk_G{2..8}_WfView` (K4a) đều mở ra **cùng một ma trận workflow type-10**, không lọc theo nhóm. Đây là đúng finding ① tôi đã nêu ở gate K4a; K4b **không sửa mà thêm cái thứ 8**. Lead đã disposition FLAG-3 = K4c/K5 theo build-order ⇒ tôi KHÔNG chặn commit vì nó, nhưng ghi rõ: hôm nay `&group=1` ở dòng `Khkk_WfView` chỉ có tác dụng **phân biệt active-state**, KHÔNG có tác dụng lọc nội dung. Ai đọc URL mà tưởng nó lọc thì hiểu sai.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 5 — Mirror 2 app
|
||||
|
||||
Bảng sha256 đã ghi ở **M2**: `KhkkListPage.tsx` = `fa4c7aa268f35eeb` ở CẢ HAI app (khớp đúng `fa4c7aa2` spec yêu cầu xác minh sau khi lead cp lại), `KhkkCreatePage.tsx` = `545b4c2fe0ebae88` ×2, `types/khkk.ts` = `7a99b86518948093` ×2. Không file nào lệch.
|
||||
|
||||
`Layout.tsx` lệch by-design và **lệch có khai ở cả hai phía**:
|
||||
- admin `Khkk_WfView` → `/system/approval-workflows-v2/ContractSigningPlan` (Designer), **cố ý KHÔNG mang `group`** — chú thích ngay tại chỗ (`fe-admin/.../Layout.tsx:37-39`) nói rõ Designer không đọc tham số này. Tôi đồng ý: mirror mù ở đây mới là lỗi.
|
||||
- 5 dòng `/khkk/*` còn lại của admin ĐÃ mang `group=1` (`:41-45`) ⇒ không có ca "một app nói dối, một app nói thật".
|
||||
- admin không nới regex `Khkk_G*` — hệ quả 42 leaf nhóm 2..8 drop im lặng bên admin; **đã khai bằng khối ràng-buộc-ngược tại `isAdminHidden`** (`:199-210`) và là trạng thái K4a chấp nhận, không phải phát sinh mới ở K4b.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 6 — Hồi quy đường cũ không-param
|
||||
|
||||
| Đường | Kỳ vọng | Đo được | |
|
||||
|---|---|---|---|
|
||||
| `/khkk/list` trần (root `KeHoachKyKet`, fe-user staticMap `:83`) | xem TẤT CẢ nhóm | `groupParam === null` ⇒ `group = null` ⇒ `group ?? undefined` ⇒ axios bỏ tham số ⇒ BE `ApprovalGroup is null` ⇒ không thêm vế `Where` | ĐÚNG |
|
||||
| tiêu đề trang khi trần | không in tên nhóm | `group !== null ? … : 'Kế hoạch ký kết HĐ'` | ĐÚNG |
|
||||
| `/khkk/create` không param | giữ default cũ | `presetGroup = null` ⇒ `useState(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)`, hằng `= 1` (`types/khkk.ts:122`) | ĐÚNG |
|
||||
| `/khkk/list?group=rác` | không tự đoán nhóm 1 | regex `/^[1-8]$/` trượt ⇒ `null` ⇒ xem tất cả | ĐÚNG |
|
||||
|
||||
**MINOR-2:** trên `KhkkCreatePage`, hai nút "Huỷ" (`:165`, `:373`) đều `navigate('/khkk/list')` **không mang `group`** — người dùng đi từ leaf nhóm 5 vào lập phiếu, bấm Huỷ thì rơi về danh sách MỌI nhóm. Bất đối xứng với chiều đi (nút "Lập kế hoạch" ở `KhkkListPage:186` có mang `group`). Không sai dữ liệu, chỉ mất ngữ cảnh.
|
||||
|
||||
**MINOR-3:** đi từ `/khkk/create?group=3` sang `/khkk/create` (trần) thì ô chọn GIỮ 3, trong khi chú thích `:64` viết "không có tham số thì giữ nguyên default cũ" — chữ và việc lệch nhau một nhịp (nhánh `if (presetGroup !== null)` `:82` cố ý không đụng khi về null). Hành vi thực tế hợp lý hơn chữ; sửa CHỮ là đủ.
|
||||
|
||||
**Đã kiểm giao thoa preset × rào nhóm-đã-dùng:** vào `?group=3` rồi chọn PE đã có kế hoạch nhóm 3 ⇒ `selectPe` (`:147-154`) nhảy sang nhóm trống đầu tiên; nếu người dùng đổi leaf sang nhóm đã dùng thì `groupTaken` bật, nút Lưu **disabled** (`:380`) + dòng đỏ (`:313`). Preset KHÔNG phá được rào 409. ĐẠT.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 7 — Mỏ neo số dòng (tái phát bài K4a ②)
|
||||
|
||||
🔴 **MAJOR-B — mỏ neo bị CHÍNH diff này làm sai, và sai ngay lúc land.**
|
||||
`fe-user/src/components/Layout.tsx:204-205` viết:
|
||||
> ``Tên tham số `filter` / `view` lấy ĐÚNG từ `KhkkListPage` (`:61` `get('filter')` → `FILTER_TO_PHASE`, `:62` `get('view') === 'deleted'`)``
|
||||
|
||||
Đo hai đầu:
|
||||
- `git show 7a903cf:fe-admin/src/pages/khkk/KhkkListPage.tsx | grep -n` → `61: get('filter')`, `62: get('view')` — **đúng ở thời điểm K4a viết**.
|
||||
- Trên đĩa hôm nay (sau K4b) → **`74:` `get('filter')`, `76:` `get('view')`** — lệch +13 / +14, đúng bằng khối chú thích + derive mà K4b chèn vào đầu component.
|
||||
|
||||
Vì K4a và K4b **vào CHUNG một commit** (lead đã khai), mỏ neo này sẽ sai **từ byte đầu tiên** của commit — không phải trôi dần theo thời gian. Đây đúng lớp lỗi tôi đã nêu ở gate K4a mục ②, tái phát sau đúng một wave, lần này **xuyên file** (K4b sửa file A làm hỏng chú thích trong file B). Mỉa mai đo được: chính khối chú thích đó, cách 2-3 dòng, dặn hai lần *"grep tên hằng — đừng neo số dòng"* và *"số dòng trượt theo edit"*.
|
||||
|
||||
**Tiêu chí nghiệm thu để gỡ:** bỏ `:61`/`:62`, thay bằng mỏ neo grep-được (ví dụ trích chính chuỗi `searchParams.get('filter')` / `FILTER_TO_PHASE`), hoặc cập nhật thành `:74`/`:76` **và** đo lại sau khi mọi edit của cả 2 wave đã nằm yên. Sửa 1 dòng, không đụng logic, không cần build lại.
|
||||
|
||||
**Quan sát kèm (KHÔNG tính cho K4b — nợ có sẵn từ K2):** `ContractSigningPlanFeatures.cs:332` viết ``client hiện tại (`KhkkCreatePage.tsx:73-75` ×2 app) CHƯA gửi trường này``. Hai vế đều đã sai TRƯỚC K4b: `:73-75` pre-K4b là chú thích của query `workflows`, không phải chỗ dựng body; và client **CÓ** gửi `approvalGroup` (`KhkkCreatePage.tsx:119`, do chính K2 thêm). K4b chỉ đẩy nó lệch thêm 21 dòng. Ghi vào sổ nợ, đừng tính vào cổng này.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 8 — Bảng 6 hạng mục + nghiệm thu
|
||||
|
||||
| # | Hạng mục | Kết quả | Ghi chú |
|
||||
|---|---|---|---|
|
||||
| 1 | Wire BE / feature claim | **PASS** | `group` đi trọn FE→BE ở CẢ 2 nhánh; BE lọc thật, `total`/số-trang nói thật |
|
||||
| 2 | Toàn vẹn schema | **PASS** | 0 migration, 0 đổi cột; `ApprovalGroup` là `int` non-null default 1 nên vế lọc không sinh ca NULL |
|
||||
| 3 | Bảo mật | **PASS** | vế lọc đặt SAU IDOR (chỉ thu hẹp) ×2 handler; `[Authorize(Policy="KeHoachKyKet.Read")]` giữ nguyên cả class lẫn action; không endpoint mới |
|
||||
| 4 | Chất lượng mã | **PASS-WITH-FLAGS** | build 0E + tsc ×2 EXIT=0 (tôi tự chạy); mirror sha ×3 khớp; **MAJOR-B** mỏ neo sai |
|
||||
| 5 | Độ phủ test | **PASS-WITH-FLAGS** | §7 test-after cho feature mới ⇒ không chặn; **MAJOR-A** vế lọc `/deleted` 0 test, xoá đi vẫn xanh toàn bộ |
|
||||
| 6 | Chất lượng văn xuôi | **N-A** | không có nội dung hướng ra ngoài trong diff K4b |
|
||||
|
||||
**Nghiệm thu spec-cụm2 (đo, không suy):**
|
||||
- *"bấm leaf→leaf ĐỔI kết quả không cần F5"* → **ĐẠT**, 6/6 ca chuyển ở bảng MỤC 1.
|
||||
- *"URL G1 6 leaf mang `group=1`"* → **ĐẠT**: fe-user 6/6 (kể cả `WfView`); fe-admin 5/5 mục trỏ `/khkk/*`, riêng `WfView` trỏ Designer nên cố ý không mang `group` (lệch đã khai ở cả hai file).
|
||||
|
||||
**Xử lý 4 FLAG của lane:** FLAG-1 → lead vá đủ 3 chỗ BE + 2 nhánh FE, tôi xác nhận ĐÚNG (còn nợ test). FLAG-2 → tôi phán **KHÔNG phải lỗi**, phase-decoded là lựa chọn đúng (lập luận + điều kiện phản chứng ở MỤC 2). FLAG-3 → carry sang K4c, nhưng K4b vừa đẻ thêm instance thứ 8 (MAJOR-C). FLAG-4 → giữ, `search` nằm trong `queryKey` nên không sai dữ liệu.
|
||||
|
||||
---
|
||||
|
||||
## VERDICT
|
||||
|
||||
**GATE-K4B: PASS-WITH-FLAGS (3 MAJOR, 5 MINOR) — không có lỗi CHẶN.**
|
||||
|
||||
Đủ điều kiện vào commit gộp K4a+K4b. Đề nghị làm 1 việc rẻ TRƯỚC khi commit:
|
||||
- **MAJOR-B** (sửa 1 dòng chú thích `fe-user/.../Layout.tsx:204-205`) — vì nó sai ngay tại commit này chứ không trôi dần, sửa sau sẽ phải đẻ commit riêng cho một dòng chữ.
|
||||
|
||||
Hai cái còn lại vào sổ nợ K4c, KHÔNG chặn hôm nay:
|
||||
- **MAJOR-A** — 1 test cho vế lọc nhóm ở `/deleted` (2 phiếu xoá khác nhóm, `?group=n` trả đúng 1).
|
||||
- **MAJOR-C** — `WorkflowMatrixViewPage` đọc `?group=` (đang là tham số trang trí ở 8 URL).
|
||||
|
||||
MINOR 1-5 tuỳ lead, không cái nào đụng dữ liệu.
|
||||
|
||||
END — gate K4b đóng.
|
||||
|
||||
---
|
||||
|
||||
## MỤC 9 — Re-đo NGAY TRƯỚC khi phát verdict (bài S166 "cây phình giữa lượt review")
|
||||
|
||||
Giữa lượt review cây CÓ đổi: HEAD `14ea2ea` → `bc1e7c9` (2 commit `wal:` mới), `git diff --stat` tổng 541→664 dòng thêm. Tôi đo lại để verdict không neo vào ảnh chụp cũ:
|
||||
|
||||
- `git diff --stat 14ea2ea..HEAD` = **3 file, TẤT CẢ trong `.claude/`** (memory implementer-frontend, `sub-implfe-k4b.md`, chính artifact này). **0 file code.**
|
||||
- `git diff --stat -- src fe-admin fe-user tests docs` = **13 file, 444+/59−**, từng dòng số **trùng khít** lần đo đầu (Layout admin 28, ListPage 90 ×2, CreatePage 24 ×2, types 25 ×2, Layout user 50, Controller 5, Features 9, MenuKeys 42, DbInitializer 79, STATUS 12).
|
||||
- sha256 3 file mirror đo lại: `fa4c7aa2` / `545b4c2f` / `7a99b865` ×2 app — **không đổi**.
|
||||
|
||||
⇒ Verdict phát trên đúng thứ tôi đã soi. Không có code lọt vào sau lưng cổng.
|
||||
|
||||
🔴 **MAJOR-D — nguy cơ lúc commit, KHÔNG phải lỗi mã.** `git status --porcelain -- src tests` cho ra **`?? tests/SolutionErp.Infrastructure.Tests/Application/KhkkGroupMenuSeedTests.cs`** — file test của K4a vẫn **CHƯA ĐƯỢC TRACK**. Commit gộp mà lead dùng `git commit -am` hoặc liệt file bằng tay thiếu dòng này thì bộ test 49 dòng menu đi kèm K4a sẽ **rơi lại ngoài commit**: mã seed lên repo, test chứng minh nó thì không. Cổng rẻ, chạy trước khi gõ commit:
|
||||
|
||||
```
|
||||
git status --porcelain -- src tests | grep '^??' # phải RỖNG
|
||||
```
|
||||
|
||||
Nếu chưa rỗng thì `git add` đúng đường dẫn đó rồi mới commit.
|
||||
|
||||
---
|
||||
|
||||
## TOTAL — chốt cuối (đính chính con số ở MỤC 8: là **4 MAJOR**, không phải 3; MAJOR-D sinh ra sau lượt re-đo)
|
||||
|
||||
**GATE-K4B: PASS-WITH-FLAGS — 4 MAJOR + 5 MINOR, 0 lỗi CHẶN.**
|
||||
|
||||
| mã | mức | nội dung | làm khi nào |
|
||||
|---|---|---|---|
|
||||
| MAJOR-A | major | vế lọc nhóm ở `/deleted` **0 test** — xoá đi vẫn xanh toàn bộ (`sợi-dây-1-argument`) | sổ nợ K4c |
|
||||
| MAJOR-B | major | mỏ neo `fe-user/.../Layout.tsx:204-205` trỏ `KhkkListPage :61/:62`, đĩa là `:74/:76` — sai từ byte-0 của commit | **trước commit** (sửa 1 dòng chữ) |
|
||||
| MAJOR-C | major | `?group=` trên `WfView` là tham số trang trí, 0 người đọc; K4b thêm instance thứ 8 | sổ nợ K4c/K5 (lead đã disposition) |
|
||||
| MAJOR-D | major | `tests/…/KhkkGroupMenuSeedTests.cs` còn `??` chưa track — commit gộp dễ bỏ rơi test của K4a | **trước commit** (`git add` rồi commit) |
|
||||
| MINOR-1..5 | minor | bấm-lại-leaf không nhả chip · "Huỷ" mất `group` · chữ-vs-việc ở `:64` CreatePage · `phase` thừa trong khoá khi deleted · search `/deleted` chỉ khớp `MaKeHoach` | tuỳ lead |
|
||||
|
||||
Nghiệm thu spec-cụm2: **2/2 ĐẠT** (leaf→leaf không cần F5; 6 leaf G1 mang `group=1`).
|
||||
Cả 4 FLAG của lane đã xử lý: FLAG-1 xác nhận vá đúng · FLAG-2 phán **không phải lỗi** (có điều-kiện-phản-chứng) · FLAG-3 carry K4c · FLAG-4 giữ.
|
||||
|
||||
END — gate K4b đóng, artifact hoàn chỉnh tới dòng này.
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
# GATE-K4C — IN-PROGRESS
|
||||
|
||||
> Reviewer adversarial gate, wave K4c (chốt cụm-2), S166. Ghi TỪNG MỤC ngay khi đo xong (chống #53: file luôn hợp-lệ giữa chừng).
|
||||
> Verdict CHỈ hợp-lệ khi có dòng `END` cuối file. Chưa có END = lượt chạy chưa xong, ĐỪNG đọc verdict.
|
||||
|
||||
## 0. Bối cảnh nhận từ lead
|
||||
|
||||
- Diff CHƯA commit, lead khai HEAD `ce35f90`, 2 file:
|
||||
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` (+68/−5) — WfView lọc nhóm client-side
|
||||
- `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanCrudTests.cs` (+1 Fact) — MAJOR-A `/deleted` lọc server-side
|
||||
- Lane artifacts: `sub-implfe-k4c.md` (DONE, 21/21 + 2 control-âm, 2 FLAG minor) · `sub-test-k4a.md` §MAJOR-A (8/8, RED→GREEN 2 chiều)
|
||||
|
||||
## 1. Đo — điền dần
|
||||
|
||||
### 1.0 Phạm vi diff (đo, không tin khai)
|
||||
|
||||
- HEAD thật lúc review = `2023f41` (`wal: flush 20260801T1045`), **KHÔNG** phải `ce35f90` như lead khai — `ce35f90` là commit code cuối (K5), sau đó có 1 commit `wal:` chèn vào. Không ảnh hưởng nội dung diff (2 wal-commit không đụng `src|tests|fe-*`), nhưng nhãn base trong spec lead SAI 1 nấc.
|
||||
- `git diff --numstat` (working tree vs HEAD), lọc cây code:
|
||||
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` **+68 / −5** ✅ khớp khai.
|
||||
- `tests/.../ContractSigningPlanCrudTests.cs` **+106 / −2** (lead khai "+1 Fact" — đúng về SỐ FACT, nhưng LOC lớn hơn vì còn sửa helper `CreateAsync` + thêm helper `ListDeletedAsync`; không phải chỉ-thêm-1-Fact thuần).
|
||||
- `git status --porcelain -- src tests fe-user fe-admin` = đúng 2 dòng ` M` trên, **0 dòng `??`** ⇒ cây code KHÔNG phình.
|
||||
- `??` toàn repo = **1** file duy nhất: chính artifact này (`sub-reviewer-gate-k4c.md`). 8 file `M` còn lại đều là agent-memory / run-artifact / `.session-counter.json` — ngoài cây code, không tính scope drift.
|
||||
- fe-admin: **0 diff** ✅ (không có dòng nào của `fe-admin` trong git status).
|
||||
|
||||
### 1.1 SOI-1 — lọc client-side WfView
|
||||
|
||||
- Vị ngữ: `allWorkflows.filter(wf => wf.code === khkkGroupWorkflowCode(khkkGroup))`, `khkkGroupWorkflowCode = n => \`KHKK-N${n}\`` (`fe-user/src/types/khkk.ts:163`). Khớp **BE**: seeder `DbInitializer.cs:609` `var code = $"KHKK-N{n}"` ✅ và rào (v) `ContractSigningPlanFeatures.cs:420` regex `^KHKK-N([1-8])$` ✅. **Cả 2 số dòng trích trong comment đều ĐÚNG** (đã grep, không trượt).
|
||||
- `AwDefinitionDto.code` tồn tại (`fe-user/src/types/approvalWorkflowV2.ts:41`) ✅. Lọc theo `code` (không theo `id`/`name`) ⇒ giữ đủ mọi **version** cùng nhóm — đúng như comment khai, và đúng với `history: AwDefinitionDto[]`.
|
||||
- Guard `/^[1-8]$/` — ca `"03"`: regex TRƯỢT (đòi đúng 1 ký tự) ⇒ `khkkGroup = null` ⇒ **bỏ lọc, hiện tất cả**, KHÔNG bị `Number("03")=3` nuốt thành nhóm 3. **Lane khai đã xử = ĐÚNG, verify độc lập xong.**
|
||||
- Đồng nhất 3 chỗ: `KhkkListPage.tsx:80` và `KhkkCreatePage.tsx:67` dùng **y hệt** `/^[1-8]$/` ⇒ không đẻ luật thứ 2 cho cùng một tham số.
|
||||
- Đường sinh link không bao giờ đẻ `"03"`: `Layout.tsx:216` lấy `group` từ capture của regex `^Khkk_G([1-8])_(...)$` ⇒ luôn 1 chữ số.
|
||||
- Regression type ≠ 10: `typeInt === 10 &&` là **liên từ ĐẦU TIÊN** ⇒ type 1/2/3 (PE + Contract) luôn cho `khkkGroup = null` ⇒ `workflows === allWorkflows`, `actions = undefined` (PageHeader có `{actions && ...}` nên render **y hệt** trước). Mọi consumer downstream (`workflows.length === 0`, `workflows.map`) không đổi ⇒ **0 đổi hành vi cho matrix PE/HĐ**. ✅
|
||||
- Rỗng-vì-lọc ⟂ rỗng-thật: tách 2 nhánh THẬT (`:157-165`), nhánh lọc còn nói thêm "loại phiếu này có N quy trình ở nhóm khác" — bảo vệ đúng ca người nhóm N5 tưởng cả module chưa cấu hình. Điều kiện `allWorkflows.length > 0 &&` là **phép so sánh** (không phải `{0 && ...}`) ⇒ không rò số `0` ra UI. ✅
|
||||
|
||||
### 1.2 SOI-2 — queryKey KHÔNG đổi: đúng hay rò cache?
|
||||
|
||||
**Phán: ĐÚNG, không rò.** 3 vế:
|
||||
1. Payload không phụ thuộc `group`: query gọi `/approval-workflows-v2?applicableType=10&isUserSelectable=true` (`:63-71`) — BE `ApprovalWorkflowsV2Controller.cs:24-26` chỉ nhận `applicableType` + `isUserSelectable`, **không có** trục `group` ⇒ 8 nhóm dùng chung ĐÚNG một payload. Thêm `group` vào `queryKey` = 8 lần gọi mạng cho cùng một body + 8 ô cache trùng nội dung. Bỏ ra là đúng.
|
||||
2. Không stale khi đổi leaf: `khkkGroup`/`workflows` là **giá trị dẫn xuất trong render** từ `useSearchParams()`, KHÔNG phải `useState` khởi tạo-một-lần ⇒ điều hướng leaf→leaf (cùng route, React Router không remount) vẫn tính lại ngay. Đây đúng là cái bẫy "vá-5" đã cắn `KhkkListPage` ở K4b, lần này **tránh được by-construction**, không cần `useEffect`.
|
||||
3. Cache dùng chung là ĐIỀU MONG MUỐN: nhảy N1→N2 không refetch, lọc lại tại chỗ.
|
||||
|
||||
Cái giá (đã cân, chấp nhận): tải trọn bộ type-10 rồi mới cắt — với 8 workflow seed thì không đáng kể.
|
||||
|
||||
### 1.3 SOI-4 + SOI-5 — build + cây
|
||||
|
||||
| Phép đo | Lệnh | Kết quả |
|
||||
|---|---|---|
|
||||
| tsc fe-user | `npx tsc -b` (reviewer tự chạy, KHÔNG đọc panel IDE) | **exit 0**, 0 dòng lỗi ✅ |
|
||||
| tsc fe-admin | `npx tsc -b --force` | **exit 0** ✅ |
|
||||
| fe-admin diff | `git status --porcelain -- fe-admin` | **rỗng** ✅ |
|
||||
| Cây phình | `git status --porcelain -- src tests fe-user fe-admin \| grep '^??'` | **0 hit** ✅ (control dương: cùng lệnh bỏ path-filter ra 1 hit = artifact này ⇒ phép đo KHÔNG rỗng) |
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
GATE-K5: PASS — 0 blocker · 6/6 SOI đạt (mất-phiếu + regression GĐ3/4 chứng được là KHÔNG xảy ra) · 1 CLARIFY + 5 MINOR treo · rào commit: `menus.json` untracked ở gốc, đừng `git add -A`
|
||||
|
||||
> **Re-đo TRƯỚC verdict** (bài S166 F-1 ②): HEAD trôi `ff4d4dc → 13f7f82 → 80b8cf5` trong lượt review, **toàn `wal: flush`** — `git diff --name-only 13f7f82..HEAD -- src tests fe-admin fe-user` = **0**. Cây code vẫn **đúng 4 file, +466/−108**, 2 cặp sha mirror y nguyên, 0 untracked trong vùng code ⇒ verdict đo trên đúng thứ sắp commit.
|
||||
|
||||
# Gate K5 — adversarial review cây pipeline GĐ2 mở 8 sub-folder nhóm (S166, 2026-08-01)
|
||||
|
||||
Vai: reviewer (READ-only, verdict-only). Ghi TỪNG MỤC, Edit sau mỗi mục (chống #53).
|
||||
|
||||
## §0 Mốc đo (neo trước khi soi)
|
||||
- Lệnh: `git status --porcelain` + `git rev-parse HEAD` lúc 1st-measure.
|
||||
- 🔴 **LỆCH TIỀN-ĐỀ #1:** spec lead nói "diff chưa commit trên HEAD `88e7ecf`". Đĩa đo được **HEAD = `ff4d4dc9d0b745867f83b5349f3c239d6b2ee798`**. Phải truy `git log` xem `88e7ecf` nằm đâu trước khi tin base.
|
||||
- 🔴 **RÀO GIT #1:** `git status` có `?? menus.json` ở ROOT repo (untracked, ngoài spec 4 file). Cần đo: file gì, có bị commit ké không.
|
||||
- Cây làm việc lúc mốc-0 (8 file M + 3 ??): 4 file code đúng spec + 4 file `.claude/**` + 3 untracked.
|
||||
|
||||
## §1 Phân lập diff + rào git (đo, không tin lời khai)
|
||||
- `git log`: `88e7ecf` LÀ tổ tiên của HEAD ⇒ tiền-đề lead đúng về **nội dung**, sai về **tên mốc**: giữa `88e7ecf` và HEAD có **3 commit `wal: flush`**. `git diff --name-only 88e7ecf..HEAD` = **6 file, TOÀN BỘ `.claude/**`**, 0 file code ⇒ base code vẫn là `88e7ecf`. Verdict K5 đo trên `git diff` (working-tree vs HEAD).
|
||||
- 🔴 Cây PHÌNH GIỮA LƯỢT (lần thứ 3 trong 3 gate liên tiếp): HEAD `ff4d4dc` lúc đo-0 → `13f7f82` sau ~2 tool-call. Phân lập: `git diff --stat -- fe-admin fe-user src tests` = **đúng 4 file, +466/−108** khớp spec TỪNG SỐ.
|
||||
- `git status --porcelain -- src tests fe-admin fe-user` = **0 dòng `??`** ⇒ không có file code chưa track (bài "HIGH nằm ở git" KHÔNG tái phát trong vùng code).
|
||||
- 🔴 **RÀO GIT — `menus.json` 44.656 B nằm ở ROOT repo, untracked** (mtime 10:25, nội dung = payload `/api/menus/me`, sản phẩm phụ của lane cicd verify prod). Không phải file của K5, nhưng nếu lead commit bằng `git add -A`/`git add .` thì nó lên repo. Đây là finding **về THAO TÁC COMMIT sắp tới**, không phải về diff K5.
|
||||
- Cùng loại: `.claude/agent-memory/implementer-frontend/archive/2026-H2.md` untracked (hợp lệ, nên add có chủ đích).
|
||||
|
||||
## §2 Build gate — TÔI TỰ CHẠY, không nhận lời khai
|
||||
| lệnh | nơi chạy | exit |
|
||||
|---|---|---|
|
||||
| `npx tsc -b` | `fe-user` | **0** |
|
||||
| `npx tsc -b` | `fe-admin` | **0** |
|
||||
Lời khai lane (`tsc -b` ×2 exit 0 + `npm run build` ×2) khớp phần tôi đo lại. Bundle hash lane khai (`index-CZUcTUUg.js` / `index-BZWvx0Y8.js`) tôi **KHÔNG** kiểm lại — chưa deploy nên không có ô canonical để đối chứng; ghi rõ là **lời khai chưa đo**, đừng đọc thành đã-verify (bài K4a ③).
|
||||
|
||||
## §3 SOI-1 — Node nhóm đúng nguồn + route khớp K4b ✅
|
||||
- **1 nguồn, 0 map thứ hai.** `grep KHKK_APPROVAL_GROUPS|khkkGroupMenuLabel` toàn `fe-user/src`+`fe-admin/src`: consumer = hook K5 (`:217/:228/:230`) + `KhkkCreatePage` + `KhkkListPage`, **tất cả import từ `@/types/khkk`**. Không có mảng 8 phần tử nào khác, không có bảng nhãn thứ hai.
|
||||
- 8 node dựng bằng `KHKK_APPROVAL_GROUPS.map(...)` (hằng `[1..8] as const`), nhãn `khkkGroupMenuLabel(n)` = `"{n}. " + KHKK_APPROVAL_GROUP_LABELS[n]` — **derive, không chép**.
|
||||
- 🟢 **ĐỐI CHIẾU LIVE (tôi tự đo, không nhận lời khai):** so nhãn cây suy ra ⟂ nhãn thật trong payload `/api/menus/me` (`menus.json` prod) → **8/8 KHỚP TỪNG KÝ TỰ**, cả 8 node `Khkk_G1..G8` đều `displayLabel = null`. Tức hôm nay cây và sidebar gọi cùng một nhóm bằng cùng một tên.
|
||||
- **Route:** `onOpenAll → /khkk/list?group=${n}`. Đối chiếu K4b: `Layout.tsx:217` `khkkLeafPath(group,'List')` = `/khkk/list?group=${group}`, và leaf tĩnh `Khkk_List: '/khkk/list?group=1'` (`:63`) ⇒ **chuỗi TRÙNG KHÍT**.
|
||||
- **Nối end-to-end (3 chặng, đo cả 3):** `KhkkListPage.tsx:79-80` đọc `searchParams.get('group')` + rào `^[1-8]$` → `:124` gửi `group` lên server → BE `ContractSigningPlanFeatures.cs:757-758` `q.Where(p => p.ApprovalGroup == request.ApprovalGroup)`. Không phải tham-số-trang-trí (khác hẳn bài K4a ①).
|
||||
- **Sáng đúng mục sidebar:** `Layout.tsx:388-390` `isActive = pathname khớp && queryMatches(...)` (so khớp TẬP query) ⇒ `?group=n` từ cây ăn khớp leaf nhóm n. Lời khai lane "trang tự sáng đúng mục" **đúng, đã kiểm**.
|
||||
|
||||
## §4 SOI-2 — Phân phiếu, fallback, bất-biến Σ ✅ (1 MINOR)
|
||||
- **KHÔNG có ca phiếu rơi khỏi cả 9 ngăn.** Vòng `:215-224` là if/else **toàn phần**: khớp `typeof g === 'number' && GROUPS.includes(g)` → ngăn n; **mọi thứ còn lại** (null · undefined · thiếu field · 0 · 9 · NaN · chuỗi) → `khkkUngrouped`. Không nhánh nào `continue`/bỏ qua.
|
||||
- **Σ enforce ở đâu — trả lời thẳng: ở CẤU TRÚC, không phải ở kỷ luật.** `khkkLeaves` và các ngăn cùng sinh từ **một mảng `khkkItems`** bằng một phép phân hoạch; `stage.leaves` giữ vai danh sách phẳng canonical và `stageCount()` (`PipelineStageFolders.tsx:96`) vẫn đếm trên nó ⇒ chip số của folder cha **không thể** lệch tổng các ngăn. `console.warn` `:252` chỉ là chuông, và là **DEV-only**.
|
||||
- Thứ tự đúng: ngăn "(chưa phân nhóm)" `push` ở `:238-245` **TRƯỚC** khi `reduce` tính Σ ở `:251` ⇒ chuông không tự báo động giả.
|
||||
- **Ngăn lạc gần như không thể xảy ra** (đo BE): `ApprovalGroup` là `int` non-null, validator `InclusiveBetween(1,8)` (`:353-356`), default 1 (`:399`), backfill Mig 71 = 1, và **0 đường update** nào ghi lại field này. ⇒ nhánh fallback là phòng thủ, không phải đường sống.
|
||||
- 🟡 **MINOR-1 — nhãn ngăn lạc hứa một đằng, đi một nẻo:** `onOpenAll: () => navigate('/khkk/list')` (`:243`). Không kèm `?group=` ⇒ BE **không lọc gì cả**, trang mở ra là **toàn bộ phiếu của cả 8 nhóm**, trong khi nhãn và tooltip nói "Mở danh sách: (chưa phân nhóm)". Sidebar cũng không sáng mục nào (query rỗng không khớp leaf nào). BE hiện **không có** bộ lọc "không thuộc nhóm nào" để trỏ tới, nên đây là hạn chế thật chứ không phải quên gõ. Đề nghị: bỏ `onOpenAll` cho riêng ngăn này (component đã có nhánh `<span>` khi thiếu `onOpenAll`, `:151-155`) — mất một cú bấm gây hiểu nhầm, không mất gì khác.
|
||||
|
||||
## §5 SOI-3 — Ngăn rỗng render THẬT ✅
|
||||
Đo đường đi, không đọc lời khai — **3 chốt, không chốt nào lọc theo `length`**:
|
||||
1. Dựng: `KHKK_APPROVAL_GROUPS.map(...)` — `.map` trên hằng 8 phần tử, **0 `.filter`** trong cả khối.
|
||||
2. Vẽ: `PipelineStageFolders.tsx:287-295` `stage.groups !== undefined ? stage.groups.map(g => <GroupFolder…>)` — vẽ **thẳng mảng nhận được**, không điều kiện phụ.
|
||||
3. Trong ngăn: `GroupFolder` `count === 0` → vẫn render `<details>` đủ (chevron + tên bấm được + `SoonChip`), thân in *"Chưa có phiếu ở nhóm này."*
|
||||
- Nhánh *"Chưa có dữ liệu ở giai đoạn này."* (`:296`) nay **bị nhánh `groups` chặn trước** ⇒ GĐ2 rỗng hiện 8 ngăn thay vì 1 dòng. Đúng spec "HIỆN ĐỦ 8 kể cả rỗng".
|
||||
- Không đụng `hideEmpty`: vẫn lọc ở tầng **stage** bằng `stageCount()` = `leaves.length` ⇒ đang tìm-kiếm mà gói không có KHKK thì cả folder GĐ2 ẩn, **không** đổ ra 8 ngăn rỗng.
|
||||
|
||||
## §6 SOI-4 — GĐ3/GĐ4 regression ✅ 0 ĐỔI HÀNH VI (chứng bằng tương đương từng vế)
|
||||
Nghi-can số-1 là đúng chỗ phải nghi: builder GĐ3/4 **bị viết lại vòng lặp**. Tôi chứng tương đương 4 vế thay vì tin "cùng tập, cùng thứ tự":
|
||||
| vế | CŨ (`seen` + lặp `peItems`) | MỚI (`Set` + lặp id) | kết |
|
||||
|---|---|---|---|
|
||||
| tập id | mọi `p.contractId` truthy | `if (p.contractId) add` | **bằng** |
|
||||
| thứ tự | lần-xuất-hiện-ĐẦU trong `peItems` | `Set` giữ **thứ tự chèn**, chèn khi duyệt `peItems` | **bằng** |
|
||||
| khử trùng | `seen.has → continue` | `Set` tự khử | **bằng** |
|
||||
| dòng phụ | `p.tenGoiThau` (p = phiếu ĐẦU mang id đó) | `peItems.find(p => p.contractId === contractId)` = **cũng là phiếu ĐẦU** | **bằng** |
|
||||
- **`c.type` KHÔNG bị đụng**: vẫn chỉ xuất hiện ở `onOpen` GĐ4 → `/hard-copies?type=${c.type}` (`:322`), khớp `HardCopiesPage.tsx:64-67`. Hành vi 7-loại-HĐ nguyên vẹn.
|
||||
- **stage4 an toàn theo cấu trúc**: thân `if (linkage.canHardCopy && c?.hasSealedCopy)` chỉ đọc `c`, **0 tham chiếu `p`** — nên việc biến `p` biến mất khỏi scope vòng lặp không thể làm rơi HĐ. (Nếu có tham chiếu thì `tsc` đã đỏ; tôi kiểm cả hai đường.)
|
||||
- **Union hôm nay đúng 1 nguồn** `pe.contractId` ⇒ tập HĐ **y hệt trước K5** ⇒ **0 ca HĐ đang hiện bị biến mất**. Vế "HĐ 2..N liên-danh vô hình" là **giới hạn dữ liệu có từ trước**, K5 không làm xấu đi.
|
||||
- ⚪ Ghi chú (không phải lỗi): `peItems.find` nằm trong vòng ⇒ O(n·m); n = số phiếu 1 gói thầu nên vô hại.
|
||||
- 🟡 **CLARIFY-2 — một dòng checklist đọc được 2 nghĩa:** *"GĐ2 by approvalGroup · GĐ3/4 by c.type"*. Lane đọc là **"kiểm trục, đừng đụng"**; câu hỏi SOI-4 của lead cũng đọc y vậy ("hành vi 7-loại-HĐ … có biến mất ca nào không"). Nghĩa còn lại — *GĐ3/4 cũng mở sub-folder theo 7 loại HĐ* — thì K5 mới làm nửa checklist. **Hai người cùng chọn một nghĩa không chứng minh nghĩa đó đúng ý owner.** Đề nghị ghim cách đọc vào commit message để nó không quay lại thành "sót".
|
||||
|
||||
## §7 SOI-5 — Chỗ cắm FLAG-1: 0 field bịa ✅
|
||||
- `grep` mọi dòng `+` của diff chứa `lines` hoặc `contractIds`: **9 hit, TOÀN BỘ nằm trong `//` comment**. Dòng chạy được duy nhất đọc dữ liệu là `if (p.contractId)` — field **có thật** (`PeListItem:126`).
|
||||
- Control âm: `grep "lines\|contractIds" fe-user/src/types/purchaseEvaluation.ts` = **0 hit** ⇒ đúng, `lines[]`/`contractIds` **không tồn tại** trên DTO list; nếu ai đó lỡ gõ thành mã chạy thì `tsc` đỏ ngay. Lời khai FLAG-1 của lane **đo lại thấy đúng**.
|
||||
- Hình dạng "gom TẬP id" (`Set<string>`) là vế wire được thật, không phải hứa suông: thêm nguồn 2 = thêm 1 dòng tại `:290-291`, không phải sửa vòng lặp.
|
||||
|
||||
## §8 SOI-6 — SHA-pair ×2, kế thừa, và phép đo prod
|
||||
- **Mirror:** `sha256` 2 file đổi ×2 app:
|
||||
`usePipelineStages.ts` = `edff99314e8b75de39b0123bcdad5d98d8081a5bd294e2e05b1ef784094a19ac` (khớp hash lane khai)
|
||||
`PipelineStageFolders.tsx` = `bed52f39d595c86a24afd27180146871d52e29943731ffb52f7caf23e66de936`
|
||||
Thêm phép **đối chứng độc lập**: `diff` từng cặp file = **rỗng cả hai** ⇒ identical theo byte, không chỉ theo hash.
|
||||
- **`INDEX_PAGE_SIZE` = 200 KHÔNG đổi**, `indexTruncated` KHÔNG đổi, dòng cảnh báo *"Chỉ mục tải 200 bản ghi mới nhất…"* vẫn ở chân folder cha (`:301-305`). FLAG-3 (cảnh báo không lặp ở từng ngăn) **đúng như lane khai** — kế thừa, không phải mới.
|
||||
- 🔵 **Đo prod (tôi tự curl, admin token, `http=200`):** `GET /api/contract-signing-plans?pageSize=200` → **`total = 0`**. Bảng KHKK trên prod **rỗng hoàn toàn**.
|
||||
- ⇒ Phân bố `approvalGroup` là **chân-lý-rỗng**: `{}`. Tôi **không** dùng nó để khẳng định hay bác bỏ điều gì về phân bố nhóm — khai thẳng là chưa đo được, không chờ.
|
||||
- ⇒ Nhưng nó cho **một sự thật dùng được ngay cho UAT**: hôm nay mở cây trên prod, GĐ2 của **mọi** gói thầu sẽ hiện **đúng 8 dòng rỗng chip "SẮP"**, 0 phiếu. Đó là ĐÚNG theo spec — cần nói trước để người UAT không đọc thành "hỏng".
|
||||
- 🟡 **MINOR-3 — chữ "SẮP" trên ngăn rỗng:** `SoonChip` vốn nghĩa *"tính năng sắp có"*; đặt lên nhóm rỗng nó thành *"nhóm này sắp có"*, trong khi sự thật là *"nhóm này chưa có phiếu"*. Lane bám tiền lệ folder-GĐ nên **nhất quán**, nhưng K5 nhân nó lên **8 lần mỗi gói** và (theo phép đo prod ở trên) **100% ngăn đều rỗng ngày đầu**. Chữ trong thân ngăn đã nói đúng ("Chưa có phiếu ở nhóm này.") nhưng chỉ thấy sau khi bung. Quyết định thuộc owner, không phải lỗi mã.
|
||||
- 🟡 **MINOR-4 — a11y: `<button>` lồng trong `<summary>`** (`PipelineStageFolders.tsx:141-160`). `<summary>` mang role=button ngầm ⇒ đây là **nội dung tương tác lồng nhau**, trình đọc màn hình có thể không đọc nút bên trong. Đã kiểm tiền lệ tại base `88e7ecf`: cả 3 `<summary>` của `PipelineTreePanel` và cả 3 của `PurchaseEvaluationsListPage` **chỉ chứa `span`/`svg`** ⇒ K5 là **khuôn MỚI**, không có tiền lệ để bám. Bàn phím/chuột vẫn chạy đúng (`preventDefault` + `stopPropagation` chặn nhịp đóng/mở).
|
||||
- 🟡 **MINOR-5 — bằng chứng mạnh nhất chết theo phiên:** "15/15 OK" của lane chạy bằng script `new Function` dựng tại chỗ, **không commit**. FE **không có hạ tầng test nào** (`fe-user/package.json` chỉ có `dev|build|lint|preview`, không `vitest`/`jest`). Vật canh bất-biến Σ còn lại trong repo là `console.warn` **DEV-only** — bản prod bị loại bỏ khi build. Tức nếu mai ai sửa vòng phân hoạch thành lọc, **không gì đỏ**. Không đòi lane dựng hạ tầng test trong wave này; ghi để đừng ai đọc "15/15" thành "đã có lưới an toàn".
|
||||
- ⚪ MINOR-6: với GĐ2, `toKhkkLeaf` chạy **2 lần/phiếu** (1 cho `khkkLeaves` đếm, 1 cho ngăn) ⇒ `stage.leaves` thành mảng **chỉ để đếm**, leaf trong đó không bao giờ được vẽ. Vô hại, nhưng là mỡ thừa nếu sau này gói có nhiều phiếu.
|
||||
|
||||
## §9 Các cổng còn lại
|
||||
| Cổng | Kết | Ghi chú |
|
||||
|---|---|---|
|
||||
| Cat 1 Wire/claim | **PASS** | 0 `// Mock`, 0 `alert(`, 0 TODO/placeholder trong dòng `+`. Không phải wave BE nên không có verb để curl; đã curl prod để đo dữ liệu (§8). |
|
||||
| Cat 2 Schema | **N-A** | `git diff --name-only -- src tests` = **0 file**. 0 migration, 0 entity, 0 DTO. |
|
||||
| Cat 3 Security | **PASS** | Gate GĐ2 vẫn `KHKK_MENU_KEYS = ['KeHoachKyKet']` — đúng key policy của endpoint, KHÔNG OR `Khkk_*` (gotcha **#85**/#82 giữ nguyên). Ngăn nhóm nằm **bên trong** thân GĐ2, mà `locked` chặn trước cả `groups` ⇒ không mở thêm mặt tấn công, không đẻ lệnh gọi API mới. |
|
||||
| Cat 4 Code quality | **PASS** | `tsc -b` ×2 = 0 (tôi chạy). Mirror byte-identical. Anti-fiddle: đúng 4 file trong phạm vi spec, 0 file lạc. `LeafList` tách ra là **bê nguyên văn** khối JSX cũ (đối chiếu từng dòng khối `-` với thân hàm mới: `ul`/`li key`/`className`/badge/secondary trùng khít) ⇒ tách hàm KHÔNG kèm sửa lén giao diện. |
|
||||
| Cat 5 Test | **N-A (khai rõ)** | FE không có hạ tầng test — xem MINOR-5. Không có test BE để tăng vì 0 file BE. |
|
||||
| Cat 6 Writing | **N-A** | Diff là mã + chú thích nội bộ, không phải văn bản hướng ra ngoài. Chuỗi hiển thị mới đều tiếng Việt đủ dấu: *"Chưa có phiếu ở nhóm này."*, *"(chưa phân nhóm)"*, *"Mở danh sách: …"*. |
|
||||
|
||||
## §10 VERDICT
|
||||
**PASS — 0 blocker.** 6/6 mục SOI của lead đều đo được và đều đạt; 2 chỗ tôi nghi nhất (mất phiếu im lặng · regression GĐ3/4) đều **chứng được là không xảy ra**, bằng tương đương từng vế chứ không bằng lời khai. Phần lõi K5 là loại hiếm: bất-biến Σ được bảo đảm bởi **cấu trúc** (một mảng nguồn, một phép phân hoạch, một chỗ đếm), nên nó không phụ thuộc vào ai đó nhớ cộng cho khớp.
|
||||
**Còn treo (không chặn commit):** MINOR-1 nhãn ngăn lạc đi sai chỗ · CLARIFY-2 cách đọc dòng checklist GĐ3/4 · MINOR-3 chữ "SẮP" · MINOR-4 nút lồng trong summary · MINOR-5 không có lưới test · MINOR-6 mỡ thừa.
|
||||
**Trước khi commit — 1 việc THAO TÁC:** `menus.json` (44.656 B) đang nằm untracked ở gốc repo; **đừng `git add -A`**, hãy add đúng 4 file code (+ artifact lane/gate nếu muốn).
|
||||
|
||||
**Câu nặng nhất:** cây nay hiện đủ 8 nhóm, nhưng prod đang có **0 phiếu KHKK** (`total=0`, tôi tự curl) — nên thứ người UAT nhìn thấy ngày đầu là **8 dòng "SẮP" rỗng ở mọi gói thầu**, đúng spec mà rất dễ bị đọc thành hỏng; cần nói trước.
|
||||
|
||||
GATE-K5-END
|
||||
@ -1,4 +1,4 @@
|
||||
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
|
||||
TEST-K4A: DONE — 8/8 PASS (6 gốc + T7 finding-D + 1 gate-K4b MAJOR-A) · Infra 569→577 · Domain 45 · tổng 622 · 0 hồi quy · 0 prod-code touched
|
||||
|
||||
# Lane test K4a — test-after 49 row menu KHKK 8-nhóm (S166, 2026-08-01)
|
||||
|
||||
@ -121,4 +121,37 @@ bộ lọc revoke thì suite vẫn xanh.
|
||||
- **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)
|
||||
## §MAJOR-A — RESUME 2: nợ gate-K4b, lọc nhóm màn "Đã xoá" (`GET /contract-signing-plans/deleted`)
|
||||
|
||||
- [x] **Chọn file = `ContractSigningPlanCrudTests.cs`** (KHÔNG phải file K4a của tôi): nó có sẵn
|
||||
`KhkkFixture` **có `AuditingInterceptor`** — điều kiện SỐNG CÒN vì màn "Đã xoá" đọc row
|
||||
`IsDeleted=1`; fixture thường xoá CỨNG ⇒ màn rỗng ⇒ test đo nhầm cơ chế (bài S155/S161-W2).
|
||||
Kèm sẵn `SeedPeAsync` + `CreateAsync` + `DeleteAsync`. Dựng lại ở file K4a = nhân bản ~120 LOC.
|
||||
- [x] **+1 Fact `ListDeletedContractSigningPlans_FiltersByApprovalGroup_ServerSide`** (test thứ 8
|
||||
của lớp) + mở rộng helper `CreateAsync` thêm optional `approvalGroup` + helper mới
|
||||
`ListDeletedAsync`. Cùng 1 phiếu PE, 2 nhóm khác nhau (1 và 3) — hợp lệ vì rào "1 PE = 1 kế
|
||||
hoạch sống" khoá theo CẶP (PeId, ApprovalGroup) `:401-404`; workflow seed Code `QT-KHKK-V2`
|
||||
nằm NGOÀI họ `KHKK-N{n}` nên rào (v) `:420-424` cho qua.
|
||||
- [x] 5 vế đo (mỗi vế đều assert **`Total`**, không chỉ `Items` — `Total` = dấu vân tay lọc-SERVER
|
||||
vì `CountAsync` chạy sau mọi `Where`; lọc client-side cho `Items` y hệt mà `Total` = cả thế giới):
|
||||
(1) không-param ⇒ 2/Total 2 · (2) group=1 ⇒ 1/Total 1 đúng phiếu G1 · (3) group=3 ⇒ đúng phiếu
|
||||
G3 (chứng không phải "luôn trả cái đầu") · (4) group=7 ⇒ RỖNG (lọc không khớp ≠ bỏ qua lọc) ·
|
||||
(5) **nhánh ADMIN** ⇒ vẫn lọc đúng (vế lọc nằm NGOÀI khối IDOR `:884-893`; chỉ đo 1 vai thì
|
||||
đẩy vế lọc vào trong `if (!admin)` vẫn xanh). +2 sanity trước phép đo chính (2 row THẬT sự
|
||||
`IsDeleted` ∧ THẬT sự khác nhóm).
|
||||
- [x] **CHỨNG CÓ RĂNG — RED→GREEN 2 CHIỀU, ĐO THẬT, KHÔNG CHẠM PROD:**
|
||||
thế-giới-hỏng ("handler bỏ qua param") **tương đương chính xác** với `Handle(ApprovalGroup: null)`
|
||||
trên MÃ THẬT ⇒ mô phỏng bằng cách tạm đổi ĐÚNG 1 chữ **trong test** (`approvalGroup: 1` → `null`).
|
||||
- 🔴 RED đo được: `Failed: 1` — `Expected g1.Items to contain 1 item(s), but found 2`
|
||||
(đúng chữ ký lỗi mà bản chưa vá sẽ tạo ra: 8 URL `?group=n` trả cùng tập mọi-nhóm).
|
||||
- 🟢 Hoàn nguyên → `Passed: 1`. Verify không dư lượng: `grep "approvalGroup: null)"` chỉ còn
|
||||
2 dòng HỢP LỆ (vế (1) không-lọc và vế (5) admin không-lọc).
|
||||
Cách này chạy trên production code THẬT (mạnh hơn probe mimic của T7) và giữ trọn rào không-đụng-prod.
|
||||
|
||||
## §10 — SỐ CHỐT CUỐI (đo tươi sau MAJOR-A)
|
||||
|
||||
- Filter `~ContractSigningPlanCrudTests`: **8/8 PASS** · filter `~KhkkGroupMenuSeed`: 7/7 PASS.
|
||||
- **Infra: 577 PASS / 0 FAIL** (569 baseline + 7 K4a + 1 MAJOR-A) · **Domain: 45 PASS / 0 FAIL**.
|
||||
- **Tổng suite = 622.** `git status -- src/` sạch phần của tôi (0 prod-code touched).
|
||||
|
||||
END sub-test-k4a — VERDICT=8/8 PASS (+8 test: 7 K4a + 1 gate-K4b MAJOR-A, suite 622 = 45 Domain + 577 Infra, 0 hồi quy, 0 prod-code touched)
|
||||
|
||||
@ -55,9 +55,30 @@ export type PipelineLeaf = {
|
||||
onOpen?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* [K5 S166] Ngăn con TRONG một folder giai đoạn (GĐ2 = 8 nhóm duyệt KHKK).
|
||||
* 🔴 `leaves` ở đây là LEAVES THẬT (chính các phiếu con), KHÔNG phải một đường
|
||||
* `content` riêng — nhờ vậy tổng số phiếu của các ngăn luôn bằng số phiếu của
|
||||
* folder cha theo CẤU TRÚC, không phải nhờ ai đó nhớ cộng cho khớp (vá-9).
|
||||
*/
|
||||
export type PipelineGroup = {
|
||||
key: string
|
||||
label: string
|
||||
leaves: PipelineLeaf[]
|
||||
/** Bấm vào TÊN ngăn ⇒ mở trang danh sách đã lọc sẵn ngăn đó. */
|
||||
onOpenAll?: () => void
|
||||
}
|
||||
|
||||
export type PipelineStage = {
|
||||
n: PipelineStageNo
|
||||
/** Danh sách PHẲNG, là nguồn duy nhất để đếm. `groups` chỉ chia lại chính nó. */
|
||||
leaves: PipelineLeaf[]
|
||||
/**
|
||||
* Có mặt ⇒ thân folder vẽ theo NGĂN thay vì danh sách phẳng. Ngăn RỖNG vẫn
|
||||
* hiện (mirror tiền lệ folder giai đoạn chưa có gì: chip "Sắp") — người dùng
|
||||
* phải thấy đủ khung 8 nhóm để biết cái nào trống, chứ không phải đoán.
|
||||
*/
|
||||
groups?: PipelineGroup[]
|
||||
/** Không có quyền xem module GĐ này ⇒ folder khoá + "Sắp", KHÔNG gọi API. */
|
||||
locked?: boolean
|
||||
/** Query GĐ này lỗi ⇒ đếm "—", cây KHÔNG vỡ. */
|
||||
@ -85,6 +106,106 @@ function SoonChip() {
|
||||
)
|
||||
}
|
||||
|
||||
type StageStyle = (typeof STAGE_STYLE)[PipelineStageNo]
|
||||
|
||||
/** Danh sách leaf phẳng — dùng chung cho thân folder LẪN thân từng ngăn (K5). */
|
||||
function LeafList({ leaves, style }: { leaves: PipelineLeaf[]; style: StageStyle }) {
|
||||
return (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{leaves.map(leaf => (
|
||||
<li key={leaf.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={leaf.onOpen}
|
||||
disabled={!leaf.onOpen}
|
||||
className={cn(
|
||||
'block w-full px-3 py-2 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60',
|
||||
leaf.onOpen ? 'hover:bg-slate-50' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Mã to hơn dòng phụ 1 nấc: font-mono 11px NHÌN nhỏ ngang
|
||||
sans 11px nên 2 dòng bẹt như nhau (soi FD2 r1-D). */}
|
||||
<span className={cn('min-w-0 flex-1 truncate font-mono text-[12px] font-medium', style.code)}>
|
||||
{leaf.primary}
|
||||
</span>
|
||||
{leaf.badgeLabel && (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||
leaf.badgeClass ?? 'bg-slate-100 text-slate-600',
|
||||
)}
|
||||
>
|
||||
{leaf.badgeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{leaf.secondary && <div className="truncate text-[11px] text-slate-500">{leaf.secondary}</div>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [K5 S166] Một ngăn trong folder giai đoạn (GĐ2 = 1 nhóm duyệt).
|
||||
* 🔴 Hai thao tác TÁCH BẠCH trên cùng một dòng: bấm vùng dòng = mở/đóng ngăn
|
||||
* (hành vi mặc định của <summary>), bấm ĐÚNG TÊN ngăn = sang trang danh sách đã
|
||||
* lọc sẵn nhóm đó. Nút tên phải chặn nổi bọt, nếu không mỗi lần đi trang lại
|
||||
* kèm một lần đóng/mở ngăn nhấp nháy phía sau.
|
||||
* Mặc định ĐÓNG: 8 ngăn mở sẵn là một bức tường chữ, mà cây này nằm ở cột trái.
|
||||
*/
|
||||
function GroupFolder({ group, style }: { group: PipelineGroup; style: StageStyle }) {
|
||||
const count = group.leaves.length
|
||||
const empty = count === 0
|
||||
return (
|
||||
<details className="group/grp">
|
||||
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60 [&::-webkit-details-marker]:hidden">
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/grp:rotate-90 motion-reduce:transition-none"
|
||||
/>
|
||||
{group.onOpenAll ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
group.onOpenAll?.()
|
||||
}}
|
||||
title={`Mở danh sách: ${group.label}`}
|
||||
// Ngăn rỗng vẫn bấm sang trang được (để lập phiếu mới cho nhóm đó)
|
||||
// ⇒ vẫn là chữ đang hoạt động: slate-500 giữ đủ tương phản AA.
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate rounded text-left text-[11px] font-medium underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/60',
|
||||
empty ? 'text-slate-500' : style.head,
|
||||
)}
|
||||
>
|
||||
{group.label}
|
||||
</button>
|
||||
) : (
|
||||
<span className={cn('min-w-0 flex-1 truncate text-[11px] font-medium', empty ? 'text-slate-500' : style.head)}>
|
||||
{group.label}
|
||||
</span>
|
||||
)}
|
||||
{count > 0 ? (
|
||||
<span className={cn('shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium', style.count)}>{count}</span>
|
||||
) : (
|
||||
<SoonChip />
|
||||
)}
|
||||
</summary>
|
||||
<div className="ml-3 border-l border-slate-200">
|
||||
{empty ? (
|
||||
<div className="px-3 py-1.5 text-[11px] italic text-slate-400">Chưa có phiếu ở nhóm này.</div>
|
||||
) : (
|
||||
<LeafList leaves={group.leaves} style={style} />
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
function StageFolder({ stage, currentStage }: { stage: PipelineStage; currentStage: PipelineStageNo }) {
|
||||
// Folder của TRANG hiện tại mở sẵn; 3 folder kia đóng. Vẫn collapse/expand được.
|
||||
const [open, setOpen] = useState(stage.n === currentStage)
|
||||
@ -163,43 +284,19 @@ function StageFolder({ stage, currentStage }: { stage: PipelineStage; currentSta
|
||||
</div>
|
||||
) : stage.content !== undefined ? (
|
||||
stage.content
|
||||
) : stage.groups !== undefined ? (
|
||||
// [K5 S166] Có ngăn ⇒ vẽ theo ngăn. Ngăn rỗng KHÔNG bị lọc bỏ ở đây:
|
||||
// việc "hiện đủ 8 nhóm hay không" do bên dựng quyết (xem builder), chỗ
|
||||
// này chỉ vẽ đúng thứ nhận được.
|
||||
<div>
|
||||
{stage.groups.map(g => (
|
||||
<GroupFolder key={g.key} group={g} style={style} />
|
||||
))}
|
||||
</div>
|
||||
) : stage.leaves.length === 0 ? (
|
||||
<div className="px-3 py-2 text-[11px] italic text-slate-400">Chưa có dữ liệu ở giai đoạn này.</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{stage.leaves.map(leaf => (
|
||||
<li key={leaf.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={leaf.onOpen}
|
||||
disabled={!leaf.onOpen}
|
||||
className={cn(
|
||||
'block w-full px-3 py-2 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60',
|
||||
leaf.onOpen ? 'hover:bg-slate-50' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Mã to hơn dòng phụ 1 nấc: font-mono 11px NHÌN nhỏ ngang
|
||||
sans 11px nên 2 dòng bẹt như nhau (soi FD2 r1-D). */}
|
||||
<span className={cn('min-w-0 flex-1 truncate font-mono text-[12px] font-medium', style.code)}>
|
||||
{leaf.primary}
|
||||
</span>
|
||||
{leaf.badgeLabel && (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||
leaf.badgeClass ?? 'bg-slate-100 text-slate-600',
|
||||
)}
|
||||
>
|
||||
{leaf.badgeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{leaf.secondary && <div className="truncate text-[11px] text-slate-500">{leaf.secondary}</div>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<LeafList leaves={stage.leaves} style={style} />
|
||||
)}
|
||||
{stage.truncated && !stage.failed && !stage.loading && (
|
||||
<div className="border-t border-slate-100 px-3 py-1 text-[10px] italic text-slate-400">
|
||||
|
||||
@ -24,7 +24,14 @@ import { api } from '@/lib/api'
|
||||
import { usePermission } from '@/hooks/usePermission'
|
||||
import type { PipelineStage } from '@/components/pipeline/PipelineStageFolders'
|
||||
import { ContractPhaseColor, ContractPhaseLabel, type ContractListItem } from '@/types/contracts'
|
||||
import { KHKK_PHASE_BADGE, KHKK_PHASE_LABELS, type KhkkListItemDto, type PagedResult } from '@/types/khkk'
|
||||
import {
|
||||
KHKK_APPROVAL_GROUPS,
|
||||
KHKK_PHASE_BADGE,
|
||||
KHKK_PHASE_LABELS,
|
||||
khkkGroupMenuLabel,
|
||||
type KhkkListItemDto,
|
||||
type PagedResult,
|
||||
} from '@/types/khkk'
|
||||
import type { Paged } from '@/types/master'
|
||||
import {
|
||||
PeDisplayStatusColor,
|
||||
@ -155,8 +162,9 @@ export type BuildStagesOptions = {
|
||||
* Trả `buildStages(peItems)` — dựng 4 folder cho MỘT gói thầu (1 cặp Dự án ×
|
||||
* Hạng mục) từ danh sách phiếu PE của gói đó.
|
||||
* GĐ1 = chính các phiếu ấy
|
||||
* GĐ2 = KHKK có `purchaseEvaluationId` thuộc tập phiếu
|
||||
* GĐ3 = HĐ trỏ bởi `pe.contractId`
|
||||
* GĐ2 = KHKK có `purchaseEvaluationId` thuộc tập phiếu, chia thành 8 NGĂN theo
|
||||
* nhóm duyệt (+ ngăn "(chưa phân nhóm)" khi có phiếu lạc) — K5
|
||||
* GĐ3 = HĐ trong TẬP id của gói (xem forward-provision B3-A5 ở thân hàm)
|
||||
* GĐ4 = đúng các HĐ ấy khi `hasSealedCopy === true`
|
||||
*/
|
||||
export function usePipelineStages() {
|
||||
@ -185,20 +193,72 @@ export function usePipelineStages() {
|
||||
}),
|
||||
}
|
||||
|
||||
// ── GĐ2 — Kế hoạch Hợp đồng ────────────────────────────────────────────
|
||||
const khkkLeaves = linkage.canKhkk
|
||||
? peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? []).map(k => ({
|
||||
id: k.id,
|
||||
primary: k.maKeHoach ?? '(chưa cấp mã)',
|
||||
secondary: k.peTenGoiThau,
|
||||
badgeLabel: KHKK_PHASE_LABELS[k.phase],
|
||||
badgeClass: KHKK_PHASE_BADGE[k.phase],
|
||||
onOpen: () => navigate(`/khkk/${k.id}`),
|
||||
}))
|
||||
: []
|
||||
// ── GĐ2 — Kế hoạch Hợp đồng (8 ngăn theo NHÓM DUYỆT) ───────────────────
|
||||
const khkkItems = linkage.canKhkk ? peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? []) : []
|
||||
const toKhkkLeaf = (k: KhkkListItemDto) => ({
|
||||
id: k.id,
|
||||
primary: k.maKeHoach ?? '(chưa cấp mã)',
|
||||
secondary: k.peTenGoiThau,
|
||||
badgeLabel: KHKK_PHASE_LABELS[k.phase],
|
||||
badgeClass: KHKK_PHASE_BADGE[k.phase],
|
||||
onOpen: () => navigate(`/khkk/${k.id}`),
|
||||
})
|
||||
const khkkLeaves = khkkItems.map(toKhkkLeaf)
|
||||
|
||||
// [K5 S166 — vá-9] Chia phiếu về ngăn theo `approvalGroup` CỦA CHÍNH PHIẾU.
|
||||
// Phiếu thiếu nhóm / nhóm ngoài miền 1..8 KHÔNG được rơi ra ngoài im lặng —
|
||||
// gom vào ngăn "(chưa phân nhóm)" để nhìn thấy được mà đi sửa dữ liệu.
|
||||
// (`approvalGroup` khai `number` nhưng payload cũ còn trong cache React Query
|
||||
// có thể chưa có field — K2 cũng để `!= null` guard ở 2 trang.)
|
||||
const khkkByGroup = new Map<number, KhkkListItemDto[]>()
|
||||
const khkkUngrouped: KhkkListItemDto[] = []
|
||||
for (const k of khkkItems) {
|
||||
const g = k.approvalGroup
|
||||
if (typeof g === 'number' && (KHKK_APPROVAL_GROUPS as readonly number[]).includes(g)) {
|
||||
const bucket = khkkByGroup.get(g)
|
||||
if (bucket) bucket.push(k)
|
||||
else khkkByGroup.set(g, [k])
|
||||
} else {
|
||||
khkkUngrouped.push(k)
|
||||
}
|
||||
}
|
||||
// 🔴 HIỆN ĐỦ 8 NGĂN KỂ CẢ RỖNG — mirror tiền lệ folder giai đoạn chưa có gì
|
||||
// (chip "Sắp"): khung nhóm là thứ người dùng cần thấy để biết chỗ nào trống
|
||||
// và bấm vào lập phiếu, ẩn đi thì họ phải đoán hệ thống có mấy nhóm.
|
||||
const khkkGroups = KHKK_APPROVAL_GROUPS.map(n => ({
|
||||
key: `khkk-g${n}`,
|
||||
label: khkkGroupMenuLabel(n),
|
||||
leaves: (khkkByGroup.get(n) ?? []).map(toKhkkLeaf),
|
||||
// Khớp ĐÚNG URL leaf sidebar nhóm n (K4b) ⇒ bấm ngăn hay bấm menu ra cùng
|
||||
// một trang, và trang đó tự sáng đúng mục bên trái.
|
||||
onOpenAll: () => navigate(`/khkk/list?group=${n}`),
|
||||
}))
|
||||
// Ngăn "chưa phân nhóm" CHỈ hiện khi thật sự có phiếu rơi vào — khác 8 ngăn
|
||||
// trên (8 nhóm là khung cố định của nghiệp vụ, còn đây là chỗ chứa bất thường).
|
||||
if (khkkUngrouped.length > 0) {
|
||||
khkkGroups.push({
|
||||
key: 'khkk-g0',
|
||||
label: '(chưa phân nhóm)',
|
||||
leaves: khkkUngrouped.map(toKhkkLeaf),
|
||||
onOpenAll: () => navigate('/khkk/list'),
|
||||
})
|
||||
}
|
||||
|
||||
// Bất-biến Σ: mỗi phiếu vào ĐÚNG một ngăn ⇒ tổng các ngăn == tổng folder.
|
||||
// Bảo đảm bằng cấu trúc (vòng lặp trên phân hoạch `khkkItems`), dòng dưới chỉ
|
||||
// là chuông báo khi ai đó sửa vòng lặp thành lọc-trùng/lọc-sót. Dev-only:
|
||||
// người dùng cuối không cần thấy, nhưng người sửa code thì cần.
|
||||
const khkkGrouped = khkkGroups.reduce((s, g) => s + g.leaves.length, 0)
|
||||
if (import.meta.env.DEV && khkkGrouped !== khkkLeaves.length) {
|
||||
console.warn(
|
||||
`[pipeline GĐ2] Σ ngăn (${khkkGrouped}) ≠ Σ folder (${khkkLeaves.length}) — có phiếu bị nhân đôi hoặc rơi mất khi chia nhóm.`,
|
||||
)
|
||||
}
|
||||
|
||||
const stage2: PipelineStage = {
|
||||
n: 2,
|
||||
leaves: khkkLeaves,
|
||||
groups: khkkGroups,
|
||||
locked: !linkage.canKhkk,
|
||||
failed: linkage.khkkFailed,
|
||||
loading: linkage.khkkLoading,
|
||||
@ -208,21 +268,43 @@ export function usePipelineStages() {
|
||||
// ── GĐ3 / GĐ4 — HĐ trỏ bởi phiếu (GĐ4 = tập con đã có bản cứng) ─────────
|
||||
// HĐ nằm ngoài index (list kẹp 200) vẫn được nêu bằng leaf tối giản thay vì
|
||||
// biến mất im lặng — nhưng KHÔNG suy được `hasSealedCopy` ⇒ chỉ vào GĐ3.
|
||||
const seen = new Set<string>()
|
||||
//
|
||||
// [K5 S166 — forward-provision B3-A5] Lấy HĐ của gói bằng một TẬP id gộp từ
|
||||
// NHIỀU nguồn, thay vì đọc thẳng một ô `pe.contractId`. Hôm nay tập ấy chỉ có
|
||||
// một nguồn, nhưng hình dạng đã đúng nên thêm nguồn sau là thêm một dòng.
|
||||
//
|
||||
// 🔴 VÌ SAO CẦN: 1 phiếu có thể đẻ N hợp đồng (liên danh — endpoint
|
||||
// `create-contract` trả `contractIds: string[]`, xem `PeDetailTabs.tsx:2186`),
|
||||
// NHƯNG BE chỉ ghi ngược HĐ ĐẦU vào phiếu:
|
||||
// `CreateContractFromEvaluationFeatures.cs:143-145`
|
||||
// "Idempotency: pe.ContractId = HĐ ĐẦU … N HĐ liên-danh truy qua changelog"
|
||||
// ⇒ HĐ thứ 2 trở đi hiện KHÔNG có đường nào để cây nhìn thấy: `PeListItem`
|
||||
// không có `lines[]`, `ContractListItem` không có `purchaseEvaluationId`,
|
||||
// `Contract.cs` cũng không có khoá ngược. Đây là giới hạn DỮ LIỆU, không
|
||||
// phải chỗ này thiếu logic — đừng "sửa" bằng cách bịa field không tồn tại.
|
||||
// ⇒ KHI BE mở đường (một trong hai: list DTO trả `contractIds[]`, hoặc dòng
|
||||
// phiếu có `contractId` riêng), nối vào ngay dưới đây và xoá ghi chú này.
|
||||
const contractIdsOfPackage = new Set<string>()
|
||||
for (const p of peItems) {
|
||||
if (p.contractId) contractIdsOfPackage.add(p.contractId)
|
||||
// ← chỗ cắm nguồn thứ hai (HĐ 2..N). Ví dụ khi có: `p.contractIds?.forEach(...)`
|
||||
// hoặc `p.lines?.forEach(l => l.contractId && add(l.contractId))`.
|
||||
}
|
||||
|
||||
const stage3Leaves: PipelineStage['leaves'] = []
|
||||
const stage4Leaves: PipelineStage['leaves'] = []
|
||||
for (const p of peItems) {
|
||||
if (!p.contractId || seen.has(p.contractId)) continue
|
||||
seen.add(p.contractId)
|
||||
const contractId = p.contractId
|
||||
// `Set` đã khử trùng ⇒ không cần biến `seen` riêng như trước.
|
||||
for (const contractId of contractIdsOfPackage) {
|
||||
const c = linkage.contractById.get(contractId)
|
||||
// Tên gói thầu để làm dòng phụ khi HĐ nằm ngoài cửa sổ index 200.
|
||||
const fallbackName = peItems.find(p => p.contractId === contractId)?.tenGoiThau ?? null
|
||||
if (linkage.canContract) {
|
||||
stage3Leaves.push({
|
||||
id: contractId,
|
||||
// [F-3 S162] c vắng = HĐ NGOÀI cửa sổ index 200 — đừng dán nhãn
|
||||
// "(chưa cấp mã)" (trộn 2 nghĩa khác nhau).
|
||||
primary: c ? (c.maHopDong ?? '(chưa cấp mã)') : '(ngoài chỉ mục 200 — mở để xem)',
|
||||
secondary: c?.tenHopDong ?? p.tenGoiThau,
|
||||
secondary: c?.tenHopDong ?? fallbackName,
|
||||
badgeLabel: c ? ContractPhaseLabel[c.phase] : null,
|
||||
badgeClass: c ? ContractPhaseColor[c.phase] : null,
|
||||
onOpen: () => navigate(`/contracts/${contractId}`),
|
||||
|
||||
@ -55,9 +55,30 @@ export type PipelineLeaf = {
|
||||
onOpen?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* [K5 S166] Ngăn con TRONG một folder giai đoạn (GĐ2 = 8 nhóm duyệt KHKK).
|
||||
* 🔴 `leaves` ở đây là LEAVES THẬT (chính các phiếu con), KHÔNG phải một đường
|
||||
* `content` riêng — nhờ vậy tổng số phiếu của các ngăn luôn bằng số phiếu của
|
||||
* folder cha theo CẤU TRÚC, không phải nhờ ai đó nhớ cộng cho khớp (vá-9).
|
||||
*/
|
||||
export type PipelineGroup = {
|
||||
key: string
|
||||
label: string
|
||||
leaves: PipelineLeaf[]
|
||||
/** Bấm vào TÊN ngăn ⇒ mở trang danh sách đã lọc sẵn ngăn đó. */
|
||||
onOpenAll?: () => void
|
||||
}
|
||||
|
||||
export type PipelineStage = {
|
||||
n: PipelineStageNo
|
||||
/** Danh sách PHẲNG, là nguồn duy nhất để đếm. `groups` chỉ chia lại chính nó. */
|
||||
leaves: PipelineLeaf[]
|
||||
/**
|
||||
* Có mặt ⇒ thân folder vẽ theo NGĂN thay vì danh sách phẳng. Ngăn RỖNG vẫn
|
||||
* hiện (mirror tiền lệ folder giai đoạn chưa có gì: chip "Sắp") — người dùng
|
||||
* phải thấy đủ khung 8 nhóm để biết cái nào trống, chứ không phải đoán.
|
||||
*/
|
||||
groups?: PipelineGroup[]
|
||||
/** Không có quyền xem module GĐ này ⇒ folder khoá + "Sắp", KHÔNG gọi API. */
|
||||
locked?: boolean
|
||||
/** Query GĐ này lỗi ⇒ đếm "—", cây KHÔNG vỡ. */
|
||||
@ -85,6 +106,106 @@ function SoonChip() {
|
||||
)
|
||||
}
|
||||
|
||||
type StageStyle = (typeof STAGE_STYLE)[PipelineStageNo]
|
||||
|
||||
/** Danh sách leaf phẳng — dùng chung cho thân folder LẪN thân từng ngăn (K5). */
|
||||
function LeafList({ leaves, style }: { leaves: PipelineLeaf[]; style: StageStyle }) {
|
||||
return (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{leaves.map(leaf => (
|
||||
<li key={leaf.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={leaf.onOpen}
|
||||
disabled={!leaf.onOpen}
|
||||
className={cn(
|
||||
'block w-full px-3 py-2 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60',
|
||||
leaf.onOpen ? 'hover:bg-slate-50' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Mã to hơn dòng phụ 1 nấc: font-mono 11px NHÌN nhỏ ngang
|
||||
sans 11px nên 2 dòng bẹt như nhau (soi FD2 r1-D). */}
|
||||
<span className={cn('min-w-0 flex-1 truncate font-mono text-[12px] font-medium', style.code)}>
|
||||
{leaf.primary}
|
||||
</span>
|
||||
{leaf.badgeLabel && (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||
leaf.badgeClass ?? 'bg-slate-100 text-slate-600',
|
||||
)}
|
||||
>
|
||||
{leaf.badgeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{leaf.secondary && <div className="truncate text-[11px] text-slate-500">{leaf.secondary}</div>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [K5 S166] Một ngăn trong folder giai đoạn (GĐ2 = 1 nhóm duyệt).
|
||||
* 🔴 Hai thao tác TÁCH BẠCH trên cùng một dòng: bấm vùng dòng = mở/đóng ngăn
|
||||
* (hành vi mặc định của <summary>), bấm ĐÚNG TÊN ngăn = sang trang danh sách đã
|
||||
* lọc sẵn nhóm đó. Nút tên phải chặn nổi bọt, nếu không mỗi lần đi trang lại
|
||||
* kèm một lần đóng/mở ngăn nhấp nháy phía sau.
|
||||
* Mặc định ĐÓNG: 8 ngăn mở sẵn là một bức tường chữ, mà cây này nằm ở cột trái.
|
||||
*/
|
||||
function GroupFolder({ group, style }: { group: PipelineGroup; style: StageStyle }) {
|
||||
const count = group.leaves.length
|
||||
const empty = count === 0
|
||||
return (
|
||||
<details className="group/grp">
|
||||
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60 [&::-webkit-details-marker]:hidden">
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/grp:rotate-90 motion-reduce:transition-none"
|
||||
/>
|
||||
{group.onOpenAll ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
group.onOpenAll?.()
|
||||
}}
|
||||
title={`Mở danh sách: ${group.label}`}
|
||||
// Ngăn rỗng vẫn bấm sang trang được (để lập phiếu mới cho nhóm đó)
|
||||
// ⇒ vẫn là chữ đang hoạt động: slate-500 giữ đủ tương phản AA.
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate rounded text-left text-[11px] font-medium underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/60',
|
||||
empty ? 'text-slate-500' : style.head,
|
||||
)}
|
||||
>
|
||||
{group.label}
|
||||
</button>
|
||||
) : (
|
||||
<span className={cn('min-w-0 flex-1 truncate text-[11px] font-medium', empty ? 'text-slate-500' : style.head)}>
|
||||
{group.label}
|
||||
</span>
|
||||
)}
|
||||
{count > 0 ? (
|
||||
<span className={cn('shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium', style.count)}>{count}</span>
|
||||
) : (
|
||||
<SoonChip />
|
||||
)}
|
||||
</summary>
|
||||
<div className="ml-3 border-l border-slate-200">
|
||||
{empty ? (
|
||||
<div className="px-3 py-1.5 text-[11px] italic text-slate-400">Chưa có phiếu ở nhóm này.</div>
|
||||
) : (
|
||||
<LeafList leaves={group.leaves} style={style} />
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
function StageFolder({ stage, currentStage }: { stage: PipelineStage; currentStage: PipelineStageNo }) {
|
||||
// Folder của TRANG hiện tại mở sẵn; 3 folder kia đóng. Vẫn collapse/expand được.
|
||||
const [open, setOpen] = useState(stage.n === currentStage)
|
||||
@ -163,43 +284,19 @@ function StageFolder({ stage, currentStage }: { stage: PipelineStage; currentSta
|
||||
</div>
|
||||
) : stage.content !== undefined ? (
|
||||
stage.content
|
||||
) : stage.groups !== undefined ? (
|
||||
// [K5 S166] Có ngăn ⇒ vẽ theo ngăn. Ngăn rỗng KHÔNG bị lọc bỏ ở đây:
|
||||
// việc "hiện đủ 8 nhóm hay không" do bên dựng quyết (xem builder), chỗ
|
||||
// này chỉ vẽ đúng thứ nhận được.
|
||||
<div>
|
||||
{stage.groups.map(g => (
|
||||
<GroupFolder key={g.key} group={g} style={style} />
|
||||
))}
|
||||
</div>
|
||||
) : stage.leaves.length === 0 ? (
|
||||
<div className="px-3 py-2 text-[11px] italic text-slate-400">Chưa có dữ liệu ở giai đoạn này.</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{stage.leaves.map(leaf => (
|
||||
<li key={leaf.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={leaf.onOpen}
|
||||
disabled={!leaf.onOpen}
|
||||
className={cn(
|
||||
'block w-full px-3 py-2 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60',
|
||||
leaf.onOpen ? 'hover:bg-slate-50' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Mã to hơn dòng phụ 1 nấc: font-mono 11px NHÌN nhỏ ngang
|
||||
sans 11px nên 2 dòng bẹt như nhau (soi FD2 r1-D). */}
|
||||
<span className={cn('min-w-0 flex-1 truncate font-mono text-[12px] font-medium', style.code)}>
|
||||
{leaf.primary}
|
||||
</span>
|
||||
{leaf.badgeLabel && (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||
leaf.badgeClass ?? 'bg-slate-100 text-slate-600',
|
||||
)}
|
||||
>
|
||||
{leaf.badgeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{leaf.secondary && <div className="truncate text-[11px] text-slate-500">{leaf.secondary}</div>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<LeafList leaves={stage.leaves} style={style} />
|
||||
)}
|
||||
{stage.truncated && !stage.failed && !stage.loading && (
|
||||
<div className="border-t border-slate-100 px-3 py-1 text-[10px] italic text-slate-400">
|
||||
|
||||
@ -24,7 +24,14 @@ import { api } from '@/lib/api'
|
||||
import { usePermission } from '@/hooks/usePermission'
|
||||
import type { PipelineStage } from '@/components/pipeline/PipelineStageFolders'
|
||||
import { ContractPhaseColor, ContractPhaseLabel, type ContractListItem } from '@/types/contracts'
|
||||
import { KHKK_PHASE_BADGE, KHKK_PHASE_LABELS, type KhkkListItemDto, type PagedResult } from '@/types/khkk'
|
||||
import {
|
||||
KHKK_APPROVAL_GROUPS,
|
||||
KHKK_PHASE_BADGE,
|
||||
KHKK_PHASE_LABELS,
|
||||
khkkGroupMenuLabel,
|
||||
type KhkkListItemDto,
|
||||
type PagedResult,
|
||||
} from '@/types/khkk'
|
||||
import type { Paged } from '@/types/master'
|
||||
import {
|
||||
PeDisplayStatusColor,
|
||||
@ -155,8 +162,9 @@ export type BuildStagesOptions = {
|
||||
* Trả `buildStages(peItems)` — dựng 4 folder cho MỘT gói thầu (1 cặp Dự án ×
|
||||
* Hạng mục) từ danh sách phiếu PE của gói đó.
|
||||
* GĐ1 = chính các phiếu ấy
|
||||
* GĐ2 = KHKK có `purchaseEvaluationId` thuộc tập phiếu
|
||||
* GĐ3 = HĐ trỏ bởi `pe.contractId`
|
||||
* GĐ2 = KHKK có `purchaseEvaluationId` thuộc tập phiếu, chia thành 8 NGĂN theo
|
||||
* nhóm duyệt (+ ngăn "(chưa phân nhóm)" khi có phiếu lạc) — K5
|
||||
* GĐ3 = HĐ trong TẬP id của gói (xem forward-provision B3-A5 ở thân hàm)
|
||||
* GĐ4 = đúng các HĐ ấy khi `hasSealedCopy === true`
|
||||
*/
|
||||
export function usePipelineStages() {
|
||||
@ -185,20 +193,72 @@ export function usePipelineStages() {
|
||||
}),
|
||||
}
|
||||
|
||||
// ── GĐ2 — Kế hoạch Hợp đồng ────────────────────────────────────────────
|
||||
const khkkLeaves = linkage.canKhkk
|
||||
? peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? []).map(k => ({
|
||||
id: k.id,
|
||||
primary: k.maKeHoach ?? '(chưa cấp mã)',
|
||||
secondary: k.peTenGoiThau,
|
||||
badgeLabel: KHKK_PHASE_LABELS[k.phase],
|
||||
badgeClass: KHKK_PHASE_BADGE[k.phase],
|
||||
onOpen: () => navigate(`/khkk/${k.id}`),
|
||||
}))
|
||||
: []
|
||||
// ── GĐ2 — Kế hoạch Hợp đồng (8 ngăn theo NHÓM DUYỆT) ───────────────────
|
||||
const khkkItems = linkage.canKhkk ? peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? []) : []
|
||||
const toKhkkLeaf = (k: KhkkListItemDto) => ({
|
||||
id: k.id,
|
||||
primary: k.maKeHoach ?? '(chưa cấp mã)',
|
||||
secondary: k.peTenGoiThau,
|
||||
badgeLabel: KHKK_PHASE_LABELS[k.phase],
|
||||
badgeClass: KHKK_PHASE_BADGE[k.phase],
|
||||
onOpen: () => navigate(`/khkk/${k.id}`),
|
||||
})
|
||||
const khkkLeaves = khkkItems.map(toKhkkLeaf)
|
||||
|
||||
// [K5 S166 — vá-9] Chia phiếu về ngăn theo `approvalGroup` CỦA CHÍNH PHIẾU.
|
||||
// Phiếu thiếu nhóm / nhóm ngoài miền 1..8 KHÔNG được rơi ra ngoài im lặng —
|
||||
// gom vào ngăn "(chưa phân nhóm)" để nhìn thấy được mà đi sửa dữ liệu.
|
||||
// (`approvalGroup` khai `number` nhưng payload cũ còn trong cache React Query
|
||||
// có thể chưa có field — K2 cũng để `!= null` guard ở 2 trang.)
|
||||
const khkkByGroup = new Map<number, KhkkListItemDto[]>()
|
||||
const khkkUngrouped: KhkkListItemDto[] = []
|
||||
for (const k of khkkItems) {
|
||||
const g = k.approvalGroup
|
||||
if (typeof g === 'number' && (KHKK_APPROVAL_GROUPS as readonly number[]).includes(g)) {
|
||||
const bucket = khkkByGroup.get(g)
|
||||
if (bucket) bucket.push(k)
|
||||
else khkkByGroup.set(g, [k])
|
||||
} else {
|
||||
khkkUngrouped.push(k)
|
||||
}
|
||||
}
|
||||
// 🔴 HIỆN ĐỦ 8 NGĂN KỂ CẢ RỖNG — mirror tiền lệ folder giai đoạn chưa có gì
|
||||
// (chip "Sắp"): khung nhóm là thứ người dùng cần thấy để biết chỗ nào trống
|
||||
// và bấm vào lập phiếu, ẩn đi thì họ phải đoán hệ thống có mấy nhóm.
|
||||
const khkkGroups = KHKK_APPROVAL_GROUPS.map(n => ({
|
||||
key: `khkk-g${n}`,
|
||||
label: khkkGroupMenuLabel(n),
|
||||
leaves: (khkkByGroup.get(n) ?? []).map(toKhkkLeaf),
|
||||
// Khớp ĐÚNG URL leaf sidebar nhóm n (K4b) ⇒ bấm ngăn hay bấm menu ra cùng
|
||||
// một trang, và trang đó tự sáng đúng mục bên trái.
|
||||
onOpenAll: () => navigate(`/khkk/list?group=${n}`),
|
||||
}))
|
||||
// Ngăn "chưa phân nhóm" CHỈ hiện khi thật sự có phiếu rơi vào — khác 8 ngăn
|
||||
// trên (8 nhóm là khung cố định của nghiệp vụ, còn đây là chỗ chứa bất thường).
|
||||
if (khkkUngrouped.length > 0) {
|
||||
khkkGroups.push({
|
||||
key: 'khkk-g0',
|
||||
label: '(chưa phân nhóm)',
|
||||
leaves: khkkUngrouped.map(toKhkkLeaf),
|
||||
onOpenAll: () => navigate('/khkk/list'),
|
||||
})
|
||||
}
|
||||
|
||||
// Bất-biến Σ: mỗi phiếu vào ĐÚNG một ngăn ⇒ tổng các ngăn == tổng folder.
|
||||
// Bảo đảm bằng cấu trúc (vòng lặp trên phân hoạch `khkkItems`), dòng dưới chỉ
|
||||
// là chuông báo khi ai đó sửa vòng lặp thành lọc-trùng/lọc-sót. Dev-only:
|
||||
// người dùng cuối không cần thấy, nhưng người sửa code thì cần.
|
||||
const khkkGrouped = khkkGroups.reduce((s, g) => s + g.leaves.length, 0)
|
||||
if (import.meta.env.DEV && khkkGrouped !== khkkLeaves.length) {
|
||||
console.warn(
|
||||
`[pipeline GĐ2] Σ ngăn (${khkkGrouped}) ≠ Σ folder (${khkkLeaves.length}) — có phiếu bị nhân đôi hoặc rơi mất khi chia nhóm.`,
|
||||
)
|
||||
}
|
||||
|
||||
const stage2: PipelineStage = {
|
||||
n: 2,
|
||||
leaves: khkkLeaves,
|
||||
groups: khkkGroups,
|
||||
locked: !linkage.canKhkk,
|
||||
failed: linkage.khkkFailed,
|
||||
loading: linkage.khkkLoading,
|
||||
@ -208,21 +268,43 @@ export function usePipelineStages() {
|
||||
// ── GĐ3 / GĐ4 — HĐ trỏ bởi phiếu (GĐ4 = tập con đã có bản cứng) ─────────
|
||||
// HĐ nằm ngoài index (list kẹp 200) vẫn được nêu bằng leaf tối giản thay vì
|
||||
// biến mất im lặng — nhưng KHÔNG suy được `hasSealedCopy` ⇒ chỉ vào GĐ3.
|
||||
const seen = new Set<string>()
|
||||
//
|
||||
// [K5 S166 — forward-provision B3-A5] Lấy HĐ của gói bằng một TẬP id gộp từ
|
||||
// NHIỀU nguồn, thay vì đọc thẳng một ô `pe.contractId`. Hôm nay tập ấy chỉ có
|
||||
// một nguồn, nhưng hình dạng đã đúng nên thêm nguồn sau là thêm một dòng.
|
||||
//
|
||||
// 🔴 VÌ SAO CẦN: 1 phiếu có thể đẻ N hợp đồng (liên danh — endpoint
|
||||
// `create-contract` trả `contractIds: string[]`, xem `PeDetailTabs.tsx:2186`),
|
||||
// NHƯNG BE chỉ ghi ngược HĐ ĐẦU vào phiếu:
|
||||
// `CreateContractFromEvaluationFeatures.cs:143-145`
|
||||
// "Idempotency: pe.ContractId = HĐ ĐẦU … N HĐ liên-danh truy qua changelog"
|
||||
// ⇒ HĐ thứ 2 trở đi hiện KHÔNG có đường nào để cây nhìn thấy: `PeListItem`
|
||||
// không có `lines[]`, `ContractListItem` không có `purchaseEvaluationId`,
|
||||
// `Contract.cs` cũng không có khoá ngược. Đây là giới hạn DỮ LIỆU, không
|
||||
// phải chỗ này thiếu logic — đừng "sửa" bằng cách bịa field không tồn tại.
|
||||
// ⇒ KHI BE mở đường (một trong hai: list DTO trả `contractIds[]`, hoặc dòng
|
||||
// phiếu có `contractId` riêng), nối vào ngay dưới đây và xoá ghi chú này.
|
||||
const contractIdsOfPackage = new Set<string>()
|
||||
for (const p of peItems) {
|
||||
if (p.contractId) contractIdsOfPackage.add(p.contractId)
|
||||
// ← chỗ cắm nguồn thứ hai (HĐ 2..N). Ví dụ khi có: `p.contractIds?.forEach(...)`
|
||||
// hoặc `p.lines?.forEach(l => l.contractId && add(l.contractId))`.
|
||||
}
|
||||
|
||||
const stage3Leaves: PipelineStage['leaves'] = []
|
||||
const stage4Leaves: PipelineStage['leaves'] = []
|
||||
for (const p of peItems) {
|
||||
if (!p.contractId || seen.has(p.contractId)) continue
|
||||
seen.add(p.contractId)
|
||||
const contractId = p.contractId
|
||||
// `Set` đã khử trùng ⇒ không cần biến `seen` riêng như trước.
|
||||
for (const contractId of contractIdsOfPackage) {
|
||||
const c = linkage.contractById.get(contractId)
|
||||
// Tên gói thầu để làm dòng phụ khi HĐ nằm ngoài cửa sổ index 200.
|
||||
const fallbackName = peItems.find(p => p.contractId === contractId)?.tenGoiThau ?? null
|
||||
if (linkage.canContract) {
|
||||
stage3Leaves.push({
|
||||
id: contractId,
|
||||
// [F-3 S162] c vắng = HĐ NGOÀI cửa sổ index 200 — đừng dán nhãn
|
||||
// "(chưa cấp mã)" (trộn 2 nghĩa khác nhau).
|
||||
primary: c ? (c.maHopDong ?? '(chưa cấp mã)') : '(ngoài chỉ mục 200 — mở để xem)',
|
||||
secondary: c?.tenHopDong ?? p.tenGoiThau,
|
||||
secondary: c?.tenHopDong ?? fallbackName,
|
||||
badgeLabel: c ? ContractPhaseLabel[c.phase] : null,
|
||||
badgeClass: c ? ContractPhaseColor[c.phase] : null,
|
||||
onOpen: () => navigate(`/contracts/${contractId}`),
|
||||
|
||||
@ -15,10 +15,11 @@
|
||||
// Mirror layout admin `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx`
|
||||
// line 853-949 cho 7 checkbox label (read-only — drop input onChange).
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Network, CheckCircle2, Pin } from 'lucide-react'
|
||||
import { Link, useLocation, useSearchParams } from 'react-router-dom'
|
||||
import { Network, CheckCircle2, Pin, Layers } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { api } from '@/lib/api'
|
||||
import { khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'
|
||||
import type { AwAdminOverviewDto, AwDefinitionDto, AwLevelDto } from '@/types/approvalWorkflowV2'
|
||||
|
||||
// Mig 23 ApplicableType enum mirror (BE Domain/ApprovalWorkflowsV2)
|
||||
@ -50,6 +51,9 @@ const LEVEL_PALETTE = [
|
||||
|
||||
export function WorkflowMatrixViewPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
// Trang này gắn ở 3 route (/purchase-evaluations|/contracts|/khkk + /workflow-matrix)
|
||||
// ⇒ link "xem tất cả" phải bám đường dẫn ĐANG đứng, không hardcode một cái.
|
||||
const { pathname } = useLocation()
|
||||
const rawType = Number(searchParams.get('type'))
|
||||
// [S159-đợt5] +3 = Contract (ApplicableType.Contract — HĐ dùng CHUNG 1 bộ workflow V2
|
||||
// cho mọi ContractType ⇒ 7 leaf 'Luồng duyệt' của 7 nhóm HĐ đều trỏ về matrix này).
|
||||
@ -68,9 +72,32 @@ export function WorkflowMatrixViewPage() {
|
||||
|
||||
// Em chỉ render type được chọn — BE đã filter applicableType.
|
||||
const summary = data?.types.find(t => t.applicableType === typeInt)
|
||||
const workflows: AwDefinitionDto[] = summary?.history ?? []
|
||||
const allWorkflows: AwDefinitionDto[] = summary?.history ?? []
|
||||
const typeLabel = summary?.applicableTypeLabel ?? TYPE_LABEL[typeInt] ?? 'Quy trình duyệt'
|
||||
|
||||
// ── [K4c S166] Lọc theo NHÓM DUYỆT KHKK ────────────────────────────────────
|
||||
// 8 leaf "Luồng duyệt" của 8 nhóm (K4b) đều trỏ về CÙNG trang này, chỉ khác
|
||||
// `?group=n`. Không lọc thì cả 8 leaf hiện y hệt nhau — mục menu nói dối tên nó.
|
||||
// 🔴 Chỉ áp cho type 10 (Kế hoạch ký kết): `group` là khái niệm của riêng KHKK.
|
||||
// Type khác, hoặc `group` rác/thiếu ⇒ BỎ QUA hoàn toàn, hành vi cũ giữ nguyên
|
||||
// (thà hiện đủ còn hơn lọc theo một con số vô nghĩa rồi ra trang trắng).
|
||||
const rawGroup = searchParams.get('group')
|
||||
const khkkGroup =
|
||||
typeInt === 10 && rawGroup !== null && /^[1-8]$/.test(rawGroup) ? Number(rawGroup) : null
|
||||
|
||||
// 🔴 LỌC CLIENT-SIDE, CÓ CHỦ ĐÍCH: query trên kia đã tải TRỌN bộ quy trình của
|
||||
// type này rồi (`/approval-workflows-v2?applicableType=10`), nhóm chỉ là lát cắt
|
||||
// của đúng bộ đó ⇒ KHÔNG thêm trục `group` vào `queryKey`. Thêm vào sẽ đẻ 8 lần
|
||||
// gọi mạng cho cùng một payload, và 8 ô cache cùng nội dung.
|
||||
// Khớp bằng `code` (`AwDefinitionDto.code`) chứ không bằng tên/thứ tự: BE seed
|
||||
// `KHKK-N{n}` (`DbInitializer.cs:609`) và cũng chính `code` này là thứ BE dùng để
|
||||
// gác phiếu-pin-đúng-nhóm (`ContractSigningPlanFeatures.cs:420` regex `^KHKK-N([1-8])$`).
|
||||
// Một quy trình có thể có nhiều VERSION cùng `code` ⇒ lọc giữ đủ các version.
|
||||
const workflows =
|
||||
khkkGroup !== null
|
||||
? allWorkflows.filter(wf => wf.code === khkkGroupWorkflowCode(khkkGroup))
|
||||
: allWorkflows
|
||||
|
||||
return (
|
||||
// [Plan AA hotfix S24 t1] Bro UAT request: dịch hết content sang trái.
|
||||
// px-6 → px-2 (24px → 8px) cho title + table sát sidebar.
|
||||
@ -80,9 +107,34 @@ export function WorkflowMatrixViewPage() {
|
||||
<span className="flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-slate-500" />
|
||||
Luồng duyệt — {typeLabel}
|
||||
{khkkGroup !== null && (
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-violet-300 bg-violet-100 px-2 py-0.5 text-xs font-semibold text-violet-800">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
Nhóm N{khkkGroup}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
description="Cấu hình do Admin quản trị. Có thắc mắc liên hệ Admin."
|
||||
description={
|
||||
// Đang xem lát cắt của một nhóm thì phải nói RÕ nhóm nào — nếu không,
|
||||
// 8 leaf menu mở ra 8 trang trông hệt nhau và người dùng không biết
|
||||
// mình đang đứng ở đâu.
|
||||
khkkGroup !== null
|
||||
? `${khkkGroupMenuLabel(khkkGroup)} — cấu hình do Admin quản trị. Có thắc mắc liên hệ Admin.`
|
||||
: 'Cấu hình do Admin quản trị. Có thắc mắc liên hệ Admin.'
|
||||
}
|
||||
actions={
|
||||
khkkGroup !== null ? (
|
||||
// Giữ nguyên đường dẫn hiện tại (trang này gắn ở 3 route), chỉ bỏ
|
||||
// `group` ⇒ về lại chế độ xem cả 8 nhóm.
|
||||
<Link
|
||||
to={`${pathname}?type=${typeInt}`}
|
||||
className="rounded-md border border-slate-300 px-2.5 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-50"
|
||||
>
|
||||
Xem tất cả nhóm
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
@ -99,7 +151,18 @@ export function WorkflowMatrixViewPage() {
|
||||
|
||||
{!isLoading && !isError && workflows.length === 0 && (
|
||||
<div className="rounded-xl border border-dashed border-slate-300 bg-slate-50/40 p-8 text-center text-sm text-slate-500">
|
||||
Chưa có quy trình nào được Admin ghim cho loại phiếu này. Liên hệ Admin.
|
||||
{/* 🔴 Rỗng-vì-lọc ⟂ rỗng-vì-chưa-có-gì là HAI chuyện khác nhau. Dùng
|
||||
chung một câu thì người dùng nhóm N5 tưởng cả module chưa cấu hình,
|
||||
trong khi 7 nhóm kia đang chạy bình thường. */}
|
||||
{khkkGroup !== null ? (
|
||||
<>
|
||||
Chưa có quy trình nào được Admin ghim cho nhóm{' '}
|
||||
<span className="font-medium text-slate-700">N{khkkGroup}</span>
|
||||
{allWorkflows.length > 0 && <> (loại phiếu này có {allWorkflows.length} quy trình ở nhóm khác)</>}. Liên hệ Admin.
|
||||
</>
|
||||
) : (
|
||||
<>Chưa có quy trình nào được Admin ghim cho loại phiếu này. Liên hệ Admin.</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces; // ICurrentUser — bơm cho AuditingInterceptor
|
||||
using SolutionErp.Application.Common.Models; // PagedResult<T> — kết quả màn "Đã xoá"
|
||||
using SolutionErp.Application.ContractSigningPlans;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
@ -202,14 +203,28 @@ public class ContractSigningPlanCrudTests
|
||||
|
||||
// ---- Chạy lệnh qua handler THẬT, mỗi lệnh 1 context mới ----
|
||||
|
||||
/// <param name="approvalGroup">
|
||||
/// [K4b S166] null ⇒ handler tự lấy nhóm mặc định 1 (`ContractSigningPlanScope.DefaultApprovalGroup`).
|
||||
/// Truyền số để lập phiếu ở nhóm duyệt khác — dùng cho test lọc nhóm màn "Đã xoá".
|
||||
/// </param>
|
||||
private static async Task<CreateContractSigningPlanResult> CreateAsync(
|
||||
KhkkFixture f, Guid peId, Guid workflowId, string? ghiChu = null)
|
||||
KhkkFixture f, Guid peId, Guid workflowId, string? ghiChu = null, int? approvalGroup = null)
|
||||
{
|
||||
await using var db = f.NewDb();
|
||||
var handler = new CreateContractSigningPlanCommandHandler(
|
||||
db, f.Actor, new ContractSigningPlanCodeGenerator(db, f.Clock));
|
||||
return await handler.Handle(
|
||||
new CreateContractSigningPlanCommand(peId, workflowId, ghiChu), CancellationToken.None);
|
||||
new CreateContractSigningPlanCommand(peId, workflowId, ghiChu, approvalGroup),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
private static Task<PagedResult<ContractSigningPlanListItemDto>> ListDeletedAsync(
|
||||
KhkkFixture f, ICurrentUser actor, int? approvalGroup)
|
||||
{
|
||||
var db = f.NewDb(actor);
|
||||
var handler = new ListDeletedContractSigningPlansQueryHandler(db, actor);
|
||||
return handler.Handle(
|
||||
new ListDeletedContractSigningPlansQuery(approvalGroup), CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task DeleteAsync(KhkkFixture f, Guid planId, string? reason, TestCurrentUser actor)
|
||||
@ -560,4 +575,93 @@ public class ContractSigningPlanCrudTests
|
||||
.Single(p => p.Id == created.Id).DeletedBy
|
||||
.Should().Be(admin.UserId, "người xoá thật được ghi lại là Admin, không phải chủ phiếu");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// (9) [gate-K4b MAJOR-A @S166] Màn "Đã xoá" phải LỌC NHÓM ở SERVER
|
||||
// (`ListDeletedContractSigningPlansQueryHandler` :896-897, vá @K4b).
|
||||
//
|
||||
// Vì sao đáng 1 test riêng: sidebar KHKK có 8 leaf "Đã xóa" per-nhóm, mỗi leaf trỏ
|
||||
// `?group=n`. Trước bản vá, endpoint KHÔNG đọc param ⇒ 8 URL khác nhau trả CÙNG một
|
||||
// tập mọi-nhóm — hỏng IM LẶNG (màn vẫn có dữ liệu, chỉ là dữ liệu của nhóm khác).
|
||||
//
|
||||
// Đo 4 điều, và 3 trong 4 là để phép đo KHÔNG rỗng:
|
||||
// (a) `Total` chứ không chỉ `Items` — `Total` là DẤU VÂN TAY của lọc-server:
|
||||
// `CountAsync` chạy SAU mọi `Where`. Ai "lọc" bằng cách lấy trang to rồi lọc
|
||||
// trong bộ nhớ sẽ cho `Items` Y HỆT nhưng `Total` = cả thế giới ⇒ số trang nói dối.
|
||||
// (b) group=1 ⇒ phiếu nhóm 1 · group=3 ⇒ phiếu nhóm 3 (KHÔNG phải "luôn trả cái đầu");
|
||||
// (c) group=7 (không có phiếu) ⇒ RỖNG — lọc không khớp phải ra rỗng, không phải
|
||||
// "bỏ qua lọc" (đây chính là hình dạng của lỗi trước bản vá);
|
||||
// (d) đo bằng CẢ actor thường LẪN Admin: vế lọc nhóm nằm NGOÀI khối IDOR
|
||||
// (`:884-893`), nên chỉ đo 1 vai thì đẩy vế lọc vào trong `if (!admin)` vẫn xanh.
|
||||
//
|
||||
// 🔴 CHỨNG CÓ RĂNG (không cần chạm production): ở thế giới KHÔNG có vế
|
||||
// `.Where(p => p.ApprovalGroup == request.ApprovalGroup)`, tham số bị bỏ qua hoàn toàn
|
||||
// ⇒ `Handle(group:1)` chạy Y HỆT `Handle(group:null)`. Mà `Handle(null)` được đo NGAY
|
||||
// TRONG test này = 2 phiếu ⇒ vế `group:1` phải là 1 sẽ ĐỎ. Nói cách khác cặp
|
||||
// (null ⇒ 2) ∧ (1 ⇒ 1) chính là 2 thế giới đặt cạnh nhau, đo trên MÃ THẬT.
|
||||
// ===================================================================
|
||||
[Fact]
|
||||
public async Task ListDeletedContractSigningPlans_FiltersByApprovalGroup_ServerSide()
|
||||
{
|
||||
using var f = new KhkkFixture();
|
||||
var seeded = await SeedPeAsync(f, supplierCount: 2, winnerIndexes: [0], quotes: [[100m, 7m]]);
|
||||
|
||||
// Cùng 1 phiếu PE nhưng 2 NHÓM DUYỆT khác nhau — rào "1 PE = 1 kế hoạch sống" khoá
|
||||
// theo cặp (PeId, ApprovalGroup) (`:401-404`) nên đây là đường hợp lệ.
|
||||
// Workflow seed có Code "QT-KHKK-V2" (ngoài họ `KHKK-N{n}`) ⇒ rào (v) `:420-424` cho qua.
|
||||
var planG1 = await CreateAsync(f, seeded.PeId, seeded.WorkflowId, approvalGroup: 1);
|
||||
var planG3 = await CreateAsync(f, seeded.PeId, seeded.WorkflowId, approvalGroup: 3);
|
||||
|
||||
await DeleteAsync(f, planG1.Id, "xoá phiếu nhóm 1", f.Actor);
|
||||
await DeleteAsync(f, planG3.Id, "xoá phiếu nhóm 3", f.Actor);
|
||||
|
||||
// ── Sanity TRƯỚC phép đo chính: 2 row THẬT SỰ đã xoá mềm và THẬT SỰ khác nhóm.
|
||||
// Thiếu 2 vế này thì "lọc ra 1 phiếu" có thể đúng vì lý do hoàn toàn khác.
|
||||
await using (var db = f.NewDb())
|
||||
{
|
||||
var raws = db.ContractSigningPlans.IgnoreQueryFilters().AsNoTracking()
|
||||
.Where(p => p.PurchaseEvaluationId == seeded.PeId)
|
||||
.Select(p => new { p.Id, p.ApprovalGroup, p.IsDeleted }).ToList();
|
||||
raws.Should().HaveCount(2);
|
||||
raws.Should().OnlyContain(r => r.IsDeleted, "cả 2 phải là xoá MỀM, nếu không màn Đã xoá rỗng");
|
||||
raws.Single(r => r.Id == planG1.Id).ApprovalGroup.Should().Be(1);
|
||||
raws.Single(r => r.Id == planG3.Id).ApprovalGroup.Should().Be(3, "2 phiếu phải KHÁC nhóm — cùng nhóm thì phép lọc vô nghĩa");
|
||||
}
|
||||
|
||||
// ── (1) KHÔNG truyền nhóm ⇒ thấy CẢ HAI. Đây cũng chính là ảnh của thế-giới-hỏng.
|
||||
var all = await ListDeletedAsync(f, f.Actor, approvalGroup: null);
|
||||
all.Items.Should().HaveCount(2);
|
||||
all.Total.Should().Be(2, "không lọc thì tổng là cả 2 phiếu đã xoá");
|
||||
|
||||
// ── (2) group=1 ⇒ ĐÚNG 1 phiếu nhóm 1.
|
||||
var g1 = await ListDeletedAsync(f, f.Actor, approvalGroup: 1);
|
||||
g1.Items.Should().HaveCount(1);
|
||||
g1.Total.Should().Be(1,
|
||||
"Total đếm SAU vế Where — nếu vẫn là 2 thì handler đang trả trang đã lọc trên tập chưa lọc (số trang nói dối)");
|
||||
g1.Items[0].Id.Should().Be(planG1.Id);
|
||||
g1.Items[0].ApprovalGroup.Should().Be(1);
|
||||
|
||||
// ── (3) group=3 ⇒ ĐÚNG phiếu nhóm 3 (chứng không phải "luôn trả phiếu đầu tiên").
|
||||
var g3 = await ListDeletedAsync(f, f.Actor, approvalGroup: 3);
|
||||
g3.Items.Should().HaveCount(1);
|
||||
g3.Total.Should().Be(1);
|
||||
g3.Items[0].Id.Should().Be(planG3.Id);
|
||||
g3.Items[0].ApprovalGroup.Should().Be(3);
|
||||
|
||||
// ── (4) group=7 (không phiếu nào) ⇒ RỖNG, không phải "bỏ qua lọc".
|
||||
var g7 = await ListDeletedAsync(f, f.Actor, approvalGroup: 7);
|
||||
g7.Items.Should().BeEmpty("nhóm không có phiếu nào thì màn Đã xoá của nhóm đó phải trống");
|
||||
g7.Total.Should().Be(0);
|
||||
|
||||
// ── (5) NHÁNH ADMIN: lọc nhóm nằm NGOÀI khối IDOR nên phải đúng ở cả 2 vai.
|
||||
var admin = new TestCurrentUser(Guid.NewGuid(), "Quản trị", "admin@test.local", AppRoles.Admin);
|
||||
var adminAll = await ListDeletedAsync(f, admin, approvalGroup: null);
|
||||
adminAll.Total.Should().Be(2, "Admin bỏ qua IDOR nên thấy cả 2 phiếu đã xoá");
|
||||
|
||||
var adminG3 = await ListDeletedAsync(f, admin, approvalGroup: 3);
|
||||
adminG3.Items.Should().HaveCount(1);
|
||||
adminG3.Total.Should().Be(1,
|
||||
"đẩy vế lọc nhóm vào trong `if (!admin)` sẽ làm vế này ĐỎ trong khi 4 vế trên vẫn xanh");
|
||||
adminG3.Items[0].Id.Should().Be(planG3.Id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user