wal: flush 20260801T1054

This commit is contained in:
pqhuy1987
2026-08-01 10:54:57 +07:00
parent 2023f417d0
commit d163f6d3d9
6 changed files with 162 additions and 99 deletions

View File

@ -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 N1N8 [relaunch p2 sau p1 chết #53 — chi tiết `runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k2.md` §1-§5]:** 🔴 **BÀI CHÍNH — tiền-đề lead SAI, đo `git diff` TRƯỚC khi gõ:** lead khai CreatePage "mid-wire, 8 khai báo chưa vào JSX" (TS6133 sẽ đỏ) ⇒ đo đĩa thật thì **8/8 ĐÃ có call-site**; Detail cũng land đủ. Việc thật ≈ **1/4** mô tả — chỉ ListPage dở (p1 kịp thêm mỗi dòng import). Khuôn S114 "WRITE-lane garble ≠ mất code" xác nhận lần nữa: lead đọc transcript dở-dang → kết luận theo Ý ĐỊNH đang chạy, không theo đĩa. **Viết lại phần đã đúng = phá.** ⇒ p2 chỉ sửa ListPage 3 điểm: badge `N{g}` NHÉT VÀO ô "Mã kế hoạch" (không đẻ cột — bảng đã cuộn ngang @1366 do cây GĐ 19rem, ghi chú S162 nằm sẵn trong file) · +cột "Tổng duyệt" Σ`totalApprovedAmount` (vá-8 spec, BE trả **0** cho phiếu chưa duyệt ⇒ ép `—` kẻo đọc nhầm "đã duyệt 0 đ") · **`colSpan` 7→8 hai chỗ** (loading + empty row — quên = lỗi CÂM tới lúc bảng rỗng). `npm run build` = `tsc -b && vite build` + `noUnusedLocals:true` ⇒ build LÀ phép thử TS6133 thật, không cần tin IDE (#68). cp 4 file → SHA-pair **5/5 IDENTICAL** (Panel không sửa = **đối chứng dương**, hash `2B64144EF6E8` BẰNG baseline ⇒ chứng phép đo đúng thứ). diff-stat đối xứng 269+/36 ×2 app. 0 `.cs` (13 `.cs` M/?? là lane BE), 0 route/menu (K4a). Tag `[s164, khkk-nhom-duyet, do-dia-truoc-khi-go, colspan-loi-cam, badge-trong-o-khong-de-cot, panel-doi-chung-duong]`
- **S161 (07-29) ARC W1+W2+W3đ1 KHKK FE — 3 lượt, chi tiết = sub-md `runs/2026-07-29-S161-khkk-*/` (mọi lượt #53, đĩa cứu trọn):** **W1** Designer type-10: containment **2-LỚP render+payload** (`levelFlagsPayload(e, lock)` ép false 2/2 call-site — chỉ `disabled` = containment GIẢ vì `copyFromDefinition` tuồn state); 2 tiền-đề đề-bài SAI tự bẻ (FE không có bảng nhãn — nhãn từ BE `applicableTypeLabel`; khuôn type-9 không tồn tại → mirror `Contract:3`). **W2** 3 page + types ×2 SHA-pair; types-theo-ENTITY khi BE song song; header types = HỢP-ĐỒNG 12-endpoint (reviewer đối chiếu ra 6 đứt → lead vá; bài: hợp-đồng phải LEAD-định-nghĩa literal); WfView 2-app-2-đường (user matrix?type=10 whitelist+10 · admin Designer deep-link); place-3 menuKeys SKIP-có-khai (0 consumer). **W3đ1** `KhkkWorkflowPanel` mirror PE-panel 1063-dòng theo bảng **GIỮ/BỎ tường minh** (BỎ đủ 2 đường cấm + price-picker/budget/4-radio); cây workflow THÔ — **FE tự suy ✓/●/○ từ con-trỏ đôi** (StepIndex 0-based vs Level.Order giá-trị); bổ `approverUserId` opinion (W2 khai thiếu). Tag `[s161, khkk-fe-3-wave, containment-2-lop, types-theo-entity, giu-bo-tuong-minh, fe-suy-status-con-tro]`
- **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 MinMax'`; "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 N1N8 — 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** bị giục (thiếu mirror-SHA hoặc build ×2 = trả lại). `[s152, no-lower-bar]`
- hơn (S65S97: 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 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 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 í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`.

View File

@ -5,6 +5,13 @@
---
## 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``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"``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`).

View File

@ -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.

View File

@ -1,4 +1,4 @@
IMPLFE-K4C: IN-PROGRESS
IMPLFE-K4C: DONE
# Lane FE K4c — WfView lọc theo nhóm (S166, 2026-08-01) — chốt cụm-2
@ -10,5 +10,55 @@ Phạm vi: **fe-user ONLY** — fe-admin không có `WorkflowMatrixViewPage` (le
- [ ] 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)
(append bên dưới)
## §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`**`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…``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

View File

@ -0,0 +1,15 @@
# 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
(đang đo)

View File

@ -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
`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)