From e4d8244467cf62b51e32982b0583a2dab45375ae Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 31 Jul 2026 13:39:03 +0700 Subject: [PATCH] wal: flush 20260731T1339 --- .claude/WAL.md | 11 +- .../danh-muc-sp002-transcribe.md | 13 ++ .../2026-07-31-S164-4gd-khkk-fanout/run.md | 5 +- .../spec-cum2-chi-tiet-31-07-2026.md | 32 ++++ .../sub-invest-fable-b2-cum3.md | 171 ++++++++++++++++++ .../sub-reviewer-c2-l1.md | 89 ++++++++- .../sub-reviewer-c2-l2.md | 66 ++++++- 7 files changed, 375 insertions(+), 12 deletions(-) create mode 100644 .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-cum2-chi-tiet-31-07-2026.md create mode 100644 .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum3.md diff --git a/.claude/WAL.md b/.claude/WAL.md index 8b68a06..7343d0c 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -40,10 +40,13 @@ next: 5/6 OG CHỐT (1 schema-GỘP+UI-1-1 · 2 đội-hình-cũ · 7 N-phiếu+ B2 cụm-2 invest ✅ wf_15a42015-aea SẠCH (5/5 section, K4 13v·K5 6v·K6 4v, 0-mig; BÁC 2 tiền-đề lead: 13-role=13/13-R+C+U [:2427] · fe-admin khỏi-nới-regex [ẩn subtree :186-190]; lead spot-check 2/2 ĐẠT; drift chain 55→64→113 key · policies →452; precedent-split khai 2 khuôn ngoài-All⟂trong-All). - wf: SONG SONG — B2-r cụm-2 wf_954b8cf0-e6b (3 lane reviewer c2-l{1,2,3}) + B2 cụm-3 invest wf_a21c8ff1-bc7 - (1 lane fable: K7 bridge KHKK→HĐ 3-nấc + K8 kịch-bản dry-run per-bước từ PE/2026/A/049). - CHỜ ANH: (I) bản-15-trang · (II) B2-14-dòng+"/"=hoặc · (III) ai bấm Đưa-vào-HĐ (K7 default người-có-quyền - + dialog, đánh dấu chờ) · OG-6 soát bảng. Sổ fable-lane: fail 3/6 (3 att đan-xen gần nhất SẠCH). + B2-r cụm-2 ✅ (l1 PWF 4/7-đúng-0-sai 2-MAJOR [route-ship-cùng-K4a · drift-DELTA] · l2 PWF 3H/7M [queryKey + +group · thứ-tự · nhãn-N2-mất-phá-dỡ · phase-mount-only] · 🔴 l3 CHẾT-RUỘT 188B = lane OPUS đầu tiên gãy, + phủ-bù lead-note CÓ KHAI) → spec-cum2-chi-tiet-31-07-2026.md FINAL 15 vá. + wf: B2 cụm-3 invest wf_a21c8ff1-bc7 ĐANG CHẠY (K7 bridge 3-nấc + K8 dry-run per-bước). Về → review c3 + (3 lane) → B3 fable-real review chốt → trình anh bảng tổng → B4 build. + CHỜ ANH: (I) bản-15-trang · (II) B2-14-dòng+"/"=hoặc · (III) ai bấm Đưa-vào-HĐ · OG-6 soát bảng (+8 nhãn + menu đính vào transcribe — vá-7 c2). Sổ lane-chết: fable 3/6 · opus 1/9. verify: grep -c 'END spec v2' .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/danh-muc-sp002-transcribe.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/danh-muc-sp002-transcribe.md index 6b702d7..91e09c8 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/danh-muc-sp002-transcribe.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/danh-muc-sp002-transcribe.md @@ -15,6 +15,19 @@ | N7 = C1+C2+C3 | PD/PM | TP.CCM → SM → QS → N/A | | N8 = CEO | CEO | TP.CCM → TP.PRO → N/A → N/A | +## 🏷️ 8 NHÃN MENU (sẽ hiện trên sidebar eOffice — anh soát CÙNG LƯỢT với bảng dưới; đổi sau khi seed = tốn thêm bước backfill + restart) + +| # | Nhãn menu đề xuất | +|---|---| +| N1 | 1. Thiết bị, máy móc, vật tư phụ (A1) | +| N2 | 2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3) | +| N3 | 3. Hoạt động văn phòng (A4) | +| N4 | 4. Vật tư (B1) | +| N5 | 5. Thầu phụ xây dựng (B2) | +| N6 | 6. Thầu phụ MEP (B3) | +| N7 | 7. BCH công trường (C1-C3) | +| N8 | 8. HĐ chưa ủy quyền — CEO (5.2) | + ## A — PHÒNG CCM **A1 · Thiết bị, máy móc, vật tư phụ / Equipment, machinery, and auxiliary materials (5) → N1** diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md index b4f86a2..13cc457 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md @@ -13,8 +13,9 @@ | 2.c1 | Cụm-1 K1+K2+K3 chi tiết — `WF invest-fable b2c1-att2 s164` | /fable-real invest | ✅ att-2 33.048B 5/5 section 17 test (att-1 skeleton-472B chết; luật ĐAN-XEN cứu) — 3 QUYẾT + 1 mâu-thuẫn OG-3 (K3 phải port finalize) + roster 1 ô ⚠️ Trưởng | | 2.c2/c3 | Cụm-2 K4+K5+K6 · cụm-3 K7+K8 chi tiết | /fable-real invest | ☐ | | 2.c1-r | Review cụm-1 — 3 lane `WF reviewer c1-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ 3/3 sạch · L1 2-blocker+GỠ-(i)-:342 · L2 FAIL 3H (menu-ngoài-All chết · ép-false ≥3 site sót · KhkkCreatePage mâu thuẫn) · L3 PWB 5H → **spec-cum1-final 12 vá + OG-9** | -| 2.c2 | Cụm-2 K4+K5+K6 — `WF invest-fable b2c2 s164` wf_15a42015-aea | /fable-real invest | ⏳ | -| 2.c2-r/c3 | Review cụm-2 · invest+review cụm-3 | | ☐ | +| 2.c2 | Cụm-2 K4+K5+K6 — `WF invest-fable b2c2 s164` | /fable-real invest | ✅ SẠCH 5/5 (K4 13v·K5 6v·K6 4v, 0-mig; BÁC 2 tiền-đề lead có chứng) | +| 2.c2-r | Review cụm-2 — `WF reviewer c2-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ l1 PWF 2-MAJOR · l2 PWF 3H/7M · 🔴 l3 CHẾT-RUỘT 188B (opus đầu tiên gãy — phủ bù lead-note có khai) → **spec-cum2-final 15 vá** | +| 2.c3 | Cụm-3 K7+K8 — `WF invest-fable b2c3 s164` wf_a21c8ff1-bc7 | /fable-real invest | ⏳ | | 3 | Chốt spec cuối | /fable-real review | ☐ | | 4 | Fan-out build bám spec+checklist | Opus 5 MAX (hmw) | ☐ | | 5 | Dry-run E2E toàn trình | theo spec wave cuối | ☐ | diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-cum2-chi-tiet-31-07-2026.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-cum2-chi-tiet-31-07-2026.md new file mode 100644 index 0000000..b79f673 --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-cum2-chi-tiet-31-07-2026.md @@ -0,0 +1,32 @@ +# SPEC CỤM-2 FINAL — K4 + K5 + K6 (lead kết tinh sau B2-r cụm-2, @S164) + +## ① Đề bài +K4 sidebar 8 nhóm × 6 leaf (49 row menu mới) · K5 cây sub-folder groups · K6 admin thu gọn 12-key. NỀN = `sub-invest-fable-b2-cum2.md` (5/5 section, 0-mig) — **15 VÁ dưới thắng khi xung đột** (từ `sub-reviewer-c2-l{1,2}.md`; ⚠️ lane c2-l3 chết ruột 188B — lens đo-được/thứ-tự phủ bù bằng overlap L1-F2/F6 + L2-H2/H3/M4 + lead-note §④, KHAI RÕ không giấu). + +## ② 15 vá + +1. 🔴 **[L1-F2 ⨯ L2-H2 hội tụ] Route regex `resolvePath` ship CÙNG K4a** (tách khỏi K4c) — leaf không route bị `if (!path) return null` nuốt IM LẶNG (`fe-user/Layout.tsx:336-338`); không tách thì sau K4a: 7 group RỖNG + 42 leaf VÔ HÌNH by-construction. Chỉ mục WfView-lọc-theo-Code (`Code==='KHKK-N'+n`) mới thật sự chờ K3. +2. 🔴 **[L1-F6 ⨯ L2-H3] Drift khai DELTA** (+49 key / +196 policy), CẤM neo 2 mốc tuyệt đối (64→113 chỉ đúng nếu cụm-1 land trước — thứ tự ship đang cho song song); commit kèm lệnh đo lại `MenuKeys.All` thực tế. +3. 🔴 **[L2-H1] TanStack `queryKey` list PHẢI CHỨA `group`** — thiếu thì 8 leaf dùng chung 1 ô cache = silent-wrong build xanh (đúng họ bài S159 2-khóa-cache). +4. **[L1-F4] SQL nghiệm thu N designer**: JOIN `MenuItems × Permissions × Roles(Admin, CanRead=1)` — row tồn tại ≠ hiện sidebar (`GetMyMenuTreeQuery.cs:96` lọc HasAccess); đo trên env nghiệm thu (prod), không phải Dev. +5. **[L2-M3] `KhkkListPage` derive `phase` từ `searchParams`** (hiện `useState` mount-only `:63-65`, route không keyed ⇒ bấm leaf→leaf KHÔNG re-lọc; cùng file `deletedView:61` đã đọc searchParams đúng — đồng bộ về 1 kiểu). ×2 app. +6. **[L2-M4] Trả "Thao tác/Create preset `?group=`" VỀ K4b** (0 phụ thuộc K3 — chỉ cần picker vá-4 cụm-1). +7. **[L2-M5] 8 nhãn menu ĐÍNH vào file OG-6** (`danh-muc-sp002-transcribe.md`) cho anh soát 1 lượt trước K4a + sửa nhãn N2 thiếu vế **"phá dỡ"** ("Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công"); nhãn đổi-sau-seed = +labelBackfill + restart (không phải sửa chữ tự do — comment `DbInitializer.cs:1959-1961`). +8. **[L2-M6 ⨯ L1-F3] Ràng buộc ngược tại `ADMIN_HIDDEN_KEYS`**: comment "gỡ `KeHoachKyKet` khỏi đây ⇒ PHẢI nới regex `Layout.tsx` fe-admin trước — 48 leaf sẽ drop im lặng #50". KHÔNG nới regex fe-admin đợt này (fe-admin **thiếu `WorkflowMatrixViewPage`** — nới sẽ đẻ leaf WfView chết trên admin; verified L1-BÁC-2). +9. **[L2-M7] Cây K5 HIỆN ĐỦ 8 sub-folder kể cả rỗng** (mirror sidebar đúng nghĩa + tiền lệ prod: folder GĐ "SẮP" vẫn hiện) — bỏ đề xuất ẩn-nhóm-rỗng của lane; fallback "(chưa phân nhóm)" GIỮ cho phiếu NULL-group. +10. **[L2-M1] Sửa LÝ DO giữ site-2** (group-key KHÔNG cần CanRead row — `HasAccess` đệ quy theo con `:96-97`); kết luận giữ-3-site GIỮ NGUYÊN. +11. **[L2-M2] +1 dòng site-4 revoker**: `RevokeTemporarilyHiddenModulesAsync:2286` (gọi `:2097` GIỮA site-2/3) prefix Hrm*/Off*/Personal — đã soi, **0 match Khkk**; A5 restart ×2 là phép thử nó. +12. **[L1-F5] Acceptance label đo bằng SQL cột `Label, DisplayLabel`** (fe-admin không đọc displayLabel — `MenuItem.cs:12-13`; fe-user `effectiveLabel:204-206`). +13. **[L1-F7] Vá comment stale `DbInitializer.cs:1959-1961`** (tự trỏ ":1893-1905" — thật `:1926-1943`) cùng diff K4a. +14. **[cụm-2 surprise] Viết lại comment fe-admin `:179-181`** ("S57 BỎ ẩn Danh mục" — QĐ7 đảo lần 2 cùng chỗ) cùng commit K6 — khai lineage 2 lần đảo. +15. **[precedent-split] Khai 2 khuôn menu-key trong doc K4**: `Ct_*`/`Pe_*` CỐ Ý ngoài-All (comment `:2193-2195`) ⟂ catalog/AwV2/Khkk mới TRONG-All — người build đọc 1 khuôn đừng tưởng khuôn kia sai. + +## ③ Checklist final cụm-2 +- [ ] K4a (ship 1 khối): 49 row menu VÀO All + labelBackfill G1 + route regex resolvePath ×(fe-user; fe-admin GHI RÀNG BUỘC) + 3-site grant (+dòng site-4 đã-soi) + nhãn N2 sửa + comment :1959/:179 vá. Acceptance: Admin + 1 user thường THẤY 8 group sidebar fe-user; 42 leaf BẤM ĐƯỢC (không null-route); drift DELTA khai + đo lại All; SQL JOIN đếm designer đúng N; restart ×2 số row menu/permission KHÔNG đổi (idempotent + revoker-proof). +- [ ] K4b: 4 leaf list + Thao tác preset — derive phase+group từ searchParams; `queryKey` += group; acceptance bấm leaf→leaf ĐỔI kết quả không cần F5; URL G1 6 leaf mang `group=1`. +- [ ] K4c (sau K3): WfView lọc `Code==='KHKK-N'+n`. +- [ ] K5: `groups` mang leaves thật (không đường `content`) + GĐ2 by `approvalGroup` + GĐ3/4 by `c.type` + HIỆN ĐỦ 8 + fallback chưa-phân-nhóm + Σ sub-folder == tổng phiếu; SHA-identical 4 file pipeline ×2 app (lệnh đo từng file); build ×2 PASS. +- [ ] K6: blacklist 12-key tường minh + acceptance liệt kê ĐÚNG N mục lá (tên + đếm, dư/thiếu 1 = FAIL) + `git diff --stat -- fe-user/src` = 0 file + comment ràng-buộc-ngược land. +- [ ] Test (lead-note bù L3): seed-menu idempotent ×2 · labelBackfill đổi-label-không-nhân-row · route-map FE có leaf nào null-path = FAIL (unit nhỏ nếu khuôn có; không thì acceptance tay + chụp). Baseline ≥590+k khai k. + +END spec-cum2-final — nền K4 13v·K5 6v·K6 4v + 15 vá · 1 lane review chết ruột ĐÃ KHAI + phủ bù. diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum3.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum3.md new file mode 100644 index 0000000..878b83a --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum3.md @@ -0,0 +1,171 @@ +# sub-invest-fable-b2-cum3 — B2 CỤM-3: SPEC CHI TIẾT K7 (cầu KHKK→HĐ + đường ống V2) + K8 (dry-run E2E) + +> Run `2026-07-31-S164-4gd-khkk-fanout` · engine `/fable-real` · investigator-codebase (read-only spec-writer) +> Luật đan xen: đọc đủ MỘT section → VIẾT NGAY. Kế thừa S156/S160 — không soi lại vùng đã đo. + +## K7 Cầu KHKK→HĐ + đường ống V2 + +### K7.0 RE-ĐO 3 nấc (đo lại hôm nay, không tin sổ) + 1 BÁC tiền đề đề bài + +| Nấc | Kết quả đo @S164 | Evidence | +|---|---|---| +| (a) cột/index | ✅ CÓ | `ContractSigningPlanLine.cs:25` `ContractId Guid?` + comment "[C6 review-schema] HĐ sinh ra từ dòng này (W5)"; index non-unique `ContractSigningPlanLineConfiguration.cs:26` | +| (b) write-site | ✅ = 0 (tự grep, không mượn số lead) | grep `ContractId` trong `ContractSigningPlanFeatures.cs` → 3 hit đều READ: `:67` DTO field, `:574` projection, `:1185` comment; `ContractSigningPlanWorkflowService.cs` → 1 hit `:54` = COMMENT FK. **0 assignment** | +| (c) test E2E | ✅ = 0 test bridge | `tests/` có 4 file SigningPlan (Approval/Crud/Schema/AuthorizePolicy) + `CreateContractFromEvaluationMultiWinnerTests.cs` (bridge PE cũ) — KHÔNG file nào test cầu KHKK→HĐ | + +🔴 **BÁC tiền đề K7(b) của đề bài** — "SỬA 3 lỗ đường ống (GetEligiblePhases V2-aware + inbox V2 + guard trình)" là kiến thức S156 ĐÃ LỖI THỜI. Cả 3 đã LAND @W6-S161: +1. **List + Detail V2-aware**: `ContractFeatures.cs:306-318` — vế (a) `myWorkflowIds.Contains(x.c.ApprovalWorkflowId)` per-HĐ (helper `ResolveUserApprovalWorkflowIdsAsync:403-411`) + vế (b) `HardCopyActorRoles:391-392` thấy DaPhatHanh. Comment `:381-387` CẤM nhét ChoDuyet vào `GetEligiblePhases` (hàm dùng chung với ListDeleted `:441` → nhét là RÒ HĐ ChoDuyet xóa-mềm sang màn "Đã xóa"). **K7 KHÔNG sửa `GetEligiblePhases` — implementer đọc đề bài cũ mà "làm cho xong" là tạo lỗ.** +2. **Inbox V2**: `ContractFeatures.cs:532-534` `v2InboxIds = ResolveV2InboxIdsAsync(...)` (bản Contract ĐÃ TỒN TẠI) + where `:543` `|| v2InboxIds.Contains(c.Id)` + `AdminInboxPhases:503-513` đã +ChoDuyet (DR-4). +3. **Guard trình**: `ContractWorkflowService.cs:73-92` đã nới `Drafter ∨ DeptManager ∨ Procurement ∨ isCreator` (vá lỗ PMH-403 đo 07-29). + +⇒ K7(b) co lại thành **V3: test-khóa 3 vế** (regression lock — xem K7.4 T8), KHÔNG build lại. Phần build THẬT của K7 = bridge (a) + FE (c). + +### K7.1 (a) BRIDGE MỚI — `POST /api/contract-signing-plans/{id}/create-contract` + +**Khuôn mẫu:** mirror bridge PE→HĐ cũ `CreateContractFromEvaluationFeatures.cs` (endpoint `PurchaseEvaluationsController.cs:340-343`) + guard workflow V2 của `CreateContractCommand` (`ContractFeatures.cs:74-81`). Luật spec-4gd §①: GIỐNG NHAU — CHỈ KHÁC FORM — ĐỪNG CHẾ THÊM. + +**Command** (file mới `Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs`): +```csharp +public record CreateContractFromSigningPlanCommand( + Guid PlanId, + List LineIds, // OG-1: schema GỘP — N line cùng NCC → 1 HĐ; UI-1-1 truyền 1 phần tử + ContractType ContractType, + Guid ApprovalWorkflowId, // V2 type-3 — user chọn trong dialog + string? TenHopDong = null) : IRequest; // 1 call = 1 HĐ (khác bridge PE trả List) +``` +⚠️ **Lệch đề bài có chủ đích:** đề ghi input `{planId, lineIds[], contractType}`; PHẢI thêm `ApprovalWorkflowId` vì V2 KHÔNG phân theo ContractType — `ApprovalWorkflow.cs:57` `Contract = 3, // HĐ general (any ContractType)` ⇒ "pin theo ContractType" tự động là BẤT KHẢ; chọn quy trình = việc của dialog (mirror ContractCreatePage dropdown type-3). + +**Guard tuần tự trong handler (mỗi guard 1 message VN rõ):** +1. plan tồn tại + `Phase == DaDuyet` (mirror rào (i) `ContractSigningPlanFeatures.cs:330-332`). +2. `LineIds` non-empty, distinct, TẤT CẢ ∈ plan.Lines. +3. TẤT CẢ line cùng `SupplierId` — 1 HĐ = 1 NCC (`Contract.SupplierId` đơn). Liên-danh N NCC = N lần bấm (UI-1-1 mỗi lần 1 line nên tự thỏa). Message: "Các dòng chọn thuộc nhiều NCC khác nhau — mỗi Hợp đồng chỉ 1 NCC, tạo lần lượt." +4. TỪNG line `ApprovedAmount != null` — null → Conflict message NÊU TÊN hạng mục: "Dòng '{TenHangMuc}' chưa có giá duyệt chốt — phiếu phải qua finalize (K2-freeze) trước khi đưa vào HĐ." **KHÔNG fallback ProposedAmount** (số chưa chốt). +5. TỪNG line `ContractId == null` — chống double-bridge per-line (thay guard `pe.ContractId` single của bridge cũ `:59-60`). Message: "Dòng '{TenHangMuc}' đã có Hợp đồng." +6. `ApprovalWorkflowId`: tồn tại + `ApplicableType == Contract(3)` — copy trọn 2 nhánh NotFound/Conflict theo khuôn `EnsureWorkflowTypeAsync` (`ContractSigningPlanFeatures.cs:209-220`) đổi hằng type. + +**Build Contract (map field):** `Type=request.ContractType` · `Phase=DangSoanThao` · `SupplierId=line.SupplierId` · `ProjectId=plan.ProjectId` (`ContractSigningPlan.cs:25`) · `DepartmentId=plan.DepartmentId` (`:26`) · `DrafterUserId=currentUser` · **`GiaTri = Σ ApprovedAmount(lines chọn)`** — phạm-vi-gộp = ĐÚNG tập LineIds trong call, không hơn không kém (DoD spec-4gd K7) · `TenHopDong = request.TenHopDong ?? "{pe.TenGoiThau} — {TenHangMuc}"` · `ApprovalWorkflowId = request.ApprovalWorkflowId` (**pin V2 — khác bridge cũ chỉ pin V1 `:68-71,108`**) · `WorkflowDefinitionId = null` (KHÔNG pin V1 song song — HĐ mới đi V2 thuần, nhánh dispatch `ContractWorkflowService.cs:111` ưu tiên V2) · optional `BudgetManualName="NS tham chiếu KHKK {MaKeHoach}"` + `BudgetManualAmount=Σ PeReferenceAmount(lines)` (mirror `:106-107`, tham khảo). + +**Mã HĐ:** gen NGAY lúc bridge — mirror tiền lệ bridge PE `:113` + bài S88 self-committing codegen (`:78-81`): gen TRƯỚC khi Add/track (codegen tự SaveChanges — comment `ContractSigningPlanFeatures.cs:352-357` đã trả giá bài này); terminal V2 có guard `if (MaHopDong is null)` (`ContractWorkflowService.cs:384`) nên không double-gen. Trade-off cháy seq RG-001 khi HĐ không phát hành → khai ở K8 guard. + +**Write-site ContractId (nấc b đóng):** sau khi Add contract + SaveChanges chung 1 lần: `line.ContractId = contract.Id` cho TỪNG line chọn (load lines TRACKED — đây là assignment ĐẦU TIÊN toàn codebase). + 2 changelog mirror `:117-141`: `ContractChangelogs` (Insert, "Tạo HĐ {mã} từ KHKK {MaKeHoach}") + `ContractSigningPlanChangelogs` (Update, ContextNote=contractId). Atomic: 1 SaveChanges cuối. + +**Nối cây GĐ3 (quyết):** `pe.ContractId ??= contract.Id` (chỉ khi null) — `usePipelineStages.ts` đi GĐ3 DUY NHẤT qua `pe.contractId` (skill contract-workflow đã khai); không set = HĐ sinh ra NHƯNG cây 4-folder mù GĐ3. Hệ quả 2 chiều khai rõ: (i) bridge PE→HĐ cũ sẽ 409 cho PE đó (`:59-60` — đúng ý, khóa đường cũ tự nhiên); (ii) liên-danh N-HĐ cây vẫn chỉ link HĐ[0] — hố sẵn có của bridge cũ (`:145`), nâng cấp cây đọc `line.contractId` = backlog W9, KHÔNG làm đợt này (đụng 2 file SHA-identical ràng buộc K5). + +**Controller:** `ContractSigningPlansController` — `[HttpPost("{id:guid}/create-contract")]` `[Authorize(Policy = "Contracts.Create")]` — 🔴 đúng-key-policy-endpoint-đích (gotcha #85): tài nguyên sinh ra là Contract ⇒ key `Contracts.Create`, KHÔNG phải `KeHoachKyKet.*`. Trả `201 { contractId, maHopDong }`. + +**Ai bấm + chọn ContractType (câu III):** ⏳ PENDING-OWNER — default spec: "người có quyền tạo HĐ" = ai mang policy `Contracts.Create` thấy nút + tự chọn ContractType trong dialog (khớp l1-F15 spec-4gd "mặc định người bấm chọn trong dialog"). Đánh dấu chờ anh; đổi answer chỉ đổi điều kiện hiện nút FE, máy BE không đổi. + +### K7.2 (b) Đường ống — trạng thái sau re-đo + +- ĐÃ VÁ (K7.0): list/detail/inbox/guard-trình. Việc K7 = **V3 test-khóa** + xác nhận runtime bằng curl trong acceptance (A3/A4). +- CÒN THIẾU thật: **0** — HĐ pin V2 sinh từ bridge đi được trọn: trình (`:70-101` set ChoDuyet + levelOrder=1 vì AWId≠null `:97`) → ApproveV2Async (`:111-113`) → terminal gen mã + DaPhatHanh (`:381-392`). +- ⚠️ Tiền đề vận hành (không phải code): prod phải có ≥1 `ApprovalWorkflow` ApplicableType=3 active + roster thật. Dev hiện chỉ có `QT-HD-V2-001` "mẫu UAT" (sqlcmd Dev @S164); prod CHƯA đo được (SSH chết khi load SQL-client — S134/S148) → chuyển thành pre-flight K8 B0. + +### K7.3 (c) Nút FE ×2 app + +- Vị trí: `fe-user/src/pages/khkk/KhkkDetailPage.tsx` khối actions PageHeader `:291-318` (cạnh nút Xóa/Danh sách) + mirror `fe-admin/.../KhkkDetailPage.tsx` (tiền lệ PeWorkflowPanel byte-identical 2 app — S155). +- Điều kiện hiện: `phase === KhkkPhase.DaDuyet && can('Contracts','Create')` (#85 — đúng key endpoint; KHÔNG OR `Khkk_*`). +- Dialog "Đưa vào Hợp đồng": (1) chọn **1 line** (UI-1-1 OG-1 — radio list các line `contractId == null`, hiện TenHangMuc + NCC + ApprovedAmount; line đã bridge → badge "Đã có HĐ" + link, disabled); (2) Select ContractType 7 enum; (3) Select Quy trình duyệt HĐ (nguồn = mirror dropdown ContractCreatePage, filter ApplicableType=3); (4) TenHopDong prefill. Submit → POST → toast mã HĐ + navigate/link HĐ; invalidate query detail. +- Line có `ApprovedAmount == null` → disabled + tooltip "chưa có giá chốt". + +### K7.4 (d) Test — 7 case (fixture khuôn `ContractSigningPlanApprovalTests.cs` sẵn có) + +| # | Case | Assert | +|---|---|---| +| T1 | Happy 1-line: plan DaDuyet, line ApprovedAmount=X | 1 HĐ: GiaTri==X · Phase=DangSoanThao · ApprovalWorkflowId pin đúng · WorkflowDefinitionId null · MaHopDong non-null · `line.ContractId==hđ.Id` · `pe.ContractId==hđ.Id` · 2 changelog | +| T2 | Gộp N=2 line CÙNG NCC (máy OG-1) | GiaTri==Σ 2 ApprovedAmount · CẢ 2 line cùng ContractId | +| T3 | 1 line ApprovedAmount=null | Conflict, message chứa TenHangMuc; DB 0 HĐ mới + 0 line đổi (atomic) | +| T4 | plan.Phase=ChoDuyet | Conflict "chỉ từ phiếu Đã duyệt" | +| T5 | line đã có ContractId | Conflict (double-bridge per-line) | +| T6 | 2 line KHÁC SupplierId 1 call | Conflict "mỗi HĐ 1 NCC" | +| T7 | ApprovalWorkflowId type-10 (KHKK) | Conflict (guard type-3 — chống forge, mirror `:74-81`) | +| T8 (khóa V3) | HĐ pin V2 ở ChoDuyet: approver-V2-không-role-legacy | thấy List (vế `:315-316`) + Inbox (`:543`); cấp-2 CHƯA thấy inbox khi đang cấp-1 | + +### K7.5 (e) Acceptance đo được (chạy sau deploy, số cụ thể) + +- [ ] A1 `POST .../create-contract` → 201; sqlcmd `SELECT ContractId FROM ContractSigningPlanLines WHERE Id IN (…)` = đúng contractId, đúng N dòng chọn. +- [ ] A2 sqlcmd `Contracts.GiaTri` == Σ `ApprovedAmount` các line chọn — khớp từng đồng. +- [ ] A3 login user CHỈ có chân V2 (0 role legacy): `GET /api/contracts?phase=10` + `GET /api/contracts/{id}` → thấy HĐ (trước W6: rỗng). +- [ ] A4 `GET /api/contracts/inbox`: approver cấp-hiện-tại CÓ HĐ; user cấp-sau CHƯA. +- [ ] A5 duyệt hết trạm V2 → `Phase=9 (DaPhatHanh)`; `MaHopDong` đúng RG-001 (`{ProjectCode}/{abbr}/SOL&{SupCode}/{Seq:D2}`) — sinh từ lúc bridge, terminal không double-gen. +- [ ] A6 POST lần 2 cùng line → 409; FE line badge "Đã có HĐ" + disabled. +- [ ] A7 `dotnet test` ≥ baseline+8 (khai k con số); cicd PASS; FE-ship → byte-verify #77 ×2 app. + +### K7.6 Danh sách việc (n = 8) + +V1 BE command+validator+handler bridge (file mới) · V2 BE endpoint controller + policy `Contracts.Create` · V3 test-khóa đường ống (T8 — KHÔNG sửa GetEligiblePhases) · V4 FE fe-user nút+dialog · V5 FE fe-admin mirror · V6 FE types/hook + invalidate (×2 app) · V7 test T1-T7 · V8 acceptance A1-A7 + khai số test. + +## K8 Dry-run E2E + +### K8.0 Tham số chốt + 2 QUYẾT + +- **Nguồn:** `PE/2026/A/049` (gói 14 Mat PVC) — theo đề bài. **QUYẾT-1 nhóm:** ống PVC = vật tư chính ⇒ nhóm **B1 · Vật tư → workflow `KHKK-N4`** (transcribe `danh-muc-sp002-transcribe.md:52` "B1 · Vật tư / Materials (23) → N4"; hàng khớp chọn trong 23 dòng B1, fallback `B1-23 Vật tư chính khác` `:75`). KHÔNG phải A1 (`:20` = thiết bị/vật tư PHỤ → N1). +- **QUYẾT-2 loại HĐ:** `HopDongNhaCungCap` (abbr "NCC" CÓ trong RG-001 v02 — né hố "MB" không gốc quy định, S160 F-06.1/F-21). +- **Đội hình:** cột "Ai" = chép từ **bảng danh tính K3-acceptance** (vá-11 cụm-1, đo trên bản LIVE) — Dev KHÔNG có `QT-DN-V2-001` (sqlcmd Dev @S164 chỉ có `QT-HD-V2-001` type-3 UAT) ⇒ KHÔNG điền trước email từ Dev. Anchor đã chốt: trạm CCM = **TP.CCM Phan Văn Chương** (finalize-gate OG-3/OG-9) · trạm cuối = **CEO (anh Trường)**. Giả định 4 trạm — số trạm THẬT theo seed K3, người chạy bám panel duyệt. +- **Nhánh chính = A (finalize tại a Chương)** — chứng tính năng MỚI OG-9; nhánh B (lên CEO) = phiếu ZZTEST thứ 2 nếu còn giờ. +- Login API prod: parse field `accessToken` (KHÔNG phải `token` — đo S160). + +### K8.1 Bảng bước (B0-B16 = 17 bước; mỗi bước Ai · Hành động · Kỳ vọng · Bằng chứng) + +**B0 PRE-FLIGHT (chặn cả buổi nếu thiếu):** (i) `scripts/backup-sql.ps1` backup prod; (ii) verify prod có `ApprovalWorkflow` ApplicableType=3 ACTIVE (đội hình HĐ thật) + `KHKK-N4` active + roster khớp bảng K3 — thiếu → admin tạo bằng designer fe-admin TRƯỚC; (iii) verify `PE/2026/A/049`: Phase=DaDuyet + có winner IsWinner + chưa có plan nhóm-4 sống (rào `ContractSigningPlanFeatures.cs:342-347`, sau K2 = per-(PeId,group)); (iv) **dặn team** (Zalo/email): "hệ thống bắn notification phiếu ZZTEST trong khung giờ X — bỏ qua" — KHÔNG có cơ chế tắt notify per-record, chỉ dặn (DoD l3-F16); (v) hỏi anh **câu giờ-G** ở K8.2. Bằng chứng: ảnh backup + sqlcmd 3 dòng verify. + +| # | Ai | Hành động | Kỳ vọng UI/API | Bằng chứng | +|---|---|---|---|---| +| B1 | BCH drafter [K3-bảng] | fe-user `/khkk` → Tạo mới: picker chọn `PE/2026/A/049`, workflow `KHKK-N4`, GhiChu tiền tố **ZZTEST** | 201 + `MaKeHoach = KHKK/2026/{seq}` hiện header | ss-k8-01 + mã phiếu | +| B2 | BCH drafter | Điền line: gán hạng mục B1-xx (PVC) per line, ProposedAmount per NCC×hạng mục; save | Lines lưu; line thiếu hạng mục bị submit-guard chặn (vá-5 cụm-1) | ss-k8-02 | +| B3 | BCH drafter | Trình duyệt (Nháp→ChoDuyet) | Badge "Chờ duyệt"; phiếu vào inbox trạm-1; notification bắn | ss-k8-03 | +| B4 | Trạm-1 [K3-bảng] | Login → `/khkk` Đang duyệt → mở phiếu → KhkkWorkflowPanel bấm Duyệt + ý kiến "ZZTEST duyệt trạm 1" | Advance cấp/bước; LevelOpinion UPSERT hiện Section ý kiến | ss-k8-04 | +| B5 | Trạm-2 [K3-bảng] | Như B4 tại trạm 2 | Advance; user trạm-1 hết thấy phiếu trong inbox | ss-k8-05 | +| B6 | **TP.CCM Phan Văn Chương** | Trạm CCM: tick **"Duyệt kết thúc tại cấp"** (AllowApproverFinalize — OG-9 nhánh A) | `Phase=DaDuyet` KHÔNG qua CEO; `EndedByLevelFinalize=true`; ApprovedAmount ghi tại choke-point + FREEZE (QĐ9 mirror Mig 67) | ss-k8-06 + sqlcmd `EndedByLevelFinalize,ApprovedAmount` | +| B7 | *(chỉ nhánh B — phiếu ZZTEST-2)* CEO Trường | Duyệt final trạm cuối | `Phase=DaDuyet` đường chuẩn | ss-k8-07 | +| B8 | BCH/Admin | Mở lại detail: số khóa 🔒; thử sửa line | ApprovedAmount hiển thị khóa; sửa line bị phase-guard chặn (vá-6 cụm-1); sqlcmd số không đổi | ss-k8-08 + sqlcmd | +| B9 | Người có `Contracts.Create` [⏳ câu III] | Bấm **"Đưa vào Hợp đồng"** (K7): dialog chọn **1 line** (UI-1-1) + ContractType=NhaCungCap + workflow HĐ type-3 | 201 → HĐ draft `GiaTri == ApprovedAmount` line; `MaHopDong` RG-001 có ngay; line badge "Đã có HĐ" | ss-k8-09 + mã HĐ | +| B10 | Người bấm B9 | Mở HĐ draft (TenHopDong tiền tố ZZTEST), kiểm NCC/dự án/giá → Trình duyệt | Phase→ChoDuyet (guard trình đã nới `:73-92`); vào inbox approver V2 HĐ | ss-k8-10 | +| B11 | Approver HĐ [đội hình type-3] | Duyệt lần lượt hết trạm V2 | Terminal: `Phase=DaPhatHanh` (mã giữ nguyên — guard `:384`); notification "đã phát hành" | ss-k8-11 | +| B12 | HRA/CCM [HardCopyActorRoles] | GĐ4: mở HĐ (leaf `Hdc_*`) → upload scan bản cứng (purpose SealedCopy) | Badge `hasSealedCopy` (EXISTS `ContractFeatures.cs:348-355`); vai bản-cứng THẤY HĐ DaPhatHanh (`:317-318`) | ss-k8-12 | +| B13 | Bất kỳ + Admin | Mở cây 4-folder (usePipelineStages) | Đủ 4 tầng: PE A/049 → KHKK ZZTEST (folder nhóm 4) → HĐ ZZTEST → bản cứng; đường nối `khkk.purchaseEvaluationId` · `pe.contractId` · `hasSealedCopy` | ss-k8-13 | +| B14 | Lead | Chốt sổ: bảng mã thật (PE/2026/A/049 · KHKK/2026/xxx · mã HĐ) + tick DoD K8 spec-4gd `:35` | Đủ: ZZTEST prefix · toàn trình hết trạm · cây 4 GĐ · screenshot + mã thật | bảng trong session-log | + +**B15 CLEANUP + danh sách bản-ghi-KHÔNG-xóa-được (sống chung):** +- **HĐ ZZTEST DaPhatHanh** — HỐ-1: `DeleteContract` guard numeric `Phase >= DangInKy` (`ContractFeatures.cs:632-633`, S160) chặn xóa cả 9/10/98/99, admin không thoát qua API. Sống chung: giữ vĩnh viễn với tiền tố ZZTEST, HOẶC sqlcmd soft-delete tay (`IsDeleted=1`) sau khi anh gật. +- **KHKK ZZTEST DaDuyet** — 🔴 **HỐ-1-KHKK (đo mới @S164):** delete allow-list `{DangSoanThao, TuChoi}` nằm SAU nhánh admin (`ContractSigningPlanFeatures.cs:1145-1148`) ⇒ phiếu DaDuyet KHÔNG xóa được KỂ CẢ Admin + **chiếm slot (A/049, nhóm-4)** — PE thật này hết lập phiếu N4 cho tới khi xử. Lối thoát: sqlcmd `UPDATE ContractSigningPlans SET IsDeleted=1,…` — global filter làm rào `:342-344` giải phóng slot (soft-deleted không tính "sống"). +- **Seq đã cháy:** `ContractCodeSequences` (mã HĐ ăn STT 01 của cặp prefix (dự án thật, NCC thật)) + `KHKK/2026` seq — không thu hồi. +- **Changelog/Approval/LevelOpinion** các phiếu trên + notification đã bắn — vĩnh viễn by-design. +- PE A/049: KHÔNG đụng số liệu (chỉ bị set `pe.ContractId` bởi bridge B9 — khai với anh; muốn gỡ = sqlcmd NULL lại sau khi soft-delete HĐ). + +**B16 ROLLBACK:** hỏng giữa chừng → DỪNG, không xóa gì, chụp lỗi + ghi bước đứng; phiếu KHKK kẹt ChoDuyet → approver "Từ chối" (TuChoi → xóa được qua UI `:1145`); HĐ kẹt ChoDuyet → admin Reject→TuChoi; restore DB = last-resort KHÔNG dùng (prod đang sống, restore = mất data người khác — S91). + +### K8.2 Câu hỏi giờ-G cho anh (1 câu, trước B1) + +Vòng-1 chạy thẳng trên **PE/2026/A/049 thật** (giá: KHKK ZZTEST DaDuyet + HĐ ZZTEST DaPhatHanh mang tên dự án/NCC THẬT nằm vĩnh viễn trong list tới khi anh cho sqlcmd soft-delete + cháy 1 seq mã HĐ của cặp prefix thật) — HAY dựng **bộ ZZTEST riêng** (ZZTEST Project + Supplier + PE tự tạo từ GĐ1, 0 đụng data thật, thêm ~30 phút setup)? Đề bài chỉ định A/049 ⇒ default = A/049 nếu anh im lặng; "PE test riêng" = vòng-2 (đề bài đã chừa). + +## Thứ tự + phụ thuộc + +``` +K1 ──→ K2 ──→ K3 ──────────────────────┐ + │ │ ▼ + │ └─ phiếu duyệt được → DaDuyet + ApprovedAmount (port finalize) + └─ TenHangMuc/ApprovalGroup/freeze-răng K7 (V1→V2→V7 BE; V3∥; V4/V5/V6 FE; V8 sau deploy) +cụm-2: K4a ∥ ngay · K4b/K4c sau K2/K3 · K5 ∥ · K6 ∥ │ + ▼ + K8 = CUỐI CÙNG (sau MỌI wave deploy prod + cicd PASS + B0) +``` + +- **K7 SAU cụm-1, lý do đo được (3):** (i) message-chặn T3 cần `TenHangMuc` — cột chỉ có sau Mig 71 K2; (ii) guard/rào per-(PeId, ApprovalGroup) + phase-guard line = K2 vá-6, K7 build trên trạng thái đó; (iii) **không có K3 thì `ApprovedAmount` NULL 100%** (cột "CHỈ ghi tại choke-point finalize" `Line.cs:23` — nhánh finalize là việc K3.a) ⇒ bridge chặn mọi phiếu, K7 không test nổi happy-path. +- **K7 ⊥ cụm-2:** không cần K4/K5/K6 về máy (điểm chạm cây duy nhất = set `pe.ContractId`, không đổi file cây). +- **Rebase:** K7 thêm code vào `ContractSigningPlanFeatures.cs` + FE KhkkDetailPage — cụm-1 vá 2/6/8 chạm CÙNG file ⇒ K7 khởi công SAU khi cụm-1 merge, đọc lại spec-cum1 §② trước khi viết (đừng vá lại `:342` — vá-2 đã CẤM). +- **K8 sau TẤT CẢ:** dry-run đi qua menu 48-leaf (K4), cây sub-folder (K5), admin thu gọn (K6), designer 8 nhóm (K3), bridge (K7) — thiếu wave nào lộ wave đó; + B0 pre-flight prod. + +## Checklist tổng cụm-3 + +- [ ] K7-BE: POST bridge 201 · `Line.ContractId` ghi (write-site ĐẦU TIÊN toàn codebase) · pin `ApprovalWorkflowId` type-3 · `GiaTri == Σ ApprovedAmount` tập line chọn · null → chặn message CÓ TÊN hạng mục · double-bridge per-line 409 · policy `Contracts.Create` (#85 đúng-key-endpoint). +- [ ] K7-KHÔNG-LÀM (chống làm-thừa-tạo-lỗ): `GetEligiblePhases` GIỮ NGUYÊN (`ContractFeatures.cs:381-387` cấm nhét ChoDuyet) · ListDeleted GIỮ NGUYÊN (`:442-447` cố ý không ghép vế V2). +- [ ] K7-FE ×2 app: nút chỉ `DaDuyet && can('Contracts','Create')` · dialog 1-line UI-1-1 · badge "Đã có HĐ" · byte-verify #77 (wave có FE-ship). +- [ ] K7-test: T1-T8 xanh, suite ≥ baseline+8 (khai k), cicd PASS. +- [ ] K7-acceptance: A1-A7 số thật (curl + sqlcmd), dán vào PR. +- [ ] K8-pre: B0 đủ 5 mục + câu giờ-G (K8.2) có trả lời. +- [ ] K8-run: B1-B14 đủ bằng chứng (ss-k8-01..13 + bảng 3 mã thật PE/KHKK/HĐ). +- [ ] K8-hậu: B15 danh sách bản-ghi-vĩnh-viễn ghi vào session-log (gồm HỐ-1-KHKK mới) · B16 không kích hoạt hoặc ghi lý do. +- [ ] DoD gốc spec-4gd: K7 `:34` + K8 `:35` tick từng dòng — không tick gộp. + +END sub-invest-fable-b2-cum3 — TOTAL: K7 8v · K8 17-bước · 8 test diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c2-l1.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c2-l1.md index 178a0d4..8ccc15a 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c2-l1.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c2-l1.md @@ -1,11 +1,94 @@ # c2-l1 -LANE 1/3 — LENS QUYẾT-vs-CODE. Đối tượng: `sub-invest-fable-b2-cum2.md` (K4 sidebar 49-row · K5 cây groups · K6 admin 12-key blacklist). +LANE 1/3 — LENS QUYẾT-vs-CODE. Đối tượng: `sub-invest-fable-b2-cum2.md` (K4 sidebar 49-row · K5 cây groups · K6 admin 12-key blacklist; 0 mig). + +**Dòng-1:** `C2L1: 4/7 ĐÚNG · 0 SAI · 3 LỆCH` — 7 finding (2 MAJOR · 3 MEDIUM · 2 LOW), 0 blocker về QUYẾT. ## Findings -(ĐANG LÀM) +### F-1 [MEDIUM · LỆCH lý-do, giữ kết-luận] "group-key CẦN CanRead row để render" — bác bằng code + +- Spec `:24` biện hộ site-2 extend +7 group vì "group-key CẦN CanRead row để render". +- `GetMyMenuTreeQuery.cs:96` — `static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess);` (filter `:97-101`) ⇒ node **không có CanRead vẫn được giữ** nếu bất kỳ con nào readable. +- ⇒ CanRead cho `Khkk_G2..G8` **không phải điều kiện cần**. Quyết định (extend site-2) vẫn hợp lý vì tính nhất-quán với G1, nhưng LÝ DO sai ⇒ đẻ bất-biến giả cho lượt sau. +- Đề nghị: đổi câu lý do thành "đồng bộ với G1 (`KhkkKeys():2185-2189` đang cấp CanRead cho `Khkk_G1`), KHÔNG phải điều kiện render". + +### F-2 [🔴 MAJOR · mâu thuẫn NỘI TẠI: acceptance A1/A2 không thể PASS ở nấc spec tự xếp] + +- Spec `:119` — "Acceptance A1/A3-A6 đo được **NGAY sau K4a**"; `:104` — ship K4a một mình thì "leaf **RENDER được**, LỌC chưa đúng"; `:33` gọi đó là "silent-wrong". +- Code bác: `fe-user/src/components/Layout.tsx:336-338` + ``` + const path = resolvePath(node.key) + const location = useLocation() + if (!path) return null + ``` + ⇒ leaf không có route bị **DROP IM LẶNG** (đúng gotcha #50 mà chính spec trích ở mục 7/10; comment cùng file `:86` + `:98` nói y vậy). +- Route cho `Khkk_G{n}_{...}` chỉ sinh ở **K4c mục 10** (regex `resolvePath`), mà sơ đồ `:114` + thứ tự `:117` xếp K4c **CUỐI CÙNG** vì "chờ K3". +- ⇒ Trạng thái thật sau K4a-một-mình: 7 group `Khkk_G2..G8` hiện **rỗng**, **42 leaf KHÔNG hiện** ⇒ A1 ("48 leaf HIỆN sidebar") FAIL by-construction; A2 cũng không bấm được. Failure-mode **tệ hơn** mô tả của spec (vô hình ≠ lọc sai). +- Đề nghị (rẻ): **tách K4c mục 10 khỏi phụ-thuộc K3** và ship CÙNG K4a. Regex chỉ cần khuôn URL (`/khkk/list?group=n`, `&filter=…`, `?view=deleted`, `/khkk/create?group=n`, `/khkk/workflow-matrix?type=10&group=n`) — **không** cần Code `KHKK-N{n}`. Chỉ **mục 11** (WfView lọc `Code === 'KHKK-N'+n`) mới thật sự chờ K3. Sửa kèm: `:104`, `:119`, sơ đồ `:106-115`, checklist `:125`. + +### F-3 [MEDIUM · chú-thích đặt nhầm app] BÁC-2 đúng, nhưng K6 là "điều kiện tiên quyết ẩn" của K4c-13 + +- Verify 2 dòng spec nêu: `fe-admin/src/components/Layout.tsx:186-190` `filterForAdmin` **đệ quy** (`.filter(n => !isAdminHidden(n.key)).map(n => ({...n, children: filterForAdmin(n.children)}))`), gọi tại `:315` ⇒ ẩn root = subtree không render ✅ ĐÚNG. +- fe-admin **không có** `WorkflowMatrixViewPage`: grep toàn repo — file chỉ ở `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx`; fe-admin chỉ có **comment** nhắc tên tại `:130`; route `/khkk/workflow-matrix` chỉ tồn tại `fe-user/src/App.tsx:83` ✅ ĐÚNG (comment lệch-có-chủ-đích `:129-133` khớp verbatim). +- 🔴 Nhưng spec mục 13 + K6-4 dặn ghi comment khai lệch **tại regex fe-user**. Rủi ro nằm ở **fe-admin**: ai gỡ `'KeHoachKyKet'` khỏi `ADMIN_HIDDEN_KEYS` lượt sau ⇒ 42 leaf render, `resolvePath` fe-admin trả null (`fe-admin/Layout.tsx:268-270`) ⇒ drop im lặng. +- Đề nghị: comment nằm NGAY tại `ADMIN_HIDDEN_KEYS` / `isAdminHidden` (`fe-admin/Layout.tsx:182-184`), nội dung "gỡ `KeHoachKyKet` khỏi set ⇒ phải nới regex `Khkk_G{n}_*` cùng lượt, nếu không 42 leaf drop im lặng (#50)". + +### F-4 [MEDIUM · dụng cụ đo sai tầng] C1: SQL `MenuItems LIKE 'AwV2%'` KHÔNG chốt được N + +- Đo thật Dev LocalDB (`SolutionErp_Dev`): `SELECT [Key] FROM MenuItems WHERE [Key] LIKE 'AwV2%' OR [Key] LIKE 'Khkk%'` ⇒ **AwV2 = 2 row** (`AwV2_DuyetNcc`, `AwV2_DuyetNccPhuongAn`), **Khkk = 7 row** (`Khkk_G1` + 6 leaf) ✅ khớp "7 key cũ". `AwV2_Contract` = **0 hit** toàn `src/ · scripts/ · fe-*/src` (chỉ có so chuỗi `code === 'Contract'` ở `fe-admin/Layout.tsx:169`). +- 🔴 Row MenuItems tồn tại **không** kéo theo hiện sidebar: `AwV2_Contract` ∉ `MenuKeys.All` (`MenuKeys.cs:180-183` chỉ 3 key AwV2) ⇒ site-1 `:2058` không phát row Admin, cũng không nằm trong `reviewKeys` (`:2210-2224`) ⇒ `resolved` thiếu key ⇒ `HasAccess=false` (`GetMyMenuTreeQuery.cs:96`) ⇒ **bị lọc khỏi cây dù row tồn tại**. N=8 chỉ xảy ra khi prod có CẢ row MenuItems LẪN row Permissions CanRead (grant tay). +- Đề nghị đổi phép đo C1 sang JOIN, chạy trên **env nghiệm thu** (prod), không phải Dev: + `SELECT m.[Key] FROM MenuItems m JOIN Permissions p ON p.MenuKey=m.[Key] JOIN Roles r ON r.Id=p.RoleId WHERE r.Name='Admin' AND p.CanRead=1 AND m.[Key] LIKE 'AwV2%'`. + +### F-5 [LOW-MED · A5 đo nhãn sai dụng cụ] `DisplayLabel` che `Label` ở fe-user + +- `MenuItem.cs:12-13` khai rõ: `IsVisible=false` → ẩn khỏi `/menus/me` fe-user, fe-admin vẫn thấy; `DisplayLabel="X"` → **fe-user render X thay Label**, fe-admin giữ Label. +- Xác nhận code: `fe-user/Layout.tsx:204-206` `effectiveLabel = (displayLabel && trim) || label`; fe-admin **0 hit** `displayLabel`. +- ⇒ A5 "labelBackfill đổi đúng 1 nhãn G1" đo bằng UI ×2 app có thể ra **2 nhãn khác nhau** nếu prod từng đổi tên qua MenuVisibility (Mig 27) ⇒ chẩn đoán nhầm "backfill không ăn". +- Đề nghị: A5 đo CỘT — `SELECT [Key],Label,DisplayLabel FROM MenuItems WHERE [Key]='Khkk_G1'`, khai rõ nếu `DisplayLabel IS NOT NULL`. + +### F-6 [🔴 MAJOR · nền drift treo vào thứ tự ship] "64→113 / 256→452" chỉ đúng NẾU cụm-1 land trước + +- Số học tự nhất quán ✅: 49 = 7 + 42 · 64×4 = 256 · 113×4 = 452 · 64+49 = 113. +- Nền hiện tại đo thật: `MenuKeys.All` đếm tay = **55** (Dashboard 1 + Master-set 4 + Catalogs 5 + Contracts/Forms/Reports 3 + PE 1 + Hrm 2 + HrmConfig 5 + Vehicles/Drivers 2 + Off 3 + PhongHop 4 + DeXuat 4 + DonTu 4 + DatXe/ItTicket/ChamCong/AttReport/HrmDash 5 + Personal 1 + KeHoachKyKet 1 + System-set 7 + AwV2 3) — khớp canonical `docs/STATUS.md:470-471` (Policies **220** = 55×4 · Menu keys **55**). Cụm-1 khai 55→64 (+9) tại `spec-cum1-chi-tiet-31-07-2026.md:8`. +- 🔴 Nhưng cụm-2 tự xếp `:118` "K6 + K4a chạy **song song cụm-1** ngay hôm nay (0 chặn)" + sơ đồ `:109-110` vẽ K4a độc lập ⇒ K4a **có thể land TRƯỚC K1**. Khi đó dòng đúng là **55→104 · 220→416**; ghi "64→113 · 256→452" = ghi **số SAI vào canonical** (đúng loại lỗi mà B1/detector đang canh). +- Đề nghị: khai drift theo **DELTA** (`+49 key · +196 policy`) + lệnh đo lại `MenuKeys.All` từ mã tại thời điểm commit; hoặc ràng buộc cứng "K4a ship SAU K1". +- Phụ: `MenuItems LIKE 'Khkk%'` = 56 sau K4a ✅ (7 đo thật + 49 mới) — root `KeHoachKyKet` không khớp LIKE nên 56 đúng. + +### F-7 [LOW · neo số trong chú thích] comment `:1959-1961` tự trỏ upsert STALE + +- Spec mục 3 trích comment `DbInitializer.cs:1959-1961` ("sửa chữ trong `tree.Add` VÔ TÁC DỤNG") — comment đó tự ghi upsert ở "`:1893-1905`", nhưng upsert THẬT ở **`:1926-1943`** (đúng như spec mục 2 ghi). +- Diff K4a chạm ngay khối này ⇒ sửa luôn cite trong comment (bài S161: diff sửa mã phải sửa MỌI số/neo trong chú thích). + +## Bảng verify claim chịu lực (grep/đo, KHÔNG suy) + +| # | Claim (spec) | Kết | Dòng chứng | +|---|---|---|---| +| a1 | site-1 All-loop CHỈ Admin | ĐÚNG | `DbInitializer.cs:2047` method · `:2049` `FindByNameAsync(AppRoles.Admin)` · `:2058` `foreach (var menuKey in MenuKeys.All)` · `:2061` `RoleId = adminRole.Id` | +| a2 | `KhkkKeys():2185-2189` feed `reviewKeys` | ĐÚNG (EXACT) | `:2185-2189` (7 key: `Khkk_G1`+6 leaf) → `:2216 .Concat(KhkkKeys()).Concat(HdcKeys())` trong `reviewKeys :2210-2224`; method `SeedAllRolesReviewReadPermissionsAsync :2144`, gọi `:2090` | +| a3 | site-2 = CanRead mọi role, skip-existing | ĐÚNG | `:2225` `roleManager.Roles` · `:2251` `continue` (non-Pe skip) · `:2254-2260` insert `CanRead=true, CanCreate=isPe(false), CanUpdate=false, CanDelete=false` | +| a4 | site-3 độc quyền cờ GHI, comment `:2418-2419` | ĐÚNG (verbatim) | `:2415` method · `:2418-2419` "Khkk_G1 = group điều hướng, CanRead-all đã có từ nhánh read-only" · `:2440-2447` nâng R/C/U + D · site-2 không bao giờ set C/U/D cho key non-Pe | +| a5 | "KHÔNG retire site nào" | ĐÚNG | 3 site không giao nhau về cờ; thứ tự `:2090` site-2 → `:2097` revoke (chỉ `Hrm*`/`Off*`/`Personal`) → `:2110` site-3 ⇒ Khkk không bị revoke | +| a6 | đủ chỗ sửa cho 13-role THẤY leaf | **LỆCH** | thiếu ràng buộc thứ tự cho `resolvePath` fe-user ⇒ **F-2**. Không thiếu: `fe-*/lib/menuKeys.ts` (**0 hit `Khkk`** cả 2 file) · `IsVisible` mặc-định `true` (`MenuItem.cs:15` + `MenuItemConfiguration.cs:18`) nên `fe-user/Layout.tsx:200` không nuốt row mới | +| b | `:2427` 13/13 role · Delete=Drafter+Admin `:2437` · keys từ đâu | ĐÚNG | `:2427` `var roles = await roleManager.Roles.ToListAsync()` (KHÔNG lọc) · `AppRoles.cs:23-28` All = **13** · đo `SELECT COUNT(*) FROM Roles` = **13** · `:2437` `role.Name == Admin \|\| Drafter` · **biến `keys` = `:2420-2425`, 7 phần tử, KHÔNG có `Khkk_G1`** ⇒ convention "group không nhận cờ ghi" là THẬT ⇒ site-3 "+42 leaf (không group)" nhất quán | +| b2 | BÁC-1: "7 vai" là của PE, không áp KHKK | ĐÚNG | `:2669-2675` `roleNames` = Drafter/DeptManager/Procurement/CostControl/ProjectManager/Director/AuthorizedSigner = **7** | +| c | fe-admin khỏi regex (đệ quy + thiếu page) | ĐÚNG (+F-3) | `fe-admin/Layout.tsx:186-190` đệ quy, gọi `:315` · `WorkflowMatrixViewPage` chỉ ở fe-user (`fe-user/App.tsx:83` route `/khkk/workflow-matrix`) · comment `:129-133` | +| d1 | bẫy `content` bắt truyền count tay | ĐÚNG (EXACT) | `PipelineStageFolders.tsx:68-69` "Bắt buộc khi dùng `content`" · `:74-76 stageCount` · nhánh `content` render `:164-165` TRƯỚC leaves ⇒ mất empty-state `:166-167` + `
    ` `:169-202` · type `:58-72` · `ml-3 border-l` `:156` · `STAGE_STYLE:39-44` · `hideEmpty` `:228` | +| d2 | 3 file mirror; `PipelineTreePanel` không cần đổi | ĐÚNG | `PipelineTreePanel.tsx:16-17` import `usePipelineStages`+`PipelineStageFolders` · `:93 buildStages` · `:299 n.CanRead || n.Children.Any(HasAccess)` ⇒ **cha render khi CON có CanRead**. Extend site-2 cho 7 group-key = tuỳ chọn (thêm ~91 row Permission), không phải điều kiện render. Giữ hay bỏ đều được — nhưng phải sửa lý do, kẻo lượt sau suy sai sang cây khác. + +**M2 — "3 site" bỏ sót site thứ 4 lịch sử (revoker #84); tao đo hộ: an toàn.** `RevokeTemporarilyHiddenModulesAsync` (`DbInitializer.cs:2286`) được gọi ở `:2097` — **GIỮA** site-2 (`:2090`) và site-3 (`:2110`). Prefix revoke đo được `:2297-2300` = `Hrm*` / `Off*` / `Personal` ⇒ `Khkk*` KHÔNG dính. Kết luận lane đúng nhưng chưa từng soi chỗ đã lật 447/494 row (#84). Đề nghị: thêm 1 dòng "site-4 revoker: đã soi, 0 match `Khkk`" — A5 (restart ×2) chính là phép thử nó. + +**M3 — `phase` không re-sync theo URL ⇒ A2 "lọc đúng group+phase" sẽ trượt khi bấm leaf→leaf.** `fe-user/src/pages/khkk/KhkkListPage.tsx:63-65` khởi tạo `useState(initialFilter ? FILTER_TO_PHASE[...] : null)` — chỉ chạy lúc MOUNT; grep cả file: `setPhase(` duy nhất `:153` (chip lọc). Route `/khkk/list` không keyed (`fe-user/src/App.tsx:81` `element={}`) ⇒ đổi query KHÔNG remount. `deletedView` thì đọc thẳng searchParams `:61` nên phản ứng đúng — lệch nhau trong cùng 1 file. Pre-existing với 4 leaf G1, nhưng ×8 nhóm và A2 đo trúng nó. Đề nghị: K4b thêm việc "derive `phase` từ searchParams" HOẶC hạ A2 xuống "mỗi leaf mở bằng F5". + +**M4 — "Thao tác" (Create) bị dời từ K4b sang K4c ⇒ trễ 1 wave vô cớ + lệch spec owner đã đọc.** `spec-4gd-khkk-tong-quat:29` xếp "**K4b** 4 leaf list + **Thao tác** (chờ K2 param)"; `sub-invest:29` đổi tiêu đề K4b thành "4 leaf list" và đẩy Create preset xuống `:39` (mục 12, K4c "chờ K3"). Preset chỉ cần picker của vá-4 cụm-1 (K2) — 0 phụ thuộc K3. Đề nghị: trả mục 12 về K4b (đi cùng 10a ở H2). + +**M5 — 8 nhãn menu tự chốt ngoài artifact OG-6, mà chi phí sửa-sau KHÔNG rẻ như lane nói.** `sub-invest:131` "owner chỉnh CHỮ tự do… không chặn wave nào" >< chính `:11` khai "sửa chữ trong `tree.Add` VÔ TÁC DỤNG DB đang chạy" (comment `DbInitializer.cs:1959-1961`) ⇒ mỗi lần owner đổi chữ sau khi seed = +1 dòng labelBackfill + restart. Bảng OG-6 owner đang soát (`danh-muc-sp002-transcribe.md:7-16`) chỉ có cột Nhóm/Ký-chính/Nháy — **không chứa 8 nhãn này**. Kèm 1 sai lệch nội dung: nhãn N2 "Kết cấu, hạ tầng & DV hỗ trợ thi công (A2+A3)" **mất vế "phá dỡ"** (`transcribe:29` A2 = "Kết cấu, hạ tầng, phá dỡ"; dòng `:30` A2-01 = "Thầu phụ hạ tầng & phá dỡ"). 6/8 nhãn còn lại khớp transcribe (N7 rút gọn thành tên KHỐI "BCH công trường" — chấp nhận được). Đề nghị: đính 8 nhãn vào chính file OG-6 để owner soát 1 lượt, trước K4a. + +**M6 — Quyết "fe-admin KHÔNG nới regex" buộc chặt vào K6, mà K6 là display-only đảo được bất cứ lúc nào.** `sub-invest:40`/`:94` (dead-by-K6) >< `:93` (route admin còn nguyên, thu gọn = menu). Nếu owner bỏ ẩn `KeHoachKyKet` trên admin ⇒ 48 leaf DROP IM LẶNG (#50), không ai nhớ ràng buộc. Đề nghị: nới regex fe-admin luôn (rẻ, 2 file đang SHA-identical) HOẶC ghi ràng buộc ngược ngay tại `ADMIN_HIDDEN_KEYS`: "gỡ key này ⇒ PHẢI nới regex `Layout.tsx` trước". + +**M7 — [Q6 tự-chốt] "Group RỖNG không render" đi ngược scope "cây mirror sidebar".** `sub-invest:55`. Spec ① `:7` gọi mảnh-3 là "cây mirror sidebar"; DoD K5 `spec-tổng:30` = "sub-folder khớp sidebar". Sidebar LUÔN đủ 8 nhóm (menu-row cố định) còn cây ẩn nhóm rỗng ⇒ 2 mặt hết mirror; user tìm "nhóm 5" trên cây tưởng mất. Lý do lane nêu (8 folder rỗng = noise) hợp lý nhưng đây là quyết-định hiển-thị của owner. Đề nghị: hỏi 1 câu / hoặc mặc định hiện đủ 8 + toggle "ẩn nhóm rỗng". + +### LOW + +**L1 — A5 đo nhãn G1 có thể PASS-ở-DB nhưng SAI-ở-UI.** `MenuItem.cs:13,16`: `DisplayLabel` override label cho fe-user (`IsVisible=false` ẩn khỏi `/menus/me`). labelBackfill sửa `Label`, không sửa `DisplayLabel`. Đo Dev: 10/10 row `Khkk*` + `KeHoachKyKet` có `DisplayLabel = NULL`, `IsVisible = 1` ⇒ hiện an toàn. Đề nghị A5 thêm vế `AND DisplayLabel IS NULL`. + +**L2 — [PASS mạnh, có số] Tập key K6 ĐỦ trên Dev; C1 chốt N = 7.** Đo `SolutionErp_Dev`: `MenuItems` TOTAL = **142** (khớp con số STATUS ghi cho prod); root (ParentKey NULL) = **12** = {Dashboard 10, PurchaseEvaluations 25, KeHoachKyKet 26, Hrm 28, Off 29, Personal 30, Contracts 31, HopDongCung 32, Forms 40, Reports 50, Master 80, System 90} ⇒ 10/12 nằm trong `ADMIN_HIDDEN_KEYS`, 2 chừa đúng {Dashboard, System}; con của System = 7 {ApprovalWorkflowsV2, MenuVisibility, Permissions, PeWorkflows, Roles, Users, Workflows} ⇒ ẩn 2 (Workflows, PeWorkflows), chừa 5. `AwV2_*` = đúng **2 row**, **0 `AwV2_Contract`** ⇒ **N = 7 lá** (Dashboard + Users + Roles + Permissions + MenuVisibility + 2 AwV2). Còn lại chỉ cần xác nhận prod. + +**L3 — "route-map ×2 app khai trong PR" trỏ artifact không tồn tại.** 0 hit `route-map` trong `docs/PROJECT-MAP.md|STATUS.md|HANDOFF.md`; và cụm-2 KHÔNG đẻ route React nào — 48 leaf tái dùng `/khkk/list|create|workflow-matrix` đã có (`fe-user/src/App.tsx:81-83`). Surface drift thật = `resolvePath` trong `Layout.tsx` (fe-user) + 2 row STATUS. Đề nghị đổi chữ thành "bảng key→URL dán PR body". + +**L4 — Thứ tự sidebar 4 GĐ không liền mạch (đo Dev, xem L2):** GĐ1=25, GĐ2=26, **Hrm 28 / Off 29 / Personal 30**, GĐ3=31, GĐ4=32. Sau K4a, GĐ2 phình thành 8 nhóm ngồi giữa dải bị chen. Owner S159 vẽ "menu = 4 giai đoạn" (`DbInitializer.cs:1772-1773`) nên đây là thứ anh ấy sẽ thấy ngay. Không thuộc scope — đề nghị hỏi 1 câu (đổi Order 27/28), cơ chế upsert re-set Order đã có (`:1926-1943`). + +**L5 — Lý luận bất-đối-xứng giữa K4c và K5 về bề mặt admin.** `PipelineTreePanel` render trong `fe-admin/src/pages/khkk/KhkkListPage.tsx:139` và `fe-admin/src/pages/contracts/HardCopiesPage.tsx:117` ⇒ sau K6 hai trang này mất lối vào từ menu admin (chỉ deep-link). Lane đã khai chuyện "dead-by-K6" cho regex nhưng KHÔNG khai cho K5. Giữ mirror (đúng §3.9) nhưng nên khai 1 dòng để lượt sau không tưởng thừa. + +**L6 — B2 khả thi, 0 nợ drift ẩn (đo trước):** 6/6 cặp file ×2 app hiện SHA-identical — `usePipelineStages.ts` 7144eeb7… · `PipelineStageFolders.tsx` d449cc8d… · `PipelineTreePanel.tsx` d70ac315… · `KhkkListPage.tsx` 40a650bf… · `KhkkCreatePage.tsx` ea51e762… · `types/khkk.ts` 8b757f5b… (`types/khkk.ts` ĐÃ tồn tại sẵn ⇒ K5 mục 5 là mở rộng, không phải tạo mới). + +### Điểm chịu được soi (khai để không hạ chuẩn 1 chiều) + +- **P1 — mục 5 "sửa grounding 13 vai" ĐÚNG:** `DbInitializer.cs:2427` loop `roleManager.Roles` (mọi role), `allowDelete = Admin || Drafter` (`:2436-2437`); "7 vai" thật sự là seeder PE `:2669-2675`. Lane bác đúng đề bài, có chứng. +- **P2 — "extend `KhkkKeys()`" là BẮT BUỘC, không thừa:** `InReviewScope(key) => key.StartsWith("Pe_")` (`DbInitializer.cs:2158`) ⇒ đưa 49 key vào `MenuKeys.All` KHÔNG tự kéo chúng vào site-2. +- **P3 — trích dẫn dòng sạch 100%:** tao soi ~25 cite (seed `:1760/:1771/:1775/:1776/:1780-1787/:1788/:1789/:1791-1794/:1795/:1797/:1798/:1801-1803/:1809/:1827/:1853` · `KhkkKeys():2185-2189` · `SeedKeHoachKyKetAccessAsync:2415` + keys `:2420-2425` · `isAdminHidden:182-184` + `filterForAdmin:186-190` + comment S57 `:179-181` · fe-user `Layout.tsx:59-62/:79/:82-83/:341-347` · `KhkkListPage:60-61/:74/:75-82` · `PipelineStageFolders:58-72/:68-69/:74-76` · `usePipelineStages:189+/:222-224/:231/:240`) — **0 sai**. +- **Q5 trả lời:** việc "viết lại comment `:177-184`" CÓ trong danh sách (K6 mục 2) và mô tả đúng: `:179-181` = "[S57] BỎ ẩn Danh mục… (đảo S29 hide)" ⇒ QĐ7 là lần đảo THỨ 2. Không rơi. +- **Q1 trả lời gọn:** K4a-labelBackfill ✓ · K4b `?group` ✓ (thiếu queryKey — H1) · K4c WfView-lọc-Code ✓ (mục 11, KHÔNG rớt) · Create-preset ✓ nhưng sai wave (M4) · K5 GĐ3/4 group-by `c.type` ✓ (mục 4, có làm, không chỉ GĐ2) · K5 fallback "(chưa phân nhóm)"/"(ngoài chỉ mục)" ✓ · K6 `git diff --stat -- fe-user/src = 0 file` ✓ (C2). **Mục RƠI thật sự = 1: `queryKey`.** +- **Q4 trả lời:** precedent-split khai ĐỦ 2 khuôn (`sub-invest:9` + evidence `DbInitializer.cs:2193-2195`). Chỗ người build dễ nhầm = (a) tưởng "vào All ⇒ site-2 tự có" — bác bằng P2; (b) tưởng "không có row ⇒ không render" — bác bằng M1. ## Verdict -(ĐANG LÀM) +**PASS-WITH-FLAGS** — 16 finding (3 HIGH · 7 MED · 6 LOW), 0 blocker chặn ship nếu H1+H2+H3 được áp trước khi K4a/K4b vào việc. +Nền cụm-2 chắc: tập key K6 đo được là ĐỦ (L2), trích dẫn sạch 100% (P3), 2 claim chịu-lực đúng (P1/P2). Lỗ nằm ở 3 chỗ quen thuộc: **cache-key** (H1), **thứ-tự-vs-acceptance** (H2), **số-tuyệt-đối-trong-doc** (H3).