17 KiB
SUB-INVEST /fable-real — B2 CỤM-2: SPEC CHI TIẾT K4 + K5 + K6
Run
2026-07-31-S164-4gd-khkk-fanout· agent investigator-codebase (Fable) · luật ĐAN XEN: đọc đủ 1 section → viết ngay section đó.
K4 Sidebar 8 nhóm × 6 leaf
K4a — 49 row menu + MenuKeys.All + grant 3-site (0 phụ thuộc — chạy song song cụm-1)
- MenuKeys.cs — 49 key VÀO
MenuKeys.All(vá-1 cụm-1, 3-lane hội tụ): 7 groupKhkk_G2..G8+ 42 leafKhkk_G{n}_{WfView|List|Create|Pending|Approved|Deleted}(n=2..8). Đề xuất factoryKhkkGroupKey(n)/KhkkLeafKey(n,action)mirror khuônContractTypeGroup. G1 GIỮ NGUYÊN 7 key cũ không-infix (Khkk_G1+Khkk_WfView… — seedDbInitializer.cs:1780-1787): đổi key = mồ côi permission-row (S155, 13 row/key). ⚠️ Khai precedent-split để reviewer khỏi tưởng mâu thuẫn:Ct_*/Pe_*leaf CỐ Ý ngoài All (DbInitializer.cs:2193-2195"All… KHÔNG chứa Pe_* leaf") ⟂ catalog/AwV2 leaf TRONG All — cụm-2 đi nhánh TRONG-All theo vá-1 (ít site mirror hơn: Admin tự grant). Policy sinh thêm 49×4 registered-unused = vô hại, endpoint vẫn chỉ dùngKeHoachKyKet.*(#85). - Seed menu-row: thêm loop G2..G8 cạnh
:1780-1787, khuôn PE-loop:1896-1913— group (parentKeHoachKyKet, Order n, icon FileCheck) + 6 leaf y nhãn/icon/Order G1 (:1782-1787: Network/List/Plus/CheckCircle2/CheckCheck/Trash2, Order 1-6). Upsert:1926-1943: key mới đi nhánh INSERT; key cũ chỉ re-set Order. - labelBackfill G1 (dict
:1953-1995, apply:1997-2005— sửa chữ trongtree.AddVÔ TÁC DỤNG DB đang chạy, comment:1959-1961):Khkk_G1"1. Kế hoạch ký kết HĐ (NCC-TP)" → nhãn nhóm 1. Bảng nhãn 8 nhóm (đề xuất từdanh-muc-sp002-transcribe.md:5-16; owner chỉnh CHỮ được, KEY không đổi):Key Nhãn đề xuất Khkk_G1 (labelBackfill) 1. Thiết bị, máy móc, vật tư phụ (A1) Khkk_G2 2. Kết cấu, hạ tầng & DV hỗ trợ thi công (A2+A3) Khkk_G3 3. Hoạt động văn phòng (A4) Khkk_G4 4. Vật tư (B1) Khkk_G5 5. Thầu phụ xây dựng (B2) Khkk_G6 6. Thầu phụ MEP (B3) Khkk_G7 7. BCH công trường (C1-C3) Khkk_G8 8. HĐ chưa ủy quyền (CEO) - Grant 3-site — QUYẾT: KHÔNG retire site nào, EXTEND cả 3. Evidence:
- Site-1
SeedAdminPermissionsAsyncloopMenuKeys.All:2058chỉ Admin-role (:2049-2050) → 49 key vào All ⇒ Admin TỰ có row, 0 code thêm. - Site-2
KhkkKeys():2185-2189(feedreviewKeys:2216→ CanRead MỌI role, skip-existing): extend += 7 group + 42 leaf. KHÔNG retire — group-key CẦN CanRead row để render mà site-3 CỐ Ý không cấp group (comment:2418-2419"Khkk_G1 = group điều hướng, CanRead-all đã có từ nhánh read-only"). - Site-3
SeedKeHoachKyKetAccessAsync:2415-2472(UPGRADE-if-exists, R+C+U 13/13 role, Delete=Drafter+Admin:2437): keys:2420-2425extend += 42 leaf (giữ convention không cấp cờ ghi cho group). KHÔNG retire — All-loop chỉ Admin; reviewKeys chỉ CanRead; cờ GHI chỉ site-3 phát.
- Site-1
- 🔴 Sửa grounding đề bài (evidence ngược): bảng kỳ-vọng 13 role KHÔNG phải "7 vai có grant + 6 vai 0-leaf".
SeedKeHoachKyKetAccessAsynclooproleManager.Roles:2427= 13/13 role R+C+U (O-A "hiển thị hết để góp ý":2408-2410). "7 vai" là seeder PE:2669-2675— không áp KHKK. Bảng kỳ-vọng đúng: 13/13 thấy đủ 48 leaf; cột Delete ✓ chỉ Drafter+Admin. - Drift khai cùng commit (bài cụm-1): STATUS 2 row — Menu keys 64→113 (55→64 đã khai cụm-1) · Policies 256→452 (+49×4). MenuItems DB +49 (Khkk% tổng 56 row). Route-map ×2 app khai trong PR (K4c).
K4b — 4 leaf list nhận ?group= (CHỜ K2: param group BE ContractSigningPlanFeatures.cs:622-625 + DTO approvalGroup)
KhkkListPage.tsx×2 app (mirror SHA — header tự khai:3-4): đọcsearchParams.get('group')cạnhfilter/view:60-61→ truyềngroupvào params API:75-82, CẢ nhánh/deleted:74. Chip lọc giữ state-only:11-12(không ghi URL — bug UAT S155);groupnằm trong URL leaf nên NavLink active-logic ăn tự nhiên.- 6 URL leaf G1 CŨ phải mang
group=1(phiếu cũ backfillApprovalGroup=1— QĐ6 spec-tổng:16): sửa staticMap fe-userLayout.tsx:59-62+:82-83(List/khkk/list?group=1· Pending?group=1&filter=ChoDuyet· Approved?group=1&filter=DaDuyet· Deleted?group=1&view=deleted· Create/khkk/create?group=1· WfView?type=10&group=1). RootKeHoachKyKet:79GIỮ/khkk/listkhông-group = xem tất. Query-string là DANH TÍNH leaf (active-check:341-347+ bài TRANSIENT:318-331) → group phân biệt G1..G8 sạch. - Nếu thiếu K2-param mà ship K4b: leaf G2..G8 List hiển thị TẤT phiếu (silent-wrong, không lỗi build) → acceptance mục 2 bắt buộc đo SAU K2.
K4c — regex staticMap + WfView/Create (CHỜ K3: 8 Code KHKK-N1..N8)
- fe-user
Layout.tsx: regex MỚI saustaticMap[key]:131, khuôn Pe-regex:154:/^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$/→ List/khkk/list?group=n· Pending+&filter=ChoDuyet· Approved+&filter=DaDuyet· Deleted+&view=deleted· Create/khkk/create?group=n· WfView/khkk/workflow-matrix?type=10&group=n. (G1 dùng key cũ nên đi staticMap mục 8; regex[1-8]phòng key G1-infix tương lai, không hại.) - WfView lọc theo nhóm (vá l2-F3 B1b):
WorkflowMatrixViewPagehiện chỉ lọc type (:52-71) → đọcgroup→ chỉ hiện workflowCode === 'KHKK-N'+n(convention Code do K3 seed; map group↔Code = 1 chỗ export const dùng chung). - Create preset:
KhkkCreatePage×2 app đọcgroup→ preselect picker nhóm (picker + bodyapprovalGroupđã là việc vá-4 cụm-1; đây CHỈ preset giá trị). - fe-admin mirror — ĐÃ CHỐT bằng K6-read (không còn treo): fe-admin
Layout.tsxPe-regex:134CỐ Ý không WfView (:129-133thiếuWorkflowMatrixViewPagetoàn cục — lead chốt 2026-07-27 giữ lệch) VÀ K6 ẩn rootKeHoachKyKetkhỏi sidebar admin ⇒ fe-admin KHÔNG nới regexKhkk_G{n}_*(dead-by-K6). Chỉ fe-user Layout đổi ở K4b/c;KhkkListPage/KhkkCreatePagefe-admin VẪN sửa (mirror SHA — sống qua deep-link). Ghi comment khai lệch tại regex fe-user.
Acceptance K4 (đo được, UI-first — bài cụm-1)
- A1. Login Admin ×2 app: root GĐ2 → 8 group × 6 leaf = 48 leaf HIỆN sidebar (đếm từ UI, không chỉ curl).
- A2. Login vai thường (nv.test Drafter): CŨNG 48 leaf (13/13 grant — mục 5); bấm TỪNG LOẠI leaf ở nhóm ≠1: List/Pending/Approved/Deleted lọc đúng group+phase; Create preset đúng nhóm; WfView chỉ matrix
KHKK-N{n}. - A3. Bảng 13 role × {R,C,U,D} khớp seeder (R/C/U ✓ 13/13 · D ✓ Drafter+Admin): PermissionsPage matrix + 1 curl DELETE vai thường → 403 (2-tầng #82).
- A4. Key cũ 0 mồ côi:
SELECT DISTINCT MenuKey FROM Permissions WHERE MenuKey LIKE 'Khkk%'⊆MenuItems.Key; 7 key G1 cũ nguyên row. - A5. Restart API ×2 lần: MenuItems
Khkk%= 56 row đúng (7 cũ + 49 mới), không nhân; labelBackfill đổi đúng 1 nhãn G1. - A6. Drift 2 row STATUS (Menu keys 113 · Policies 452) cùng commit + route-map ×2 app khai PR.
K5 Cây sub-folder
QUYẾT khuôn: thêm groups?: PipelineGroup[] mang leaves[] THẬT — KHÔNG đi đường content. Evidence bẫy: stageCount (PipelineStageFolders.tsx:74-76) — nhánh content bắt truyền count tay (:68-69 "Bắt buộc khi dùng content — component không đếm hộ được") còn nhánh leaves tự đếm; content còn mất luôn hideEmpty/failed/leaf-styling. groups với leaves thật ⇒ count DERIVED, bất biến cấu trúc stage.leaves == concat(groups[].leaves) ⇒ Σ đúng by-construction.
- Type (
PipelineStageFolders.tsxvùng:58-72):export type PipelineGroup = { key: string; label: string; leaves: PipelineLeaf[] }+PipelineStage.groups?: PipelineGroup[]. GIỮstage.leaves= flat tổng (nguồnstageCount:74-76KHÔNG đổi — badge folder cha đếm như cũ). - Render: trong khối
ml-3 border-l(:156), nếustage.groups?.length→ mỗi group 1 sub-<details>(chevron + nhãn + chip đếmg.leaves.length, tái dùng tokenSTAGE_STYLE:39-44), ruột = đúng<ul>leaf hiện tại (:169-202) tách thành component nội bộLeafListdùng chung 2 nhánh. Group RỖNG không render (cây per-gói-thầu — hiện đủ 8 sub-folder rỗng là noise; nhãn/thứ tự khớp sidebar KHI hiện). Không đụnghideEmpty:228. - Builder GĐ2 (
usePipelineStages.ts:189-206): groupkhkkLeavestheok.approvalGroup(DTO field K2 — cụm-1 K2 chủ), sort n tăng dần;approvalGroupnull/lạ → group fallback"(chưa phân nhóm)"HIỆN CUỐI, KHÔNG rơi (chống rơi-im-lặng NULL-group; sau backfill=1 fallback đóng vai canary lệch backfill). - Builder GĐ3/GĐ4 (
:211-243): group theoc.type7 loại (c.typeđã dùng:240). 🔴 GĐ3 leaf "ngoài chỉ mục 200" (:222-224không cóc⇒ KHÔNG biết type) → vào fallback "(ngoài chỉ mục)" — đừng ép vào loại. GĐ4 luôn cóc(:231cầnc?.hasSealedCopy) ⇒ đủ 7-loại sạch. Nhãn loại tái dùng map sẵn trongtypes/contracts(đối chiếu khi build; thiếu thì thêm map 7 nhãn "01.-07." khớp label menuCt_*—DbInitializer.cs:1988-1994). - Single-source nhãn nhóm + Code (dùng chung K4c/K5):
types/khkk.ts×2 app exportKHKK_GROUP_LABELS: Record<1|..|8, string>(đúng bảng nhãn K4a-3) +khkkGroupWorkflowCode(n) = 'KHKK-N'+n. Sidebar + cây + WfView filter cùng 1 nguồn — không drift chữ. - Mirror ×2 app: 2 file đích danh SHA-identical (
usePipelineStages.tsheader:3+PipelineStageFolders.tsxheader:10tự khai luật mirror) +types/khkk.ts. ⚠️ Bộ pipeline có file thứ 3PipelineTreePanel.tsx(l3-F19 gọi tên nó): K5 dự kiến KHÔNG đụng — verify bằnggit diff; NẾU đụng thì SHA-compare luôn.
Acceptance K5
- B1. Cây trên trang GĐ2/GĐ3 hiện sub-folder đúng nhóm/loại, nhãn + thứ tự khớp sidebar (cùng
KHKK_GROUP_LABELS). - B2.
certutil -hashfile(hoặcGet-FileHash) từng cặp file ×2 app:usePipelineStages.ts·PipelineStageFolders.tsx·types/khkk.ts— SHA256 identical;PipelineTreePanel.tsxkhông đổi (git diff) hoặc SHA-compare nếu đổi. - B3.
npm run build×2 app PASS. - B4. Đếm: badge folder cha == Σ chip sub-folder (kể cả "(chưa phân nhóm)"/"(ngoài chỉ mục)") — thử 1 gói thầu có phiếu 2 nhóm + 1 HĐ ngoài index; KHÔNG leaf nào biến mất so với trước K5.
K6 Admin thu gọn
Hiện trạng: isAdminHidden fe-admin Layout.tsx:182-184 = đúng 1 luật key.startsWith('Ct_'); filterForAdmin:186-190 lọc ĐỆ QUY mọi tầng ⇒ ẩn 1 root = cả subtree chết theo — tập key chỉ cần liệt ROOT (+2 con của System), không phải từng leaf.
- Mở rộng tập key tường minh (QĐ7 — GIỮ blacklist, KHÔNG đảo whitelist):
CHỪA (không vào set):
const ADMIN_HIDDEN_KEYS = new Set<string>([ 'PurchaseEvaluations', // GĐ1 root (:1797) — Pe_* subtree chết theo 'KeHoachKyKet', // GĐ2 root (:1775) — Khkk_* 8 nhóm chết theo 'Contracts', // GĐ3 root (:1771) — Ct_* đã có prefix-rule, root nay ẩn nốt 'HopDongCung', // GĐ4 root (:1776) — Hdc_* chết theo 'Forms', // :1788 'Reports', // :1789 'Master', // Danh mục root (:1760) — Suppliers/Projects/Departments/Catalogs/HrmConfig(:1816 parent Master) chết theo 'Hrm', // :1809 'Off', // :1827 'Personal', // :1853 'Workflows', // :1795 — CON của System, phải liệt riêng (Wf_* chết theo) 'PeWorkflows', // :1798 — CON của System (PeWf_* chết theo) ]) function isAdminHidden(key: string): boolean { return ADMIN_HIDDEN_KEYS.has(key) || key.startsWith('Ct_') }Dashboard(QĐ7 giữ) ·System+ 4 leaf quản trịUsers/Roles/Permissions/MenuVisibility(:1791-1794) ·ApprovalWorkflowsV2(:1801) + subtree AwV2 (2 leaf seed:1802-1803+ 8 designer K3). - Sửa comment ĐỦ KHỐI
:177-184: khối cũ khai "[S57] BỎ ẩn Danh mục — admin quản master trực tiếp" (:179-181) — QĐ7 ĐẢO lời đó ⇒ comment phải viết lại (khai đảo + trỏ spec S164), kẻo lượt sau đọc comment cũ tưởng bug (bài cụm-1 "đọc comment đủ khối"). - Display-only, KHÔNG đụng quyền (2 tầng #82): route fe-admin còn nguyên — admin deep-link
/purchase-evaluations… vẫn 200. Thu gọn = menu, không phải revoke; khai rõ trong PR để không ai tưởng mất quyền. - Hệ quả cho K4c (chốt nghi vấn WfView): fe-admin Pe-regex
:134CỐ Ý không WfView (:129-133— fe-admin KHÔNG cóWorkflowMatrixViewPage, lead chốt 2026-07-27 "giữ lệch, đừng vá") + K6 ẩnKeHoachKyKetroot ⇒ fe-admin Layout KHÔNG cần nới regexKhkk_G{n}_*(leaf không bao giờ render admin-side). Ghi 1 comment tại chỗ regex fe-user để khai lệch có chủ đích.
Acceptance K6
- C1. Liệt kê đúng N mục LÁ còn thấy (đủ tên — dư/thiếu 1 = FAIL). Công thức: N =
Dashboard+Users+Roles+Permissions+MenuVisibility+ |leaf AwV2 trong DB|. Seed đọc được 2 leaf AwV2 (:1802-1803); ⚠️ uncertain: rowAwV2_Contractcó thể tồn tại DB ngoài seed (regex:166-172nhận codeContract) — đo trước bằngSELECT [Key] FROM MenuItems WHERE [Key] LIKE 'AwV2%'rồi chốt N: 7 (nếu 2 AwV2) / 8 (nếu 3); SAU K3 = N+8 designerKHKK-N1..N8. - C2. fe-user KHÔNG đổi:
git diff --stat <base>..<head> -- fe-user/src= 0 file (đo diff, KHÔNG đo bundle-hash — #69/l3-F12). - C3. l3-F9: K6 kéo sớm trước K3 ⇒ mục "đếm được 8 designer trong sidebar admin" DỜI sang acceptance K3 (không đo ở K6).
- C4. Deep-link admin 1 route bị ẩn menu (vd
/purchase-evaluations) vẫn 200 — chứng display-only.
Thứ tự + phụ thuộc
Vẽ lại (trả lời câu "K4a chạy được sau K1 hay cần K2 DTO?"): K4a KHÔNG cần cả K1 lẫn K2 — menu-row + grant + MenuKeys.All thuần DbInitializer/MenuKeys.cs, 0 đụng schema/DTO. Nhưng ship K4a MỘT MÌNH thì 42 leaf trỏ ?group=n mà BE chưa hiểu param ⇒ list hiện TẤT phiếu (silent-wrong, mục K4b-9) — leaf RENDER được, LỌC chưa đúng.
Cụm-1: K1 ──► K2 ──► K3 (K3 đang treo OG-9)
│ │
Cụm-2: K6 ───┐│ │ (độc lập 100% — kéo SỚM NHẤT: còn rút việc fe-admin
K4a ──┼┘ │ khỏi K4c [K6-4]; đổi lại C3/l3-F9: đếm-8-designer dời sau K3)
K4b ◄─┘ (cần K2: param group :622-625 + DTO approvalGroup + backfill=1)
K5 ◄── (cần K2: approvalGroup trên KhkkListItemDto; GĐ3/4 group-by c.type
tự do nhưng CÙNG FILE — làm 1 lượt sau K2 cho đỡ 2 lần mirror-SHA)
K4c ◄── (cần K3: 8 Code KHKK-N1..N8 cho WfView filter + preset Create)
Thứ tự ship đề xuất: K6 → K4a → [K2 land] → K4b + K5 → [K3 land] → K4c.
- K6 + K4a chạy song song cụm-1 ngay hôm nay (0 chặn).
- K4a land trước K2 = chấp nhận cửa sổ leaf-lọc-chưa-đúng trên Dev; nếu sợ UAT hiểu nhầm bug thì gộp K4a ship cùng K4b sau K2 (đo acceptance A2 luôn thể). Acceptance A1/A3-A6 đo được NGAY sau K4a; A2 BẮT BUỘC sau K2.
- K4c KHÔNG ship-trước-degrade: filter WfView theo Code
KHKK-N{n}khi chưa seed 8 workflow ⇒ matrix RỖNG (tệ hơn không filter) — đợi K3.
Checklist tổng cụm-2
- 0 EF migration toàn cụm-2 — menu = seed DB + restart API (bài S155); có ai đề mig là sai khuôn.
- K4: 13 việc (K4a 1-6 · K4b 7-9 · K4c 10-13) + acceptance A1-A6 (A2 đo sau K2).
- K5: 6 việc + acceptance B1-B4 (SHA-identical 3 file ×2 app · build ×2 PASS · Σ sub-folder == folder cha).
- K6: 4 việc + acceptance C1-C4 (liệt đúng N lá kèm tên · fe-user git-diff 0 file · 8-designer dời sau K3).
- Drift khai CÙNG COMMIT K4a: STATUS 2 row Menu keys 64→113 · Policies 256→452 (nối tiếp 55→64 cụm-1) — đừng Goodhart giữ-số-đứng-yên.
- Test: +1 BE (extend test seed-grant cụm-1 phủ 49 key mới — khai k=1, tổng ≥590+k' cụm-1 +1); còn lại là acceptance UI/SQL đo tay (14 mục A/B/C).
- Mọi wave: cicd PASS · wave FE-ship byte-verify #77 (K4b/c, K5, K6) · wave BE-only smoke API (K4a) — l3-F17.
- Nhãn 8 nhóm (K4a-3) = đề xuất từ transcribe — owner chỉnh CHỮ tự do, KEY không đổi; không chặn wave nào (khác OG-6 vốn chỉ chặn K1-seed).
END sub-invest-fable-b2-cum2 — TOTAL: K4 13v · K5 6v · K6 4v · 1 test