diff --git a/.claude/WAL.md b/.claude/WAL.md index b8040ad..44a6843 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -31,9 +31,14 @@ next: 5/6 OG CHỐT (1 schema-GỘP+UI-1-1 · 2 đội-hình-cũ · 7 N-phiếu+ thêm răng #81-EXT site :342) · catalog authz mirror CatalogsController · MÂU THUẪN OG-3 THẬT (BE chưa port finalize — KHKK_EARLY_FINALIZE_NOTE system/ApprovalWorkflowsV2Page:151) → spec §④ OG-3 ĐÃ SỬA (K3 3 việc a/b/c). Roster ô ⚠️ "Trưởng" 3 ứng viên — verify LIVE trước seed. - wf: B2-r cụm-1 wf_2a967a21-79e ĐANG CHẠY (3 lane reviewer opus: c1-evi · c1-cov · c1-seq). - CHỜ ANH: câu (I) bản-15-trang quy-trình-chữ khác bản-6-trang? · (II) B2-14-dòng + "/"=hoặc · (III) ai bấm - "Đưa vào HĐ"+chọn form. Pattern sổ: fable-lane StructuredOutput-fail 3/4 (att-2 này SẠCH — n=4). + B2-r cụm-1 ✅ 3/3 lane (L1 2/9-claim-đúng GỠ-(i)-:342 [invest đọc-ngược-comment] · L2 FAIL 3H · L3 PWB + 5H) → spec-cum1-chi-tiet-31-07-2026.md FINAL (nền 33KB + 12 VÁ thắng-xung-đột; blocker: key-VÀO-All + + sweep-ép-false-AllowApproverFinalize ≥4-site + KhkkCreatePage-×2-app + submit-guard-CatalogEntryId). + 🆕 OG-9 (L2-M3): K3 bật duyệt-sớm KHKK = ĐẢO phán quyết "PHÁ VỠ" @S155 (5 nơi ghi) — cần anh xác nhận + TƯỜNG MINH (câu IV). Chặn K3 (a)-(e); K1/K2 KHÔNG chặn. + wf: B2 cụm-2 wf_15a42015-aea ĐANG CHẠY (1 lane fable đan-xen: K4 sidebar 49-row · K5 cây groups · K6 admin). + CHỜ ANH: (I) bản-15-trang · (II) B2-14-dòng+"/"=hoặc · (III) ai bấm Đưa-vào-HĐ · (IV=OG-9) đảo PHÁ-VỠ-S155. + Sổ fable-lane: StructuredOutput-fail 3/5 (2 att gần nhất SẠCH — đan-xen tương quan?). 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/run.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/run.md index 462eb5f..b4f86a2 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 @@ -12,7 +12,9 @@ | 1b | Review spec tổng quát — 3 lane `WF reviewer lens{1,2,3} s164` | /fable-clone review (tier:opus ×3) | ✅ 3/3 sạch 0-empty · 41 finding thô → synthesis 14 vá + 2 OG mới → **spec v2** | | 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.x-r | Review spec chi tiết per cụm | /fable-clone review | ☐ | +| 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 | | ☐ | | 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-cum1-chi-tiet-31-07-2026.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-cum1-chi-tiet-31-07-2026.md new file mode 100644 index 0000000..9c6b0b4 --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-cum1-chi-tiet-31-07-2026.md @@ -0,0 +1,27 @@ +# SPEC CỤM-1 FINAL — K1 + K2 + K3 (lead kết tinh sau B2-r, @S164) + +## ① Đề bài +K1 master-data danh mục (86 row) · K2 phiếu mang hạng-mục + nhóm + freeze-răng · K3 8 workflow nhóm + port-finalize + designer. Nguồn NỀN = `sub-invest-fable-b2-cum1.md` (33KB, 17 test) — **12 VÁ dưới đây THẮNG khi xung đột** (từ B2-r 3 lane: `sub-reviewer-c1-l{1,2,3}.md` — 41 finding, hội tụ cross-lane). + +## ② 12 vá (build ĐỌC CẢ nền LẪN vá) + +1. 🔴 **[3-lane hội tụ] Menu key PHẢI VÀO `MenuKeys.All`** — nền khai `ContractCatalog` + 8 `AwV2_KhkkN*` "ngoài All" ⇒ **0 permission row ⇒ sidebar chết** (loop grant `DbInitializer.cs:2058` nguồn = All; khuôn THẬT: 5 leaf catalog hiện hành `MenuKeys.cs:168` + 3 leaf AwV2 `:182` đều TRONG All). Fix: cả 9 key **VÀO All** + KHAI drift canonical (Menu keys 55→64 · Policies 220→256 — khai 2 row STATUS cùng lúc, đừng Goodhart giữ-số-đứng-yên). Acceptance += "login Admin THẤY leaf trong sidebar ×2 app" + 1 test seed-grant. +2. 🔴 **[L1-F5] GỠ phương án (i) tại site `:342`** — nền đọc NGƯỢC comment (`WorkflowService.cs:36-43` mô tả NGUỒN COPY và khẳng định KHKK CỐ Ý không có site gán-qua-BIẾN; `:342` nằm trong `ReturnOrRejectAsync` chỉ nhận TraLai/TuChoi literal — DaDuyet **unreachable** qua đó). GIỮ phương án (ii): test khóa khẳng định điều đó. Cấm "sửa" :342. +3. 🔴 **[L2-H2] K3 thêm việc (d): sweep cụm ép-false** — `grep -rn "AllowApproverFinalize|AllowApproverSkipToFinal"` toàn src + fe-*: gỡ đủ **CREATE ép-false server-side** `ApprovalWorkflowV2AdminFeatures.cs:380-381` + **PUT Conflict** `:591-594`/`:605-610` + **FE `levelFlagsPayload`** `ApprovalWorkflowsV2Page.tsx:247-258` (`earlyFinalizeLocked :638`) — không chỉ note `:151-152`. Test mới: "tạo version qua CreateCommand type-10 GIỮ được cờ". +4. 🔴 **[L2-H3] `KhkkCreatePage` ×2 app PHẢI SỬA** (mâu thuẫn nền: command +`ApprovalGroup` required nhưng page ghi "KHÔNG-ĐỔI") — body POST thêm `approvalGroup` + picker nhóm. Acceptance += "tạo phiếu nhóm 2 **TỪ UI** ×2 app" (curl-only không đủ). +5. 🔴 **[L2-M6/L1-F6] Chốt nhất quán `CatalogEntryId`**: auto-gen line lúc tạo phiếu được mồi null NHƯNG **submit-guard CHẶN null** (mọi line phải gán hạng mục trước trình) + validator line-mới-qua-API bắt buộc có. DoD "line khác nhóm → 409" phải chạy trên phiếu CÓ line thật (chống PASS-bằng-tập-rỗng). +6. **[L2-M4] Kéo `phase-guard line-edit sau DaDuyet` từ K4b VỀ K2** — Σ `Features.cs:709-711` tính LIVE, soft-delete line làm số phiếu-đã-duyệt trôi; freeze chưa kín nếu guard nằm cụm-2. +7. **[L2-M5/L1-F13] Mig 71 BỔ SUNG item 6 = cột `EndedByLevelFinalize`** (K2.a hiện chỉ 1-5) + tên mig phản ánh (vd `AddKhkkApprovalGroupCatalogAndFinalizeRuntime`) + khai ghost-window K2→K3 (cột nằm im tới khi K3 land). +8. **[L1-F8] Bảng consumer += `Features.cs:712-713` Σ ProposedAmount** (nền sót — list có 2 Σ) + quyết: list thêm Σ Approved sau freeze để list/detail không lệch số. +9. **[L1-F7] Chứng NULL-unique vá lại**: unique cũ bị DROP trong chính Mig 71 — mắt xích thật chặn 2-line-NULL-cùng-(Plan,Supplier) phải nêu ĐÚNG (submit-guard vá-5 + backfill line cũ gán từ header-group; đừng dựa index đã chết). +10. **[L1-F9/L2-M2] Sửa nhãn IgnoreQueryFilters**: số đúng = "0 hit trong `DbInitializer.cs`" (toàn src/Backend = 29 hit/5 file — claim "duy nhất"/"0-hit" là sai lây từ comment code); đơn thuốc seed `:2970` thiếu IgnoreQueryFilters vẫn ĐÚNG. +11. **[L1-e] Roster: acceptance đo DANH TÍNH** — K3.e += mục "mở workflow nhóm N, danh sách người duyệt KHỚP bảng đội hình từng người" + xử 2 cặp gần-trùng (`truong.le` vs `tring.le` vs `truong.nguyen` · `thanh.lethanh` "Lê Thanh Bình" vs `binh.le` "Lê Văn Bình") bằng đối chiếu **dept + chức danh trên bản LIVE**, không match tên trần; dept-lookup `:185` thêm null-guard. +12. **[L1-F3/F4] Trích dẫn `:44-46` ĐỦ + khai supersede**: nguyên văn có "chủ dự án phán 'PHÁ VỠ' @S155" — spec PHẢI khai K3 = **ĐẢO phán quyết cũ theo lời owner @S164**, kèm việc (e): sửa comment `:36-43`/`:44-46` + 2 lệnh tự-kiểm `:292-293` dán PR + cập nhật `KhkkWorkflowPanel.tsx:8` ×2 app + HANDOFF:77. 🔴 **[OG-9 — L2-M3] chờ anh xác nhận TƯỜNG MINH** (câu IV đã gửi): bật lại duyệt-sớm cho KHKK = đảo "PHÁ VỠ" @S155. **Chặn K3 việc (a)-(e); K1/K2 KHÔNG chặn.** + +## ③ Checklist final cụm-1 +- [ ] K1: 7 mục nền + **"Admin thấy leaf sidebar ×2 app"** + test seed-grant + drift 2 row STATUS khai cùng commit. +- [ ] K2: 8 mục nền + **submit-guard null-CatalogEntryId (test ÂM trên phiếu có line)** + phase-guard line-edit + Σ×3 nhất quán + Mig-71 6 item + 3-file + backfill=1 + **UI tạo-phiếu-nhóm-2 ×2 app** + bảng 18 consumer (17 nền + :712-713). +- [ ] K3 (SAU OG-9): 6 mục nền + sweep ép-false ≥4 site + test giữ-cờ-qua-CreateCommand + đo danh tính roster + comment/panel/HANDOFF cập nhật + verify LIVE trước seed. +- [ ] Mọi wave: test ≥590+k khai k · cicd PASS · BE-only smoke API (#77 chỉ khi FE-ship). + +END spec-cum1-final — nền 17 test + vá: ~5 test mới · 1 OG mới (OG-9) · 2 blocker menu/ép-false đã đóng đường. diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md index 58f0545..9e9cc74 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md @@ -1,12 +1,161 @@ # c1-l1 QUYẾT-vs-code Lane 1/3 review cụm-1 — lens: QUYẾT (spec chốt) đối chiếu CODE trên đĩa. -Đối tượng: `.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md` +Đối tượng: `.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md` (33.048B, 258 dòng). +Mọi dòng dưới đây đo từ đĩa @2026-07-31, HEAD `e5f53d0`. + +## Điểm số + +- **Citation hygiene: 23/25 chuẩn** — file:line trỏ đúng chỗ gần như tuyệt đối. +- **Claim chịu lực: 2/9 ĐÚNG · 4 LỆCH · 3 SAI** — lỗi KHÔNG nằm ở "trỏ sai dòng", mà ở **suy ra cái gì từ dòng đã trỏ đúng** và ở **cái không được trỏ**. +- **Blocker: 2** (F-1, F-2) · **Phải sửa trước khi implement: 4** (F-3, F-4, F-6, F-13). ## Findings -(ĐANG LÀM) +### F-1 [HIGH · BLOCKER] K3 8 menu-con khai "NGOÀI `MenuKeys.All`" ⇒ 0 permission row ⇒ sidebar không bao giờ hiện + +- Spec dòng 214 (K3.c): "MenuKeys.cs +8 const `AwV2_KhkkN1..AwV2_KhkkN8` — **NGOÀI `All`** (0 policy mới, tiền lệ `HopDongCung`/leaf `Pe_*`)". K3.c liệt: const + MenuItem row + `Layout.tsx` resolvePath + mirror `menuKeys.ts`. **Không có bước cấp quyền.** +- Đĩa: `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:2058` — `foreach (var menuKey in MenuKeys.All)` là nguồn DUY NHẤT của grant admin generic. Key ngoài `All` KHÔNG sinh row `Permissions`. +- `src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs` — grep `Admin|IsAdmin` = **0 hit** ⇒ cây menu KHÔNG có nhánh admin-bypass, không có đường vớt. +- Tiền lệ mà spec viện dẫn nói NGƯỢC lại: mọi key ngoài `All` trong repo đều đi kèm danh sách grant TƯỜNG MINH — `Pe_*` → `peKeys` `DbInitializer.cs:2196-2208`; `HopDongCung` → `.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })` `:2214`; `Khkk_*` → `KhkkKeys()` `:2185-2189` + `SeedKeHoachKyKetAccessAsync` `:2415`. +- Và 3 leaf `AwV2_*` đang sống thì **NẰM TRONG `All`**: `src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:182` — `ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2`. Tức 8 leaf mới phá chính khuôn anh em ruột của nó. +- Hệ quả đo được: acceptance K3.e mục 3 ("sidebar đếm được **8 menu-con**") sẽ FAIL tại UAT. +- **Đề nghị:** K3.c phải khai thêm 1 việc — hoặc đưa 8 key vào danh sách grant explicit (mẫu `KhkkKeys()` `:2185-2189`), hoặc đưa vào `All` và chấp nhận +32 policy kèm sửa 2 số canonical `docs/STATUS.md` (Policies 220 / Menu keys 55). + +### F-2 [HIGH · BLOCKER] K1.d mắc đúng lỗi F-1, nhưng K1.f không có phép đo nào bắt được + +- Spec dòng 57 (K1.d): "+const `MenuKeys.ContractCatalog` (**NGOÀI `All`** …) + 1 MenuItem row con group `Catalogs` … + **grant admin (SeedAdminPermissionsAsync loop)**". Hai vế này loại trừ nhau: nguồn của loop chính là `MenuKeys.All` (`DbInitializer.cs:2058`). +- Acceptance K1.f (dòng 69-75) có 7 mục: 3-file mig · seed 86 · restart ×2 · curl 200/403 · test +5 · DoD 86 dòng · số STATUS. **Không mục nào chạm menu.** +- Tức: bản cài đặt trong đó trang danh mục **không vào được từ sidebar** vẫn PASS 7/7. Đây là câu trả lời cho phép thử "impl SAI nào vẫn PASS?". +- Ghi chú thêm: 4 leaf catalog hiện có (`Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems`) đều nằm trong `All` (`MenuKeys.cs:169`, dòng thứ 29 của khối `All`) và nằm trong Master-set review-scope `DbInitializer.cs:2181-2183` — leaf mới ngoài `All` cũng sẽ vắng mặt ở review-scope, tức non-admin không thấy. +- **Đề nghị:** +1 acceptance đo được ("đăng nhập admin → sidebar `Danh mục` có leaf mới → click mở đúng trang `:kind=contract-catalog`") và khai đúng đường grant. + +### F-3 [HIGH] K3.⚠️ đảo ngược một quyết định owner @S155 mà không khai — trích dẫn `:44` bị cắt đúng chỗ nói lý do + +- Spec dòng 180 viết: "…(KHÔNG có nhánh AllowApproverFinalize; MEMORY-verified **`:44` comment không port CeoApprovalThreshold**)". +- Nguyên văn `src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs:44-46`: + > `(b) 🔴 KHÔNG PORT 2 nhánh kết thúc sớm (AllowApproverFinalize, CeoApprovalThreshold) và cũng KHÔNG port AllowApproverSkipToFinal (:318-352 nguồn copy) — chủ dự án phán "PHÁ VỠ" @S155. W3 chỉ đường THƯỜNG: PMH → CCM → CEO ký thật.` +- Spec trích 1 trong 3 tên nhánh, và tên bị bỏ đúng là `AllowApproverFinalize` — thứ K3.⚠️(a) định port. Mệnh đề owner-veto cũng bị bỏ. +- **Công bằng với lane:** `spec-4gd-khkk-tong-quat-31-07-2026.md:46` (hàng OG-3) CÓ ghi lời owner @S164 ("*a Chương sẽ quyết định là cái đó cần CEO ko hay là anh ấy đc duyệt*") ⇒ có cơ sở hợp lệ để SUPERSEDE quyết định @S155. Vấn đề là B2-cụm-1 không hề nói đây là đảo quyết cũ, nên người đọc sau sẽ gặp comment `:44-46` và tưởng code đang bị làm trái ý owner. +- **Đề nghị:** K3.⚠️ trích `:44-46` đầy đủ + thêm đúng 1 câu "OG-3 @S164 SUPERSEDE veto @S155", và đưa việc **sửa lại comment `:44-46`** vào danh sách việc của K3. + +### F-4 [MED-HIGH] K3 thiếu việc code thứ 4: khối comment `:36-43` sẽ thành lời khai sai ngay khi (a) land + +- K3.⚠️ liệt 3 việc (a)(b)(c); (c) chỉ gỡ khoá + note FE `ApprovalWorkflowsV2Page.tsx:151-152`. +- Nhưng `ContractSigningPlanWorkflowService.cs:37-43` đang khai: "🔴 KHAI RÕ: service này có **ĐÚNG MỘT** nhánh set DaDuyet, KHÔNG có site thứ hai kiểu admin-override của nguồn copy". Và `:292-293` cài sẵn 2 lệnh tự-kiểm: + ``` + grep -rn "Phase = ContractSigningPlanPhase.DaDuyet" src/Backend --include=*.cs + grep -rn "ApplyApprovedValuesOnFinalize" src/Backend --include=*.cs + ``` +- Sau khi port nhánh duyệt-sớm, mệnh đề "ĐÚNG MỘT" hoặc vẫn đúng (nếu nhánh mới nhảy vào cùng choke-point) hoặc thành sai (nếu sinh site thứ 2) — **phải chứng, không phải đoán**. +- Đây đúng class lỗi mà chính spec dùng để bắt FE ở K3.⚠️(c) ("không gỡ = config-lie NGƯỢC"), chỉ khác chiều: lần này là comment BE nói "không có" trong khi code đã có. +- **Đề nghị:** thêm acceptance "chạy 2 lệnh `:292-293`, dán output vào PR, hai lệnh phải khớp nhau" + sửa `:36-43` cùng commit K3. + +### F-5 [MED] K2.b đọc sai comment `:39`; site `:342` không phải bẫy — và phương án (i) sẽ TẠO RA bẫy đó + +- Spec dòng 105 xếp `WorkflowService.cs:342` là "`= targetPhase` (gán qua BIẾN — đúng class bẫy `:39` comment)" rồi gắn "🔴 PHẢI XỬ", đề 2 phương án: (i) gọi `ApplyApprovedAmounts` ngay trước `:342` khi `targetPhase == DaDuyet`, (ii) assert + test khoá hành vi. +- Đọc `:39` trong ngữ cảnh `:36-43`: câu đó đang mô tả **NGUỒN COPY** (`ContractWorkflowService.cs:190-193`), và cả khối khẳng định KHKK **cố ý không có** site đó, lý do nêu ở `:40-42`: "hợp-đồng transition KHKK chỉ nhận 4 action cố định, KHÔNG có tham số `targetPhase`". +- Đo lại từ đĩa, đúng như code khai: + - `:342` nằm trong `private async Task ReturnOrRejectAsync(...)` khai báo tại `:310`. + - Toàn repo có **đúng 2 call-site**: `:128-129` truyền literal `ContractSigningPlanPhase.TraLai`, `:133-134` truyền literal `ContractSigningPlanPhase.TuChoi`. + - Cửa vào là `TransitionAsync(Guid planId, string action, …)` `:86-90` — chữ ký **không có** `targetPhase`; `AllowedActions` `:73` chặn trước mọi truy vấn `:95-96`; switch đóng kín với `default: throw` `:137-139`. + - Thân hàm còn tự khoá thêm: `:318` `isReturn = targetPhase == TraLai`, `:320-323` chỉ chấp nhận phiếu đang `ChoDuyet`, `:346-347` xoá con-trỏ, `:349-355` ghi log "Trả lại/Từ chối". +- ⇒ `targetPhase == DaDuyet` **không reachable hôm nay**. Phương án (i) sẽ mở đường thứ 2 tới `DaDuyet` chạy xuyên thân hàm return/reject (xoá con-trỏ + log sai nhãn) — tức tự tay dựng đúng cái bẫy `:37-43` nói là đã tránh được. +- **Đề nghị:** bỏ (i), giữ (ii); đổi nhãn ô bảng từ "🔴 PHẢI XỬ" thành "an toàn do thiết kế — khoá bằng test hành vi". Test K2.g #6 giữ nguyên, nhưng phải phát biểu là "không action string nào đưa phiếu tới DaDuyet mà bỏ qua freeze". + +### F-6 [MED-HIGH] K2 tự mâu thuẫn về `CatalogEntryId` bắt buộc; hệ quả là rào "phiếu phải có hạng mục" không tồn tại ở đâu + +Ba câu trong cùng một cụm, đọc liền nhau thì triệt tiêu nhau: +- dòng 87 (K2.a-4): "Chặn phát sinh mới bằng validator: **line MỚI/SỬA bắt buộc có `CatalogEntryId`** (chỉ row legacy được giữ null)". +- dòng 113 (K2.c): auto-gen `Features.cs:389` "mồi `CatalogEntryId = null, TenHangMuc = null`" — tức **mọi** line của **mọi** phiếu mới đều null. +- dòng 126 (K2.d): submit-guard "Line `CatalogEntryId == null` … → **CHO QUA**". +- Đối chiếu code: `ContractSigningPlanFeatures.cs:383-400` auto-sinh line trong chính handler create, `:389` `plan.Lines.Add(new ContractSigningPlanLine { … })`. +- ⇒ Phiếu vừa tạo (100% line null) submit và duyệt trọn được mà không mang hạng mục nào. Mục tiêu K2 ("1 phiếu = 1 nhóm, N hạng mục") không được rào tại bất kỳ tầng nào, còn DoD "line khác nhóm → 409" thì PASS trivially vì tập line-có-catalog rỗng. +- **Đề nghị:** khai rõ luật NOT-NULL chỉ áp cho lines-editor K4b; và hoặc thêm rào submit "≥1 line có `CatalogEntryId`" với acceptance đo được, hoặc defer TƯỜNG MINH sang K4b kèm ghi nợ. + +### F-7 [MED] Chứng "2 line NULL cùng (Plan,Supplier) không lọt" hở mắt xích — mắt xích thật nằm ở module khác + +- Spec dòng 87 lập luận: "Data cũ AN TOÀN (unique cũ bảo đảm ≤1 row/cặp ⇒ tối đa 1 NULL-row/cặp)". Vế **data cũ** này ĐÚNG: `ContractSigningPlanLineConfiguration.cs:22-24` là `HasIndex(new { ContractSigningPlanId, SupplierId }).IsUnique().HasFilter("[IsDeleted] = 0")`, và index mới cũng filter `[IsDeleted]=0` nên row xoá mềm không tham gia. Migrate sẽ không vỡ. +- Nhưng unique cũ **bị DROP trong chính Mig 71** (K2.a-4) ⇒ sau migrate nó không còn che auto-gen nữa. Thứ thật sự chặn auto-gen sinh 2 line cùng supplier là index ở **module khác**, spec không hề nhắc: `PurchaseEvaluationConfiguration.cs:105` — `b.HasIndex(x => new { x.PurchaseEvaluationId, x.SupplierId }).IsUnique();` (không filtered). Nhờ nó mà vòng lặp `Features.cs:383` (`foreach w in winners`) với `:392 SupplierId = w.SupplierId` không thể ra 2 line trùng. +- Kết luận của spec đúng, nhưng chuỗi chứng đứt: bảo đảm đang được chuyển từ một index **sắp bị xoá** sang một index **chưa từng được nêu tên**. +- **Đề nghị:** cite `PurchaseEvaluationConfiguration.cs:105` như bất-biến chịu lực và khoá bằng 1 test; nếu K4b sau này cho thêm line tay thì bất-biến này hết hiệu lực, phải khai luôn. + +### F-8 [MED] Tiền: list DTO có HAI tổng, spec chỉ xử một — sau freeze, list và detail sẽ nói 2 con số khác nhau + +- `ContractSigningPlanFeatures.cs:709-713` (projection list): `Count(lines)` `:709` · `Sum(PeReferenceAmount)` `:710-711` · **`Sum(ProposedAmount)` `:712-713`**. +- Bảng 17-consumer K2.f dòng 148 chỉ có `:711`. `:712-713` **không xuất hiện** trong bảng. +- K2.e (dòng 132) định nghĩa lại semantics cho row tách: `PeReferenceAmount = 0`, nhưng không nói `ProposedAmount` của row tách bằng bao nhiêu ⇒ tổng đề-xuất trên list hoặc tụt hoặc nhân đôi, không dấu vết. +- List cũng không có tổng `ApprovedAmount`. Sau `DaDuyet`, detail hiện `ApprovedAmount` (display-gate `budgetFrozen`, K2.b dòng 107) còn list vẫn hiện `ProposedAmount` ⇒ hai màn hai con số cho cùng một phiếu đã chốt. +- **Đề nghị:** thêm `:712-713` vào bảng consumer; quy định `ProposedAmount` của row tách; và cho list đọc `ApprovedAmount ?? ProposedAmount` khi phiếu đã đóng băng (hoặc khai tường minh là chấp nhận lệch). + +### F-9 [MED] Claim "hit `IgnoreQueryFilters` đầu tiên trong src/Backend — S155 đo 0-hit" là SAI + +- Spec dòng 38 (K1.b): "(Đây là hit `IgnoreQueryFilters` **đầu tiên** trong src/Backend — S155 đo 0-hit — cố ý, khai trong PR.)" +- Đo lại: **29 occurrence / 5 file** trong `src/Backend`: + `SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs` 1 · `SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` 18 · `SolutionErp.Application/Contracts/ContractFeatures.cs` 3 · **`SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` 3** (đúng file cụm-1 sẽ sửa) · `SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs` 4. +- Kèm theo: `PurchaseEvaluationsController.cs:186` cũng đang khai "DUY NHẤT dùng `IgnoreQueryFilters()` trong toàn `src/Backend` (D3)" — hai claim độc quyền cùng sai, claim của spec nhiều khả năng thừa kế từ claim này. +- Nội dung kỹ thuật vẫn ĐÚNG và đáng giữ: seeder phải `IgnoreQueryFilters()` mới né được #75/#76, còn khuôn `SeedRealMasterDataAsync` thì đúng là không có (`DbInitializer.cs:2970` `var existingProjectCodes = await db.Projects.Select(p => p.Code).ToListAsync();` — không IgnoreQueryFilters, đúng như spec mô tả). Chỉ cái nhãn "đầu tiên" là sai. +- **Đề nghị:** bỏ nhãn "đầu tiên / S155 đo 0-hit"; nếu tiện thì ghi errata cho `PurchaseEvaluationsController.cs:186`. + +### F-10 [LOW-MED] Ba citation trỏ vào USE-SITE trong khi việc phải sửa nằm ở DECLARATION + +| Spec nói | Thứ ở đó | Declaration thật | +|---|---|---| +| `LineDto :570-574` +2 field (dòng 131) | projection `.Select(l => new ContractSigningPlanLineDto(…))` | `public record ContractSigningPlanLineDto(` — `Features.cs:59` | +| `DetailDto :608-616` +2 field (dòng 131) | lời gọi ctor `return new ContractSigningPlanDetailDto(` `:608` | `public record ContractSigningPlanDetailDto(` — `Features.cs:119` | +| `types/khkk.ts:203` `KhkkLineDto` +2 field (dòng 149) | field `lines: KhkkLineDto[]` của interface Detail | interface `KhkkLineDto` (chỗ khác trong file) | + +Ngược lại 2 citation cùng loại thì chuẩn: `ContractSigningPlanListItemDto` `:34` (đúng decl) và `ListContractSigningPlansQuery` `:622-626` (đúng decl). **Đề nghị:** với mỗi DTO ghi CẶP (decl + mọi construction site), vì sửa 1 nơi thì không biên dịch được. + +### F-11 [LOW] Đường entity K1.a không nằm "cạnh WorkItem" + +- Spec dòng 11: `src/Backend/SolutionErp.Domain/Master/ContractCatalogEntry.cs` "(cạnh WorkItem — folder Domain/Master hiện hữu)". +- Đĩa: `Domain/Master/` = `Catalogs/`, `Department.cs`, `Project.cs`, `Supplier.cs`, `SupplierStatus.cs`, `SupplierType.cs`. `WorkItem.cs` nằm ở `Domain/Master/Catalogs/` (cùng `MaterialItem.cs`, `ServiceItem.cs`, `UnitOfMeasure.cs`). +- Ảnh hưởng thật: namespace `SolutionErp.Domain.Master` vs `SolutionErp.Domain.Master.Catalogs` ⇒ `using` ở Configuration, Features, `ApplicationDbContext`, `IApplicationDbContext`. **Đề nghị:** chốt `Domain/Master/Catalogs/ContractCatalogEntry.cs` (Application path `Application/Master/Catalogs/` mà spec chọn thì đã đúng khuôn). + +### F-12 [LOW] Index-swap cite lệch 2 dòng + +- Spec dòng 86: "DropIndex unique cũ … (tên EF-sinh — lấy đúng từ `ContractSigningPlanLineConfiguration.cs` vùng **:24-26**)". +- Đĩa: unique cũ ở **`:22-24`**; `:25` là `HasIndex(x => x.SupplierId)`, `:26` là `HasIndex(x => x.ContractId)` — hai index thường, không liên quan. Đi theo `:24-26` là đụng nhầm. +- Cùng mục: "`:29 WithMany(p => p.Lines)` giữ nguyên" ✅ chính xác. + +### F-13 [MED] "Item 6" của Mig 71 là tham chiếu treo — và trả lời câu hỏi (c) "hợp lý hay trộn concern" + +- K3.⚠️(b) dòng 181: "gắn vào **Mig 71 item 6 (K2.a)**". §Thứ tự commit dòng 242: "Mig 71 gồm cả **item 6** `EndedByLevelFinalize`". +- K2.a (dòng 83-88) liệt **1..5**. Không có item 6. Người làm K2 mà chỉ đọc K2.a sẽ không thêm cột ⇒ K3 mất tiền đề "0-mig" và test K3.d #5 (`EndedByLevelFinalize=1`) fail. +- **Trả lời (c): là TRỘN CONCERN**, ba lý do đo được: (1) tên mig `AddContractSigningPlanGroupAndLineCatalog` không phủ cột này, mà tên mig là nhãn duy nhất người sau đọc trong `__EFMigrationsHistory`; (2) giữa commit K2 và commit K3 cột là ghost — defined-but-unread, đúng class ghost-wire; (3) chính spec phải viết thêm 2 chỗ cross-ref để bù, và đã hụt 1 chỗ (đây). +- Vẫn chấp nhận được NẾU làm đủ 3 việc: liệt item 6 tường minh vào K2.a · đổi tên mig cho phủ · khai ghost-window trong PR. Tách Mig 72 thì sạch concern hơn nhưng tốn 1 mig — trade-off để lead chốt, không phải lỗi. + +### F-14 [MED] Roster K3: acceptance chỉ đo BAO NHIÊU, không đo AI ⇒ chọn sai người vẫn PASS trọn bộ + +- 11/11 mapping email trong bảng K3.a **đối chiếu đúng** `DbInitializer.cs:1613-1632`, kể cả cảnh báo bẫy `long.chau@` vs `long.nguyen@` (`:1616` vs `:1629`) — phần này lane làm tốt. +- Nhưng cơ chế resolve cho ô ⚠️ "Trưởng" **không đủ an toàn**: + - Ứng viên thật trong DB: `truong.le@` "Lê Trần Đăng Trường" `:1626` · `tring.le@` "Lê Tự Đăng Trình" `:1625` · và còn `anh.nguyen@` "Nguyễn Thị Kim Ánh" `:1624` mà spec không nhắc (CCM có 6 NV `:1621-1626`, roster chọn 4). + - Fail-soft theo khuôn `:178-183` chỉ bắt email **không tồn tại**. Chọn nhầm một email **có tồn tại** thì seed 8/8 thành công ⇒ K3.e mục 2 (đếm 8) PASS, mục 4/5 PASS ⇒ sai người duyệt lọt trọn vẹn. Không mục acceptance nào của K3.e hỏi "ai". + - Test K3.d #2 `SeedKhkk_Shape_PerWorkflow` chỉ đếm hình dạng (Order1×4 + Order2×1), không assert danh tính 11 email. +- Cặp nhập nhằng thứ 2 không được gắn ⚠️: PRO "Thanh" → `thanh.lethanh@` "Lê **Thanh Bình**" `:1615`, trong khi CCM "Binh" → `binh.le@` "Lê Văn **Bình**" `:1621`. Nếu roster gốc ghi "Bình" thì cùng lớp nhập nhằng như "Trưởng". +- Thêm một lỗ của khuôn được sao chép: `:185` `var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");` **không có null-guard**. K3 cần cả `PRO`/`CCM`/`BOD`, mà fail-soft spec mô tả chỉ phủ user. +- **Đề nghị:** (1) test #2 assert đủ 11 email theo bộ ba (Code, StepOrder, LevelOrder), không chỉ đếm; (2) chốt ô ⚠️ TRƯỚC khi viết seed, không để thành hậu-kiểm; (3) fail-soft nên log ĐỦ email thiếu thay vì `return` ở email hỏng đầu tiên (một email hỏng hiện giết cả 8 workflow); (4) phủ dept vào fail-soft. + +### F-15 [LOW] Picker đổi nghĩa nhưng giữ tên cũ; `existingGroups` là collection-projection trong ctor record + +- `Features.cs:1199-1202` hiện loại PE đã có plan (`:1201-1202` — spec cite chuẩn). K2.c bỏ vế loại-trừ ⇒ DTO `ApprovedPeAwaitingPlanDto` (`:150`) không còn "AwaitingPlan" nữa; tên thành sai nghĩa cho cả BE lẫn FE. +- `existingGroups: int[]` là subquery collection tương quan đặt trong `select new ApprovedPeAwaitingPlanDto(…)` — cần kiểm dịch EF Core trước khi cam kết (rủi ro không translate / client-eval), hoặc lấy bằng 1 query phụ rồi ghép. + +## Những claim CHỊU ĐƯỢC soi (khai để lead khỏi soi lại) + +1. **QUYẾT-1 catalog authz — ĐÚNG 4/4 vế.** `CatalogsController.cs:13` `[Route("api/catalogs")]` · `:14` `[Authorize]` class-level · **12** action ghi đều `[Authorize(Roles = "Admin")]` tại `:23,31,40,53,61,70,83,91,100,113,121,130` (đúng dải `:23-:130` spec nói) · grep `Authorize(Policy = "(WorkItems|MaterialItems|UnitsOfMeasure|ServiceItems)` = **0 hit**. Comment `:9` của chính controller cũng khai cùng ý ("Authorize = đăng nhập đủ … ghi cần Admin"). Lý do (ii) của spec (picker cần READ rộng, khoá `KeHoachKyKet.Read` sẽ 403) khớp gotcha #85. +2. **Chứng cứ máy-freeze — ĐÚNG từng dòng.** `ContractSigningPlanLine.cs:17-19` comment snapshot + `:20 PeReferenceAmount` · `:23 ApprovedAmount` kèm chú "CHỈ ghi tại choke-point finalize (W3)" · `WorkflowService.cs:277 ApplyApprovedValuesOnFinalize(plan);` đứng **trước** `:278 plan.Phase = DaDuyet` · `:301-305` helper với `??=` · `:288-298` khối LUẬT, trong đó `:290` đúng nguyên văn spec trích. +3. **Liệt kê site gán Phase — ĐỦ, không sót.** Grep `Phase\s*=[^=]` toàn repo cho KHKK ra đúng 4 write-site vào `plan.Phase`: `Features.cs:373` (initializer, không có dấu chấm — spec cảnh báo đúng), `WorkflowService.cs:174`, `:278`, `:342`. Phần còn lại là đọc (`:173/:245/:341`), là `Changelog.FromPhase/ToPhase` (`:493-494`), là comment (`:39/:292`), hoặc nằm trong tests. DbInitializer 0 site — đúng như spec khai. +4. **Citation khác đều chuẩn:** `Features.cs:350` + `:460` (2 call-site `EnsureWorkflowTypeAsync`, spec ghi `:458-462` — nằm trong dải) · `:500` Include Lines · `:524` supplierIds · `:568-574` projection line · `:622-626` query record · `:711` Σ PeReference · `:1201-1202` picker · `DbInitializer.cs:174-176` idempotent per-type · `:178-183` fail-soft · `:1780-1787` Khkk_G1 + 6 leaf · `:1801` node ApprovalWorkflowsV2 · `:2970` khuôn thiếu IgnoreQueryFilters · `Layout.tsx:166-172` resolvePath 3-code + `:174 return null` (rớt im lặng #50 — đúng) · `ApprovalWorkflowsV2Page.tsx:147` slot 10, `:151-152` note khoá, `:164-171` DEFAULT_CODE_BY_TYPE · `KhkkCreatePage.tsx:8` "FE KHÔNG gửi lines" + `:56-66` lọc `isUserSelectable` · `KhkkDetailPage.tsx:425-441` bảng lines (lưu ý: thêm cột phải sửa cả `` ở `:443+`, ngoài dải cite) · `types/khkk.ts:203` · `ApprovalWorkflow.cs:158 AllowApproverFinalize` có sẵn (đúng: 0 mig cho cột này). +5. **Số canonical khớp `docs/STATUS.md:6`** (Mig **69** · **96 bảng** · **590 test** · Policies 220 · Menu keys 55) ⇒ "96→97", "69→70→71", "baseline +17" đều nhất quán. ## Verdict -(ĐANG LÀM) +**PASS_WITH_BLOCKERS** — spec cụm-1 có nền chắc (citation 23/25 chuẩn, phần chứng-cứ freeze và authz mirror miễn nhiễm soi), nhưng **không được implement nguyên trạng**. + +- **2 blocker phải sửa trước khi viết dòng code nào:** F-1 (8 menu-con K3 ngoài `All` ⇒ sidebar chết, chính acceptance K3.e sẽ FAIL) và F-2 (K1.d cùng lỗi, K1.f mù không bắt được). +- **4 phải sửa trong spec trước khi giao:** F-3 (khai việc đảo quyết owner @S155 + trích đủ `:44-46`), F-4 (việc code thứ 4: comment `:36-43` + 2 lệnh tự-kiểm `:292-293`), F-6 (mâu thuẫn `CatalogEntryId` bắt buộc ⇄ auto-gen null ⇒ rào hạng mục không tồn tại), F-13 (item 6 treo + tên Mig 71 không phủ). +- **1 phải gỡ khỏi spec:** F-5 phương án (i) — nó dựng đúng cái bẫy mà `:37-43` nói đã tránh; giữ (ii). +- Còn lại F-7…F-12, F-14, F-15 là siết chặt + sửa nhãn. +- Câu trả lời gọn cho 6 câu hỏi giao: (a) **ĐÚNG** · (b) chứng-cứ **ĐÚNG**, disposition site `:342` **SAI** · (c) **TRỘN CONCERN**, chấp nhận được kèm 3 điều kiện · (d) kết luận **ĐÚNG** nhưng chuỗi chứng **HỞ**, và "line mới bắt buộc có CatalogEntryId" **mâu thuẫn** K2.c · (e) mapping **ĐÚNG 11/11**, cơ chế resolve **KHÔNG đủ an toàn** · (f) 17/17 dòng cite **chuẩn**, nhưng **thiếu** `:712-713` Σ ProposedAmount. diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md index d95a6cf..71e311c 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l2.md @@ -1,12 +1,96 @@ # c1-l2 đủ-vs-spec Lane 2/3 — review cụm-1 (`sub-invest-fable-b2-cum1.md`) đối chiếu spec-v2 (`spec-4gd-khkk-tong-quat-31-07-2026.md`). -Lens: **ĐỦ-vs-SPEC** (nghĩa vụ nào của spec-v2 KHÔNG được cụm-1 gánh). +Lens: **ĐỦ-vs-SPEC** (nghĩa vụ nào của spec-v2 KHÔNG được cụm-1 gánh) — mọi claim đo lại trên đĩa. + +**VERDICT: FAIL (cổng tiền-implement) — 17 finding / 3 HIGH.** Nền vững (anchor ~95% chính xác, 0 trôi số canonical); thủng ở **acceptance mù**: cả 3 HIGH đều thuộc lớp *"impl SAI vẫn PASS 100% checkbox"*. Vá bằng chỉnh acceptance + bổ sweep, KHÔNG cần làm lại invest. + +--- + +## A. Bảng phủ 9 quyết định §② (Q1) + +| QĐ | Nghĩa vụ | Cụm-1 gánh ở | Kết | +|---|---|---|---| +| ②.1 OG-1 gộp | schema GỘP-capable | PRE `:79` ghi tên; việc THẬT = swap UNIQUE `:86` nhưng không dán nhãn OG-1; 0 acceptance riêng | ⚠️ 0-bit gate | +| ②.2 per-nhóm + line hạng mục | cột + UNIQUE + rào + Σ + 16-consumer | `:84-87` · `:111-112` · `:132` · `:136-153` (17 dòng) | ✅ (trừ M1, M6) | +| ②.3 8 Code + K2-chủ-cột | seed 8 + validator + "K3 chỉ đọc" | `:185-192` · `:210` · `:130` | ✅ | +| ②.4 vai 2 tầng | **"khai limitation trong doc K1"** | chỉ 2 chú-thích cột `:21`,`:40` — **0 deliverable doc** trong commit K1 `:240`, **0 acceptance** `:69-75` | ❌ **RƠI** | +| ②.5 NĐUQ tham khảo | cột text, seed null | `:23` | ✅ | +| ②.6 backfill=1 (+42-leaf→K4a) | default 1 + COUNT==Σ8 | `:83` + acceptance `:168` | ✅ | +| ②.7 admin ẩn | cụm-1 chỉ cần CHỪA subtree AwV2 | `:214` | ✅ (đúng phạm vi) | +| ②.8 nguồn + OG-6 | chặn seed-land | `:7` · `:241` · `:256` = **3 chỗ** | ✅ | +| ②.9 freeze | freeze tại finalize + né #81-EXT | K2.b `:90-107` | ⚠️ H2 + M4 | + +**PRE-gate (2 câu hỏi đích danh):** OG-6 chặn seed-land K1 = **CÓ**, 3 chỗ (`:7`/`:241`/`:256`). OG-2 verify-LIVE trước seed K3 = **CÓ**, 3 chỗ (`:176`/`:229`/`:243`) — và **khả thi thật**: `ApprovalWorkflowsV2Controller.cs:17-21` `GET api/approval-workflows-v2` `[Authorize]` trần, DTO có `IsActive` (`ApprovalWorkflowV2AdminFeatures.cs:176`) ⇒ phân biệt được LIVE/archived. + +**14 vá b1b thuộc cụm-1 (8 mục):** #2 ✅ · #3 ✅ · #4 ✅ · #5 ✅BE/❌UI (M1) · #6 ✅ · #7 ✅ · #11 ✅ · #13 ✅. + +--- ## Findings -(ĐANG LÀM) +### HIGH + +**H1 — leaf `ContractCatalog` NGOÀI `All` + "grant qua SeedAdminPermissionsAsync loop" = tự mâu thuẫn ⇒ menu KHÔNG BAO GIỜ hiện.** +Cụm-1 `:57`. Đo: `DbInitializer.cs:2058` `foreach (var menuKey in MenuKeys.All)` ⇒ key ngoài `All` không sinh Permission row. `GetMyMenuTreeQuery.cs` build cây THUẦN từ `db.Permissions` + filter `HasAccess = CanRead || child` — **không có admin-bypass**; inherit-root chỉ **4** (Contracts/Workflows/PurchaseEvaluations/PeWorkflows), **`Catalogs` KHÔNG có**. ⇒ 0 row ⇒ leaf bị filter, kể cả Admin. +Tiền lệ dẫn SAI: `Pe_*` sống được CHÍNH VÌ `PurchaseEvaluations` là inherit-root; `HopDongCung` thì 0 menu-row. Khuôn THẬT của leaf catalog: `MenuKeys.cs:168` `Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems` — **cả 5 đều TRONG `All`**. +Lý do (iv) "0 policy/menu-key mới = 0 drift STATUS row Policies" (`:47`) = **Goodhart**: giữ số doc đứng yên bằng cách làm menu chết. Acceptance K1.f `:69-75` **không có mục nào đo "leaf hiện trong sidebar"** ⇒ lỗi ship với 7/7 xanh. +Đề nghị: chọn 1 trong 2 — (a) vào `All` và KHAI drift `Menu keys` 55→56 + `Policies` 220→224 (2 row cùng lúc); (b) grant tường minh kiểu `SeedKeHoachKyKetAccessAsync`. Thêm acceptance "login Admin thấy leaf ở nhóm Danh mục" + 1 test seed-grant. + +**H2 — K3.⚠️ liệt 3 việc code, SÓT ≥3 site "ép false" ⇒ tính năng Chương-tick chết ở lần admin lưu lại quy trình đầu tiên, mà mọi acceptance K3 vẫn PASS.** +Cụm-1 `:181` liệt (a) nhánh service (b) cột runtime (c) gỡ khoá FE `:151-152`. Sót: +- `ApprovalWorkflowV2AdminFeatures.cs:380-381` — CREATE **ép false server-side**: `AllowApproverSkipToFinal/Finalize = typeEnum != ApprovalWorkflowApplicableType.ContractSigningPlan && l.…` (comment `:377-379` "ép false server-side (FE đã ép ở levelFlagsPayload — đây chặn curl bypass)"). +- `:591-594` PUT sửa-tại-chỗ: echo lệch → `ConflictException` "không được bật/tắt cờ …". +- `:605-610` dòng MỚI mang cờ → `ConflictException`. +- FE `ApprovalWorkflowsV2Page.tsx:247-258` `levelFlagsPayload(e, lockEarlyFinalize)` ⇒ `allowApproverFinalize: lockEarlyFinalize ? false : …`, bật bởi `:638 earlyFinalizeLocked = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN`. Gỡ note `:151-152` mà không gỡ 2 chỗ này ⇒ UI mở nhưng payload vẫn false. +Vì sao acceptance mù: K3.d test 2 & 5 đo trên workflow **SEED** (ghi thẳng DbContext, không qua command) ⇒ xanh. Đây đúng lớp **#81-EXT** mà chính doc viện dẫn — doc quét kỹ `Phase = ` nhưng **không quét `AllowApproverFinalize`**. +Đề nghị: bổ sweep `grep -rn "AllowApproverFinalize\|AllowApproverSkipToFinal"` vào K3, liệt đủ site vào PR + 1 test "tạo version mới qua CreateCommand cho type-10 GIỮ được cờ". + +**H3 — `CreateContractSigningPlanCommand` +`ApprovalGroup` required, nhưng `KhkkCreatePage` bị dispose "KHÔNG-ĐỔI" ⇒ tạo phiếu vỡ (hoặc feature vô hình); acceptance curl-only không bắt.** +Cụm-1 `:111` (+`ApprovalGroup int`, validator `InclusiveBetween(1,8)`) ⟂ `:151` K2.f row 16-17 `KhkkCreatePage.tsx:8` = "KHÔNG-ĐỔI đợt này" ⟂ `:112` lại đòi sửa `KhkkCreatePage` ×2 app. +Đo: `fe-admin/src/pages/khkk/KhkkCreatePage.tsx:72-77` body = `{ peId: purchaseEvaluationId, approvalWorkflowId, … }` → `api.post('/contract-signing-plans', body)` — **không có `approvalGroup`**. Header `:3` khai "File MIRROR SHA256 identical với fe-admin" ⇒ phải sửa cả 2 app. Không sửa: `approvalGroup=0` → 400 mọi lần tạo. Nếu impl né bằng default=1: mọi phiếu FE tạo ra đều nhóm 1 ⇒ feature vô hình. +K2.h `:169` đo bằng **curl** ⇒ bỏ qua FE hoàn toàn. Đề nghị: 1 dòng acceptance "tạo phiếu nhóm 2 **từ UI** ×2 app". + +### MEDIUM + +**M1 — nửa-UI của OG-7 có trong văn, 0 acceptance / 0 test / 0 dòng commit.** `:112` mô-tả nút "Tạo thêm cho nhóm khác" + `existingGroups: int[]`; K2.g `:157-163` không có test; K2.h `:167-172` không có mục; commit-K2 `:242` chỉ ghi "DTO + FE types". Impl SAI vẫn PASS: BE mở rào, UI giữ 409 cũ ⇒ đúng thứ owner nhìn thấy thì vắng. + +**M2 — nhãn đo-lường SAI: "hit `IgnoreQueryFilters` đầu tiên trong src/Backend — S155 đo 0-hit"** (`:38`). Đo lại: **29 hit** trong `src/Backend` (trừ Migrations) — 18 `ApprovalWorkflowV2AdminFeatures.cs` · 4 `PeSoftDeleteFeatures.cs` · **3 `ContractSigningPlanFeatures.cs`** (chính module đang spec) · 3 `ContractFeatures.cs` · 1 `PurchaseEvaluationsController.cs`. Số ĐÚNG & chịu lực = **0 hit trong `DbInitializer.cs`** (đo: 0). Đơn thuốc vẫn ĐÚNG (chứng `DbInitializer.cs:2970` `db.Projects.Select(p => p.Code)` thiếu `IgnoreQueryFilters` — verified). Sửa nhãn hoặc bỏ nhãn. + +**M3 — K3.⚠️(a) mở lại quyết định owner đã ĐÓNG bằng chữ "PHÁ VỠ" @S155, doc không nhắc một chữ.** 5 nơi ghi: `ContractSigningPlanWorkflowService.cs:44-46` · `ApprovalWorkflowV2AdminFeatures.cs:377` · `fe-{admin,user}/src/pages/khkk/KhkkWorkflowPanel.tsx:8` · `docs/HANDOFF.md:77` (vẫn nằm ở mục "🔸 Không chặn W1-W2" = CHƯA chốt). Spec v2 §④ OG-3 có câu owner @S164 nghe như tái-cho-phép và lead đã dán "🔄 SỬA @B2c1", nhưng câu đó nói về **NĐUQ/ngưỡng CEO**; đảo một phán quyết mang chữ "PHÁ VỠ" phải hỏi owner TƯỜNG MINH. → trả lời Q4. + +**M4 — freeze tuyên "xong" ở cụm-1 nhưng lỗ duy nhất bịt bằng việc cụm-2.** `:107` tự khai: "xoá mềm line sau DaDuyet làm Σ `:711` trôi ÂM THẦM — chặn bằng phase-guard", mà phase-guard nằm ở **K4b (cụm-2)**. Đo: `Features.cs:709-711` Σ tính LIVE từ `ContractSigningPlanLines` ⇒ soft-delete 1 line đổi tổng phiếu ĐÃ DUYỆT. K2.g test 5 chỉ đo `ApprovedAmount != null`. + +**M5 — neo chéo gãy: "Mig 71 item 6" không tồn tại.** `:181` ("gắn vào Mig 71 **item 6 (K2.a)**") + `:242` ("Mig 71 gồm cả item 6") ⟂ K2.a `:83-88` chỉ có **item 1-5**. Impl đọc K2.a rời → Mig 71 thiếu `EndedByLevelFinalize` → K3 kẹt hoặc phải đẻ Mig 72 trái QUYẾT `:242`. + +**M6 — `CatalogEntryId` bắt-buộc hay không: 3 câu không thể cùng đúng.** `:87` "line MỚI/SỬA **bắt buộc** có `CatalogEntryId`" ⟂ `:113` auto-gen create "mồi `CatalogEntryId = null`" (mọi phiếu tạo sau Mig 71 đều sinh line null) ⟂ `:126` "null → CHO QUA". Cách đoán quyết định bẫy NULL-unique (`:87`) có nổ hay không. + +### LOW + +- **L1** — `:105` gắn cờ 🔴 "PHẢI XỬ" cho `WorkflowService.cs:342` do đọc nhầm chú thích `:39`. Đo: `ContractSigningPlanWorkflowService.cs:36-42` đã KHAI RÕ service có **ĐÚNG MỘT** nhánh set DaDuyet, và `:39` là trích **nguồn-copy** `ContractWorkflowService.cs:190-193` như thứ CỐ Ý không chép. `:342` nằm trong `ReturnOrRejectAsync` (`:310-318`, `targetPhase ∈ {TraLai,TuChoi}`; guard `:320-323` đòi `Phase == ChoDuyet`). Phương án (i) "gọi `ApplyApprovedAmounts` ngay trước `:342`" = thuốc SAI. Phương án (ii) test-khoá-hành-vi hợp lệ, bổ cho `ContractSigningPlanApprovalTests.cs:523-533` (T3 hiện chỉ khoá write-path Admin-approve). +- **L2** — `:11` đặt file `Domain/Master/ContractCatalogEntry.cs` "(cạnh WorkItem)"; `WorkItem.cs` thật ở `src/Backend/SolutionErp.Domain/Master/Catalogs/WorkItem.cs`. 2 vế không cùng đúng. +- **L3** — GroupCode dòng CEO: `:19` liệt giá trị `CEO` ⟂ `:40` bảo lấy heading "(A1..C3, **5.2**)" ⇒ ra `"5.2"`. Heading nguồn: `danh-muc-sp002-transcribe.md:131` "## 5.2 — CEO". +- **L4** — `ContractSigningPlanFeatures.cs:32` tự khai luật "positional record, field append-CUỐI (đừng chèn giữa — vỡ FE mapping)"; K2.e `:131` liệt 3 DTO cần +field nhưng không nói vị trí. +- **L5** — `AddColumn … defaultValue: 1` (`:83`) để lại DEFAULT constraint vĩnh viễn ⇒ insert quên set `ApprovalGroup` IM LẶNG thành nhóm 1 thay vì fail (cùng gốc M6/H3). +- **L6** — đếm QUYẾT: lead hỏi "3 QUYẾT của lane", đo được **5** (`:44`+`:47` policy · `:84` loose-Guid · `:90`+`:96` freeze-reuse · `:210` convention-parse · `:242` gộp-Mig-71). +- **L7** — `:86` trỏ "vùng `:24-26`" để lấy tên index EF-sinh; index thật ở `ContractSigningPlanLineConfiguration.cs:22-24`. +- **L8** — acceptance `:167` (`SUM(PeReferenceAmount)` trước == sau migrate) gần 0-bit: Mig 71 không chạm cột tiền ⇒ không impl-sai nào làm nó TRƯỢT. Răng thật ở test `:163` (mồi 0 khi tách dòng) — nên nêu vế đó. + +--- + +## Kháng được soi (đo lại, ĐÚNG) + +- **P1** — bảng #81-EXT `:100-105`: `grep -rnE "Phase\s*=[^=]"` → đúng 4 site (`Features.cs:373` · `WorkflowService.cs:174/:278/:342`); "DbInitializer 0 site KHKK" đo được **0**. Cảnh báo "đừng grep `\.Phase`" xác nhận đúng. +- **P2** — 13/13 email+tên K3.a khớp `DbInitializer.cs:1613-1632` từng dòng; bẫy `long.nguyen@` (HRA `:1629`) có thật; ô ⚠️ Trưởng/Trình có thật (`:1625` Lê Tự Đăng Trình ⟂ `:1626` Lê Trần Đăng Trường). +- **P3** — "KHKK không đọc bảng NS live": đo lại chỉ **1 hit source** (`Features.cs:162` nhãn DTO) ⇒ lý do bỏ 11 cột PE Mig 67 đứng vững. +- **P4** — khuôn authz catalog: `CatalogsController.cs:13-14` + đúng **12** `[Authorize(Roles = "Admin")]` + policy-per-action **0 hit**. Số khớp tuyệt đối. +- **P5** — 86 & phân bố 5/10/5/23/14/15/13/1 tự-kiểm từ transcribe: A1=5 · A2 1+A3 9=10 · A4=5 · B1=23 · B2=14 · B3=15 · C1 6+C2 3+C3 4=13 · CEO=1 ⇒ Σ=86 ✓; EN có mặt **86/86**; 8 cột map đủ (NduqNote seed null, khớp ⚠️3). +- **P6** — 0 trôi số canonical: STATUS `Tests 590` (`:472`) · `SQL tables 96` (`:466`) · `Migrations 69` (`:465`) ⇒ "Mig 70/71" · "96→97" · "baseline ~590 +17" đều đúng. +- **P7** — OG-2 verify-LIVE khả thi đúng như mô tả (route + `IsActive` đều có). +- **P8** — anchor `Features.cs` :34 · :342-347 · :350 · :389 · :458-462 · :570-574 · :622-625 · :711 · :1201-1202 và `Line.cs:15/:17-20/:23/:25` · `LineConfig:22-26/:29` · `Layout.tsx:166-172` — **đo lại khớp hết**. ## Verdict -(ĐANG LÀM) +**FAIL** (cổng tiền-implement) — 17 finding: **3 HIGH · 6 MEDIUM · 8 LOW**, 8 điểm kháng-soi. +Chặn: H1 (menu chết), H2 (feature owner-yêu-cầu chết im lặng), H3 (tạo phiếu vỡ). Cả 3 cùng một lớp: **acceptance không có phép đo nào TRƯỢT khi impl sai**. +Cần owner gật: **M3** (đảo phán quyết "PHÁ VỠ" @S155). diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md index e73c41a..9545fd1 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l3.md @@ -1,13 +1,108 @@ # c1-l3 đo-được + thứ-tự -> LANE 3/3 review cụm-1 (spec B2-r). Lens: **ĐO-ĐƯỢC** (test-case có assert? checklist có lệnh+số?) + **THỨ-TỰ** (commit/mig/restart/deploy/seed/phụ-thuộc-chéo). -> Target chính: `sub-invest-fable-b2-cum1.md` (33.048B) · liếc `spec-4gd-khkk-tong-quat-31-07-2026.md` §③. -> ⚠️ Path lệch trong spec: TASK yêu cầu `sub-reviewer-c1-l3.md`, footer RUN-TRACE ghi `sub-reviewer-2.md` → theo TASK (fix-mới thắng footer-cũ, S139); khai trong report. +> LANE 3/3 review cụm-1 (spec B2-r). Lens: **ĐO-ĐƯỢC** (test có assert? checklist có lệnh+số?) + **THỨ-TỰ** (commit/mig/restart/deploy/seed/phụ-thuộc-chéo). +> Target: `sub-invest-fable-b2-cum1.md` (33.048B) · `spec-4gd-khkk-tong-quat-31-07-2026.md` §③/§④. +> ⚠️ Path lệch trong spec giao việc: TASK yêu cầu `sub-reviewer-c1-l3.md`, footer RUN-TRACE ghi `sub-reviewer-2.md` → theo TASK (fix-mới thắng footer-cũ, S139), khai ở đây. +> **Đo bằng máy trong lượt này:** `sqlcmd (localdb)\MSSQLLocalDB SolutionErp_Dev` (plans=0 · lines=0 · sys.tables=96) · `sha256sum` 3 cặp FE · 9 grep code-tree · 2 đếm trên transcribe. ## Findings -(ĐANG LÀM) +**Tổng: 15 (5 HIGH · 6 MED · 4 LOW) + 5 điểm chịu được soi.** + +### HIGH + +**H1 · [Q3 thứ-tự] Mig 71 "item 6" (`EndedByLevelFinalize`) KHÔNG có trong mục schema K2.a, và K2 không có phép kiểm nào phát hiện được nó vắng mặt.** +- Evidence: `sub-invest-fable-b2-cum1.md:83-88` (K2.a liệt **5** item cho Mig 71, không có item 6) ⟂ `:181` (K3 khai "gắn vào **Mig 71 item 6** (K2.a)") ⟂ `:242` (thứ-tự commit khai "Mig 71 gồm cả item 6"). K2.h `:167-172` = 0 dòng về cột này; K2 test k=7 `:157-163` = 0 test. +- Hệ quả đo được: implementer dựng Mig 71 **theo đúng K2.a** → thiếu cột → **PASS 6/6 dòng K2.h + 7/7 test K2** → K3 (tự khai "0-mig") kẹt tại commit-time. +- Cột mồ côi (câu hỏi Q3): `:242` chỉ nói "nếu lead tách thì K3 tự mang Mig 72", KHÔNG nói gì nếu K3 **đổi/bỏ** thiết kế duyệt-sớm ⇒ còn lại 1 cột `NOT NULL DEFAULT 0` **0 người đọc** = đúng lớp ghost-wire "defined-but-unread". +- Đề nghị: chuyển item 6 vào chính K2.a (đánh số 6, có Down), thêm 1 dòng K2.h `sqlcmd … SELECT COUNT(*) FROM sys.columns WHERE object_id=OBJECT_ID('ContractSigningPlans') AND name='EndedByLevelFinalize'` → **1**, và 1 câu rollback: "K3 bỏ duyệt-sớm ⇒ Mig 72 DropColumn cùng đợt". + +**H2 · [Q3 seed/deploy] Khuôn seeder mà K3.a chỉ định là khuôn ĐANG BỊ CỔNG DEMO — bê nguyên thì prod 0 workflow, mà mọi dòng acceptance vẫn xanh trên Dev.** +- Evidence: `sub-invest-fable-b2-cum1.md:183` chọn khuôn `DbInitializer.cs:171-217` = `SeedSampleApprovalWorkflowsV2Async`; hàm đó được gọi tại `DbInitializer.cs:139` **bên trong `if (!demoSeedDisabled)`** (`:135-140`). Prod chạy `DemoSeed:Disabled=true` (comment `:111-118`, bài S91). +- Spec cân nhắc gate **chỉ cho K1** ("Gate: UNGATED", `:35`) và **im lặng cho K3**; ngược lại `:244` khẳng định "prod IIS restart → … seed tự chạy" — sai với seeder demo-gated. +- Acceptance không bắt được: `:230` `sqlcmd … → đúng 8` không nêu DB nào (K1.f `:70` nêu rõ localdb Dev ⇒ mặc định đọc là Dev, nơi `demoSeedDisabled=false`). Test K3-1 gọi seeder **trực tiếp** ⇒ cũng không đi qua cổng. +- Đề nghị: K3.a ghi thẳng "UNGATED, gọi NGOÀI khối `if (!demoSeedDisabled)` (cạnh `SeedRealMasterDataAsync`)" + 1 dòng acceptance đo **trên prod** (`curl /api/approval-workflows-v2?applicableType=10` đếm 8) chứ không chỉ localdb. + +**H3 · [Q4 trái khuôn + Q2 checklist mù] Leaf menu đặt NGOÀI `MenuKeys.All` thì `SeedAdminPermissionsAsync` KHÔNG cấp được — leaf tàng hình với MỌI user kể cả Admin; K1 không có dòng nào bắt.** +- Evidence mâu thuẫn nội tại: `:57` vừa khai "`MenuKeys.ContractCatalog` (**NGOÀI `All`**)" vừa khai "grant admin (**SeedAdminPermissionsAsync loop**)". Loop đó là `DbInitializer.cs:2058` `foreach (var menuKey in MenuKeys.All)` ⇒ key ngoài `All` **không bao giờ** được cấp. +- Không có đường cứu: `GetMyMenuTreeQuery.cs:96-99` lọc `CanRead` từ row Permission thật, **không có Admin-bypass trong file**; `Catalogs` KHÔNG phải inherit-root (`:70-73` chỉ Contracts/Workflows/PurchaseEvaluations/PeWorkflows — gotcha #35). +- Tiền lệ bị bê nhầm: `Pe_*` ngoài `All` vì là leaf **sinh động qua factory** (`MenuKeys.cs:142-158`, comment `:150`); còn 4 leaf anh em CÙNG group `Catalogs` thì **đều nằm trong `All`** (`MenuKeys.cs:168`) ⇒ đây là rời khuôn chứ không phải theo khuôn. +- Đối chứng cho thấy đây là lỗi CHECKLIST chứ không phải mơ hồ thiết kế: K3.c `:214` mắc **y hệt** (8 `AwV2_Khkk*` ngoài `All`, trong khi 2 leaf anh em `ApprovalWorkflowDuyetNccV2`/`…PhuongAnV2` nằm trong `All` — `MenuKeys.cs:182`) **nhưng K3.e `:231` có dòng "sidebar đếm được 8 menu-con"** nên bắt được; K1.f không có dòng tương ứng nên lọt. +- Đề nghị: hoặc đưa key vào `All` (đúng khuôn 4 leaf anh em, chấp nhận +4 policy ⇒ sync 2 row STATUS), hoặc giữ ngoài `All` **kèm** grant tường minh (site riêng như `SeedProcurementMasterAccessAsync`); và thêm dòng K1.f: "đăng nhập admin → sidebar Danh mục **đếm được 5 leaf**". + +**H4 · [Q2 acceptance mù] 3 lớp bảo vệ cho "backfill=1 + Σ tiền bất biến" cùng mù trên đúng cái DB mà checklist nêu tên.** +- Đo tươi (lượt này): `SolutionErp_Dev` → `ContractSigningPlans` = **0 row**, `ContractSigningPlanLines` = **0 row** (`sys.tables`=96). +- (a) `:167` `SUM(PeReferenceAmount)` trước == sau → `NULL == NULL`, đúng với **mọi** migration kể cả migration làm mất số. +- (b) `:168` `COUNT == Σ 8 nhóm` → `0 == 0`, PASS kể cả khi quên `defaultValue: 1` (EF sinh `defaultValue: 0` ⇒ phiếu thật rơi ra ngoài 8 folder). +- (c) test `Mig71_BackfillsApprovalGroup1_…` (`:157`) **không chạy được như tên gọi**: harness là `SqliteDbFixture` + `EnsureCreated()` dựng từ model (`tests/…/Common/SqliteDbFixture.cs:36-45`), và `grep -rl "Migrat" tests/` chỉ ra **DLL**, 0 file .cs ⇒ **không có migration nào từng chạy trong test suite**. Còn lại nó chỉ assert được default CLR = trùng ngôn (tautology). +- Chuẩn chống-mù đã là chuẩn NHÀ: `ProcurementMasterAccessSeedTests.cs:29-30` ghi rõ "Kèm CONTRAST assert **chống vacuous-pass**". +- Đề nghị: mỗi so-sánh-bằng phải có **control-dương** đứng trước (`SELECT COUNT(*) … > 0` mới có nghĩa), nêu rõ ĐO TRÊN DB NÀO (prod là nơi duy nhất có phiếu thật), và đổi tên/nội dung test (1) + (7) thành model-level hoặc dời sang bước tay có ghi số. + +**H5 · [Q1 test không assert + Q3 đo thiếu] Yêu cầu "🔴 PHẢI XỬ site `:342`" chưa chạy phép đo; theo số đo thì phương án (i) là code chết.** +- Đo: `ReturnOrRejectAsync` có **đúng 2 call-site**, cả hai truyền **hằng literal** — `ContractSigningPlanWorkflowService.cs:128` (`TraLai`) và `:133` (`TuChoi`) — trong `switch` đóng kín `:117` với `default` ném lỗi `:138-139` ⇒ `targetPhase` **không thể** mang giá trị `DaDuyet`. +- Chính file đã khai sẵn: `:36-42` — *"service này có **ĐÚNG MỘT** nhánh set DaDuyet … Admin muốn kết thúc phiếu vẫn phải đi đúng đường `approve`"*. Spec `:105` trích `:39` như bằng chứng NGUY HIỂM nhưng bỏ ngỏ *"nếu nhánh này reachable"* — phép đo tốn đúng 1 lệnh grep. +- Hệ quả: K2 test 6 (`:162`) khai assert *"theo phương án (i)/(ii) đã chọn"* ⇒ **1/17 test không có assert cố định** (chọn lúc code = không phải tiêu chí nghiệm thu). Nếu implementer chọn (i) thì thêm 1 nhánh gọi `ApplyApprovedAmounts` **không bao giờ chạy tới** — vừa là code chết vừa tạo thêm một site trông như đường hợp lệ tới `DaDuyet`. +- Đề nghị: chốt (ii); assert = "gọi transition với mọi action hợp lệ, không action nào đưa Phase về `DaDuyet` ngoài `approve`"; và thay lệnh sweep bằng đúng 2 lệnh mà code tự kê ở `:292-293`. + +### MED + +**M1 · [Q2 thước hỏng] Lệnh sweep `grep 'Phase\s*=[^=]'` bị giới hạn `--include="ContractSigningPlan*"` — không thể chứng minh chính cái miễn-trừ mà K2.b viện dẫn.** +- Evidence: `:171` (K2.h) "Grep `Phase\s*=[^=]` **toàn `ContractSigningPlan*`**" ⟂ `:106` (K2.b) khẳng định "**seeder/DbInitializer** hiện 0 site gán phase KHKK". File `DbInitializer.cs` không bao giờ khớp glob `ContractSigningPlan*` ⇒ lệnh này mù với đúng lớp file được miễn trừ. +- (Tôi đã đo hộ: `grep -rn "ContractSigningPlanPhase.DaDuyet" src/Backend` ngoài service = **0 hit**; `grep -rn "ContractSigningPlan" DbInitializer.cs` = **0 hit** ⇒ *nội dung* claim ĐÚNG, chỉ *lệnh* không chứng được.) +- Thêm nhiễu: lệnh trả 12 dòng, trong đó 4 dòng không phải assignment (`Features.cs:623` param default · `:173/:245/:341` `var fromPhase = plan.Phase` · `:493-494` DTO changelog); checklist không nêu **số kỳ vọng** ⇒ "liệt đủ site" nghiệm thu được bằng bất kỳ danh sách nào. +- Đề nghị: dùng 2 lệnh repo-wide ở `:292-293` + ghi **số kỳ vọng** (hiện tại: 1 site `DaDuyet`, 1 site helper). + +**M2 · [Q4/Q1] Convention `^KHKK-N([1-8])$` bị thoát bằng chính giá trị mặc định UI đang mời.** +- Evidence: `:210` chốt parse suffix Code; nhưng `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:167-170` `DEFAULT_CODE_BY_TYPE[TYPE_INT_CONTRACT_SIGNING_PLAN] = 'QT-KHKK-V2-001'` ⇒ quy trình admin tự tạo mặc định **không khớp pattern** → validator "pass + LogWarning". +- K3 test 4 (`:224`) assert chính lối thoát đó là CỐ Ý (đúng), nhưng DoD `spec §③-K3` (`spec…:26`) hứa "phiếu nhóm 3 pin workflow nhóm 5 → 409" như một bất-biến ⇒ **hứa rộng hơn thứ enforce được**. +- Đề nghị: viết vào DoD "chỉ enforce cho Code seeded `KHKK-N*`; quy trình admin tự đặt tên = ngoài rào (có warn)", hoặc để designer type-10 prefill/ép `KHKK-N{n}` khi đến từ deep-link nhóm. + +**M3 · [Q3 seed một-chiều] Seed idempotent kiểu "có Code thì bỏ qua" ⇒ hình dạng sai lần đầu bị ĐÓNG BĂNG, mà acceptance chỉ đo SỐ LƯỢNG.** +- Evidence: `:185` `if (Any(w => w.Code == code)) continue;` (không có đường cập nhật) ⟂ ô ⚠️ chưa chốt "Trưởng/Trường/Trình" `:203` — đúng cái field dễ sai nhất. Acceptance `:230` đo `COUNT = 8`; test shape (`:222`) chỉ chạy trên DB test sạch, không soi prod. +- Đề nghị: giữ PRE-gate verify-LIVE (đang có, tốt) + ghi **đường sửa** (script SQL 1 lần hoặc seeder-corrective có điều kiện) + 1 dòng acceptance đối chiếu **email từng cấp trên prod** sau seed, không chỉ đếm 8. + +**M4 · [Q2 thiếu trục] K1 và K2 đều ship FE nhưng 0 dòng acceptance FE; bất-biến mirror đang SỐNG bị bỏ trống.** +- Evidence: K1.d `:56-57` (KIND_CONFIG ×2 app + menuKeys ×2) · K2.f `:149-150` (6 file FE ×2 app) ⟂ K1.f `:69-75` và K2.h `:167-172` = **0 dòng** `npm run build`, 0 dòng so mirror. Dòng chung `:255` còn miễn byte-verify cho "wave BE-only" — K1/K2 **không** BE-only. +- Đo tươi: 3 cặp file KHKK hiện **SHA256 giống hệt** giữa 2 app (`KhkkCreatePage.tsx` ea51e7626eb1 · `KhkkDetailPage.tsx` c8ab2eadd3fd · `types/khkk.ts` 8b757f5b1422) và file tự khai bất-biến này (`fe-user/src/pages/khkk/KhkkCreatePage.tsx:3` "File MIRROR SHA256 identical với fe-admin") ⇒ K2 có thể phá âm thầm. +- Đề nghị: mỗi commit chạm FE thêm 2 dòng: `npm run build` ×2 PASS + `sha256` 3 cặp KHKK **bằng nhau** (khuôn K5 `spec…:30` đã có sẵn tiêu chí này). + +**M5 · [Q3 phụ-thuộc chéo NGƯỢC] Cái chặn va-chạm NULL của UNIQUE mới nằm ở cụm-2, và không tái hiện được trên harness.** +- Evidence: `:87` nêu bẫy "NULL == NULL trong UNIQUE của SQL Server" và giao việc chặn cho "validator: line MỚI/SỬA bắt buộc có `CatalogEntryId`" — nhưng validator duy nhất K2 đặc tả (`:118-126`) chỉ kiểm **nhóm**; editor dòng là **K4b (cụm-2)** theo chính `:113`. 0 test + 0 dòng checklist K2 cho "line mới thiếu CatalogEntryId → 400". +- Thêm: SQLite coi các NULL là **phân biệt** trong unique index ⇒ kể cả có test cũng xanh-trên-harness/đỏ-trên-prod. +- Mục "Phụ thuộc chéo" `:246` liệt chiều K2→K1, K3→K2 nhưng **thiếu chiều cụm-2 nợ cụm-1** này. +- Đề nghị: đưa rule "CatalogEntryId bắt buộc cho line mới/sửa" vào K2 (validator + 1 test), hoặc khai limitation kèm đo tay trên SQL Server. + +**M6 · [Q1 đếm không ra phủ] K2 test 5 trùng test đã có ⇒ "+17" đạt được mà hành vi mới vẫn không ai đo.** +- Evidence: `:161` `Finalize_FreezesAllLines_NoNullApprovedAmount` ⟂ test đang chạy `tests/…/Application/ContractSigningPlanApprovalTests.cs:475` `Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan` — bản cũ **mạnh hơn**: assert 0-null (`:505-506`), cách ly liên-phiếu (`:518`, `:528`), và giữ số sửa tay `??=` (`:513` 333→300). +- Đề nghị: bỏ test 5 khỏi k (k=6) hoặc đổi nội dung sang hành vi MỚI (freeze + `EndedByLevelFinalize` + line có `CatalogEntryId`), kẻo con số "+17" mua được bằng trùng lặp. + +### LOW + +**L1** `:70` acceptance `COUNT(*) WHERE IsDeleted=0` = **86** mâu thuẫn với kịch bản test 2 trên DB đã dùng: chỉ cần 1 lần xoá mềm hợp lệ là số đọc ra 85 và checklist báo FAIL giả. → nêu tiền đề "DB chưa xoá dòng nào", hoặc đo `COUNT(*)` gồm cả xoá mềm. + +**L2** `:74` "tên VN+EN đủ **86/86**" không kèm lệnh → thêm `SELECT COUNT(*) … WHERE TenEn IS NULL OR LTRIM(RTRIM(TenEn))=''` → **0**. + +**L3** Cửa sổ "trang rỗng" giữa commit-1 (`:240`, ship FE kind + leaf menu) và commit-2 (`:241`, seed chờ OG-6): user prod thấy trang danh mục trống trong khoảng thời gian không xác định. Spec chỉ nói "restart local verify bảng rỗng OK". → hoặc ẩn leaf tới khi seed land, hoặc khai chấp nhận. + +**L4** 3-file rule: `:69` "git status show 3" yếu hơn bài S161-W1 (2/3 file mig **untracked** trong khi snapshot tracked) và `:250` `git show --stat` chỉ soi được **sau** commit. → thêm cổng ngay trước commit: `git status --porcelain -- src tests | grep '^??'` phải **RỖNG**. + +### Chịu được soi (đo rồi, giữ nguyên) + +- **P1** Phân bố 86 dòng `5/10/5/23/14/15/13/1` (`:41`, `:70`, `:251`) **khớp transcribe**: đếm được 85 mã `[A-C][1-4]-\d\d` + 1 `CEO-01`; heading khai A1=5 · A2=1 + A3=9 → N2=10 · A4=5 · B1=23 · B2=14 · B3=15 · C1=6+C2=3+C3=4 → N7=13 · CEO=1. Cộng = **86**, khớp cả 2 chiều. +- **P2** `96 → 97 bảng` (`:31`, `:250`) đúng: `sys.tables` Dev đo được **96**; baseline test **590** khớp `docs/STATUS.md:6` ⇒ "+17 = 607" là con số có gốc. +- **P3** Bước verify-LIVE đội hình (`:229`) **đo được thật**: `GET /api/approval-workflows-v2` trả `AwLevelDto` gồm `ApproverUserId` + FullName + `ApproverEmail` (`ApprovalWorkflowV2AdminFeatures.cs:25-31`, projection `:189-195`) ⇒ đối chiếu 11 email là khả thi bằng curl. +- **P4** `AllowApproverFinalize` có thật ở cấp Level (`ApprovalWorkflow.cs:158` + `ApprovalWorkflowConfiguration.cs:100` default false) ⇒ khẳng định "0 mig cho cờ này" (`:180`) đứng vững. +- **P5** 5 test có assert 2 chiều/biên rõ ràng, xoá đi là mất phủ thật: K1-2 (`:62` no-resurrect), K1-4 (`:64` biên 0/9 fail + 1/8 pass), K3-2 (`:222` shape từng cấp), K3-4 (`:224` 3 chiều), K3-5 (`:225` multi-assert đường duyệt-sớm). + +### Trả lời gọn 4 câu hỏi + +- **Q1 (17 test):** 12/17 có assert cụ thể và sẽ ĐỎ nếu impl sai. Hỏng: K2-1 (không chạy được — H4c), K2-6 (không có assert cố định — H5), K2-7 nửa vế "AcrossMig" (H4c), K2-5 trùng test cũ (M6), K3-1 không đi qua cổng demo nên không bắt được H2. Thiếu hẳn test cho: cột `EndedByLevelFinalize` (H1), "line mới thiếu CatalogEntryId" (M5), "leaf menu render được" (H3). +- **Q2 (checklist):** phần lớn dòng có {lệnh · số}, nhưng **3 dòng PASS được bằng impl SAI**: K2.h Σ-tiền + COUNT-nhóm (H4a/b, đo 0-row), K1.f không có dòng nào bắt leaf tàng hình (H3), K2.h grep sai phạm vi (M1). Thiếu trục FE hoàn toàn ở K1/K2 (M4). +- **Q3 (thứ tự):** khung K1→K2→K3 hợp lý và cửa sổ config-lie #78 **KHÔNG mở ở mức commit** (seed + nhánh (a) + gỡ khoá FE cùng commit 4 — đúng lệnh `spec…§④ OG-3`). Lệch nằm ở: item-6 lọt khỏi K2.a (H1), khuôn seeder demo-gated (H2), backfill=1 đi bằng `defaultValue` trong Mig (đúng khuôn, nhưng không có phép kiểm còn răng — H4), 3-file rule đo sau commit (L4), và phụ thuộc chéo thiếu chiều cụm-2→cụm-1 (M5) + thiếu điều khoản cột-mồ-côi (H1). +- **Q4 (chế thêm trái khuôn):** 1 chỗ rời khuôn thật = leaf ngoài `MenuKeys.All` trong khi 4 leaf anh em cùng group đều ở trong (H3, lặp lại ở K3.c). 1 chỗ tạo quy ước mới = parse Code lấy nghĩa nghiệp vụ (M2) — chấp nhận được nếu khai giới hạn. Các quyết còn lại (mirror `CatalogsController` `[Authorize]` lớp + `[Authorize(Roles="Admin")]` per-write — đo khớp `CatalogsController.cs:13-14` + 12 action `:23…:130`; loose-Guid không FK; tái dùng `ApprovedAmount` thay chép 11 cột PE Mig 67) đều **bám khuôn**, không chế thêm. ## Verdict -(ĐANG LÀM) +**PASS-WITH-BLOCKERS** — nền spec chắc (khuôn đúng, 3 con số gốc kiểm được, 12/17 test có răng), nhưng **KHÔNG land như hiện trạng**: H1 + H2 + H3 mỗi cái đều để một impl SAI đi trọn checklist mà vẫn xanh, H4 làm 3 lớp bảo vệ tiền/backfill cùng mù, H5 mời implementer viết code chết. Chặn tối thiểu: vá **H1, H2, H3** trước commit K1; vá **H4, H5** trước commit K2/K3. + +END sub-reviewer-c1-l3 — TOTAL: 15 finding (5 HIGH · 6 MED · 4 LOW) + 5 điểm giữ.