diff --git a/.claude/agent-memory/implementer-frontend/archive/2026-H2.md b/.claude/agent-memory/implementer-frontend/archive/2026-H2.md new file mode 100644 index 0000000..60affc6 --- /dev/null +++ b/.claude/agent-memory/implementer-frontend/archive/2026-H2.md @@ -0,0 +1,67 @@ +# 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) — 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()` (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 `` 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 `` 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ã" ``→`` 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. diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md index adffde7..5216f8b 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k4ab.md @@ -26,4 +26,181 @@ git log origin/main..HEAD -> (rỗng — đã push) **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/**`). -(các mục §1-§7 ghi tiếp bên dưới ngay khi có bằng chứng) +### §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. + + diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k5.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k5.md new file mode 100644 index 0000000..e599a2a --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k5.md @@ -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 ? : `; `: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`: `
` 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()` 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 +