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