diff --git a/.claude/WAL.md b/.claude/WAL.md index cc96b9bd..9810d4ec 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -12,11 +12,11 @@ chain: A+B · máy per-loại OK (lẫn = 3 lỗ UI) · Thứ-5 đã có ~95% (thiếu 1 nhánh resolvePath fe-admin #50) · 5 wave. LEAD VERIFY 4/4 claim + SPEC GHI: runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md -[!] B2 /fable-clone reviewer — 3 lane opus (lens correctness · khuôn+scope · acceptance). - wf: wf_d9a14945-190 · artifact sub-review-b2-lens{1,2,3}.md · xong thì lead synthesize - → sửa spec → B3 -[ ] B3 /fable-real ×2 style: front-end-reviewer-style + back-end-reviewer-style → spec-list - style đồng nhất khuôn KHKK/DuyetNCC +[x] B2 ensemble 3-lane: FAIL-11 + FIXES-12 + FAIL-11 → 2 BLOCKER tiền-đề v1 (bỏ-type=3 · + pendingMe-không-tồn-tại) + W5 sai khuôn kép (KHKK cấm approver-delete; anchor code chết + S175) + Q1-Q5. SPEC v2 land + b2-review-synthesis.md. Root-cause A+B HELD 3/3. +[!] B3 /fable-real ×2 style: fe + be → spec-list style đồng nhất. wf: wf_f71dc3e3-3ce · + 2 lane tier=fable · artifact sub-style-{fe,be}-b3.md [ ] B4 /fable-clone ×2 style FE/BE — review lại 2 spec style [ ] B5 /fable-real reviewer — tổng hợp cuối → plan wave cụ thể "SOLTUON-12-08-2026" [ ] B6 hmw fan-out Opus 5 MAX execute toàn plan (per-wave gate + cicd 2 app + prod verify) diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/b2-review-synthesis.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/b2-review-synthesis.md new file mode 100644 index 00000000..d7607f9e --- /dev/null +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/b2-review-synthesis.md @@ -0,0 +1,42 @@ +# B2 synthesis — ensemble reviewer 3-lane (wf_d9a14945-190) → spec v2 + +> Lead refute+synthesize @S190. Lens-1 FAIL-11 (return sạch) · lens-2 PASS_WITH_FIXES-12 (#53, +> artifact cứu trọn) · lens-3 FAIL-11 (return sạch). 34 finding thô → hội tụ 8 cụm, 0 mâu thuẫn +> giữa lane; 2 BLOCKER được ≥2 lane độc lập cùng bắt bằng 2 phép đo khác nhau = chắc. + +## Cụm hội tụ → hành động (đã vào spec v2) + +1. **"Bỏ type=3" = tự chế + gây trang trắng** (L1-F1 · L2-F1 · L3-F1): trang CHỈ đọc `?type` + (`WorkflowMatrixViewPage.tsx:57,61` fallback `:1`); "alias-path KHKK" thật ra GIỮ type= trên URL + (`Layout.tsx:216`) — khuôn chứng điều NGƯỢC. → v2 GIỮ `?type=3&ct=N`; delta = trang đọc `ct` + + accordion ưu tiên `ct` + allowlist pathname (diệt luôn `Hdc_*` cross-talk L1-F5). +2. **`pendingMe` không tồn tại BE Contract** (L1-F2 · L2-F2 · L3-F2): 0-hit + control-dương KHKK + 4-hit; ASP.NET nuốt param lạ im lặng. Khuôn PE THẬT = đổi-endpoint. → v2 W3: `pendingMe=1` ⇒ FE + gọi `/contracts/inbox` + lọc type client (khai lệch-W7 chấp nhận); acceptance so-số 2 chiều. +3. **W5 sai khuôn KÉP** (L2-F7/F12 · L3-F3/F4/F5): (a) hành-vi phải theo KHKK (lời anh) — KHKK CẤM + approver xoá khi đang duyệt, allow {Nháp, TraLai, TuChoi} Drafter/Admin; (b) bằng-chứng "KHKK có + nút Xóa ở panel duyệt" = CODE CHẾT S175 (nút thật ở thanh đáy detail `KhkkDetailContent:1077-1096`); + (c) thiếu changelog + bẫy T26 Remove→Add; (d) DELETE endpoint `[Authorize]` trần + handler không + kiểm chủ sở hữu = lỗ sẵn (#82) mà v1 sắp NỚI RỘNG; (e) thiếu ca chặn `DaPhatHanh(9)`. + → v2 W5 viết lại trọn; approver-delete tách thành Q4 CHỜ ANH. +4. **W2 thiếu 3 mitigation khuôn + 2 quyết định của anh** (L2-F3/F4/F5 · L1-F3 · L3-F9/F11): cây + không chứa HĐ tạo-trực-tiếp; `!canPe` ⇒ panel trống (#82 mặt-2); cây không lọc theo loại; đọc-ý + "list thành fallback" = suy diễn đội. → v2: Q1 nhánh-an-toàn HOÁN ĐỔI (giữ list ở panel phải) · + Q2 Select loại khuôn KHKK · control-âm fe-admin + deep-link + ca HĐ-ngoài-gói. +5. **Acceptance Goodhart** (L1-F4 · L3-F6/F7/F8/F10): ô "0 dòng loại khác" xanh trên trang trắng; + grep thiếu `-F` (rg đọc `?` thành regex — đo thật 2 chiều); 1/7 mã được phủ; 9/20 ô hành-vi không + có chỗ chạy khi deploy CHỜ. → v2: vòng ct 1..7 + control ct-rác + grep -F + mục ④ nấc-đo-local + + DEFERRED-TO-DEPLOY + pin 680→680+N. +6. **Xác nhận DƯƠNG giữ nguyên** (3 lane): root-cause A+B HELD · `view=approved` diệt bug B đúng + khuôn PE · W4 cơ chế khớp trọn (17 leaf, nhánh chèn đúng khối awV2Match) · mirror baseline SHA + sạch 4 cặp · soft-delete xác nhận (AuditingInterceptor) — H-1..H-6 lens-3 ghi lại để wave khỏi đo. +7. **Thứ tự wave đổi**: W4 (lành nhất) → W1 → W2 → W3 (sau W2 vì layout quyết chỗ hiển thị) → W5. +8. **Q1-Q5 đánh số** trình anh (Q1/Q2 nhánh-đã-chọn khai · Q3 cấu-hình-câm 2 cờ anh đang thấy · + Q4 approver-delete · Q5 QT-HD-V2-001) — thay mục "ngoài wave" không số của v1 (L2-F6: rơi im lặng). + +## Bài học lead tự nhận + +v1 lead verify 4 claim bằng grep NHƯNG toàn claim "chỗ thiếu/chỗ có" — KHÔNG verify 2 claim +"CƠ-CHẾ khuôn" ("trang tự suy theo path" · "BE param sẵn") vì chúng nghe xuôi theo khuôn. Đúng bài +`feedback_claim_stronger_than_work` + lens-2 gọi tên: "spec viện dẫn khuôn X phải MỞ khuôn X ra đọc". +Ensemble 3-lane trả cổ tức đúng chỗ lead mù. diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md index b35d8893..866bccb4 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md @@ -68,3 +68,18 @@ HEAD lúc mở run: ae1dcf4a · HMW-mode ON · lead Opus 5 MAX (default) `WF reviewer b2-lens2-khuon s190` (fidelity khuôn + scope + đọc-ý cần hỏi lại anh) · `WF reviewer b2-lens3-accept s190` (acceptance chạy-được + Goodhart + W5 khuôn PE). Artifact: sub-review-b2-lens{1,2,3}.md. +- **B2 KẾT QUẢ** (85 tool-use · 486K tok · ~14'): lens-1 FAIL-11 (sạch) · lens-2 + PASS_WITH_FIXES-12 (#53, artifact cứu) · lens-3 FAIL-11 (sạch). Hội tụ 8 cụm — 2 BLOCKER + tiền-đề v1 (bỏ-type=3 gây trang-trắng · pendingMe 0-hit BE) + W5 sai khuôn kép (KHKK cấm + approver-delete + anchor "nút Xóa panel KHKK" = CODE CHẾT S175) + acceptance Goodhart + (grep thiếu -F · ô xanh-trên-ca-chết · 9/20 ô không chỗ chạy) + xác nhận DƯƠNG root-cause + A+B HELD 3/3 + H-1..H-6 khỏi đo lại. → **SPEC v2** land (GIỮ type=3&ct= · W3 đổi-endpoint + inbox · W5 khuôn KHKK thuần + changelog T26 + authz + chặn DaPhatHanh · Q1-Q5 đánh số · + mục ④ nấc-đo-local + DEFERRED-TO-DEPLOY) + `b2-review-synthesis.md`. Thứ tự wave mới: + **W4 → W1 → W2 → W3 → W5**. +- **B3 phóng** `wf_f71dc3e3-3ce` — /fable-real ×2 vai style (anh chỉ định): + label `WF fe-style b3-speclist s190` (SPEC-LIST STYLE FE — khuôn comment-header tự-khai · + đặt tên opt-in · TanStack idiom · mirror-pair kỷ luật · thanh-đáy KhkkDetailContent) · + label `WF be-style b3-speclist s190` (SPEC-LIST STYLE BE — guard-order · exception-map · + comment T26 · changelog idiom · policy per-action · test style 680→680+N). + Artifact: sub-style-{fe,be}-b3.md. diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md index 2d3e9921..7302aa69 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md @@ -1,103 +1,148 @@ -# SPEC — SOLTUON-12-08-2026 (YC-029: 5 điều chỉnh GĐ3 đồng-khuôn KHKK/Duyệt NCC) +# SPEC v2 — SOLTUON-12-08-2026 (YC-029: 5 điều chỉnh GĐ3 đồng-khuôn KHKK/Duyệt NCC) -> H21 spec-file 3-mục. Engine propose = `sub-invest-b1-fable.md` (Fable-5, 3 chặng B1/B1b/B1c). -> Lead verify @S190: 4/4 claim load-bearing spot-check KHỚP (seed `AwV2_Hd*` by-construction · -> fe-admin `Layout.tsx:175-189` 0 nhánh `Hd` + control `KhkkN:188` · fe-user `Layout.tsx:151-152` -> đúng nguyên văn · delete-guard so số tại `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs:896-897` -> — lane ghi thiếu segment path `Contracts/`, đã đính chính). Trạng thái: CHỜ B2 review (fable-clone). +> H21 spec-file 3-mục (+④ nấc đo local). Engine propose = `sub-invest-b1-fable.md` (Fable-5, 3 chặng). +> **v2 @S190 sau B2 ensemble 3-lane reviewer** (lens-1 FAIL-11 · lens-2 PASS_WITH_FIXES-12 · lens-3 +> FAIL-11 — artifact `sub-review-b2-lens{1,2,3}.md`): 2 BLOCKER tiền-đề v1 SAI đã gỡ (❶ "bỏ type=3, +> trang tự suy theo path" — trang CHỈ đọc `?type`, bỏ là rơi về Duyệt-NCC/trang-trắng; ❷ "BE param +> pendingMe sẵn" — 0-hit Contract, param lạ bị NUỐT im lặng) + W5 viết lại theo khuôn ĐÚNG (hành-vi = +> KHKK theo lời anh; bằng-chứng "KHKK có nút Xóa ở panel duyệt" v1 trỏ vào CODE CHẾT S175 — nút thật +> ở thanh đáy detail, hướng người-soạn) + Q1-Q4 tách rõ. Root-cause bug A+B: 3/3 lane xác nhận HELD. ## ① Tính-năng / đề-bài -Đồng-khuôn GĐ3 "Duyệt Hợp đồng" với KHKK + Duyệt NCC prod (luật owner S176: đã-deploy = CHUẨN), -theo 5 điều chỉnh anh chốt @S190 sau khi review prod (YC-029, ảnh kết tinh `anh-mo-ta.md`): -- **Đ1** cây toàn trình DỜI sang cột TRÁI trang `my-contracts` làm trục điều hướng (khuôn KHKK: - cây trái 400px, flat-list = fallback trong CÙNG panel, panel-3 dashed/WorkflowHistory). -- **Đ2+Đ3** menu 7 loại HĐ mang đúng danh-tính trục ContractType — hết click-nhảy-lẫn (bug A: - `?type=3` ApplicableType đá accordion; bug B: `phase=9` transient 2-leaf-cùng-sáng), leaf - "Luồng duyệt" slice đúng họ `HD-*` của loại. Máy per-loại ĐÃ XONG @S188 — toàn bộ là UI. -- **Đ4** bộ thao-tác theo hành-vi KHKK: Đang-duyệt ở lại cùng trang (`pendingMe=1`), Đã-duyệt - danh-tính chuẩn (`view=approved`), nút XÓA ở màn duyệt theo cờ per-Cấp `AllowApproverDelete`. -- **Đ5** admin Designer hiện 7 leaf per-loại (menu-row + policy + Designer + route ĐÃ seed @S188 - — thiếu đúng 1 nhánh `resolvePath` fe-admin ⇒ drop im lặng #50). +Đồng-khuôn GĐ3 "Duyệt Hợp đồng" với KHKK + Duyệt NCC prod (owner S176: đã-deploy = CHUẨN), theo 5 +điều chỉnh YC-029 (ảnh kết tinh `anh-mo-ta.md`): +- **Đ1** cây toàn trình về cột TRÁI trang `my-contracts` (chi tiết = W2, nhánh-đã-chọn Q1: HOÁN ĐỔI). +- **Đ2+Đ3** menu 7 loại HĐ đúng danh-tính ContractType — diệt bug A (accordion nhảy nhóm) + bug B + (2 leaf cùng sáng) + leaf "Luồng duyệt" slice đúng họ `HD-*`. Máy per-loại ĐÃ XONG @S188 — đây là UI. +- **Đ4** bộ thao-tác theo hành-vi KHKK: Đang-duyệt ở lại trang list · Đã-duyệt danh-tính `view=approved` + · đường XOÁ theo đúng khuôn KHKK (Nháp/Trả-lại/Từ-chối, người-soạn/Admin — KHÔNG mở approver-delete + nếu anh chưa duyệt Q4). +- **Đ5** admin Designer hiện 7 leaf per-loại đã seed sẵn (thiếu 1 nhánh resolvePath — ~5 dòng). -Tổng: **FE-only 4 wave + 1 wave BE nhỏ (0 migration)**. Đợt này PROPOSE + IMPLEMENT + gate -per-wave; **DEPLOY CHỜ** (CI runner workspace còn hỏng P0 — vá-gốc = CỬA NGOÀI-GIỜ; push gom). +Tổng: **W4/W1/W2/W3 FE-only · W5 BE nhỏ + FE (0 migration)**. DEPLOY CHỜ (CI runner P0 — cửa ngoài-giờ); +đo hành-vi bằng **④ nấc đo local**, ô không đo được = DEFERRED-TO-DEPLOY, cấm tick. -## ② Cách implement (per điều chỉnh — file:line đã verify) +## ② Cách implement (per wave — đã vá theo B2) -- **Đ1 (W2):** `fe-user/src/pages/contracts/MyContractsPage.tsx:153` grid `[320px_1fr_360px]` → - `[400px_1fr_360px]`; panel-1 = `PipelineTreePanel currentStage={3} hideHeader hideSearch` + - flat-list fallback server-side trong CÙNG panel (khuôn `KhkkListPage.tsx:171` + - `KhkkListPanel.tsx:21-24,366-371`); panel-3 = dashed khi chưa chọn / `WorkflowHistoryPanel` - khi chọn (QĐ-2 `KhkkListPage.tsx:252-256` — CẤM cây ở panel-3). Bịt gap: - `usePipelineStages.ts:377` leaf GĐ3 `navigate('/contracts/…')` → thêm opt-in - `stage3Options.onOpenContract` (analog `onOpenKhkk` `:185,:244`) + wire `PipelineTreePanel.tsx:111` - → chọn vào panel-2 qua `?id=`. Host CHỈ fe-user (fe-admin không có MyContractsPage) nhưng - `usePipelineStages` + `PipelineTreePanel` = cặp MIRROR ×2 app → sửa cả 2 bản SHA-identical. -- **Đ2+Đ3 (W1):** `fe-user/src/components/Layout.tsx:151` WfView → `/contracts/workflow-matrix?ct=N` - (BỎ `type=3` khỏi URL — trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn) · - `:152` thêm `&view=approved` (khuôn PE `:180-185`, giữ `phase=9` làm lọc thật) · sửa comment - stale `:141-142` · accordion `:470-485` ưu tiên `ct` + loại trừ pathname `/contracts/workflow-matrix` - khỏi nhánh else (phòng thủ 2 lớp) · `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` thêm - nhánh đọc `ct` → lọc `code === 'HD-'` y khuôn nhánh `group` `:84-98` (map ContractType→mã: - const cục bộ 7-entry — fe-user KHÔNG mirror lib fe-admin, `contractWorkflowFamily.ts:16-18` - tự khai fe-admin-ONLY). fe-admin KHÔNG đụng (Ct_* bị ẩn `:248` + cố ý không có trang matrix). -- **Đ4-Đang-duyệt (W3):** `fe-user/Layout.tsx:150` `Ct_*_Pending` → `/my-contracts?type=N&pendingMe=1` - (khuôn PE `:176`; BE param `pendingMe` sẵn) + `MyContractsPage` đọc `pendingMe` gửi server; - GIỮ `/inbox` làm hộp thư tổng. (`[carry:w7b-inbox-param]` liên quan — KHÔNG gộp vào đợt này.) -- **Đ5 (W4):** `fe-admin/src/components/Layout.tsx` import `findContractWfByTypeCode` từ - `@/lib/contractWorkflowFamily` + thêm sau `:188`: +- **W4 — Admin Designer 7 leaf (fe-admin, ≈5 dòng — wave LÀNH NHẤT, chạy ĐẦU):** + `fe-admin/src/components/Layout.tsx` — import `findContractWfByTypeCode` từ `@/lib/contractWorkflowFamily` + + thêm **TRONG khối `if (awV2Match)`**, sau nhánh `KhkkN` `:188`: `if (findContractWfByTypeCode(code)) return '/system/approval-workflows-v2/' + code`. - KHÔNG dùng `?ct=`/`?family=` — khuôn họ này là path-param `:typeCode` y hệt `KhkkN{n}`. - KHÔNG sửa DbInitializer (label đã khớp khuôn). -- **Đ4-nút-Xóa (W5, BE nhỏ 0-mig):** mirror khuôn PE prod `PeSoftDeleteFeatures.cs:110-114` - (3 điều-kiện: ChoDuyet · ĐÚNG-LƯỢT actor · `matchingLevel.AllowApproverDelete`). Việc: nới - delete-guard `Application/Contracts/ContractFeatures.cs:896-897` (đang so SỐ - `Phase >= DangInKy` — chặn nhầm V2 ChoDuyet=10/TraLai=98/TuChoi=99, finding S160) thành - V2-aware + nhánh xóa-bởi-approver đọc cờ (cột `ApprovalWorkflowLevels.AllowApproverDelete` - Mig 68 DÙNG CHUNG V2 — Designer round-trip sẵn cho `HD-*`: - `ApprovalWorkflowV2AdminFeatures.cs:475,883,1288`) + DTO expose cờ (khuôn - `PurchaseEvaluationDtos.cs:138`) + FE `ContractWorkflowPanel.tsx` thêm mục Xóa mirror - `KhkkWorkflowPanel.tsx:12-14,118,233` (panel SHA-mirror ×2 app). - 🔴 Lead thêm khi verify: đường xóa hiện tại là `db.Contracts.Remove(entity)` — wave PHẢI - XÁC MINH interceptor soft-delete (AuditableEntity `IsDeleted`) đứng sau Remove hay không - TRƯỚC khi nới guard; màn "Đã xóa" (`/contracts/deleted`) đang đọc soft-delete ⇒ nếu Remove - là hard-delete thật thì nhánh xóa-bởi-approver phải đi đường soft-delete như PE, KHÔNG - tái dùng Remove. + (H-2/H-3 lens-3 HELD: `code` = phần sau `AwV2_` = `HdTP`… khớp `contractWorkflowFamily.ts:69,77-80`; + route `App.tsx:80` + Designer slice sẵn. Nhánh `code === 'Contract'` `:178` = nhánh chết, 0 seed — + KHÔNG đụng.) KHÔNG sửa DbInitializer. +- **W1 — Danh-tính menu + slice per-loại (fe-user):** 🔴 **GIỮ `?type=3&ct=N`** (khuôn KHKK + `Layout.tsx:216` giữ type= trên URL — v1 đòi bỏ là TỰ CHẾ, gây trang trắng). + (a) Accordion `fe-user/Layout.tsx:470-485`: **ưu tiên đọc `ct` khi có** (map `INT_TO_TYPE_CODE[ct]`) + + **allowlist pathname** (chỉ auto-expand nhóm Ct_ từ `?type=` khi pathname ∈ {`/my-contracts`, + `/contracts/new`, `/inbox`} — diệt luôn cross-talk `Hdc_*` `/hard-copies?type=1..7` mà blacklist v1 sót); + (b) `WorkflowMatrixViewPage.tsx`: thêm nhánh đọc `ct` (clamp `/^[1-7]$/` — khuôn `group` `:82-86`, + `ct` rác ⇒ KHÔNG lọc, giữ hành vi cũ) → lọc `code === 'HD-'` y khuôn nhánh `group` `:84-98`; + map ContractType→mã = **const cục-bộ 7-entry trong fe-user** (⚠️ bản-sao-thứ-3 của họ — F-7 lens-3: + chép ĐÚNG từ `ContractApprovalWorkflowFamily.cs:33-42`, cẩn thận token gạch-nối `HD-NT-NCC`/`HD-NT-DV`; + acceptance vòng 7 mã khoá drift); + (c) `:152` Approved thêm `&view=approved` (khuôn PE `:180-185` — H-4 HELD; trang bỏ qua param lạ); + (d) sửa comment stale `:141-142` ("chung 1 bộ" → 7 bộ per-loại @S188). +- **W2 — Cây toàn trình sang trái = HOÁN ĐỔI 2 khung (fe-user; cặp hook/panel mirror ×2 app):** + Nhánh-đã-chọn **Q1 = HOÁN ĐỔI** (khai bậc-2, anh bác thì đổi): panel-1 TRÁI 400px = CÂY + (`PipelineTreePanel currentStage={3} hideHeader hideSearch`) · panel-2 GIỮA = detail/placeholder + (như cũ) · panel-3 PHẢI 360px = **DANH-SÁCH HĐ** (search + empty-state — GIỮ nguyên chức năng, chỉ + đổi chỗ; KHÔNG xoá khối UI anh không bảo xoá; HĐ tạo-trực-tiếp-ngoài-gói vẫn luôn thấy được ở đây — + vá F-3 lens-2: cây CHỈ chứa HĐ sinh từ gói). + Nhánh-đã-chọn **Q2 = Select loại trên đầu panel cây** (khuôn KHKK Select nhóm `KhkkListPanel.tsx:19-20`): + default = `?type=N` của trang; lọc leaf GĐ3 trên cây theo loại đã chọn. + Kỹ thuật: `usePipelineStages.ts:377` thêm opt-in `opts.onOpenContract` (FLAT trong opts — khuôn + `opts.onOpenKhkk :244`/`opts.onOpenPe :228`; KHÔNG bịa tầng `stage3Options` — F-4 lens-1) + panel + nhận qua `Pick<>` `PipelineTreePanel.tsx:97,111`; fe-admin KHÔNG truyền ⇒ **mặc định TẮT = navigate + như cũ** (control-âm bắt buộc). Search server-side: nối ô tìm → param `search` của `/contracts` + (sẵn `ContractsController.cs:19` — F-10 lens-2, nói thẳng: ĐỔI từ client-side). +- **W3 — Đang-duyệt ở lại trang (fe-user, FE-only — khuôn PE THẬT là đổi-endpoint):** + `Ct_*_Pending` → `/my-contracts?type=N&pendingMe=1`; `MyContractsPage` khi `pendingMe=1` ⇒ **gọi + `/contracts/inbox`** (0-param, khuôn `PurchaseEvaluationsListPage.tsx:91-97` đổi-endpoint) + lọc + `type` client-side (khai lệch-W7 CHẤP NHẬN: inbox không phân trang, payload nhỏ — đúng hiện trạng + `InboxPage.tsx:103`; nâng BE = `[carry:w7b-inbox-param]`, KHÔNG gộp đợt này). GIỮ `/inbox` hộp thư tổng. +- **W5 — Đường XOÁ đồng-khuôn KHKK (BE nhỏ + FE, 0-mig, TEST-BEFORE):** *(viết lại toàn bộ theo + F-7/F-12 lens-2 + F-3/F-4/F-5 lens-3)* + **Khuôn HÀNH-VI = KHKK** (`ContractSigningPlanFeatures.cs:1763-1783`, lời anh "tương tự như KHKK"): + xoá được ở **{DangSoanThao(+phase nháp =DangInKy` VẪN chặn - (regression S160) · xác minh soft-vs-hard-delete (lead-note ②); - ✅(ii) grep `AllowApproverDelete` Contract-delete-path ≥1 (trước: 0-hit); - ✅(iii) panel 2 app SHA-identical + nút Xóa chỉ render khi DTO cờ bật; ✅(iv) build 2 app PASS. +- **W4:** ✅(i) sidebar admin "Quy trình duyệt (Mới)" đủ **17 leaf** (2 NCC + 8 KHKK + 7 "QT Duyệt + HĐ — …" Order 11..17 — H-5); ✅(ii) click "QT Duyệt HĐ — Thầu phụ" → panel khoá mã `HD-TP`, history + CHỈ HD-TP; ✅(iii) control-âm: leaf `KhkkN1` + `DuyetNcc` vẫn mở đúng trang cũ; ✅(iv) build fe-admin PASS. +- **W1:** ✅(i) tái-hiện A (đo local ④): mở nhóm 01 → click "Luồng duyệt" → accordion GIỮ nhóm 01; + control-âm `ct=5` → mở nhóm 05; ✅(ii) tái-hiện B: `?type=1&phase=9&view=approved` → CHỈ leaf "Đã + duyệt" sáng; ✅(iii) **vòng `ct ∈ 1..7`**: trang WfView hiện **≥1** quy trình đúng họ (Dev seed đủ + 7 `HD-*` idempotent) + **0** dòng khác họ + riêng `ct=1` phải VẮNG `QT-HD-V2-001` (tồn tại trên Dev + ⇒ ô "vắng-vì-lọc" có nghĩa, không phải "vắng-vì-không-có"); control `ct=99` ⇒ KHÔNG lọc (giữ hành + vi cũ); ✅(iv) `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` **≥1 GIỮ NGUYÊN** (v2 giữ type=3; + ô 0-hit của v1 đã bỏ) + accordion mới có nhánh `ct`: `grep -nF "get('ct')" fe-user/src/components/Layout.tsx` + ≥1; ✅(v) build fe-user PASS. +- **W2:** ✅(i) `grep -F "grid-cols-[400px" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1; + ✅(ii) click leaf HĐ trên cây → panel-2 mở detail, URL Ở LẠI `/my-contracts` (đo local ④); + ✅(iii) panel-3 = danh-sách sống: search gõ → gọi `/contracts?search=` (Network tab, server-side); + ✅(iv) cặp `usePipelineStages.ts`/`PipelineTreePanel.tsx` 2 app SHA256 khớp từng cặp (baseline + `1b9b5c61…`/`4ffc96be…` — H-1); ✅(v) **control-âm fe-admin**: không truyền `onOpenContract` ⇒ click + leaf GĐ3 vẫn `navigate('/contracts/{id}')` như cũ; leaf GĐ1/GĐ2/GĐ4 hành-vi không đổi ở CẢ 2 app; + ✅(vi) **deep-link cũ** `/contracts/{id}` dán thẳng vẫn mở detail (không 404); ✅(vii) **HĐ tạo + trực-tiếp (ngoài gói)** hiện ở panel-3 danh-sách (ca F-3 lens-2); ✅(viii) build 2 app PASS. +- **W3:** ✅(i) hành-vi so-số 2 chiều (đo local ④, user có ≥1 HĐ đến-lượt + ≥1 không-đến-lượt): + bật `pendingMe=1` ⇒ list **ÍT HƠN** list thường + HĐ không-đến-lượt **VẮNG**; control-dương: user + ngoài luồng ⇒ **0 dòng** (không phải cả list); ✅(ii) leaf "Đang duyệt" ở lại `/my-contracts`; + ✅(iii) build fe-user PASS. +- **W5:** ✅(i) `dotnet test SolutionErp.slnx` PASS **680 → 680+N** (pin số; cập nhật ĐÚNG 1 row test + `docs/STATUS.md`), test-before liệt đủ ca: TraLai(98)/TuChoi(99) Drafter xoá được · DangSoanThao + Drafter xoá được (như cũ) · **ChoDuyet(10) CẤM kể cả Drafter** (khuôn KHKK — NGƯỢC v1) · + **DaPhatHanh(9) CẤM mọi đường** · user thường xoá HĐ người khác ⇒ **403** (sửa lỗ cũ — khai rõ) · + V1 nháp (2) vẫn xoá được (chống vá quá tay); ✅(ii) changelog: xoá xong đọc-ngược DB ra **đúng 1 + dòng** Action=Delete (0 dòng = dẫm T26); ✅(iii) `grep -rn "Authorize(Policy" src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` + có dòng cho DELETE; ✅(iv) nút Xóa: render đúng điều kiện (phase+vai), `ContractDetailContent` mirror + SHA ×2 app; ✅(v) build 2 app PASS. -**Ngoài wave — CHỜ ANH (không tự làm):** ① gỡ-ghim/thay `QT-HD-V2-001` bằng 7 bản `HD-*` nếu -muốn "tuyệt-đối mỗi loại 1 quy trình" (van xả `ContractWorkflowGuards.cs:141-143` là CỐ Ý — -việc Designer/DATA 0 code); ② port 3 món KHKK cố-ý-không-port (skipToFinal · duyệt-thay-CEO · -banner-kết-thúc-sớm) = cần nở `ContractDetail` DTO. +## ④ Nấc đo local (BẮT BUỘC — deploy CHỜ, ô hành-vi KHÔNG tick bằng suy luận) + +1. BE: `dotnet run --project src/Backend/SolutionErp.Api` (LocalDB `SolutionErp_Dev`; seeder idempotent + tự có 7 `HD-*` + `QT-HD-V2-001` sample — tiền-đề ô W1(iii)). +2. FE: `npm run dev` ở `fe-user` (proxy `/api → :5443`) cho W1/W2/W3 · ở `fe-admin` cho W4. +3. Dữ liệu chuẩn bị: ≥1 HĐ trên cây GĐ3 (tạo từ KHKK) · ≥1 HĐ tạo trực-tiếp ngoài gói (W2-vii) · + user đúng-lượt + HĐ không-đến-lượt (W3-i) · 1 HĐ TraLai + 1 ChoDuyet + 1 DaPhatHanh (W5). +4. Ô nào không dựng được dữ liệu local ⇒ đánh **DEFERRED-TO-DEPLOY** công khai trong run.md, CẤM tick. + +## Câu hỏi ĐÁNH SỐ trình anh (Q1/Q2 = nhánh-đã-chọn, đảo được; Q3/Q4/Q5 = chờ anh) + +- **Q1 (Thứ-1):** em chọn **HOÁN ĐỔI** 2 khung (cây → trái 400px làm trục; danh-sách → phải 360px, + GIỮ nguyên chức năng) thay vì bỏ hẳn danh-sách kiểu KHKK-thuần — vì cây KHÔNG chứa HĐ tạo-trực-tiếp + (khác KHKK: mọi phiếu KHKK đều từ PE) + không xoá khối UI anh chưa bảo xoá. Anh muốn KHKK-thuần + (bỏ list, flat-fallback trong panel cây) thì nói — đổi 1 wave. +- **Q2 (Thứ-1×2):** cây bên trái em gắn **Select loại HĐ** (khuôn Select nhóm của KHKK), default theo + menu đang đứng — leaf GĐ3 trên cây lọc theo loại. Anh muốn cây hiện đủ mọi loại không lọc thì bỏ Select. +- **Q3 (Thứ-4):** màn duyệt HĐ có port 3 món KHKK đang có không: ô "Cấp này KẾT THÚC quy trình" · + nhãn "⚑ Duyệt thay CEO" · banner "kết thúc sớm ở Bước X"? **2 cờ đầu anh ĐANG THẤY trong dialog + Sửa-tại-chỗ (ảnh-3) — hiện bật ở Admin mà máy duyệt HĐ KHÔNG đọc = cấu hình câm.** Port = nở + `ContractDetail` DTO (BE nhỏ). Chưa port thì em ghi chú "chưa áp dụng cho HĐ" vào Designer (khuôn + mờ-ô D3/S182) để hết câm. +- **Q4 (Thứ-4):** có mở **approver-delete** cho HĐ (người duyệt đúng-lượt xoá phiếu ChoDuyet theo cờ + `AllowApproverDelete` per-Cấp — tính năng PE đang có, KHKK KHÔNG có)? Mặc định đợt này **KHÔNG** — + đúng lời anh "tương tự KHKK". Anh gật thì thành wave riêng (cột DB sẵn, 0-mig). +- **Q5 (dữ liệu):** quy trình tay `QT-HD-V2-001` (bản type-3 duy nhất trên Dev, van-xả cố-ý cho mã + ngoài họ) — GIỮ làm van xả admin, hay gỡ-ghim/thay bằng 7 bản `HD-*` để "mỗi loại 1 quy trình" + tuyệt-đối cả trên dữ liệu? (0 code — việc Designer/DATA.) diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens1.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens1.md index a4314d3a..155cf541 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens1.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens1.md @@ -1,13 +1,243 @@ # LENS-1 CORRECTNESS — review đối-kháng spec SOLTUON 12-08-2026 (YC-029) +Reviewer lane-1/3 (ensemble /fable-clone) · lăng-kính **ĐÚNG-SAI KỸ THUẬT** của spec W1+W2 (+W3/W5 khi +chạm trục correctness). Mọi claim kèm `file:line` đo tại HEAD `1c677fb7`, cây làm việc 2026-08-12. + ## Verdict -_(đang đo — điền cuối lượt)_ +**FAIL — 11 findings (2 BLOCKER · 4 MAJOR · 3 MINOR · 2 xác-nhận-dương).** + +Không phải FAIL về **hướng**: 5 điều chỉnh đúng ý owner, và chẩn-đoán root-cause A + B của invest +**tôi đo lại thì ĐỨNG** (F-10, F-11). FAIL là về **tính thực-thi-được của chữ trong spec**: hai mệnh +đề nền của W1 và W3 là **SAI SỰ THẬT về code hiện tại**, và nếu implementer làm đúng y chữ thì: + +- **W1** đẻ trang "Luồng duyệt" **trắng / hiện nhầm dữ liệu Duyệt NCC** (F-1); +- **W3** đẻ danh sách "Đang duyệt" **hiện TOÀN BỘ HĐ của loại đó** mà vẫn xanh mọi acceptance (F-2). + +Cả hai đều là hạng **im lặng** — build xanh, grep xanh, chỉ người dùng mới thấy. Sửa 2 mệnh đề + +nới W2 scope (F-3) + siết 3 ô acceptance (F-4, F-6, F-8) là spec chạy được. + +--- ## Findings -_(đánh số F-1.. — append ngay khi chốt)_ +### 🔴 F-1 (BLOCKER, W1) — "trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn" là **SAI**. Bỏ `type=3` khỏi URL ⇒ trang rơi về fallback `type=1` (Duyệt NCC). -## Đề nghị sửa spec +Spec `②` dòng 37-38 và `③ W1` dựa trọn trên mệnh đề này. Đo thực tế: -_(điền cuối lượt)_ +- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:56-61`: + ```tsx + const { pathname } = useLocation() + const rawType = Number(searchParams.get('type')) + const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1 + ``` + `pathname` **KHÔNG** tham gia suy `typeInt` — comment `:54-55` tự khai công dụng của nó: *"link 'xem + tất cả' phải bám đường dẫn ĐANG đứng"*. Đó là thứ duy nhất `pathname` làm. +- "Khuôn alias-path khkk" **không phải path-inference**: alias KHKK vẫn mang type trên QUERY — + `Layout.tsx:86` `Khkk_WfView: '/khkk/workflow-matrix?type=10&group=1'` và `:216` + `/khkk/workflow-matrix?type=10&group=${group}`; route alias `App.tsx:83`. Tức khuôn thật là + **"path alias + type vẫn ở query"**, đúng ngược với thứ spec viện dẫn. +- Hệ quả số học: bỏ `type` ⇒ `searchParams.get('type')` = `null` ⇒ `Number(null)` = **0** ⇒ 0 ∉ {1,2,3,10} + ⇒ `typeInt = 1` ⇒ `queryKey ['workflow-matrix', 1]` + `params { applicableType: 1 }` (`:63-71`) ⇒ trang + tải **quy trình Duyệt NCC**. Nhãn header cũng đổi theo: `typeLabel = summary?.applicableTypeLabel` + (`:76`) ⇒ "Luồng duyệt — Duyệt NCC". +- Cộng thêm nhánh lọc `ct` mà chính W1 thêm (`code === 'HD-'`): bộ dữ liệu ApplicableType=1 + không có mã nào bắt đầu `HD-` ⇒ **0 dòng ⇒ trang trắng**. + +**Không đủ để diệt bug, và còn mở đường mới:** kể cả khi trang đọc `ct` đúng, việc bỏ `type` làm +`useEffect` accordion `Layout.tsx:473` `if (!typeParam) return` **thoát sớm** ⇒ deep-link/refresh thẳng +vào `/contracts/workflow-matrix?ct=5` **không mở nhóm 05** ⇒ leaf sáng nhưng **nằm trong nhóm đang +đóng** — đúng triệu chứng "menu không ăn khớp" mà Đ3 muốn diệt. + +**Đề nghị:** chọn MỘT trong hai, viết thẳng vào spec (đừng để implementer đoán): +(a) **Giữ `type=3` trên URL** + chỉ sửa accordion (đọc `ct` trước, xem F-5) — rẻ nhất, 0 đụng trang matrix; +(b) **Thật sự thêm path-inference** vào `WorkflowMatrixViewPage`: `pathname.startsWith('/contracts/') → 3`, +`'/khkk/' → 10`, `'/purchase-evaluations/' → query`, và khai rõ đây là **code MỚI**, kèm ô acceptance +riêng cho KHKK (`/khkk/workflow-matrix?type=10&group=1` **không được** đổi hành vi). + +--- + +### 🔴 F-2 (BLOCKER, W3) — "BE param `pendingMe` sẵn" là **SAI** cho phía Hợp đồng. Param lạ bị ASP.NET nuốt im lặng ⇒ "Đang duyệt" hiện TOÀN BỘ HĐ của loại. + +Spec `②` dòng 45-46: *"`/my-contracts?type=N&pendingMe=1` (khuôn PE `:176`; **BE param `pendingMe` sẵn**)"*. +Invest cũng khẳng định vậy (`sub-invest-b1-fable.md:84`). Đo: + +- `grep -rn "pendingMe\|PendingMe" src/Backend/SolutionErp.Application/Contracts/ src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` → **0 hit**. +- **Control-dương (chứng thước không hỏng):** cùng lệnh trên `src/Backend` trả 4 hit ở họ KHKK — + `ContractSigningPlansController.cs:42` `[FromQuery] bool pendingMe = false`, + `ContractSigningPlanFeatures.cs:1219` `bool PendingMe = false`, `:1255` `if (request.PendingMe)`. + ⇒ grep chạy đúng; phía Contract **thật sự không có**. +- Chữ ký thật của endpoint list (`ContractsController.cs:20-27`): `phase · supplierId · projectId · + type · page · pageSize · search · sortDesc` — hết. +- **Cơ chế hỏng im lặng:** model-binding ASP.NET Core **bỏ qua** query param không khai ⇒ + `GET /contracts?type=1&pendingMe=1` = `GET /contracts?type=1` ⇒ trang "Đang duyệt" liệt **mọi** HĐ + loại 1 (kể cả Nháp, Đã phát hành), 200 OK, 0 lỗi, 0 warning. +- **Khuôn PE mà spec viện dẫn cũng không phải BE-param:** PE là **FE đổi ENDPOINT** — + `PurchaseEvaluationsListPage.tsx:39` `const pendingMe = sp.get('pendingMe') === '1'` → `:91` `if (pendingMe) { … }` + (nhánh gọi endpoint inbox riêng). Phía BE PE cũng 0 hit `pendingMe`. + +**Đường đi đúng cho HĐ (và ranh của nó):** `GET /contracts/inbox` (`ContractsController.cs:41-43`) trả +`List` **không phân trang** và **không nhận `type`** ⇒ FE phải lọc `type` client-side. +Ở đây lọc client **chấp nhận được** (khác ca W7-S187: nhánh `/contracts` bị cắt 100 dòng nên lọc client +làm loại hiếm biến mất; inbox trả trọn, không cắt) — **nhưng phải viết ra trong spec**, vì nó là ngoại-lệ +của một luật đã có, không phải mặc định. + +**Đề nghị:** đổi W3 thành *"MyContractsPage: `pendingMe=1` ⇒ gọi `/contracts/inbox` rồi lọc `type` +client-side (inbox không phân trang — nêu rõ lý do được lọc client)"*, HOẶC nhận W3 là **wave BE** (thêm +`pendingMe` vào `ListContractsQuery` + tái dụng logic match approver V2/V1 của `GetMyInboxQuery`). Không +được để câu "BE đã sẵn". + +--- + +### 🟠 F-3 (MAJOR, W2) — Dời cây sang trái **làm mất danh-tính per-loại** của 7 leaf "Danh sách" — mâu thuẫn thẳng với Đ2+Đ3. Khuôn KHKK mà spec viện dẫn **có** cơ chế giữ danh-tính, spec **bỏ sót** đúng cơ chế đó. + +- Hôm nay panel-1 = danh-sách **lọc theo `type` ở SERVER** (`MyContractsPage.tsx:34,50-57`; W7-S187 cố ý + gỡ lọc client vì >100 HĐ thì loại hiếm biến mất im lặng). +- `PipelineTreePanel` **không có trục loại HĐ**: props ở `:94-128` = `currentStage · stage2Options · + hideHeader · hideSearch …`; cây dựng theo Năm → Dự án → Hạng mục (`:24-32`). ⇒ thay panel-1 bằng cây + thì **cả 7 leaf "Danh sách" render y hệt nhau** — đúng thứ mà chính repo gọi là *"mục menu nói dối tên nó"* + (`WorkflowMatrixViewPage.tsx:80`). +- **Khuôn KHKK KHÔNG bị bệnh này vì nó có lát cắt:** `KhkkListPanel.tsx:200,216` nhận prop `group`, đẩy + xuống server (`:253` `group: group ?? undefined`) **và** cắt nội dung cây (`:281` + `src.filter(k => k.approvalGroup === group)`, `:295`, `:323`) — bơm vào cây qua `stage2Content` + (`PipelineTreePanel.tsx:111` `Pick`). +- Bên GĐ3 **chưa có gì tương ứng**: `grep "opts\." fe-user/src/hooks/usePipelineStages.ts` chỉ ra + `stage1Content/stage1Count` (`:212-213`), `onOpenPe` (`:228-229`), `onOpenKhkk` (`:244`), + `stage2Content/stage2Count` (`:303-306`) — **`stage3Content` 0-hit**. + +⇒ W2 thật sự cần **4 món** chứ không phải 2 như spec liệt: `BuildStagesOptions` + `stage3Content` + +`stage3Count` + `onOpenContract`, **và** prop `stage3Options` trên panel (`Pick<…>` phải kể tên từng key — +quên tên nào thì opt-in **chết câm**, đúng lớp "guard dựng mà không ai gọi"), **cộng** quyết định +`?type=N` cắt cây thế nào. Đây là 2 file **MIRROR SHA-identical ×2 app** nên mọi sai shape nhân đôi. + +**Đề nghị:** spec phải trả lời dứt khoát: *"panel-1 ở `/my-contracts?type=N` hiển thị cây **đã cắt theo +ContractType N** (leaf GĐ3 lọc `c.type === N`), hay cây toàn-cảnh không cắt?"* — và thêm ô acceptance +đối-chứng: đứng `?type=1` và `?type=5`, hai cây **phải khác nhau** (nếu chọn cắt), hoặc khai thẳng +"cố ý giống nhau" như một quyết-định owner (khuôn QĐ-2 KHKK). + +--- + +### 🟠 F-4 (MAJOR, W1 acceptance) — Ô ✅(iii) **đo hình-dạng, không đo giá-trị**: trang TRẮNG vẫn qua. + +Nguyên văn: *"WfView `ct=1` chỉ hiện họ `HD-TP` (0 dòng QT-HD-V2-001/loại khác)"*. Ca hỏng F-1 sinh ra +**0 dòng tất cả** ⇒ mệnh đề "0 dòng của loại khác" **ĐÚNG** ⇒ gate xanh trên đúng ca hỏng nặng nhất. +Ô (iv)/(v) là grep + build, cũng xanh. ⇒ **cả W1 có thể tick trọn trong khi tính năng chết.** + +**Đề nghị** thay bằng 3 vế đo được: (1) **≥1 dòng** và mọi dòng có `code` bắt đầu `HD-TP`; +(2) header đọc **"Luồng duyệt — Hợp đồng"** (không phải "Duyệt NCC") — bắt đúng ca fallback `typeInt=1`; +(3) control-âm: `?ct=5` phải ra tập **KHÁC** `?ct=1`. + +--- + +### 🟠 F-5 (MAJOR, W1) — "Loại trừ pathname `/contracts/workflow-matrix`" là **danh-sách-đen**; case anh em `Hdc_*` đang dính đúng bug đó và vẫn sống sau khi vá. + +- Máy hiện tại (`Layout.tsx:470-485`): mọi pathname **không** bắt đầu `/purchase-evaluations` đều bị đọc + `?type=` **như ContractType** (`INT_TO_TYPE_CODE`, `:16-29`) rồi set accordion (1 slot ⇒ mutex, `:467`). +- **Case anh em còn sống:** `Hdc_*` GĐ4 — `Layout.tsx:70-76` `'/hard-copies?type=1'`…`'?type=7'`. Bấm + "Hợp đồng cứng → Thầu phụ" ⇒ nhánh else ⇒ **mở nhóm "01. HĐ Thầu phụ"** của cây menu khác. Cùng + lớp bug A, **không** nằm trong tập vá của spec. +- `Khkk_WfView` (`:86,:216`, `type=10`) thoát **do may**: `INT_TO_TYPE_CODE[10]` = `undefined` ⇒ guard + `if (code)` `:483` chặn. Không phải do thiết kế. + +**Đề nghị:** đổi hình dạng bản vá từ **đen** sang **trắng**: (1) đọc `ct` TRƯỚC (mọi pathname), có `ct` +hợp lệ ⇒ dùng nó, dừng; (2) chỉ khi vắng `ct` mới đọc `type`, và **chỉ trên allowlist** pathname mà +`type` = ContractType (`/my-contracts`, `/contracts/new`, `/inbox`, `/hard-copies`). Ô acceptance kèm: +bấm leaf `Hdc_ThauPhu` **không** làm đổi trạng thái accordion `Ct_*`. + +--- + +### 🟠 F-6 (MAJOR, W5 test-matrix) — "V1 `Phase>=DangInKy` VẪN chặn" là câu **nhập nhằng**: V1-flat cũng đứng ở `ChoDuyet=10` (≥5). Phân nhánh phải theo **cờ pin**, không theo **số phase**. + +- Guard thật: `ContractFeatures.cs:897` `if (entity.Phase >= ContractPhase.DangInKy) throw ConflictException(...)`, + `DangInKy = 5` (`ContractPhase.cs`). +- Bảng số: `DaPhatHanh=9` · `ChoDuyet=10` · `TraLai=98` · `TuChoi=99` — **tất cả ≥ 5** ⇒ đều bị chặn hôm nay + (đúng finding S160). +- 🔴 Điểm nhập nhằng: **V1-flat (Mig 21) cũng dùng `ChoDuyet=10`** làm trạng thái trung gian + (`ContractPhase.cs` comment tự khai: *"ChoDuyet = 10 — generic intermediate, CurrentWorkflowStepIndex + tracking"*). ⇒ nếu bản vá nới theo **tập phase** thì nó nới **cả V1-flat** mà vẫn tick được ô + "V1 Phase>=DangInKy vẫn chặn" (vì ô đó chỉ nói tới 5..9). Đúng lớp gotcha **#42** (dual-schema + branch phải theo field pin: `ApprovalWorkflowId != null` ⇒ V2, ngược lại V1). +- Ô test còn **thiếu 2 ca**: **V2 @ `DaPhatHanh=9`** (terminal thành công + đã gen mã HĐ — phải VẪN cấm + xóa) và **V1-flat @ `ChoDuyet=10`** (giữ cấm hay mở? owner phải chốt). + +**Đề nghị:** viết acceptance theo **ma trận (schema × phase × actor)**, mỗi ô một dòng, và nêu +discriminator là `ApprovalWorkflowId`. + +--- + +### 🟢 F-7 (GỠ NỢ — trả lời câu lead treo ở `②` dòng 62-66) — `db.Contracts.Remove` **ĐÃ LÀ soft-delete**. W5 không cần đổi đường xóa. + +- `AuditingInterceptor.cs:54-62`: mọi `AuditableEntity` ở `EntityState.Deleted` bị **lật** thành + `Modified` + `IsDeleted = true` + `DeletedAt` + `DeletedBy`. +- Handler tự khai `// ========== DELETE (soft) ==========` (`ContractFeatures.cs:885`), và màn "Đã xóa" + đọc được chính là hệ quả (`ContractsController.cs:34-40` `/contracts/deleted`). + +⇒ Bỏ hạng mục *"xác minh soft-vs-hard-delete"* khỏi W5 ✅(i), hoặc hạ xuống **1 assert trong test** +(xóa xong: row còn tồn tại, `IsDeleted == true`). Đây là chỗ spec **rộng hơn việc thật** — cắt được. + +--- + +### 🟡 F-8 (MINOR, W1) — Sau W1, `ct` từ **trang trí** thành **danh tính**, nhưng chưa ai định nghĩa `ct` rác. + +`grep "get('ct')" fe-user/src fe-admin/src` = **0 hit** ⇒ hôm nay `ct` chỉ được **ghi** (`Layout.tsx:151`) +để phân biệt active-state, **không ai đọc**. Khi trang bắt đầu đọc nó, cần clamp y khuôn `group` +(`WorkflowMatrixViewPage.tsx:84-86`: regex `/^[1-8]$/` + *"group rác/thiếu ⇒ BỎ QUA hoàn toàn… thà hiện +đủ còn hơn lọc theo một con số vô nghĩa rồi ra trang trắng"*). Spec nên nói thẳng: `ct ∉ 1..7` ⇒ **không +lọc** (hiện đủ), không phải lọc-ra-rỗng. + +### 🟡 F-9 (MINOR, W1) — Bookmark cũ mất highlight (dữ liệu vẫn đúng). + +URL đã lưu `/contracts/workflow-matrix?type=3&ct=1`, leaf mới trỏ `?ct=1`. `queryMatches:368-374`: `type` +**không** thuộc `TRANSIENT_QUERY_KEYS` (`:350`) ⇒ không ignorable ⇒ `aKeys=[ct,type]` (2) ≠ `bKeys=[ct]` (1) +⇒ **không sáng**. Không mất chức năng (miễn trang vẫn chấp nhận `type` — xem F-1 phương án (a)/(b)), +nhưng nên có 1 dòng "backward-compat: trang nhận CẢ 2 dạng URL". + +### 🟡 F-10 (MINOR, W2 acceptance — xác nhận DƯƠNG) — Baseline mirror **đang sạch**, nên ô SHA có nghĩa. + +Đo tại HEAD: `usePipelineStages.ts` `1b9b5c611663666d3c8853a88b020d57b712c3cfd9bb524790bcd0359152ff48` +(fe-user ≡ fe-admin) · `PipelineTreePanel.tsx` +`4ffc96beded5788a7463efa0e7df405c217a5d60ef7f5ca1ce83f323c4c6c334` (fe-user ≡ fe-admin). ⇒ ô ✅(iv) của +W2 không phải "gate hỏng sẵn"; nếu lệch sau wave thì đúng là lỗi của wave. + +### 🟢 F-11 (XÁC NHẬN DƯƠNG) — Root-cause A + B của invest **ĐỨNG**; cơ chế `view=approved` **thật sự** diệt 2-leaf-cùng-sáng; `phase=9` **vẫn là** bộ lọc đúng. + +- **A dựng lại được**: `Layout.tsx:151` sinh `?type=3&ct=N` → `:470-485` nhánh else (pathname không phải + `/purchase-evaluations`) → `INT_TO_TYPE_CODE[3] = 'NhaCungCap'` (`:16-29`) → `setExpandedCtCode` mà state + chỉ có **1 slot** (`:467`) ⇒ nhóm đang mở bị đóng. Chuỗi HELD. +- **B — kiểm bằng tay theo đúng luật `queryMatches`**: đứng `?type=1&phase=9&view=approved`: + leaf "Đã duyệt" (đích ghim `type,phase,view`) ⇒ `phase` **được pin nên so nghiêm** ⇒ khớp ✓; + leaf "Danh sách" (đích `type`) ⇒ `phase` bị bỏ qua **nhưng `view` KHÔNG thuộc TRANSIENT** (`:350`) ⇒ + `aKeys=[type,view]` (2) ≠ `bKeys=[type]` (1) ⇒ **tắt** ✓. Đúng như PE `:180-185`. +- `phase=9` vẫn đúng nghĩa sau S188: `ContractPhase.cs` comment tự khai *"last step done ──► + DaPhatHanh (terminal + gen mã HĐ)"* ⇒ HĐ duyệt xong V2 rơi vào **9**, không phải một mã khác. Ô lọc + không mục ruỗng. + +--- + +## Đề nghị sửa spec (theo thứ tự chặn) + +1. **W1 §② dòng 37-38** — xoá mệnh đề *"trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn"*. + Thay bằng phương án (a) **giữ `type=3`, chỉ sửa accordion** *hoặc* (b) **thêm path-inference như code MỚI** + + ô acceptance không-hồi-quy cho `/khkk/workflow-matrix?type=10`. +2. **W3 §② dòng 45-46** — xoá *"BE param `pendingMe` sẵn"*. Chốt đường: FE đổi endpoint sang + `/contracts/inbox` + lọc `type` client (kèm 1 câu vì sao lọc client được chấp nhận ở đây: inbox + **không phân trang**), hoặc nâng W3 thành wave BE. +3. **W2 §② dòng 33-34** — nới scope: `stage3Content` + `stage3Count` + `onOpenContract` + prop + `stage3Options` (liệt **từng key** trong `Pick<>`), và **chốt** cây có cắt theo `?type=N` hay không. + Sửa luôn ký hiệu: analog thật ở hook là **flat** `opts.onOpenKhkk` (`:244`) — `stage2Options` là tên + **prop của panel** (`PipelineTreePanel.tsx:97,111`), hai tầng khác nhau, spec đang trộn. +4. **W1 ✅(iii)** — thêm "≥1 dòng" + "header = nhãn Hợp đồng" + control-âm `ct=5` (F-4). +5. **W1 accordion** — đổi từ blacklist-pathname sang **`ct` trước / allowlist `type` sau**, + ô acceptance + `Hdc_*` không đụng accordion `Ct_*` (F-5). +6. **W5 ✅(i)** — ma trận (schema × phase × actor), discriminator = `ApprovalWorkflowId` (gotcha #42); + thêm ca **V2@DaPhatHanh=9 vẫn cấm** và **V1-flat@ChoDuyet=10** (owner chốt); **bỏ** hạng mục xác minh + soft-vs-hard (đã đóng ở F-7, hạ thành 1 assert). +7. **W1** — thêm 1 dòng clamp `ct ∉ 1..7 ⇒ không lọc` (F-8) và 1 dòng backward-compat URL cũ (F-9). + +--- + +### Ghi chú phạm vi + +Lane này **không** chấm 3 trục: văn phong/UX (lane khác), phân quyền/authz, và deploy — spec tự khai +DEPLOY CHỜ nên không có claim prod để curl. Không đo `dotnet build`/`npm run build` (spec là **đề xuất**, +chưa có diff). Mọi số liệu trên đo tại cây làm việc `main @ 1c677fb7`. diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens2.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens2.md index f512b848..40f4bbbb 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens2.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens2.md @@ -1,15 +1,130 @@ # sub-review-b2-lens2 — LENS-2 FIDELITY-KHUÔN + SCOPE > Reviewer lane-2/3 (ensemble /fable-clone). Lăng-kính: độ trung-thành với lời anh (YC-029 + 3 ảnh) + khuôn S176 "đã-deploy = CHUẨN" + scope-risk + mirror 2 app + wave order. +> Mọi claim dưới đây ĐO từ đĩa tại HEAD `1c677fb7` (working tree). Không có claim nào chép lại từ `sub-invest-b1-fable.md` mà không mở file gốc. ## Verdict -_(đang đo — điền cuối lượt)_ +**PASS_WITH_FIXES — 12 findings** (4 CRITICAL · 4 MAJOR · 4 minor). Spec phủ 5/5 điều chỉnh về mặt ĐỀ-BÀI, nhưng **4 câu implement trong §2 sẽ đẻ lỗi CÂM nếu implementer làm đúng chữ**, W2 (cây sang trái) thiếu 2 mitigation mà chính khuôn KHKK tự khai là BẮT BUỘC, và **W5 mirror NHẦM module** — khuôn KHKK mà anh chỉ định lại CẤM đúng cái W5 định mở. + +--- ## Findings (F-1..) -_(append từng finding ngay khi đo xong)_ +### F-1 · CRITICAL — §2 Đ2+Đ3: "bỏ `type=3` khỏi URL, trang tự suy applicableType theo path" — cái "khuôn alias-path khkk" ĐÓ KHÔNG TỒN TẠI, và trang rơi về **type 1 (Duyệt NCC)** chứ không phải 3 + +Spec `spec-…md:37-38` chỉ đạo: `Layout.tsx:151` WfView → `/contracts/workflow-matrix?ct=N` (**BỎ `type=3`**) vì "trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn". + +Đo: +- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:57,61`: + ``` + const rawType = Number(searchParams.get('type')) + const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1 + ``` + Bỏ `type` khỏi URL ⇒ `Number(null)` = 0 ⇒ **fallback `: 1`** ⇒ fetch `applicableType=1` = quy trình **Duyệt NCC**. Trang KHÔNG đọc `pathname` để suy type: `pathname` chỉ dùng cho link "xem tất cả" — chính comment `:54-56` khai vậy ("link *xem tất cả* phải bám đường dẫn ĐANG đứng"). +- "Khuôn alias-path khkk" là **phản-chứng của chính spec**: leaf KHKK `Layout.tsx:216` = `/khkk/workflow-matrix?type=10&group=${group}` — KHKK **GIỮ `type=` trên URL**, không suy theo path. Route `/khkk/workflow-matrix` (`App.tsx:83`) chỉ là alias pathname, KHÔNG mang ngữ nghĩa type. + +⇒ Làm đúng chữ spec = trang "Luồng duyệt — HĐ Thầu phụ" hiển thị **quy trình Duyệt NCC**, không lỗi đỏ, không 404. Đây là lệch khuôn S176 (KHKK đã-deploy giữ `type=`), lại là dạng hỏng CÂM. +**Sửa:** GIỮ `?type=3&ct=N` (đúng khuôn KHKK) và chữa bug A ở **accordion** (`Layout.tsx:470-485`) — vốn đã nằm sẵn trong spec như "phòng thủ 2 lớp". Lớp phòng thủ đó chính là toàn bộ liều thuốc; vế "bỏ type=3" là phần TỰ CHẾ và là phần gây hại. Nếu vẫn muốn bỏ `type=3` thì spec phải kèm 1 câu bắt buộc: thêm nhánh suy type từ `pathname` TRƯỚC, và acceptance phải có ca "URL không có type ⇒ vẫn ra HD-*". + +### F-2 · CRITICAL — §2 Đ4-W3: "BE param `pendingMe` sẵn" là SAI cho Contract ⇒ `/contracts?pendingMe=1` bị model-binding NUỐT, list ra TOÀN BỘ HĐ mà không báo lỗi + +Spec `:45-46` (và invest `:114`) viết: *"`Ct_*_Pending` → `/my-contracts?type=N&pendingMe=1` (khuôn PE `:176`; **BE param `pendingMe` sẵn**) + `MyContractsPage` đọc `pendingMe` **gửi server**"*. + +Đo: +- `grep -rn "PendingMe" src/Backend --include=*.cs` = **2 hit, CẢ HAI thuộc KHKK** (`ContractSigningPlansController.cs:42,50` + `ContractSigningPlanFeatures.cs:1219,1255`). Contract: **0 hit**. +- `ContractsController.cs:17-27` — `List(...)` nhận `page,pageSize,search,sortDesc,phase,supplierId,projectId,type`. **Không có `pendingMe`**. Query gửi thừa param ⇒ ASP.NET bỏ qua im lặng ⇒ trả full list. +- Khuôn PE THẬT (không phải "param BE"): `PurchaseEvaluationsListPage.tsx:91-97` — `if (pendingMe)` thì **ĐỔI ENDPOINT** sang `/purchase-evaluations/inbox` (kèm `type`). Còn `/contracts/inbox` **không nhận tham số nào** (`ContractsController.cs:41-43` `GetMyInboxQuery()` 0-arg) ⇒ mirror cho HĐ phải lọc `type` client-side (đúng như `InboxPage.tsx` đang làm) hoặc nới BE. + +⇒ W3 đang được spec dán nhãn "FE nhỏ" trên một tiền-đề sai. Hoặc sửa câu implement thành "pendingMe ⇒ gọi `/contracts/inbox` + lọc type client (khuôn PE)", hoặc khai W3 có delta BE. **Acceptance W3 hiện KHÔNG bắt được lỗi này**: (i) grep `pendingMe=1` vẫn xanh; (ii) "list khớp inbox cũ" — người kiểm dễ chấp nhận vì list *có* dữ liệu (chỉ là nhiều hơn đúng). Đề nghị siết acceptance thành **so số dòng bằng nhau + 1 HĐ KHÔNG-đến-lượt phải VẮNG**. + +### F-3 · CRITICAL — W2 bỏ mất 2 mitigation mà chính khuôn KHKK khai là **BẮT BUỘC**, và cây GĐ3 về mặt cấu trúc **không chứa HĐ tạo trực tiếp** + +Header `fe-user/src/components/khkk/KhkkListPanel.tsx:9-24` khai nguyên văn: *"Chọn HÌNH ≠ chấp-nhận VỠ ⇒ 4 mitigation dưới đây là BẮT BUỘC, không phải tuỳ chọn"* — M-1 (`!canPe` ⇒ fallback danh-sách phẳng), M-2 (`onSelect` thay navigate), B1 (`stage2Content` phải kèm `stage2Count`), R-11 (cây chỉ 200 dòng ⇒ tìm/lọc đi SERVER). + +Spec `:29-35` chỉ port **M-2** (`onOpenContract`) + nửa R-11 ("flat-list fallback server-side"). Thiếu: +- **M-1 analog:** nguồn của cây là index PE, `enabled: canPe` (`usePipelineStages.ts:83,102`). User chỉ có quyền HĐ (drafter HĐ, không có key Duyệt NCC) ⇒ **panel trái TRỐNG** trong khi `/contracts` vẫn trả dữ liệu ⇒ đúng mặt còn lại gotcha #82. Hôm nay `/my-contracts` không có bệnh này vì panel-1 là list thuần. +- **Cấu trúc:** `usePipelineStages.ts:333-359` — stage-3 leaves duyệt trên `contractIdsOfPackage` = hợp của `pe.contractId` + contract-id trên dòng KHKK. **HĐ tạo thẳng từ `/contracts/new?type=N`** (chính là leaf "Thao tác" và nút "+ Tạo HĐ mới" `MyContractsPage.tsx:145`) **không thuộc gói nào ⇒ 0 leaf trên cây**. Cây làm panel mặc định ⇒ nhóm HĐ này **không còn đường mặc định để thấy**; chỉ tìm ra nếu user gõ đúng từ khoá. Ở KHKK không có lỗ này vì mọi phiếu KHKK đều sinh từ PE. +⇒ W2 phải khai điều-kiện fallback ĐẦY ĐỦ: `!canPe` **HOẶC** search/lọc **HOẶC** (đề nghị) mặc định vẫn phẳng khi tập "HĐ ngoài gói" > 0. Và acceptance W2 phải có ca **"tạo 1 HĐ trực tiếp (không qua KHKK) → nó phải xuất hiện được ở panel trái"**. + +### F-4 · MAJOR — W2 ⟂ Đ2 mâu thuẫn CHÉO: cây toàn trình **không lọc theo ContractType**, trong khi cả đợt này là để "mỗi loại đúng danh tính" + +`usePipelineStages.ts:356-379` dựng stage-3 leaf từ tập id của gói, **0 vế lọc `c.type`** (đối chứng: stage-4 có đọc `c.type` nhưng chỉ để dựng URL `:389`). Cây gốc còn đứng ở trục Năm → Dự án → Gói thầu (ảnh-1), tức trục **gói thầu**, không phải trục **loại HĐ**. + +⇒ Sau W2, `/my-contracts?type=1` (leaf "01. HĐ Thầu phụ → Danh sách") sẽ hiện panel trái chứa HĐ **của cả 7 loại**. Điều đó phá đúng thứ mà Đ2/Đ3 (W1) vừa dựng, và header trang vẫn ghi "HĐ của tôi · HĐ Thầu phụ (n)" ⇒ nhãn nói dối nội dung. +Khuôn KHKK **có xử vế này**: panel-1 giữ `Select` nhóm ngay trên cây (`KhkkListPanel.tsx:19-20` khai trade-off "folder GĐ2 hiện CARD PHIẾU thay 8 ngăn N1-N8; **lối vào nhóm giữ ở `Select` nhóm ngay bên trên**"). Spec cho HĐ **không có** vế tương ứng. +⇒ Cần chốt 1 trong 2 và ghi vào spec: (a) lọc stage-3 leaf theo `type` khi host truyền `typeFilter`; hoặc (b) giữ cây full + bắt buộc có ô chọn loại/`Select` như KHKK. **Đây là câu nên hỏi anh** (xem §Đề nghị, Q2). + +### F-5 · MAJOR — Đọc-ý Thứ-1 chốt một chiều: mũi tên là "dời cây sang trái", nhưng "cột danh-sách biến thành fallback" là **suy diễn của đội**, không phải lời anh + +`anh-mo-ta.md:26-30` ghi trung thực: mũi tên đỏ từ khung CÂY (phải) chỉ SANG khung danh-sách (trái) + lời anh *"Bỏ phần này sang bên này nhé"*; và tự đánh dấu *"Danh-sách HĐ đi đâu … = lane invest đối chiếu khuôn KHKK rồi đề xuất"*. Invest chọn "flat-list = fallback trong CÙNG panel" bằng lập luận khuôn KHKK — hợp lý, **nhưng nó XÓA một khối UI mà anh không nói xóa**, trong khi cách đọc thứ hai (**HOÁN ĐỔI**: cây sang trái, danh-sách sang phải/giữa) cũng khớp y hệt mũi tên và **không mất gì**. +Cộng với F-3 (HĐ ngoài gói vô hình) + F-4 (cây không theo loại), phương án "list thành fallback" là phương án **mất chức năng nhiều nhất** trong 3 cách đọc. ⇒ **Phải hỏi anh trước khi khoá** (Q1), hoặc chọn mặc định an toàn: dời cây sang trái, **giữ danh-sách** ở panel giữa/phải và để panel-3 dashed khi chưa chọn. + +### F-6 · MAJOR — Đ4 bị THU HẸP: lời anh *"Phần thao tác, duyệt, xóa **...** → Cấu trúc tương tự như KHKK"* bị spec cắt còn 3 món, 3 món KHKK-có/HĐ-thiếu bị đẩy ra "ngoài wave" **không đánh số** + +Invest `:91` đo được 3 món cố-ý-không-port: ① ô-tích "Cấp này KẾT THÚC" (`skipToFinal`) ② badge "⚑ Duyệt thay CEO" ③ banner kết-thúc-sớm + meta `{code} v{NN}` — cả 3 đều thuộc đúng chữ **"duyệt"** trong câu của anh, và đều là **hành-vi màn duyệt KHKK trên prod** (chuẩn S176). Spec đẩy xuống dòng cuối `:100-103` dưới nhãn "Ngoài wave — CHỜ ANH (không tự làm)" — nhưng **không có câu hỏi nào được đánh số để anh trả lời**, nên thực tế nó sẽ rơi im lặng đúng như bài `feedback_absence_looks_like_clean`. +Thêm: ảnh-3 mà anh gửi có đúng ô **"Duyệt là KẾT THÚC quy trình (không trình tiếp CEO)"** (chữ xanh) và ô **"Cho phép duyệt thẳng Cấp cuối"** trong dialog Sửa-tại-chỗ (`anh-mo-ta.md:57-58`) ⇒ anh **đang nhìn thấy** 2 cờ đó trong Admin. Cờ bật được ở Designer mà màn duyệt HĐ không đọc = cấu hình câm (đúng lớp `feedback_guard_built_but_never_called`). ⇒ Nâng thành **Q3 đánh số**. + +### F-7 · CRITICAL — W5 mirror NHẦM MODULE: khuôn KHKK (thứ anh chỉ định) **CẤM approver xóa phiếu đang duyệt**; cờ `AllowApproverDelete` là của PE, KHKK chưa từng dùng + +Đo BE, không suy: +- `ContractSigningPlanFeatures.cs:1763-1766` — chỉ **Drafter hoặc Admin** được xoá (`ForbiddenException`), không có nhánh approver. +- `:1768-1783` — allow-list **{DangSoanThao, TuChoi, TraLai}**, và comment tự khai nguyên văn: *"**Xoá phiếu ĐANG duyệt vẫn cấm** (cần rào đúng-lượt như PE)"*, kèm dấu tích S177 *"anh: Trạng thái trả lại cho xóa đc luôn nhé"*. +- `grep -rn "AllowApproverDelete" src/Backend` = 27 hit, **0 hit trong `ContractSigningPlans/`** (hit nằm ở PE + `ApprovalWorkflowV2AdminFeatures` + Domain/Infra/migration). Control dương: PE có đủ cả 2 đầu (`PeSoftDeleteFeatures.cs:110-114` + `PeWorkflowPanel.tsx:129-139` + endpoint `/purchase-evaluations/{id}/by-approver` `:297`). + +⇒ Spec `:53` viết "mirror khuôn PE prod" cho một điều chỉnh mà **lời anh nói "tương tự như KHKK"**. Theo luật S176 (đã-deploy = CHUẨN) + đúng chữ của anh, đích đồng-khuôn là **KHKK**: xoá được ở **Nháp / Trả lại / Từ chối**, **cấm** xoá khi đang duyệt. Mở thêm đường approver-delete = **nhập tính năng của PE**, vượt cả khuôn được trích lẫn câu của anh, và nó đụng **đường xoá dữ liệu** ⇒ phải có chữ "đồng ý" của anh trước. +🟢 Vế còn lại của W5 thì **đúng và cần**: `ContractFeatures.cs:896` so `Phase >= DangInKy` với `DangInKy = 5` (`ContractPhase.cs:20`) trong khi V2 dùng `ChoDuyet=10 · TraLai=98 · TuChoi=99` (`:25-27`) ⇒ hiện **cả 3 trạng thái V2 đều bị chặn xoá**, kể cả phiếu vừa bị Trả lại — lệch đúng cái mà anh đã chốt cho KHKK ở S177. + +### F-12 · CRITICAL — Bằng chứng "KHKK có nút Xóa ở màn duyệt" trỏ vào **CODE CHẾT**; anchor `:118` thật ra là dòng khai ĐÃ GỠ nút đó + +Invest `:90` (và spec `:60-61`) căn cứ: *"KHKK panel mục 4 `KhkkWorkflowPanel.tsx:12-14,118,233`"*. Mở ra: +- `:117-118` nguyên văn: *"🧊 [S175] `isDraftLike` / `canSubmit` / `canDeletePlan` **ĐÃ GỠ** cùng 2 nút *Gửi duyệt* + *Xóa phiếu* — panel này nay chỉ lo màn ĐANG DUYỆT (Duyệt / Trả lại)."* +- `:438-440`: *"*Gửi duyệt* và *Xóa phiếu* **ĐÃ RỜI khỏi đây** → nay nằm ở THANH NÚT ĐÁY"*. +- `:12-14` là **mục lục comment đầu file chưa cập nhật** (vẫn liệt "4 Nút … + nút Xóa phiếu") — cite đúng dòng nhưng dòng đó nói dối. +- Dialog `confirmDelete` còn ở `:742-750` nhưng **`setConfirmDelete(true)` = 0 hit toàn file** (chỉ có `useState(false)` `:107` và 3 chỗ set `false`) ⇒ **không có đường bấm tới**. Nút Xoá thật của KHKK nằm ở host: `KhkkDetailContent.tsx:1077-1096` (thanh nút đáy, `window.confirm`, hướng tới người soạn). + +⇒ Câu "KHKK CÓ mà GĐ3 THIẾU" là **so sánh với một khối code không chạy**. Điều đúng còn lại: `ContractWorkflowPanel.tsx` **0 hit** mọi biến thể "Xóa/Delete" — HĐ thiếu **đường xoá phía người soạn ở màn 3-panel**, và đó mới là món nên port (từ `KhkkDetailContent`, không phải từ `KhkkWorkflowPanel`). +📌 Bài chung: đây là ca `feedback_stale_anchor_born_dead` + `dry-run bắt lời-hứa-chưa-land` — spec đang mang 1 anchor tự-vô-hiệu vào wave. + +### F-7b · minor — Nếu port đường xoá theo KHKK thì phải port cả **vết audit**, spec chưa nhắc + +KHKK ghi `ContractSigningPlanChangelog` (Action=Delete, PhaseAtChange, Reason) và **bắt buộc thứ tự `Remove()` TRƯỚC, `Add(changelog)` SAU** (`:1787-1803`, bug T26 S155 — làm ngược thì cascade `Immediate` DETACH row changelog ⇒ ghi 0 dòng, **im lặng**). Contract hiện xoá trần: `ContractFeatures.cs:891-901` — 0 changelog, 0 `Reason`. Wave nào nới đường xoá cũng phải mang theo vế này, kèm cùng cái bẫy thứ-tự. +🟢 **Câu hỏi treo ② của lead (soft-vs-hard delete) TRẢ LỜI ĐƯỢC NGAY**: `Contract : AuditableEntity` (`Contract.cs:8`) + `AuditingInterceptor.cs:54-63` (đổi `EntityState.Deleted` → `Modified` + `IsDeleted/DeletedAt/DeletedBy`) ⇒ `db.Contracts.Remove(entity)` (`:899`) **đang là SOFT-DELETE**, khớp màn `/contracts/deleted`. Chốt vào spec để wave khỏi mở lại — và để không ai "sửa" `Remove` thành đường khác. + +### F-8 · minor — Acceptance W1(iv) đo được, control-dương đã kiểm + +`grep -rn "workflow-matrix?type=3" fe-user/src` hôm nay = **1 hit** (`Layout.tsx:151`) ⇒ phép đo có răng (không phải 0-hit sẵn). Control dương `workflow-matrix?ct=` cũng sẽ ≥1 sau sửa. ✅ Nhưng lưu ý: nếu theo F-1 mà GIỮ `type=3`, thì **acceptance (iv) phải viết lại** — bằng không nó ép implementer làm đúng cái sai. + +### F-9 · minor — Đếm "17 leaf" của W4 ĐÚNG (đã truy `AwV2_Contract`) + +Nhánh `code === 'Contract'` trong `fe-admin/src/components/Layout.tsx:178` là **nhánh chết**: `grep -rn "AwV2_Contract" src/Backend` = **0 hit** (không có const trong `MenuKeys.cs`, không seed) ⇒ không có menu-row ⇒ sidebar sau W4 = 2 + 8 + 7 = **17**. Acceptance W4(i) giữ nguyên được. +Cơ chế W4 cũng khớp: menu-key `AwV2_Hd{token}` (`MenuKeys.cs:151`, seed `DbInitializer.cs:2303-2310` Order 11..17) ⟂ `contractWfTypeCode('HD-TP') = 'HdTP'` (`contractWorkflowFamily.ts:69,77-80`) ⇒ `findContractWfByTypeCode(code)` khớp đúng phần sau `AwV2_`. **W4 là wave lành nhất — nên chạy TRƯỚC/song song thật sự.** + +### F-10 · minor — "flat-list fallback **server-side**" là delta ngầm so hiện trạng (nhưng khả thi FE-only) + +`MyContractsPage.tsx:73-88` đang tìm **client-side** trên 100 dòng đã tải. Muốn đúng khuôn R-11 (KHKK tìm đi server) thì phải nối `q` → param `search` của `/contracts` — param **có sẵn** (`ContractsController.cs:19` + `ContractFeatures.cs:358`), nên vẫn FE-only. Spec nên nói thẳng "đổi ô tìm sang server-side" thay vì để chữ "server-side" trôi trong ngoặc, kẻo implementer giữ nguyên client-side rồi vẫn tick ✅ acceptance (iii) "gõ search → flatMode". + +### F-11 · minor — Mirror 2 app: 3 cặp file ĐANG identical ở baseline ⇒ acceptance SHA có nghĩa + +Đo `sha256sum` (16 ký tự đầu): `usePipelineStages.ts` `1b9b5c61…` (user ≡ admin) · `PipelineTreePanel.tsx` `4ffc96be…` (≡) · `ContractWorkflowPanel.tsx` `9cdf9ffe…` (≡). `MyContractsPage.tsx` **chỉ có ở fe-user** — đúng như spec khai. +2 chỗ spec nói "KHÔNG mirror" cũng đúng: `contractWorkflowFamily.ts` tự khai fe-admin-ONLY (`:16-18`) và fe-admin ẩn `Ct_*` (`Layout.tsx:246-248` `key.startsWith('Ct_')`). ✅ Không có finding — ghi lại để lượt sau khỏi đo lại. + +--- ## Đề nghị sửa spec -_(điền cuối lượt)_ +**Sửa BẮT BUỘC trước khi mở wave (3):** +1. **§2 Đ2+Đ3** — bỏ chỉ đạo "bỏ `type=3` khỏi URL"; giữ `?type=3&ct=N` (khuôn KHKK `Layout.tsx:216`), chữa bug A bằng nhánh accordion (ưu tiên `ct` + loại trừ pathname `/contracts/workflow-matrix`). Sửa kèm acceptance W1(iv). [F-1] +2. **§2 Đ4-W3** — thay "BE param `pendingMe` sẵn" bằng khuôn PE thật: `pendingMe=1` ⇒ gọi `/contracts/inbox` (0-param) + lọc `type` client-side; hoặc khai W3 CÓ delta BE. Siết acceptance: 1 HĐ không-đến-lượt phải VẮNG. [F-2] +3. **§2 Đ1-W2** — bổ sung M-1 (`!canPe` ⇒ phẳng) + ca "HĐ tạo trực tiếp, ngoài gói" vào cả cách-làm lẫn acceptance; nói rõ ô tìm chuyển server-side. [F-3, F-10] +4. **§2 W5** — viết lại theo khuôn **KHKK** chứ không phải PE: (a) nới guard `ContractFeatures.cs:896` thành allow-list **{DangSoanThao, TraLai, TuChoi}** cho **Drafter/Admin** (đúng `ContractSigningPlanFeatures.cs:1763-1783`), (b) port nút Xoá vào chỗ tương đương `KhkkDetailContent.tsx:1077-1096` — **KHÔNG** lấy `KhkkWorkflowPanel` làm khuôn (code chết), (c) kèm changelog + thứ-tự `Remove` trước `Add`, (d) **tách hẳn** phần approver-delete + `AllowApproverDelete` ra thành mục chờ anh duyệt (Q4). Acceptance phải có ca **"ChoDuyet ⇒ VẪN CẤM xoá"** — đúng chiều khuôn KHKK, ngược hẳn acceptance hiện tại `:94-96`. [F-7, F-12, F-7b] + +**3 câu hỏi ĐÁNH SỐ nên trình anh (thay cho mục "ngoài wave" hiện đang không có số):** +- **Q1 (Thứ-1):** Ý anh là **hoán đổi 2 khung** (cây sang trái, danh-sách chuyển sang giữa/phải) hay **bỏ hẳn khung danh-sách**, để danh-sách chỉ hiện khi tìm/lọc như trang KHKK? *(Cây hiện chỉ chứa HĐ sinh từ Duyệt NCC/KHKK — HĐ tạo trực tiếp sẽ không nằm trên cây.)* +- **Q2 (Thứ-1 × Thứ-2):** Khi anh bấm "01. HĐ Thầu phụ → Danh sách", khung trái nên **chỉ hiện HĐ Thầu phụ**, hay hiện cây đầy đủ mọi loại (như cây hiện nay) và thêm ô chọn loại giống trang KHKK? +- **Q3 (Thứ-4):** Màn duyệt HĐ có cần **3 món KHKK đang có** không: ô "Cấp này KẾT THÚC quy trình", nhãn "Duyệt thay CEO", và dòng báo "kết thúc sớm ở Bước X"? *(Hai cờ này anh đã thấy trong dialog Sửa-tại-chỗ ở Ảnh 3; bật ở Admin mà màn duyệt HĐ chưa đọc.)* +- *(Giữ nguyên câu ① của spec — thay/gỡ ghim `QT-HD-V2-001` — nhưng cũng nên đánh số Q4: nó là điều-kiện để lời "mỗi loại 1 quy trình" đúng tuyệt đối trên DỮ LIỆU, không chỉ trên UI.)* + +**Thứ tự wave — đề nghị đổi:** +`W4` (5 dòng, rủi ro thấp nhất, đã verify khớp cơ chế) → `W1` (bug anh đau nhất) → **chốt Q1/Q2 rồi mới** `W2` → `W3` (phụ thuộc panel của W2: sau khi panel-1 thành cây thì "Đang duyệt" hiển thị ở đâu phải được định nghĩa) → `W5` (sau Q3, gộp cùng lượt nếu anh duyệt 3 món để khỏi mở `ContractDetail` DTO hai lần). +Lý do đổi: W2 là wave DUY NHẤT có quyết-định-của-anh chưa chốt (Q1/Q2) — để nó ở giữa chuỗi sẽ chặn W3. diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens3.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens3.md index aed91a99..0a239a90 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens3.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-review-b2-lens3.md @@ -4,12 +4,246 @@ Reviewer lane-3/3 (ensemble /fable-clone). Lăng-kính: **acceptance ô-nào-gã ## Verdict -_(điền sau khi đo)_ +**FAIL** — 2 BLOCKER (tiền-đề spec SAI, làm đúng theo spec ⇒ đẻ regression nặng hơn bug đang chữa), +4 MAJOR, 4 MINOR. Không phải "sửa chữ" — W1 và W3 phải viết lại phần ② trước khi cho wave chạy. ## Findings (F-1..) -_(append từng finding ngay khi đo xong)_ +### F-1 🔴 BLOCKER — W1: "trang tự suy applicableType=3 theo path" LÀ SAI. Bỏ `type=3` ⇒ leaf 7 loại HĐ hiện quy trình **Duyệt NCC** + +Spec §② Đ2+Đ3: *"WfView → `/contracts/workflow-matrix?ct=N` (BỎ `type=3` khỏi URL — trang tự suy +applicableType=3 theo path, khuôn alias-path khkk sẵn)"*. + +Đo trên đĩa: +- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:57` `const rawType = Number(searchParams.get('type'))` +- `:61` `const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1` + +⇒ **KHÔNG có một dòng nào map pathname → applicableType.** `pathname` (`:56`) chỉ dùng để dựng link +"xem tất cả" (`:131` ``${pathname}?type=${typeInt}``). Bỏ `type` khỏi URL ⇒ `searchParams.get('type')` += `null` ⇒ `Number(null)` = `0` ⇒ rơi vào **fallback `: 1`** = ApplicableType **1 (Duyệt NCC)**. +Hệ quả cụ thể: leaf "Luồng duyệt" của cả 7 loại HĐ gọi `/approval-workflows-v2?applicableType=1` +(`:67`) → hiện **quy trình PE**; rồi nhánh lọc `ct` mới (`code === 'HD-*'`) lọc trên tập PE → **0 dòng, +trang trắng**. Cả 2 nhánh đều tệ hơn bug A/B đang chữa, và bản "hiện quy trình PE" trông *hợp lý* nên +qua mắt được người click thử. + +Control-dương cho claim "khuôn alias-path khkk sẵn": leaf KHKK **VẪN truyền type tường minh** — +`fe-user/src/components/Layout.tsx:86` `'/khkk/workflow-matrix?type=10&group=1'` và `:216` +``/khkk/workflow-matrix?type=10&group=${group}``. Tức alias-path KHKK **không** chứng minh khả năng +tự-suy; nó chứng minh điều NGƯỢC LẠI. Đây là lỗi khớp-khuôn-chéo (mượn UI của khuôn A, gán năng-lực +của khuôn B) — cùng lớp lỗi S187. + +Thêm: `Layout.tsx:151` **hiện tại đã có `ct`** — nguyên văn +``/contracts/workflow-matrix?type=3&ct=${typeInt}``. Nên delta thật của W1 chỉ là (a) trang ĐỌC `ct`, +(b) accordion ưu tiên `ct`. Việc "bỏ `type=3`" là nửa RỦI RO và **không cần** cho cả bug A lẫn bug B. + +**Đề nghị:** xoá mệnh lệnh "BỎ `type=3`" khỏi §②. Giữ URL `?type=3&ct=N`. Nếu vẫn muốn bỏ thì spec +phải giao thêm việc: thêm map `pathname → applicableType` trong `WorkflowMatrixViewPage` **và** sửa +`:131` (link "xem tất cả" đang tự re-add `?type=`) **và** đổi luôn leaf KHKK cho nhất quán — 3 việc, +không phải 0. + +### F-2 🔴 BLOCKER — W3: `pendingMe` KHÔNG tồn tại phía BE Contract. Acceptance W3 là ô "gõ đúng chữ là đạt" + +Spec §② Đ4-W3: *"(khuôn PE `:176`; BE param `pendingMe` sẵn)"*. + +Đo: `grep -rn "pendingMe" src/Backend/SolutionErp.Application/Contracts/ src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` += **0 hit**. Danh sách tham số `ContractsController.cs:16-27` có `page, pageSize, search, sortDesc, +phase, supplierId, projectId, type` — **không có `pendingMe`**. +Control-dương (chứng thước không hỏng): cùng token có hit ở `ContractSigningPlansController.cs` (KHKK) +và `fe-user/src/components/Layout.tsx:176` (PE) ⇒ 0-hit phía Contract là **0 thật**. + +Vì sao đây là BLOCKER chứ không phải "thiếu 1 dòng": ASP.NET Core **im lặng bỏ qua** query-param không +bind được ⇒ `/my-contracts?type=N&pendingMe=1` sẽ trả **TOÀN BỘ** HĐ loại N (không lọc đến-lượt-tôi), +**HTTP 200, không lỗi**. Trong khi acceptance W3(i) là `grep "pendingMe=1" Layout.tsx ≥1` — **gõ chuỗi +vào URL là xanh**. Đúng lớp bẫy S171: ô đo HÌNH-DẠNG-CHUỖI, không đo HÀNH-VI. + +**Đề nghị:** W3 đổi nhãn từ "FE nhỏ" → **BE + FE**; §② phải giao rõ: thêm `pendingMe` vào +`ListContractsQuery` + handler (khuôn precompute V2 `ResolveV2InboxIdsAsync` phía PE), hoặc thay bằng +route `/inbox?type=N` nếu inbox đã có lọc loại. Acceptance thay ô grep bằng ô so-số: +`GET /api/contracts?type=N&pendingMe=1` trả **ít hơn** `GET /api/contracts?type=N` với user có ít nhất +1 HĐ không-đến-lượt (control-dương: user Admin/ngoài luồng phải ra **0 dòng**, không phải cả list). + +### F-3 🟠 MAJOR — W5 mirror khuôn PE mà bỏ mất 3 trong 4 cột trụ của khuôn: đường-TÁCH, policy, và VẾT changelog + +Khuôn PE (`PeSoftDeleteFeatures.cs`) không chỉ là "3 điều kiện" như spec §② trích (`:110-114`): +1. **TÁCH ĐƯỜNG có chủ đích** — `:25-29` nói thẳng: đường xóa nháp cũ **GIỮ NGUYÊN không policy**, vì + gắn policy vào đó làm `Drafter` mất quyền xóa nháp của chính mình (*đo thật trên DB Dev: 11/13 role + `CanDelete=0`*). Đường mới là **endpoint RIÊNG** mang `[Authorize(Policy = "PurchaseEvaluations.Delete")]`. + Spec W5 lại đi hướng **NGƯỢC**: "nới delete-guard `ContractFeatures.cs:896-897`" = nhét cả 2 nghiệp + vụ vào MỘT handler `DeleteContractCommand` — đúng thứ khuôn PE đã cân nhắc rồi loại bỏ. +2. **VẾT changelog** — `:147-158` ghi `ContractChangelog`-tương-đương với `Action=Delete`, `PhaseAtChange`, + `UserId`, `ContextNote=lý do`. Spec W5 **không nhắc chữ changelog nào**. Đối tượng bị xóa ở đây là + **hợp đồng** (chứng từ pháp lý) mà lại xóa không vết, trong khi phiếu PE thì có vết. Hạ tầng đã sẵn: + `src/Backend/SolutionErp.Domain/Contracts/ContractChangelog.cs` tồn tại + + `IApplicationDbContext.cs:40 DbSet ContractChangelogs`. +3. **Bẫy thứ-tự T26** — `:127-144`: phải `Remove()` **TRƯỚC**, `Add(changelog)` **SAU**; đảo lại thì + cascade client-side (`CascadeDeleteTiming.Immediate`) DETACH changelog ⇒ **ghi 0 row, im lặng, không + lỗi**. Nếu W5 có changelog mà spec không chép cảnh báo này sang, wave sẽ dẫm lại đúng bug đã trả giá. +4. Chỉ có "3 rào" là được mirror. + +**Đề nghị:** §② W5 viết lại theo khuôn TÁCH ĐƯỜNG: command + endpoint riêng +(`DeleteContractByApproverCommand`, `POST/DELETE /api/contracts/{id}/by-approver`) mang policy, GIỮ +`DeleteContractCommand` cũ nguyên vẹn cho nháp; + ghi `ContractChangelog` với đúng thứ tự Remove→Add +(chép nguyên chú thích T26); + acceptance thêm ô: sau khi xóa, `GET` lịch sử đổi của HĐ đó phải còn +**đúng 1 dòng** action Delete (đọc NGƯỢC từ DB — nếu đọc ra 0 dòng là đã dẫm T26). + +### F-4 🟠 MAJOR — W5 chạm đường DELETE đang **không có policy per-action** (#82) mà acceptance không có ô authz nào + +`src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:11-14` = `[ApiController] [Route] [Authorize]` +**trần** (không Policy). `:78-83` `[HttpDelete("{id:guid}")]` **không** có `[Authorize(Policy=...)]`. +Đối chứng ngay trong cùng file: màn "Đã xóa" (GET) lại tự khai *"policy root `Contracts.Read` (chặt hơn +`[Authorize]` trần của class)"* ⇒ tác giả cũ **biết** class-level là trần, và đã siết cho GET nhưng +**chưa siết cho DELETE**. + +Nghĩa là hôm nay: **bất kỳ user đã đăng nhập nào** cũng xóa được HĐ ở phase < `DangInKy` (kể cả HĐ của +người khác — handler `ContractFeatures.cs:889-901` không đọc `ICurrentUser`, không kiểm chủ sở hữu). +W5 **nới** guard này ra phase `ChoDuyet/TraLai/TuChoi` ⇒ mở rộng đúng cái lỗ đó sang HĐ đang chạy duyệt. +Acceptance W5 (i)-(iv) **không có một ô nào** về authz. + +**Đề nghị:** thêm ô acceptance test-before: (a) user thường (không role, không trong Cấp) `DELETE` HĐ +nháp của người khác ⇒ phải 403 (hôm nay 204 — ghi rõ đây là **sửa lỗ có sẵn**, không phải regression do +W5 đẻ ra); (b) đường xóa-bởi-approver có `[Authorize(Policy = "Contracts.Delete")]`; (c) grep chứng +policy đã gắn. Nếu owner muốn giữ nguyên hành vi cũ cho nháp thì phải khai **thành lời** trong spec +(accepted-risk), không để im lặng. + +### F-5 🟠 MAJOR — W5 acceptance thiếu ca `DaPhatHanh(9)` — chỗ duy nhất mà "V2-aware" dễ mở nhầm + +`ContractPhase`: `DangInKy=5 … DaPhatHanh=9, ChoDuyet=10, TraLai=98, TuChoi=99` +(`src/Backend/SolutionErp.Domain/Contracts/ContractPhase.cs`). Guard cũ `Phase >= DangInKy` chặn +**5→99** (chặn thừa V2, đúng như finding S160). Acceptance W5(i) liệt: TraLai/TuChoi cho xóa · ChoDuyet +có điều kiện · "V1 `Phase>=DangInKy` VẪN chặn". + +Lỗ: `DaPhatHanh = 9` vừa nằm trong dải "V1 legacy ≥5" **vừa là terminal THÀNH CÔNG của V2** (chú thích +enum: *"last step done ──► DaPhatHanh (terminal + gen mã HĐ)"*). Một bản viết lại "V2-aware" kiểu +`if (v2) { allow ChoDuyet/TraLai/TuChoi/DangSoanThao } else { block >= DangInKy }` sẽ **không còn** ô +nào chặn V2-`DaPhatHanh` một cách tường minh — HĐ **đã duyệt, đã gen mã** có thể bị xóa. Không ô +acceptance nào bắt được. + +**Đề nghị:** thêm ô bắt buộc: "HĐ V2 phase `DaPhatHanh(9)` (đã gen `MaHopDong`) ⇒ mọi đường xóa đều +409/403" + ô đối xứng "HĐ V1 phase `DangSoanThao(2)` vẫn xóa được như cũ" (chống vá quá tay). + +### F-6 🟡 MINOR-nhưng-chắc-gãy — W1(iv): lệnh grep viết như trong spec **đo trượt** dưới ripgrep / `grep -E` + +Acceptance W1(iv): `grep -rn "workflow-matrix?type=3" fe-user/src` = 0-hit + control `workflow-matrix?ct=` ≥1. + +Chạy thật, cùng cây, cùng lúc: +- `grep -rn "workflow-matrix?type=3" fe-user/src` (BRE, `?` là ký tự thường) → **1 hit** + `fe-user/src/components/Layout.tsx:151`. +- `rg -n "workflow-matrix?type=3" fe-user/src` → **0 hit** (rg mặc định regex: `x?` = "x có thể vắng" + ⇒ mẫu đòi chuỗi `workflow-matritype=3`/`workflow-matrixtype=3`, không có thật). +- `rg -n "workflow-matrix?ct=" fe-user/src` → **0 hit** — tức control-dương cũng gãy theo. + +⇒ Nếu wave dùng `rg` (mặc định của Grep-tool) hoặc `grep -E`, ô W1(iv) **xanh ngay trước khi sửa gì**. +May là control-dương cũng 0 ⇒ người cẩn thận sẽ thấy thước hỏng; nhưng spec đang mời gọi đọc "control +0-hit" thành "chưa sửa xong" thay vì "thước hỏng". + +**Đề nghị:** ghi lệnh có `-F`: `grep -rnF 'workflow-matrix?type=3' fe-user/src` (hoặc escape `\?`), và +ghi rõ **kỳ vọng của control** (`≥1 ngay từ trước khi sửa` — vì `ct=` ĐÃ có ở `:151` hôm nay). + +### F-7 🟠 MAJOR — W1 đẻ **bản sao thứ BA** của họ `HD-*` trong fe-user, và acceptance chỉ phủ **1/7** mã + +Spec §②: *"map ContractType→mã: const cục bộ 7-entry — fe-user KHÔNG mirror lib fe-admin"*. +Sau W1 sẽ có 3 bản danh sách họ: BE `src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs` +(nguồn thật — seeder `DbInitializer.cs:775 foreach (…Family.Members)` sinh mã từ đây) · +`fe-admin/src/lib/contractWorkflowFamily.ts` · const mới trong fe-user. + +Lệch **CÂM**: sai 1 ký tự trong mã ⇒ `allWorkflows.filter(wf => wf.code === …)` ra **mảng rỗng** ⇒ +trang trắng, trông y hệt "loại này chưa cấu hình quy trình". Chính lib fe-admin đã ghi cảnh báo này +(`contractWorkflowFamily.ts:2-8`) và còn ghi rõ bẫy chẻ chuỗi: *"`HdNTNCC` chẻ ra `NT-NCC` hay `N-TNCC` +là hai mã khác nhau"* (`:70-73`). + +Acceptance W1(iii) chỉ đo **`ct=1` → `HD-TP`** = 1/7. Hai mã dễ sai nhất (`HD-NT-NCC`, `HD-NT-DV` — +token gạch nối) **không có ô nào**. + +**Đề nghị:** đổi ô (iii) thành vòng 7 giá trị: với mọi `ct ∈ 1..7`, trang hiện **≥1** quy trình và +**0** dòng mã khác họ; ghi kèm control-dương `ct` rác (`ct=99`) ⇒ **không lọc** (giữ hành vi cũ, không +trang trắng) — đúng khuôn `group` rác đã chốt ở `WorkflowMatrixViewPage.tsx:82-83`. + +### F-8 🟠 MAJOR — 9/20 ô acceptance là **hành-vi**, mà spec không ghi **đo ở đâu** khi CI hỏng P0 + deploy CHỜ + +Ô hành-vi: W1(i)(ii)(iii) · W2(ii)(iii) · W3(ii) · W4(i)(ii) · W5(iii-render). Spec §① tự khai +*"DEPLOY CHỜ (CI runner workspace còn hỏng P0)"* ⇒ **không có prod để bấm**, và `npm run build` (ô +duy nhất chạy được) **không chứng minh gì** về hành vi. Đây đúng lớp S171: ô đọc thì hợp lý, tới lúc +chạy mới lộ là không có chỗ chạy ⇒ wave sẽ tick bằng suy luận. + +Nấc đo local **có thật, rẻ** — nên ghi thẳng vào spec thành bước bắt buộc trước khi tick: +1. `dotnet run --project src/Backend/SolutionErp.Api` (LocalDB `SolutionErp_Dev`). Seeder chạy lúc + khởi động và **idempotent per-code** (`DbInitializer.cs:762-764,775`) ⇒ 7 `HD-*` tự có, + `IsUserSelectable = true` (`:797`) — thoả tiền-đề lọc `isUserSelectable: true` của trang + (`WorkflowMatrixViewPage.tsx:67`). Sample cũ `QT-HD-V2-001` (`:257`) vẫn còn ⇒ ô "0 dòng + QT-HD-V2-001" mới có ý nghĩa (nếu Dev sạch trơn thì ô đó xanh vì **vắng**, không phải vì **lọc**). +2. `npm run dev` ở `fe-user` (proxy `/api → localhost:5443`) — bấm đủ W1/W2/W3; `fe-admin` cho W4. +3. **Chuẩn bị DỮ LIỆU** (spec đang thiếu hẳn): W2(ii) cần ≥1 HĐ hiện trên cây GĐ3; W3(ii) cần ≥1 HĐ + `ChoDuyet` mà user đăng nhập **đúng lượt** + ≥1 HĐ **không** đến lượt (nếu không có vế thứ hai thì + ô "list khớp inbox" xanh cả khi lọc **không chạy** — xem F-2); W5(iii) cần 1 Cấp bật cờ + `AllowApproverDelete` và 1 Cấp tắt. + +**Đề nghị:** thêm mục "④ Nấc đo local" vào spec với 3 bước trên; ô nào không đo được local thì đánh +dấu **DEFERRED-TO-DEPLOY** công khai, KHÔNG tick. + +### F-9 🟡 MINOR-cao — W2 sửa cặp hook/panel **dùng chung cho GĐ1/GĐ2/GĐ4** mà không có ô hồi-quy + +Đo baseline (bất biến mirror còn NGUYÊN hôm nay — tiền-đề W2(iv) HỢP LỆ): +`fe-user|fe-admin/src/hooks/usePipelineStages.ts` = `1b9b5c61…52ff48` (khớp) · +`fe-user|fe-admin/src/components/pipeline/PipelineTreePanel.tsx` = `4ffc96be…c6c334` (khớp). + +Nhưng cặp này là cây toàn-trình 4 GĐ (S162), fe-admin **cũng** render nó trong khi fe-admin **không có** +`MyContractsPage` ⇒ tuỳ chọn `onOpenContract` phải **mặc định TẮT**. Acceptance W2 chỉ có SHA-pair + +build — cả hai đều xanh y nguyên nếu opt-in lỡ thành opt-out (SHA vẫn khớp vì sửa **cả 2 bản**!). + +**Đề nghị:** thêm ô control-âm: "trên fe-admin (không truyền `onOpenContract`), click leaf GĐ3 vẫn +`navigate('/contracts/{id}')` như cũ" + ô "leaf GĐ1/GĐ2/GĐ4 hành vi không đổi ở CẢ 2 app". + +### F-10 🟡 MINOR-cao — W5: đường xóa HĐ hiện **0 test**, và acceptance không pin số test / không nhắc STATUS + +- `grep -rl "DeleteContract" tests/` → chỉ trúng **file .dll** trong `bin/obj`, **0 file nguồn** ⇒ + `DeleteContractCommand` chưa từng có test. File duy nhất lân cận là + `tests/SolutionErp.Domain.Tests/Contracts/WorkflowPolicyTests.cs` (có nhắc `DangInKy`) nhưng nó test + **policy chuyển phase**, không test guard xóa ⇒ **không có lưới nào** đỡ nếu W5 nới sai. +- Số canonical hiện tại: **680** (`docs/STATUS.md:6`, S187→S188 "test 668→680"). ⚠️ Dòng `:20` còn ghi + **644** của khối cũ hơn — người đo vội dễ lấy nhầm; spec nên ghi rõ mốc lấy từ `:6`. +- Acceptance W5(i) chỉ nói "`dotnet test` PASS + test mới …" — không pin **680 → 680+N**, không nhắc + cập nhật **DUY NHẤT** row test trong `docs/STATUS.md` (B1). Theo `docs/rules.md §7` guard mới = + test-before BẮT BUỘC (spec có ghi ✔) nhưng "PASS" là ô 0-bit nếu số không tăng. + +**Đề nghị:** ô (i) ghi rõ: "trước: 680 · sau: 680+N (liệt N ca)" + "cập nhật đúng 1 row test ở +`docs/STATUS.md`, không chép số ra chỗ khác". + +### F-11 🟡 MINOR — W2 đổi trục điều hướng, thiếu ô **tương-thích-ngược deep-link** + +W2 chuyển chọn HĐ sang `?id=` trong `/my-contracts`. Đường cũ `/contracts/{id}` đang là đích của +`usePipelineStages.ts:377` **và** của link/thông báo cũ (bookmark, notification). Acceptance W2 không có +ô nào chứng đường cũ còn sống. +**Đề nghị:** thêm ô: "dán thẳng `/contracts/{id}` vào thanh địa chỉ ⇒ vẫn mở detail như cũ (không 404, +không redirect mất id)". + +### Held — 5 tiền-đề tôi cố bẻ mà KHÔNG gãy (ghi để wave khỏi đo lại) + +| # | Claim spec | Phép đo | Kết | +|---|---|---|---| +| H-1 | Cặp mirror ×2 app đang khớp (tiền-đề W2-iv/W5-iii) | `sha256sum` 4 file + 4 file panel | **HELD** — `usePipelineStages` `1b9b5c61…` · `PipelineTreePanel` `4ffc96be…` · `ContractWorkflowPanel` `9cdf9ffe…` · `KhkkWorkflowPanel` `e7b294c9…`, từng cặp KHỚP | +| H-2 | W4 một dòng `findContractWfByTypeCode(code)` đúng chỗ | đọc `fe-admin/src/components/Layout.tsx:175-189` | **HELD** — chèn *sau `:188`* nằm TRONG khối `awV2Match`, ở đó `code = awV2Match[1]` = phần sau `AwV2_` (`HdTP`…), đúng thứ `findContractWfByTypeCode` tra (`contractWorkflowFamily.ts:69,77-80`). Ngoài khối là sai — spec nên ghi "trong khối `if (awV2Match)`" cho chắc | +| H-3 | Designer/route đã sẵn cho `:typeCode` họ HĐ | `App.tsx:80` route `:typeCode` + `ApprovalWorkflowsV2Page.tsx:39,314` gọi `findContractWfByTypeCode` | **HELD** — W4(ii) khả thi | +| H-4 | `view=approved` chữa được bug B (2 leaf cùng sáng) | đọc `queryMatches` `fe-user/Layout.tsx:350,368-374` | **HELD** — `view` KHÔNG nằm trong `TRANSIENT_QUERY_KEYS`, `phase` chỉ được bỏ qua khi **đích không ghim** ⇒ đích "Danh sách" `[type]` không còn khớp URL `[type,phase,view]`. Đúng khuôn PE `:180-185` | +| H-5 | W4 "17 leaf" | `MenuKeys.cs:118-119` (2 NCC) + `:129-136` (8 KHKK) + `:151` `AwV2_Hd{token}` × 7 thành viên họ | **HELD** — 2+8+7 = 17 | +| H-6 | Lead-note "soft hay hard delete?" | `AuditingInterceptor.cs:56-62` | **ĐÃ TRẢ LỜI, đừng để wave đi đo lại:** `Contract : AuditableEntity` (`Contract.cs:8`) ⇒ interceptor đổi `Deleted → Modified` + set `IsDeleted/DeletedAt/DeletedBy` ⇒ `db.Contracts.Remove` **là XÓA MỀM**, giống hệt PE. Màn `/contracts/deleted` an toàn. **Nhưng** vẫn dính bẫy T26 nếu W5 thêm changelog — xem F-3 | ## Đề nghị sửa spec -_(điền cuối)_ +Theo thứ tự phải-làm-trước: + +1. **§② Đ2+Đ3: xoá mệnh lệnh "BỎ `type=3` khỏi URL"** (F-1). Giữ `?type=3&ct=N`; delta của W1 = + trang đọc `ct` + accordion ưu tiên `ct` + comment stale. Nếu owner vẫn muốn URL sạch thì spec phải + liệt thêm 3 việc (map pathname→type · sửa link `:131` · đồng bộ leaf KHKK) và nhận rủi ro. +2. **§② Đ4-W3: bỏ chữ "BE param `pendingMe` sẵn"** (F-2) — đổi W3 thành wave **BE+FE**, hoặc chuyển + sang `/inbox?type=N` nếu inbox đã lọc loại. Ô grep thay bằng ô so-số 2 chiều. +3. **§② W5: viết lại theo khuôn TÁCH ĐƯỜNG của PE** (F-3): command/endpoint riêng + policy + (`Contracts.Delete`) + `ContractChangelog` với thứ tự `Remove()` → `Add()` (chép nguyên cảnh báo + T26) + GIỮ `DeleteContractCommand` cũ cho nháp. +4. **§③ W5 thêm 3 ô:** authz (403 cho người ngoài luồng — F-4) · `DaPhatHanh(9)` vẫn chặn (F-5) · + changelog đọc-ngược ra đúng 1 dòng (F-3). +5. **§③ W1(iv): `grep -rnF` (hoặc `\?`)** + khai kỳ vọng control-dương là "≥1 **ngay từ trước** khi + sửa" (F-6); **W1(iii) mở thành vòng `ct ∈ 1..7`** + ca `ct` rác (F-7). +6. **§ mới "④ Nấc đo local"** — `dotnet run` + `npm run dev` + 3 dòng chuẩn-bị-dữ-liệu; ô nào không đo + được thì đánh **DEFERRED-TO-DEPLOY**, cấm tick (F-8). +7. **§③ W2 thêm ô control-âm fe-admin + ô deep-link `/contracts/{id}`** (F-9, F-11). +8. **§③ W5(i) pin số test 680 → 680+N + nhắc cập nhật đúng 1 row `docs/STATUS.md`** (F-10). diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md new file mode 100644 index 00000000..c5c24acd --- /dev/null +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md @@ -0,0 +1,20 @@ +# SUB — back-end-reviewer-style — B3 SPEC-LIST STYLE BE (YC-029, run SOLTUON-12-08-2026) + +> Engine: /fable-real single deep-pass · **propose-only** — lead verify rồi mới dùng. +> Đề: SPEC-LIST STYLE để worker Opus MAX (B6) viết W5 (DeleteContract allow-list + `[Authorize(Policy)]` + test-before) **KHÔNG lệch khuôn bản ĐÃ DEPLOY** (luật owner S176: đã deploy production = CHUẨN). + +## Khuôn nguồn BE + +(đang đọc — append dần) + +## SPEC-LIST STYLE BE (SB-1..) + +(append sau khi đọc khuôn) + +## Bẫy style BE repo này + +(append) + +## Checklist style ĐO ĐƯỢC cho worker + +(append) diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md new file mode 100644 index 00000000..d7e1278a --- /dev/null +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md @@ -0,0 +1,20 @@ +# sub-style-fe-b3 — SPEC-LIST STYLE FE (YC-029 B3, /fable-real deep-pass) + +> Vai: front-end-reviewer-style · Run: SOLTUON-12-08-2026 · Trạng thái: ĐANG GHI (append từng phần) +> Luật lõi owner S176: bản ĐÃ DEPLOY PROD = CHUẨN. Spec-list này = luật style ĐO ĐƯỢC cho worker Opus (B6) viết wave W1-W5 KHÔNG lệch khuôn. + +## Khuôn nguồn (file nào là chuẩn cho việc gì) + +(đang ghi) + +## SPEC-LIST STYLE FE (đánh số SF-1..) + +(đang ghi) + +## Bẫy style đã thấy trong repo + +(đang ghi) + +## Checklist style ĐO ĐƯỢC cho worker + +(đang ghi)