wal: flush 20260731T1339
This commit is contained in:
@ -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
|
||||
|
||||
@ -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**
|
||||
|
||||
@ -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 | ☐ |
|
||||
|
||||
@ -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ù.
|
||||
@ -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<Guid> 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<Guid>; // 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
|
||||
@ -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` + `<ul>` `: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 <PipelineStageFolders` ⇒ **consumer**, groups chảy qua. Bonus: 4 file hiện **byte-identical ×2 app** (10.677 B · 15.819 B) ⇒ B2 đo được ngay |
|
||||
| d3 | builder GĐ2/GĐ3/GĐ4 cite | ĐÚNG (±1) | GĐ2 `usePipelineStages.ts:188-206` (spec ghi 189-206) · `approvalGroup` **0 hit** ⇒ đúng là chờ K2 · "(ngoài chỉ mục 200)" `:224` (comment `:222-223`) · `c?.hasSealedCopy` `:231` · `c.type` `:240` |
|
||||
| e | 12 key blacklist đủ root, không ẩn nhầm | ĐÚNG (12/12 cite EXACT) | Master `:1760` · Contracts `:1771` · KeHoachKyKet `:1775` · HopDongCung `:1776` · Forms `:1788` · Reports `:1789` · Workflows `:1795` · PurchaseEvaluations `:1797` · PeWorkflows `:1798` · Hrm `:1809` · Off `:1827` · Personal `:1853` (+HrmConfig parent Master `:1816`). Root trong seed = 12 (thêm Dashboard `:1756`, System `:1790`) ⇒ phủ 10 root + 2 con-System; CHỪA Dashboard + System-4-leaf `:1791-1794` + AwV2 `:1801-1803` ⇒ **0 root nghiệp vụ sót · 0 System-leaf ẩn nhầm · AwV2 sống** |
|
||||
| f | SQL chốt N 7/8 | **LỆCH** | xem **F-4** (đo Dev = 2 AwV2; cần JOIN Permissions + đo trên prod) |
|
||||
| g | drift 64→113 · 256→452 | **LỆCH nền** | số học đúng; nền 55/220 (`docs/STATUS.md:470-471`) ⇒ xem **F-6** |
|
||||
|
||||
## Verified-OK (không phải lỗi — ghi để lane khác khỏi false-flag)
|
||||
|
||||
- `AuthorizePolicyRegressionTests.cs:139` + `:402` chỉ assert `MenuKeys.All.Should().Contain(...)`, **không** assert `|All|` ⇒ thêm 49 key KHÔNG vỡ test hiện có; "1 test BE" ở checklist `:129` là bổ sung, không phải sửa test cũ.
|
||||
- "Policy sinh thêm 49×4 registered-unused = vô hại" ✅ — `Program.cs:82` loop thuần `All × Actions`, không nơi nào đếm.
|
||||
- `group` KHÔNG nằm trong `TRANSIENT_QUERY_KEYS` (`fe-user/Layout.tsx:307` = `id,q,editHeader,page,phase,awId`) ⇒ `group` **mang danh tính** trong `queryMatches`/`isActive` (`:326`, `:347`) ⇒ quyết định K4b mục 8 (6 URL G1 phải mang `group=1`) là BẮT BUỘC, spec ghi đúng.
|
||||
- Root `KeHoachKyKet` có con ⇒ render `MenuGroup`, không đi `MenuLeaf` ⇒ không đụng active-check; staticMap `:79` trở thành đường chết vô hại.
|
||||
- fe-admin vẫn có `pages/khkk/{KhkkListPage,KhkkCreatePage,KhkkDetailPage,KhkkWorkflowPanel}.tsx` ⇒ mục 13 "fe-admin VẪN sửa 2 page (deep-link)" có cơ sở; `PipelineTreePanel` còn nhúng ở `fe-admin/pages/khkk/KhkkListPage.tsx:139` + `pages/contracts/HardCopiesPage.tsx:117` ⇒ cây K5 sống trên admin qua deep-link dù K6 ẩn menu (không mâu thuẫn; B1 nên đo trên fe-user và khai rõ).
|
||||
|
||||
## Verdict
|
||||
|
||||
(ĐANG LÀM)
|
||||
**PASS-WITH-FLAGS** — 0 blocker về QUYẾT (khuôn `groups` K5, blacklist-12 K6, 3-site-grant K4a đều đứng vững trước code). 7 flag: **2 MAJOR** phải xử trước khi ship (F-2 tách regex khỏi K3 ⇒ A1 mới đo được · F-6 khai drift theo DELTA hoặc ràng buộc thứ tự), **3 MEDIUM** (F-1 sửa lý-do · F-3 dời comment sang fe-admin · F-4 đổi SQL sang JOIN + đo prod), **2 LOW** (F-5 A5 đo cột · F-7 vá cite stale trong comment).
|
||||
|
||||
@ -1,12 +1,72 @@
|
||||
# c2-l2
|
||||
|
||||
LANE 2/3 — lens ĐỦ-vs-SPEC cho cụm-2 (K4 sidebar 49-row · K5 cây groups · K6 admin 12-key blacklist).
|
||||
Nguồn chấm: `sub-invest-fable-b2-cum2.md` + `spec-4gd-khkk-tong-quat-31-07-2026.md` (§③ cụm-2, §② QĐ6/7, OG-8) + `spec-cum1-chi-tiet-31-07-2026.md` (12 vá liên đới).
|
||||
Nguồn chấm: `sub-invest-fable-b2-cum2.md` + `spec-4gd-khkk-tong-quat-31-07-2026.md` (§③ cụm-2, §② QĐ6/7, OG-8) + `spec-cum1-chi-tiet-31-07-2026.md` (12 vá).
|
||||
Tất cả evidence dưới đây tao tự đo lại trên ĐĨA/DB (không tin lời khai của lane).
|
||||
|
||||
## Findings
|
||||
|
||||
(ĐANG LÀM)
|
||||
### HIGH
|
||||
|
||||
**H1 — `queryKey` thiếu `group` ⇒ 8 nhóm dùng CHUNG 1 ô cache (silent-wrong, build xanh).**
|
||||
- Claim của lane: K4b mục 7 (`sub-invest-fable-b2-cum2.md:31`) liệt đúng 2 chỗ — `searchParams.get('group')` cạnh `:60-61` và truyền vào `params` `:75-82` (cả nhánh `/deleted` `:74`) — **KHÔNG có `queryKey`**.
|
||||
- Evidence: `fe-user/src/pages/khkk/KhkkListPage.tsx:70` → `queryKey: ['khkk-list', { phase, search, page, deletedView }]`. File này SHA-identical với fe-admin (`sha256` 16 ký tự đầu = `40a650bf6964ab4b` cả 2 app) ⇒ lỗi nhân đôi.
|
||||
- Hệ quả: đổi `?group=` KHÔNG đổi khoá cache ⇒ leaf nhóm 2 render danh sách nhóm 1 (rồi bị ghi đè chéo). `tsc` + `npm run build` + test đều XANH. Cùng họ với S159-③ (2 khoá cho 1 resource) nhưng CHIỀU NGƯỢC: 1 khoá cho 8 resource.
|
||||
- Sev: HIGH. Đề nghị: mục 7 thêm gạch "`group` VÀO `queryKey` `:70`"; A2 thêm bước "bấm G1→G2→G1 KHÔNG F5, số phải đổi".
|
||||
|
||||
**H2 — Cỗ máy làm 42 leaf mới bấm-được (regex) nằm ở K4c (chờ K3), nhưng acceptance A1/A2 đo nó ở nấc K4a/K2 ⇒ A1 bất-khả-PASS.**
|
||||
- `sub-invest:43` A1 = "48 leaf HIỆN sidebar" và `:119` khẳng định "A1/A3-A6 đo được NGAY sau K4a". Nhưng URL cho `Khkk_G{n}_*` chỉ sinh ở mục 10 (`:37`) = K4c, mà K4c `:114`/`:120` CHỜ K3.
|
||||
- Evidence code: `fe-user/src/components/Layout.tsx:338` `if (!path) return null` trong `MenuLeaf` ⇒ key không có trong `staticMap`/regex = **DROP IM LẶNG** (chính gotcha #50 mà file này tự dán ở `:151-153`). Sau K4a-only: 7 group-row hiện, 42 leaf **KHÔNG render** ⇒ A1 đếm 6, không phải 48.
|
||||
- Sev: HIGH (acceptance khẳng định đo được ở nấc mà nó chắc chắn trượt).
|
||||
- Đề nghị: tách mục 10 → **10a** (List/Pending/Approved/Deleted/Create — 0 phụ thuộc, kéo về K4a) + **10b** (WfView filter theo Code — giữ ở K4c chờ K3). Hoặc sửa A1 thành "đo sau K4a+10a".
|
||||
|
||||
**H3 — 2 con số drift STATUS SAI trong MỌI kịch bản của chính thứ tự ship lane đề xuất.**
|
||||
- Lane: `sub-invest:27`/`:128` "Menu keys 64→**113** · Policies 256→**452** (55→64 đã khai cụm-1)".
|
||||
- Canonical đo tươi: `docs/STATUS.md:471` Menu keys = **55**; `:470` Policies = **220** (DERIVED `|MenuKeys.All| × 4`).
|
||||
- Base 64 = 55 + 9, mà 9 key cụm-1 = 1 `ContractCatalog` (K1) + 8 `AwV2_KhkkN*` (**K3**) theo `spec-cum1-chi-tiet:8`. Thứ tự ship lane đề xuất (`:117`) là `K6 → K4a → [K2] → K4b+K5 → [K3] → K4c` ⇒ lúc commit K4a, K3 **chưa** land ⇒ STATUS đang ở 55 hoặc 56, **không bao giờ** 64. Ghi "64→113" = ghi sai canonical mà `governance-detectors.ps1` đọc.
|
||||
- Sev: HIGH (số mang nhãn đo-lường; phép kiểm làm nó trượt = `grep 'Menu keys' docs/STATUS.md` ngay trước commit).
|
||||
- Đề nghị: khai **DELTA** (+49 key / +196 policy) + luật "đọc row STATUS ngay trước commit rồi cộng delta" (B1).
|
||||
|
||||
### MED
|
||||
|
||||
**M1 — Lý do giữ site-2 SAI (kết luận vẫn ổn).** `sub-invest:24` khẳng định "group-key CẦN CanRead row để render". Đo: `src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs:96-97` — `static bool HasAccess(MenuNodeDto n) => 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={<KhkkListPage />}`) ⇒ đổ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).
|
||||
|
||||
Reference in New Issue
Block a user