diff --git a/.claude/WAL.md b/.claude/WAL.md index 6410644..891af14 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -5,17 +5,5 @@ goal: DRY-RUN TOÀN TRÌNH ĐẾN HỢP ĐỒNG CỨNG (lệnh anh @S160 — ver chain: - [x] closeout arc-2 + chốt đầu việc (commit 1d3d167 local; push chờ squash session-end) -- [!] wf: run `2026-07-29-S160-khkk-dryrun-plan` — scaffold run.md (verbatim lệnh anh) + extract RG-001 v02 + 2 lane /fable-real invest (lane-1 nguồn/quy-trình GĐ3-GĐ4 + lane-2 codebase dry-run map) -- [ ] Plan cha + spec-chi-tiết + checklist per-wave (lead viết từ 2 invest + spec cũ) -- [ ] /fable-clone reviewer (ensemble) -- [ ] /fable-real reviewer chốt cuối -- [ ] hmw Opus-5-MAX chạy dry-run hết các wave theo Plan cha, kiểm từng wave - -next: hoàn thành mục [!] → lần lượt các [ ] theo thứ tự. Nguồn: FORM = D:\Dropbox\CONG_VIEC\SOLUTION\FORM (8 file — RG-001 ở đây là V02, repo parse Phase 0 cần diff) · QUY_TRINH = D:\Dropbox\CONG_VIEC\SOLUTION\QUY_TRINH (QT TRINH KY 21 bước — đã có extract S157 src-QT-TRINH-KY-HD.txt). Spec cũ: runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md (+note W1 @S160). - -verify: -- [ ] run-folder tồn tại + run.md có verbatim lệnh anh + 2 sub-file invest ghi-đĩa-trong-lúc-làm (chống #53) -- [ ] Plan cha + N spec-wave tồn tại trong run-folder -- 🔴 GATE 5-ANCHOR (HANDOFF): trước thi công MỖI wave mở 5 anchor, ≥1 trượt ⇒ chấm lại toàn bộ — PHẢI nhúng vào Plan cha -- 🔴 An-ninh HĐ (S156 treo "cứ từ từ"): dry-run chạm HĐ ⇒ invest phải khoanh vùng authz, đưa vào spec để review chốt -- prod state: menu 142 · test 562 · Mig 68/89 bảng · bundle admin D0sXA0fe/user Y6dW_5CM +- [x] 2 lane /fable-real invest XONG + LEAD-VERIFIED (7 claim byte-đúng; lane-2 #53 ×1 → resume sạch; +đo prod Contracts = 0 active + 0 deleted, sổ "7 HĐ V1" S156 STALE) +- [x] PLAN CHA + 7 SPEC-WAVE đã ghi (plan-cha + w1/w2/w3/w5/w6/w7/w8; W4/W9 defer; 12 default đảo được; 1 mig duy nhất 69/W1) + harvest diary investigator-codebase (real-fable-S diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md index 7d20f47..1d51a49 100644 --- a/.claude/agent-memory/investigator-codebase/MEMORY.md +++ b/.claude/agent-memory/investigator-codebase/MEMORY.md @@ -8,10 +8,11 @@ Read-only INTERNAL audit SOLUTION_ERP. Tools: Read/Grep/Glob/Bash + 5 RAG MCP. O - ❌ NOT: external docs/CVE/lib → investigator-api · write → implementer · test → test-specialist · architecture decision → em main ## 📌 Recent invest (1-line digest; full detail → archive/runs/git) +- **S160 (07-29) DRY-RUN toàn-trình đến HĐ cứng — 2 lane `/fable-real` [engine-label real-fable-S160; lane-2 lượt-1 #53 garble → resume sạch, đĩa mất 0 B] [`runs/2026-07-29-S160-khkk-dryrun-plan/sub-investigator-codebase-{1,2}.md` 24.2K+32.2K]:** Lane-1 nguồn: ranh GĐ3=b.13→18/GĐ4=b.19→21 ĐÃ ratify @S159 spec `:27` (đừng suy lại từ QT); RG-001 v02 CHỈ 4 viết tắt — **codegen tự chế "MB"** (`ContractCodeGenerator.cs:25`, không gốc quy định); bản cứng vật lý SINH ở b.17-18 ∈GĐ3 (GĐ4 hiển thị readonly phần nó không tạo); FO-002.01 tick-box 6 loại ≠ 7 ContractType; forms-spec = parse chính v02, 0 lệch format. Lane-2 code: `Hdc_*` = **7-leaf-per-ContractType** (loop `DbInitializer.cs:1862-1867`) KHÔNG phải 6-leaf — đề bài lead sai 2 grounding, lane tự bẻ; KHÔNG có phase KyCung (8=DangDongDau LEGACY, terminal=DaPhatHanh 9 → GĐ4 = DERIVE sau terminal, 0 mig); 🔴 `DeleteContract` guard `Phase >= DangInKy` so sánh SỐ ⇒ chặn xóa cả 9/10/98/99 (dry-run rác phải admin-override hoặc chấp nhận); mặt tấn công thật = nhánh Reject-trước-guard `ContractWorkflowService.cs:49-66` (Approve CÓ guard); "khuôn PE per-action" KHÔNG tồn tại tầng attribute (PE class-trần + handler-Forbidden) — tiền lệ per-action thật = `/deleted` `ContractsController.cs:28-29` + Suppliers; `Contracts` ∈ All ⇒ 4 policy sống sẵn 0-key-mới. Lead đo bổ sung: **prod Contracts = 0 active + 0 deleted** (admin bypass `:298/:440` — sổ "7 HĐ V1" S156 STALE); login prod parse field `accessToken` (không phải `token`). Đầu ra: plan-cha + 7 spec-wave (W1-W3,W5-W8; W4/W9 defer) cùng run-folder. Tag `[s160, dry-run-toan-trinh, hdc-7-leaf-per-type, delete-guard-numeric-compare, reject-truoc-guard, contracts-policy-co-san, mb-khong-goc, prod-contracts-0]` - **S157 (07-28) KH-ký-kết-HĐ b.7→12 [§A+§B đĩa, §C/§D PENDING lượt sau, `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/sub-invest-kehoach-1.md`]:** Owner chốt O-1 *"cấu trúc trình ký GIỐNG, khác nội dung"* ⇒ tái dùng khung V2, dựng 7 bảng NỘI-DUNG `ContractSigningPlan*` (Mig 69 thuần CreateTable; mig cuối=68 verify ls) + `ApplicableType=10` (slot trống `ApprovalWorkflow.cs:53-67`). 🔴 Bẫy twin MỚI: **Proposal-lite ApproveV2 PHÁ OR-of-N** — `ProposalFeatures.cs:427-439` flatten global-level + `ElementAtOrDefault` match 1-user, entity không có StepIndex (`Proposal.cs:27`) ⇒ 2-row-cùng-Order thành AND tuần tự; KH service PHẢI copy `ContractWorkflowService.ApproveV2Async:217-394`, Proposal chỉ làm khuôn CRUD/menu/FE. Twin đo thật: Proposal BE 849 · FE 1.956 LOC · 4 bảng (Mig 38 grep CreateTable) · PE 6.5K/14.9K cận trên. `ConflictException :115-116` tự xác minh đĩa (skill contract-workflow stale — fallback V1 chỉ sống đường display). ISO doc 9 bước ↔ `ContractPhase` 1-9 khớp 1:1 (7 LEGACY = ISO mã-hoá cứng); khúc b.7-12 thiếu CẢ ISO-doc lẫn code; FO-002.01 có luật auto-pass-quá-01-ngày (mâu thuẫn V2 no-auto-approve; O-2 SLA=tham-khảo). #53: coordinator chặn skeleton-rỗng @202K-token → cắt còn §A+§B, Edit-liền-tay từng mục. Tag `[s157, ke-hoach-ky-ket, proposal-lite-breaks-or-of-n, iso-9-steps-eq-contractphase]` - **S156 (07-27) BCH post-CEO flow bước 7→21 [§0-§3+§6 đĩa, §4-§5 PENDING lượt sau, `runs/2026-07-27-S156-bch-post-ceo-flow/sub-invest-bch-1.md`]:** Verdict **LAI** — 13→21 TÁI DÙNG Contract (7 dòng `[LEGACY]` `ContractPhase.cs:11-13` chính LÀ trạm 13-21 đã gỡ CỐ Ý Mig21/S17; V2 `ContractWorkflowService.cs:217-394` cookie-cutter sẵn, terminal gen-mã+DaPhatHanh) · 7-12 DỰNG MỚI (0-hit shopdrawing/TVGS, khuôn 12-bis + `ApplicableType=10` append-only). 🔴 Chuỗi "cơ-chế đúng, thứ đi qua KHÔNG có" phía HĐ: cầu PE→HĐ pin V1 (`CreateContractFromEvaluationFeatures.cs:68-71,108`) không nhận V2 + UpdateDraft không re-pin · `GetEligiblePhases` (`ContractFeatures.cs:334-351`) + inbox `PhaseActorRoles` (`:363-372`) 100% LEGACY-phase ⇒ approver V2/BCH KHÔNG xem/inbox được HĐ ChoDuyet · `CeoApprovalThreshold` 0-consumer HĐ (bước-18 ">5 tỷ" chưa có máy) · SLA V2 hardcode `AddDays(7)` 3 site, Step không có SlaDays · notify chỉ Drafter (`:407`) · `AttachmentPurpose` thiếu ký-nháy (enum-extend 0-mig tiền lệ PE=4) · HĐ TraLai reset-toàn-phần không 4-mode. Mig 68 `AddPeAllowApproverDelete` ĐÃ land 07-27 ⇒ mig kế = 69. Bắt lead sai grounding: 12 enum/7 LEGACY (lead ghi 9/6) — lead nhận. #53 strike-1 (return bốc hơi, đĩa nguyên → lead đọc từ đĩa, không redo). Tag `[s156, bch-post-ceo, verdict-lai, eligible-phases-legacy-only, bridge-pins-v1]` -- **S155 (07-27) PE xóa-phiếu màn DUYỆT [24 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-be-1.md`]:** DELETE `/api/purchase-evaluations/{id}` ĐÃ CÓ (`PurchaseEvaluationsController.cs:146`, authz = class `[Authorize]` TRẦN) nhưng allow-list `{DangSoanThao,TuChoi}` (`PurchaseEvaluationFeatures.cs:1404`) chặn ChoDuyet. `Remove()`=**SOFT** gián tiếp qua `AuditingInterceptor.cs:56-62` — lớp #81-EXT indirect. Lũy kế: `PeBudgetAccumulator.cs` 2 hàm/4 call-site DUY NHẤT; ChoDuyet ăn số **2 chỗ** (`ComputeAsync:54` PrevSubmitted + `ComputePendingAsync:114` PendingSelected); `TraLai` KHÔNG triệt tiêu; `TuChoi` rơi khỏi 100% phép cộng. Query 0 dòng `IsDeleted` → dựa global `HasQueryFilter` (`PurchaseEvaluationConfiguration.cs:84`) ⇒ soft-del TỰ rớt, không sửa accumulator. Snapshot Mig67 KHÔNG re-compute. 🔴 Cascade 6 FK (`:75-80`) **không bao giờ chạy** vì soft ⇒ con `BaseEntity` ở lại = rác ẩn. **Git khảo cổ TRÚNG:** "chỗ cho hủy" = nút Từ chối, gỡ `6db195d` 12-06 S60; guard `PurchaseEvaluationWorkflowService.cs:101-106` còn sống tự trỏ "Xóa phiếu khi còn Bản nháp" = chính cái lỗ. 0 tiền lệ hủy chứng-từ in-flight; khuôn gần nhất `CancelMeetingBookingHandler` (`MeetingFeatures.cs:457`, Status=Cancelled NOT IsDeleted). Tag `[s155, pe-delete-approver, s60-tuchoi-removal-hole, soft-delete-no-cascade]` -- **S155 (07-27) PE xóa-phiếu FE/authz-slice [18 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-fe-2.md`]:** Khối HÀNH ĐỘNG = `components/pe/PeWorkflowPanel.tsx:458-506` **byte-identical 2 app** (md5 `02671be6…`, cùng PeDetailTabs/PeListPanel/ListPage) ⇒ sửa-1-bên-là-sót. Gate 3 tầng: `?pendingMe=1`→`readOnly={!pendingMe}` (`ListPage:590`/`:676`) · `next=workflow.nextPhases` BE-source (`:257`) · `blockedByV2Level` (`:99-103` isAdmin ∥ actor∈currentApproval.approvers). 🔴 Plumbing xóa ĐÃ NỐI SẴN vào màn duyệt: `onDelete={del.mutate}` truyền ở `ListPage:573`+`:668` (`api.delete('/purchase-evaluations/:id')` `:90`) nhưng nút chết vì `PeDetailTabs:445` đòi `mode==='workspace' && canEditPhase && !readOnly`; nút hiện chỉ ở `:460` `phase===DangSoanThao`. **Authz 2 tầng:** PE vùng FE = **0-hit** `usePermission|PermissionGuard` (guard chỉ sống ở Master+Users, khuôn `DepartmentsPage:101 action="Delete"`); `menuKeys.ts` KHÔNG có const `Pe_*` (chỉ root `:23`) — `Pe_*` sinh BE `MenuKeys.cs:134-141`, seed `DbInitializer:1870-1877`, FE khớp bằng regex `Layout.tsx:120` (fe-admin `:107` **LỆCH**, thiếu WfView). 🔴 `Pe_*` **KHÔNG ∈ `MenuKeys.All`** ⇒ `Program.cs:82-89` không đăng-ký policy `Pe_*.{action}` (0 dynamic provider) ⇒ dùng `[Authorize(Policy="Pe_…")]` = policy-chưa-tồn-tại; **dùng được ngay = `PurchaseEvaluations.Delete`** (root ∈ All, + `GetMyMenuTreeQuery:66-72` cho `Pe_*` KẾ THỪA cờ CRUD từ root ⇒ 0 key mới, 0 migration). Ma trận enumerate `db.MenuItems` (`PermissionFeatures.cs:20`) nên ô Delete đã hiện sẵn. Q6: thêm-nút KHÔNG kích 4-place (0 route/page/staticMap `Layout.tsx:56-106`); `Pe_*` resolve nhánh regex `:119-134` KHÔNG qua staticMap. Tag `[s155, pe-delete-fe-authz, two-app-byte-identical, pe-keys-not-in-All]` +- _(**S155 (07-27) PE xóa-phiếu màn DUYỆT [24 finding]** — DELETE sẵn+allow-list chặn ChoDuyet · soft-delete indirect #81-EXT · lũy kế 2-chỗ-ăn-số · git-khảo-cổ S60 gỡ nút Từ-chối. verbatim → `archive/2026-07.md` substring:"S155 (07-27) PE xóa-phiếu màn DUYỆT")_ +- _(**S155 (07-27) PE xóa-phiếu FE/authz-slice [18 finding]** — PeWorkflowPanel byte-identical 2-app · plumbing xóa nối sẵn · `Pe_*` KHÔNG ∈ All ⇒ dùng root-policy `PurchaseEvaluations.Delete`. verbatim → `archive/2026-07.md` substring:"S155 (07-27) PE xóa-phiếu FE/authz-slice")_ - **S155 (07-27) PE menu/trash/dept-head [21 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-menu-3.md`]:** 🔴 Menu = **SEED DB** không hard-code FE — `Pe_` khai `DbInitializer.cs:1874-1877`; thêm mục = restart API, 0 migration (upsert `:1888-1914` chỉ sửa `Order`, **KHÔNG đụng Label** ⇒ rename qua `labelBackfill:1918-1924`; `DisplayLabel` **CHỈ ăn fe-user** `MenuDtos.cs:14`). 6-chỗ-mirror: +2 seed permission SONG SONG `:2092-2096`/`:2492-2496` + regex `Layout.tsx:120` (fe-admin `:107` thiếu `WfView`). Đổi KEY = mồ côi 13 row/key × 10 key ⇒ giữ key chỉ đổi label. **`Đã duyệt` chạy được HÔM NAY**: `phase=` đủ 3 tầng (`Controller:23`→`Features:565`→`:620`), DaDuyet=7 ⇒ `?type=1&phase=7`, 0 route mới. `pendingMe` KHÔNG phải param BE (FE rẽ `/inbox` `ListPage:61-68` + lọc client `:182`). `IgnoreQueryFilters` 0 src/Backend / 15 hit tests; `HasQueryFilter(`=12. An-toàn accumulator = **cấu trúc** (`PeBudgetAccumulator.cs:42`/`:100` dựng `peers` FRESH). 🎁 Changelog `: BaseEntity` + lọc CHỈ `PurchaseEvaluationId` (`:1424`) ⇒ lịch sử phiếu-xóa đọc MIỄN PHÍ; cái chôn = DETAIL bundle. 0 tiền lệ thùng-rác (hit "khôi phục" duy nhất = STRING `EmployeeFeatures.cs:162` ghost-wire). 🔴 **"Trưởng phòng" CÓ TÊN CHƯA CÓ ĐỜI**: `Department.ManagerUserId` **0/10 Dev** + 2 FE hardcode `managerUserId: null` payload SỬA (`DepartmentsPage.tsx:43`/`:57`) ⇒ tự-xoá mỗi lần sửa phòng. `PositionLevel.TruongPhong` 33/33 NULL (V1-legacy). Tag `[s155, menu-seed-db, phase-param-exists, changelog-survives-softdelete, dept-head-name-only]` - **S155 (07-27) WF-V2 sửa-tại-chỗ vs version-mới [25 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-wfver-4.md`]:** 🔴 **0 lệnh Update** — `ApprovalWorkflowsV2Controller.cs` (54d TOÀN BỘ) chỉ GET/POST/PATCH-user-selectable/DELETE ⇒ sửa 1 cờ = buộc POST = `MAX(Version per Code)+1` (`ApprovalWorkflowV2AdminFeatures.cs:325-328`) + hạ MỌI active cùng type (`:330-334`). Chặn THẬT bằng mã. 🔴 **F17:** POST tạo entity mới ⇒ **Id mới** (`:336`+`BaseEntity.cs:5`) mà phiếu đọc workflow theo **Id đã pin** (`PurchaseEvaluationWorkflowService.cs:674`) ⇒ **cờ mới KHÔNG tới phiếu đang chạy**; re-pin chỉ `PurchaseEvaluationFeatures.cs:149`/`:291` mà `:249-251` chặn chỉ Nháp/TraLai ⇒ ChoDuyet kẹt bản cũ vĩnh viễn; 0 backfill re-pin. ⇒ cờ `AllowApproverDelete` **HỎNG nếu không có lệnh Update**. Pin = **tham chiếu SỐNG** (chỉ NS có snapshot Mig67). Con trỏ **LAI**: Bước=INDEX `steps[currentIdx]` (`:686` vỡ ÂM THẦM/`:683` kẹt) · Cấp=ORDER-VALUE (`:694`→`:695` kẹt) ⇒ ranh AN-TOÀN = **không đổi tập `{Step.Order}`∪`{Level.Order}`**. 13 thao tác: 3 an-toàn (thêm-người-cùng-Order · cờ `Allow*` · đổi tên) · 2 nửa (bớt/đổi `ApproverUserId` → 403 GUID-trần `:705`, FK Restrict `PeLevelOpinionConfiguration:25-28`) · 6 phá-vỡ · 2 hỏi-owner (`CeoApprovalThreshold`·`Step.DepartmentId`). OR-of-N XÁC NHẬN (`:689` GroupBy Order) ⇒ owner không tự mâu thuẫn; ⚠️ comment `ApprovalWorkflow.cs:81-82` SAI *"KHÔNG OR-of-many"*. Blast 6+ module (198 hit/23 file). Designer **fe-admin ONLY** (`ApprovalWorkflowsV2Page.tsx`), "Nhân bản"→*"Tạo quy trình mới"*; **0 cảnh báo đang-dùng/0 disable IsActive**. F6 tick `:999-1007` +dây `:54/:110/:159/:177/:605` + fe-user `WorkflowMatrixViewPage.tsx:282` + BE `:41/:230/:188/:368`. CẤM delete-then-insert Levels → diff theo `Level.Id`. Tag `[s155, wf-v2-no-update-cmd, new-version-new-id-unreachable, pointer-index-vs-order]` - **S153 (07-26) ctx soft-memory spec [fable-real, detail → `runs/2026-07-26-S153-ctx-adopt/sub-fable-invest-S153.md`]:** SE **0 content-ts-reader sống** (session_ctx.py = git-ts+mtime) ⇒ rào-2 canonical `_TS_KEY_RE` đặt session_ctx.py, máy-7-phép = EXTEND python (tái dụng `_SECRET_RES` 1-nguồn) KHÔNG .ps1 mới; `secrets_sweep` rglob TRỌN session-dir ⇒ `_mind` tự vào lưới; `_mind` mới-nhất-ở-TRÊN ⟂ `_context` FLOW ở-CUỐI (bẫy chép-khuôn); C11(b) filter `^sub-[-.]` ⇒ tên `sub-ctx-audit-*` lọt lưới ngay. Tag `[s153, ctx-mind-spec, ts-reader-absent, one-source-regex]` diff --git a/.claude/agent-memory/investigator-codebase/archive/2026-07.md b/.claude/agent-memory/investigator-codebase/archive/2026-07.md index 3b448ac..c67a85e 100644 --- a/.claude/agent-memory/investigator-codebase/archive/2026-07.md +++ b/.claude/agent-memory/investigator-codebase/archive/2026-07.md @@ -61,3 +61,10 @@ - **[→ git 07-12 · Mig64 IsPublic shipped since] Supplier import-v2 LENS-4 public/visibility recon:** 3 SELECTION consumers ALL hit `GET /suppliers` ListSuppliersQuery (SuppliersPage:83 · PeDetailTabs:2222 · ContractCreatePage:321); DISPLAY-of-existing joins by stored SupplierId MUST NOT filter (ContractFeatures/PeFeatures/Dashboard); write-guard backstops `ContractFeatures.cs:64`+`PurchaseEvaluationSupplierFeatures.cs:49`. Rec fail-closed default + stored `IsPublic bit`. Tag `[s112b, supplier-visibility, fail-closed-default]`. - **[all verbatim → archive/2026-07.md] S98 verify-lane (detector 28→25, 8 derived-stale=FP vs canonical 61/89/440/79; grep exact misses README variant) · S97 feedback-audit (#77/#78/#79 `gotchas.md:1301/1317/1325`; #53-garble×6=cicd MEMORY-self-target ~17.5KB; Mig60 wrong-table Approvals→Mig61 Changelogs) · 07-01 Supplier-predecessor SUPERSEDED by 07-06 · S93 MFE denom=marks/AS/guards dedup-AS-xref=29, coverage exact-id OR ≥2-content-word.** - **S129 (07-16 đêm) adap 2 errata EOL 3-lane `wf_3fccd7d1`:** T0 sweep = SE **0 defect-site** (counter duy-nhất `agent-frontmatter-eol-check.ps1` byte-level pre-satisfy fix①③; pattern hỏng chỉ ở mention-site). T1: 19 `i/-text` đều declared-binary 0-suspect — **filter mẫu erratum thiếu docx/xlsx = 13 dương-giả trên SE** (tailor-per-class!); generator-drift thật duy-nhất `mfe-eval.ps1:204` Set-Content UTF8 = **BOM+CRLF** (blob mang BOM 3-byte); W0.6 = EOL-only A/B, trục-quote UNDETERMINED — đừng over-read. T2: nấc thật = **4 gated + 3 discipline** (đừng kê "7/7" phẳng — R1 MAJOR). 2 bẫy tự-mắc đáng nhớ: `git log -- | tail -1` = **first-touch KHÔNG phải policy-landing** (.gitattributes 08c7036 06-15 vs 92de58f 07-15) · story "40 = +1 agent mới" BỊA — số scoped=39, delta=`skills/README.md` ngoài glob SKILL.md (meta-count class). #53 WRITE-lane 2/3 lane — journal cứu trọn, em-main scribe. Tag `[s129, adap-errata-eol, byte-counter-pre-satisfy, bom-in-blob, tail1-first-touch-trap]` + + +## Moved @S160 (2026-07-29) — verbatim from L1 (moved-not-cut) + +- **S155 (07-27) PE xóa-phiếu màn DUYỆT [24 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-be-1.md`]:** DELETE `/api/purchase-evaluations/{id}` ĐÃ CÓ (`PurchaseEvaluationsController.cs:146`, authz = class `[Authorize]` TRẦN) nhưng allow-list `{DangSoanThao,TuChoi}` (`PurchaseEvaluationFeatures.cs:1404`) chặn ChoDuyet. `Remove()`=**SOFT** gián tiếp qua `AuditingInterceptor.cs:56-62` — lớp #81-EXT indirect. Lũy kế: `PeBudgetAccumulator.cs` 2 hàm/4 call-site DUY NHẤT; ChoDuyet ăn số **2 chỗ** (`ComputeAsync:54` PrevSubmitted + `ComputePendingAsync:114` PendingSelected); `TraLai` KHÔNG triệt tiêu; `TuChoi` rơi khỏi 100% phép cộng. Query 0 dòng `IsDeleted` → dựa global `HasQueryFilter` (`PurchaseEvaluationConfiguration.cs:84`) ⇒ soft-del TỰ rớt, không sửa accumulator. Snapshot Mig67 KHÔNG re-compute. 🔴 Cascade 6 FK (`:75-80`) **không bao giờ chạy** vì soft ⇒ con `BaseEntity` ở lại = rác ẩn. **Git khảo cổ TRÚNG:** "chỗ cho hủy" = nút Từ chối, gỡ `6db195d` 12-06 S60; guard `PurchaseEvaluationWorkflowService.cs:101-106` còn sống tự trỏ "Xóa phiếu khi còn Bản nháp" = chính cái lỗ. 0 tiền lệ hủy chứng-từ in-flight; khuôn gần nhất `CancelMeetingBookingHandler` (`MeetingFeatures.cs:457`, Status=Cancelled NOT IsDeleted). Tag `[s155, pe-delete-approver, s60-tuchoi-removal-hole, soft-delete-no-cascade]` + +- **S155 (07-27) PE xóa-phiếu FE/authz-slice [18 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-fe-2.md`]:** Khối HÀNH ĐỘNG = `components/pe/PeWorkflowPanel.tsx:458-506` **byte-identical 2 app** (md5 `02671be6…`, cùng PeDetailTabs/PeListPanel/ListPage) ⇒ sửa-1-bên-là-sót. Gate 3 tầng: `?pendingMe=1`→`readOnly={!pendingMe}` (`ListPage:590`/`:676`) · `next=workflow.nextPhases` BE-source (`:257`) · `blockedByV2Level` (`:99-103` isAdmin ∥ actor∈currentApproval.approvers). 🔴 Plumbing xóa ĐÃ NỐI SẴN vào màn duyệt: `onDelete={del.mutate}` truyền ở `ListPage:573`+`:668` (`api.delete('/purchase-evaluations/:id')` `:90`) nhưng nút chết vì `PeDetailTabs:445` đòi `mode==='workspace' && canEditPhase && !readOnly`; nút hiện chỉ ở `:460` `phase===DangSoanThao`. **Authz 2 tầng:** PE vùng FE = **0-hit** `usePermission|PermissionGuard` (guard chỉ sống ở Master+Users, khuôn `DepartmentsPage:101 action="Delete"`); `menuKeys.ts` KHÔNG có const `Pe_*` (chỉ root `:23`) — `Pe_*` sinh BE `MenuKeys.cs:134-141`, seed `DbInitializer:1870-1877`, FE khớp bằng regex `Layout.tsx:120` (fe-admin `:107` **LỆCH**, thiếu WfView). 🔴 `Pe_*` **KHÔNG ∈ `MenuKeys.All`** ⇒ `Program.cs:82-89` không đăng-ký policy `Pe_*.{action}` (0 dynamic provider) ⇒ dùng `[Authorize(Policy="Pe_…")]` = policy-chưa-tồn-tại; **dùng được ngay = `PurchaseEvaluations.Delete`** (root ∈ All, + `GetMyMenuTreeQuery:66-72` cho `Pe_*` KẾ THỪA cờ CRUD từ root ⇒ 0 key mới, 0 migration). Ma trận enumerate `db.MenuItems` (`PermissionFeatures.cs:20`) nên ô Delete đã hiện sẵn. Q6: thêm-nút KHÔNG kích 4-place (0 route/page/staticMap `Layout.tsx:56-106`); `Pe_*` resolve nhánh regex `:119-134` KHÔNG qua staticMap. Tag `[s155, pe-delete-fe-authz, two-app-byte-identical, pe-keys-not-in-All]` diff --git a/.claude/agent-memory/investigator-codebase/archive/_INDEX.md b/.claude/agent-memory/investigator-codebase/archive/_INDEX.md index aa2e07e..b35ab8f 100644 --- a/.claude/agent-memory/investigator-codebase/archive/_INDEX.md +++ b/.claude/agent-memory/investigator-codebase/archive/_INDEX.md @@ -74,3 +74,4 @@ | 2026-06-19 | PE Block-A gate audit | gate=PURE ROLE no-phase; submission-count-lock NEXISTS; Initial-vs-Adjustment same-gate=feature-new | 2026-06.md · substring:"PE Block-A budget editable-gate audit" | | 2026-06-20 | gov-landing map | WHERE-to-land 3 AI_INFRA gov broadcasts (RC-sig/User-Mark/objective-criteria); report-before-stamp⊂D7 | 2026-06.md · substring:"governance-landing map for RC-sig" | | 2026-06-20 | Harness-14 audit | budget no-decay-knob; canonical-anchor not-age; eval genuine-richer; anti-downgrade H8-partial | 2026-06.md · substring:"Harness-14 Eval/Budget/Outcome adoption-readiness" | +- 2026-07-27 · S155 PE xóa-phiếu ×2 slice (BE-duyệt 24f · FE-authz 18f) · `2026-07.md` · substring:"S155 (07-27) PE xóa-phiếu" diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/plan-cha-dry-run-toan-trinh-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/plan-cha-dry-run-toan-trinh-29-07-2026.md new file mode 100644 index 0000000..5dcff1d --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/plan-cha-dry-run-toan-trinh-29-07-2026.md @@ -0,0 +1,92 @@ +# PLAN CHA — DRY-RUN TOÀN TRÌNH ĐẾN HỢP ĐỒNG CỨNG (S160, 2026-07-29) + +> **Lệnh anh (verbatim):** `run.md` §"LỆNH ANH". Tóm: dry-run luôn đến Hợp đồng cứng · spec KHKK cũ ĐÚNG (ratified) · ra Plan cha + mỗi wave 1 spec-chi-tiết-cách-làm + checklist · `/fable-clone` review · `/fable-real` review chốt · hmw Opus-5-MAX chạy từng wave theo Plan cha + kiểm. +> **Nguồn:** spec cũ RATIFIED `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md` (+NOTE W1 @S160) · lane-1 `sub-investigator-codebase-1.md` (25 mục F-01→F-24) · lane-2 `sub-investigator-codebase-2.md` (8 mục §0-§7) · đo tươi @S160 (lead): prod Contracts **0 active + 0 deleted** (admin-bypass verify `ContractFeatures.cs:298/:440`). +> **Trạng thái:** CHỜ `/fable-clone reviewer` + `/fable-real reviewer` chốt cuối → rồi hmw thi công. + +--- + +## 1. RANH + NHÃN (chốt @S159, spec cũ `:27` — CẤM tự chế lại) + +- **GĐ1** Duyệt NCC = PE (live) · **GĐ2** Kế hoạch ký kết HĐ = b.7→12 (b.7 email BỎ — Q8 ĐÓNG) · **GĐ3** Duyệt hợp đồng = **b.13→18** (tận dụng Contract V2) · **GĐ4** Hợp đồng cứng = **b.19→21, ký cứng UPLOAD-ONLY**. +- Khúc b.13→21 = span verdict-LAI (toàn bộ tái dùng Contract), KHÔNG phải span GĐ3 (lane-1 F-09 bắt drift này trong chính run.md). +- **Nhãn GĐ2 2 đời:** "Đề xuất ký kết hợp đồng" (sáng @S159, còn trong spec cũ `:27`) → **superseded "Kế hoạch ký kết HĐ"** (chốt cuối @S159, prod `Khkk_G1` seeder). Mọi label VN mới = **"Kế hoạch ký kết HĐ"**; class/bảng English `ContractSigningPlan*` GIỮ. +- Nuance lane-1 F-11: **bản cứng vật lý SINH ở b.17-18 (∈GĐ3)** — trang GĐ4 hiển thị readonly metadata b.17-18, thao tác thật của GĐ4 chỉ là 3 trạm b.19/20/21. + +## 2. BỨC TRANH WAVE (hợp nhất 2 lane — đánh số GIỮ W1-W5 của spec ratified, nối W6-W8) + +| Wave | Tên | GĐ | Mig? | Độ lớn | Spec file | Trạng thái | +|---|---|---|---|---|---|---| +| **W1** | Schema + permission + Designer type-10 | GĐ2 | ✅ **Mig 69** (7 bảng — mig DUY NHẤT toàn plan) | BE nặng | `spec-wave-w1-*.md` | sẵn chạy | +| **W2** | CRUD phiếu nháp + căn cứ b.8-9 + FE 3 page ×2 app | GĐ2 | ❌ | FE nặng | `spec-wave-w2-*.md` | sẵn chạy (sau W1) | +| **W3** | Duyệt 3 trạm + finalize chốt giá 🔴 nặng nhất | GĐ2 | ❌ | BE nặng | `spec-wave-w3-*.md` | sẵn chạy (sau W2) | +| ~~W4~~ | Gate + checklist per-phòng FO-002.01 | GĐ2 | ❌ (1 mig nếu số hoá checkbox) | — | **KHÔNG có spec — DEFERRED** | ⛔ chặn O-Q1/O-Q2; dry-run chạy default (comment tự do + cảnh báo mềm, 0 dev — lane-1 F-18) | +| **W5** | Cầu KHKK→HĐ + pin V2 (sửa bridge cũ 1 file) | GĐ2→3 | ❌ | 2 BE + 2 FE | `spec-wave-w5-*.md` | anh ĐÃ MỞ gate chạm HĐ @S160 | +| **W6** | Đường ống HĐ V2 (view + inbox + vai-trình) | GĐ3 | ❌ | 2-3 BE | `spec-wave-w6-*.md` | sẵn chạy (độc lập W1-W3) | +| **W7** | Bản cứng `Hdc_*` 7 leaf trang thật | GĐ4 | ❌ (option A derive; B = 1 mig nếu anh muốn field tường minh) | FE nặng + 1-2 BE | `spec-wave-w7-*.md` | sẵn chạy (build song song; verify cần HĐ phase-9 từ W5/W6) | +| **W8** | Authz khoanh vùng Contract | cross | ❌ | 1-2 BE + seeder | `spec-wave-w8-*.md` | option O-A/O-B mặc định; **O-C cần anh gật riêng** (chạm service HĐ) | +| ~~W9~~ | Hồ sơ hoàn tất + thống kê lưu trữ | GĐ4 | ❌ | — | **KHÔNG có spec — DEFERRED** | không chặn vòng chạy (lane-1 F-19) | + +**Thứ tự thi công (đồ thị phụ thuộc):** +``` +W1 → W2 → W3 ──┐ (GĐ2: phiếu KHKK sống trọn vòng) + ├→ W5 → [E2E] (cầu: HĐ sinh từ KHKK, pin V2, hết kẹt ConflictException) +W6 ───────────┘ (đường ống: approver V2 xem+inbox HĐ — PHẢI trước W5 về VẬN HÀNH, + build song song W1-W3 được vì 0 đụng file KHKK) +W7 (song song từ đầu; VERIFY sau khi W5+W6 đẻ HĐ DaPhatHanh đầu tiên) +W8 (trước khi mở rộng người bấm — đề xuất ngay sau W6) +``` +**Dry-run E2E đạt khi:** W1+W2+W3+W6+W5+W7 xong — 1 phiếu PE `DaDuyet` (dự án ZZTEST) → phiếu KHKK → 3 trạm duyệt → HĐ per-winner pin V2 → 3 trạm HĐ → `DaPhatHanh` → trang Bảng cứng upload scan ký/dấu/lưu. Toàn bộ **1 migration duy nhất (Mig 69, W1)** — GĐ3+GĐ4 = 0 mig (lane-2 §6). + +## 3. BASELINE ĐO TƯƠI @S160 (căn mọi acceptance vào đây, KHÔNG chép số cũ) + +- Prod: **Contracts = 0 active + 0 deleted** (curl admin 6 phase + tổng + /deleted; admin bypass `ContractFeatures.cs:298`) — sổ S156 "7 HĐ V1 thật" đã STALE. ⇒ lỗ Reject chưa có nạn nhân thật; **dry-run chính là thứ đưa data vào** ⇒ W8 vẫn trong plan. +- Menu 142 key (root 12) · `Khkk_*` = 7 key (G1 + 6 leaf, CanRead 13/13, leaf → `/coming-soon`) · `Hdc_*` = **7 leaf per-ContractType** (KHÔNG phải 6-leaf — lane-2 §3 sửa grounding) · `Ct_*` 21 key mới. +- Test **562** (45D+517I) — acceptance neo TÍNH-CHẤT: "PASS 0 fail + số-sau ≥ số-trước-đo-ngay-trước-wave + N + tên test tồn tại", CẤM literal "562+N" (lane-2 §7). +- Mig cuối = 68 · 89 bảng · mig kế = **69** (chỉ W1). +- Bundle: admin `D0sXA0fe`/`DWDbm5As` · user `Y6dW_5CM`/`6YIAufJR`. +- `MenuKeys.KeHoachKyKet` const CÓ (`MenuKeys.cs:41` + comment dặn tái dùng) nhưng **NGOÀI `All`** ⇒ 0 policy `KeHoachKyKet.*` runtime (đo @S160). `Contracts` ∈ All `:168` ⇒ 4 policy `Contracts.*` sống sẵn. + +## 4. CHIẾN LƯỢC DATA DRY-RUN (lane-2 §4 + đo mới) + +1. **Dự án test `ZZTEST` + 1-2 NCC test** tạo qua UI (0 code): mã HĐ dry-run ăn sequence per-prefix riêng (`ContractCodeGenerator.cs:33,:40-53`) — không đụng chuỗi số dự án thật; NGOÀI seed-list ⇒ xóa mềm không bị re-seed (#75/#76). **CẤM đặt Code trùng seed-list** (twin FLOCK-01). +2. **PE side có data THẬT** — phiếu KHKK dry-run tạo từ PE test mới (mỗi vòng thử cần PE mới vì idempotency `pe.ContractId` `:59-60,:145`). +3. **KHÔNG cờ IsDryRun** (blast cardinality mọi list/inbox — bài S87/S88). +4. **Hố rollback khai trước:** HĐ ≥ phase 5 KHÔNG xóa được qua API (`ContractFeatures.cs:632-633` so sánh SỐ) → HĐ test DaPhatHanh nằm lại (lọc bằng ZZTEST) hoặc admin-override về DangSoanThao (`ContractWorkflowService.cs:191-198`) rồi xóa. Phiếu KHKK/PE DaDuyet cũng không xóa (allow-list). Sequence không rollback (gap số chấp nhận — comment repo `:113-115`). +5. **Notification bắn cho user thật** (in-app, không tắt per-record) — dặn team trước đợt dry-run. +6. **Backup DB trước đợt** (`scripts/backup-sql.ps1`). + +## 5. BẢNG DEFAULT 12 QUYẾT ĐỊNH TREO (dry-run chạy bằng default ĐẢO ĐƯỢC — anh veto lúc nào cũng kịp) + +| # | Câu treo | DEFAULT dry-run | Đảo thế nào | Nguồn | +|---|---|---|---|---| +| 1 | Q3 điều kiện rẽ b.12 | KHÔNG rẽ sớm — đường THƯỜNG 3 trạm, CEO ký thật (2 đường sớm anh từng phán "PHÁ VỠ" S155) | bật cờ workflow (0 code) | spec cũ `:67-71`, W3 `:362-365` | +| 2 | Q6 giá vào HĐ | `ApprovedAmount` KH **thắng** + ContextNote lệch vào changelog | đổi 1 nhánh if trong bridge | spec cũ §2.5 + lane-2 §2(b) | +| 3 | Q11 tạo HĐ khi chưa có KH | **cảnh báo mềm** — 2 đường song song (PE→HĐ tắt vẫn sống) | nâng 409 chặn cứng | lane-2 §2(d) | +| 4 | O-Q1 căn cứ thiếu TvgsDuyet | cảnh báo mềm khi trình | 409 | spec cũ W4 | +| 5 | O-Q2 checkbox per-phòng | comment tự do LevelOpinions (W4 DEFERRED, 0 dev) | số hoá checkbox (wave riêng) | lane-1 F-18 | +| 6 | Mã phiếu KHKK | `KHKK/{YYYY}/{Seq:D3}` qua `WorkflowAppCodeSequence` (RG-001 v02 CÂM về phiếu) | đổi format 1 chỗ CodeGen | lane-1 F-07 + spec cũ `:128` | +| 7 | Viết tắt "MB" (Mua bán) — codegen tự chế, KHÔNG có trong RG-001 v02 | **GIỮ hiện trạng** + flag (đổi codegen giữa dry-run = rủi ro) | map MuaBan→NCC hoặc loại type khỏi dry-run | lane-1 F-06/F-21, verify `ContractCodeGenerator.cs:25` | +| 8 | GĐ4 vật mang | **Option A: 0-mig DERIVE** từ `phase=9` + attachment (SealedCopy/ScannedSigned/+InitialSigned enum-extend) | Option B: +`HardCopyStatus/Note` 1 mig AddColumn | lane-2 §3 | +| 9 | Số bộ gốc / nơi lưu / ngày giao | ghi vào **note khi upload SealedCopy** (option A không có field) | option B field tường minh | lane-1 F-13/F-14 (nguồn câm) | +| 10 | Thời điểm gán mã HĐ (QT đòi b.17, máy gen ở create + terminal) | **GIỮ máy hiện trạng** (create-time `ContractFeatures.cs:113-118` — có mã TRƯỚC khi in, sớm hơn cả QT đòi) + khai lệch trình tự so QT | dời site gen | lane-1 F-16 + lane-2 §1-b.13 | +| 11 | Authz option | **O-B** per-action root-policy 22 endpoint ghi + seeder grant (O-A fallback nếu review bác); **O-C vá Reject cần anh gật riêng** | đổi option = 1-2 file | lane-2 §5 | +| 12 | Policy-prefix KHKK | `All += KeHoachKyKet` (const sẵn `:41`, comment dặn tái dùng) — thay kế hoạch `ContractSigningPlans*` của spec cũ §2.4 vì menu skeleton `Khkk_*` ĐÃ SEED | thêm key riêng nếu review bác | đo @S160 §3 trên | + +## 6. GATE 5-ANCHOR (bắt buộc TRƯỚC MỖI WAVE — HANDOFF, reviewer đề @S157, gotcha-proof) + +Trước thi công wave: mở **5 anchor** của wave đó (danh sách trong từng spec-wave §③) — **≥1 trượt ⇒ chấm lại TOÀN BỘ anchor wave đó** rồi mới code. Anchor per-wave lấy từ lane-2 §7 (đã re-verify 07-29) + spec cũ. + +## 7. KHUÔN ACCEPTANCE + CICD (mọi wave) + +- Test: `dotnet test SolutionErp.slnx` PASS 0 fail · số-sau ≥ baseline-đo-ngay-trước + N · **tên test pin cụ thể** (H5). +- FE: `npm run build` ×2 app PASS; mirror SHA khi cookie-cutter. +- Deploy: cicd 3-chân-kiềng — CI run PASS · bundle byte-marker (#77, control ÂM phải ĐỘC QUYỀN — bài đợt-5) · data prod verify **bằng curl API** (SSH-SQL client chết S134/S148; sqlcmd chỉ để local/Dev). +- Menu/permission: CHỈ qua seeder (gotcha #84) — mọi thay đổi grant đi cùng commit wave. + +## 8. PIPELINE CÒN LẠI (đúng lệnh anh) + +1. ✅ 2 `/fable-real invest` (xong, verified) → 2 ✅ Plan cha + specs (file này + 7 spec-wave) → 3 ⏳ **`/fable-clone reviewer`** (ensemble N-lane chấm bộ plan+spec) → 4 ⏳ **`/fable-real reviewer`** chốt cuối → 5 ⏳ **hmw Opus-5-MAX**: mỗi wave = 1 đợt hmw, args con-trỏ vào ĐÚNG spec-wave file (B6 args-mỏng), worker đọc spec + thi công + tự chạy checklist §③; lead + cicd-monitor verify 3-chân-kiềng sau mỗi wave; **gate 5-anchor chạy TRƯỚC mỗi đợt**. +- Điều KHÔNG tự làm khi chưa có gật riêng: **O-C** (vá `ContractWorkflowService.cs:49` Reject-guard — chạm service HĐ, O-3 còn hiệu lực cho phần SỬA service) · số hoá W4 · option B GĐ4. + + diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/run.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/run.md index 9148cae..f2b9720 100644 --- a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/run.md +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/run.md @@ -35,7 +35,7 @@ | 2 | invest lane-1: NGUỒN/QUY-TRÌNH GĐ3-GĐ4 (b.13→21 chi tiết + RG-001 v02 diff + FO-002.01 checklist-map + "hợp đồng cứng" = gì cụ thể) | `/fable-real investigator-codebase` #1 | [ ] | | 3 | invest lane-2: CODEBASE dry-run map (PE→KHKK→Contract→Hdc gap-map + reuse Contract V2 + authz khoanh vùng + dry-run mode + wave structure đề xuất) | `/fable-real investigator-codebase` #2 | [ ] | | 4 | Plan cha + spec-chi-tiết + checklist per-wave | lead (từ 2 invest + spec cũ) | [ ] | -| 5 | Review ensemble | `/fable-clone reviewer` | [ ] | +| 5 | Review ensemble — 6 lane reviewer `tier:'opus'` (C2 ≤3 file/lane + khung-rỗng lượt 1-2; label C3): `WF reviewer lens-anchor-A s160` (plan-cha+w1+w2) · `WF reviewer lens-anchor-B s160` (w3+w5+w6) · `WF reviewer lens-anchor-C s160` (w7+w8+plan-cha§5) · `WF reviewer lens-wave-logic s160` (plan-cha+w1+w3) · `WF reviewer lens-owner-consistency s160` (plan-cha+w5+w8 vs spec-cũ+O-1/O-2/O-3) · `WF reviewer lens-security s160` (w6+w8+plan-cha§4) | `/fable-clone reviewer` | [!] | | 6 | Review chốt cuối | `/fable-real reviewer` | [ ] | | 7 | hmw Opus-5-MAX chạy dry-run từng wave theo Plan cha + kiểm | hmw (worker-tier-pin) | [ ] | @@ -50,5 +50,8 @@ ## Stages - [x] 1. scaffold + extract (15:45-15:50) -- [!] 2+3. 2 lane `/fable-real investigator-codebase` model=fable PHÓNG SONG SONG ~15:55 (lane-1 nguồn GĐ3-GĐ4 → `sub-investigator-codebase-1.md` · lane-2 codebase map → `sub-investigator-codebase-2.md` — cả 2 ghi-đĩa-trong-lúc-làm, kết bằng END+TOTAL line) - - lane-2 lượt-1 ~16:03: **#53 garble** (return = mẩu "## Q2 — Bridge…"); đĩa cứu §0-§2 (Q1-Q2, 11.039B ruột thật — Q1 có RE-VERIFY line-trôi vs S156 do arc-2 thêm ListDeleted); **SendMessage-resume** làm nốt Q3-Q7 append cùng file +- [x] 2+3. 2 lane `/fable-real investigator-codebase` model=fable — XONG + LEAD-VERIFIED (lane-1 25 mục 24.222B END-ok, 3 claim verify byte-đúng · lane-2 8 mục 32.242B END-ok, 4 claim verify byte-đúng + tự bẻ 2 grounding sai của đề bài [Hdc 7-leaf-per-type · PE không per-action-attribute]) + - lane-2 lượt-1 ~16:03: **#53 garble** → đĩa cứu §0-§2 → SendMessage-resume làm nốt Q3-Q7 sạch. Tally #53 phiên: ×1. + - Lead đo bổ sung @S160: **prod Contracts = 0 active + 0 deleted** (admin-bypass `:298/:440`; sổ S156 "7 HĐ V1" STALE) · login prod field `accessToken` · `KeHoachKyKet` const có nhưng NGOÀI All. +- [x] 4. PLAN CHA + 7 SPEC-WAVE (lead viết sau verify — H21 pipeline): `plan-cha-dry-run-toan-trinh-29-07-2026.md` + `spec-wave-{w1-schema-designer,w2-crud-nhap,w3-duyet-3-tram,w5-cau-khkk-hd,w6-duong-ong-hd-v2,w7-ban-cung-hdc,w8-authz}-29-07-2026.md` (W4/W9 DEFER ghi trong plan cha). Wave-numbering: GIỮ W1-W5 spec ratified + nối W6-W8; 12 default đảo được; 1 mig duy nhất (69/W1). +- [x] harvest M9: entry S160 engine-label `real-fable-S160` vào diary investigator-codebase (curate cùng lượt 23.285→22.452B, 2 entry S155 → archive moved-not-cut) diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w1-schema-designer-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w1-schema-designer-29-07-2026.md new file mode 100644 index 0000000..f48fd2a --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w1-schema-designer-29-07-2026.md @@ -0,0 +1,39 @@ +# SPEC WAVE-1 — Schema Mig 69 + Permission + Designer type-10 (GĐ2 KHKK) + +> Nguồn RATIFIED: spec cũ §2.2 (7 bảng) + §2.4 (menu/permission) + §③ WAVE-1 (`spec-ke-hoach-ky-ket-hd-28-07-2026.md:266-308`) + NOTE W1 @S160 (đầu §WAVE-1). Plan cha default #6 #12. File này = CÁCH-LÀM cụ thể + checklist; xung đột nào với spec cũ đã khai ở mục Δ. + +## ① TÍNH NĂNG / ĐỀ BÀI + +Đặt nền DB + quyền + cho admin dựng được quy trình duyệt `type=10`, để W2 tạo phiếu được ngay. Deploy riêng an toàn: menu skeleton `Khkk_*` đã public từ arc-2 (leaf ComingSoon — dark-launch nằm ở TRANG, không ở menu). **Mig 69 = migration DUY NHẤT của toàn plan dry-run.** + +**Δ so spec cũ (2 điểm, do thực địa arc-2 đổi tiền đề):** +1. **KHÔNG thêm 4 const `ContragSigningPlans*`/menu 4 dòng** như §2.4 — menu skeleton `Khkk_G1`+6 leaf ĐÃ SEED prod (CanRead 13/13). Thay bằng: `All += KeHoachKyKet` (const sẵn `MenuKeys.cs:41`) → policy `KeHoachKyKet.{Read,Create,Update,Delete}` tự sinh (`Program.cs` foreach All×Actions) + seeder grant 4-action cho role đích. Menu-key hiển thị (`Khkk_*`) ⟂ policy-key (`KeHoachKyKet`) — 2 lớp khác nhau, khai rõ để reviewer soi. +2. Label VN = **"Kế hoạch ký kết HĐ"** (đời nhãn cuối @S159), KHÔNG phải "Đề xuất ký kết hợp đồng" còn trong spec cũ `:223`. + +## ② CÁCH IMPLEMENT (thứ tự bắt buộc) + +1. **Gate 5-anchor** (§③-A) — trượt ≥1 ⇒ chấm lại hết rồi mới code. +2. **Domain** (`src/Backend/SolutionErp.Domain/ContractSigningPlans/`): 7 entity đúng bảng spec cũ §2.2 (#1 header `ContractSigningPlans` [+`DepartmentId Guid?` C5] · #2 `Lines` [+`ContractId Guid?` C6, UNIQUE filtered `(PlanId,SupplierId) WHERE IsDeleted=0`] · #3 `DossierItems` · #4 `LevelOpinions` [UNIQUE (PlanId,LevelId), FK Cascade-Plan/Restrict-Level] · #5 `Attachments` · #6 `Changelogs` [ĐỦ 4 cột EntityType/EntityId/UserName/FieldChangesJson] · #7 `Approvals`). 2 enum: `ContractSigningPlanPhase{DangSoanThao=1,ChoDuyet=2,DaDuyet=3,TraLai=98,TuChoi=99}` · `DossierItemStatus{ChuaNop=1,DaNop=2,TvgsDuyet=3,TvgsBac=4}` (🔴 bắt đầu 1, KHÔNG 0 — falsy JS) + `DossierItemKind{MauVatLieu=1,Shopdrawing=2,Khac=99}`. +3. **`ApprovalWorkflowApplicableType` += `ContractSigningPlan = 10`** — re-verify slot 10 trống NGAY TRƯỚC khi viết (`ApprovalWorkflow.cs:53-67` dừng ở 9; anchor #2). +4. **EF Configuration ×7** — khai độ dài MỌI cột chuỗi (spec cũ `:134-137`: MaKeHoach 50 · GhiChu/ContextNote 2000 · Name 500 · Note 1000 · TvgsName 200 · Summary 1000 · SignedByFullName 200 IsRequired · FileName 500/Path 1000/ContentType 200) + `HasQueryFilter(!IsDeleted)` header + loose-Guid (KHÔNG FK vật lý sang PE/Supplier — convention Mig 49). +5. **DbSets** vào `IApplicationDbContext` + `ApplicationDbContext`. +6. **Mig 69** `AddContractSigningPlans` (3-file rule: migration + Designer + Snapshot; skill `ef-core-migration`): 7 CreateTable, 0 ALTER bảng cũ, Down = DropTable ×7. `database-agent` design-review TRƯỚC apply (per spec cũ W1 vai). +7. **MenuKeys.cs**: `All` += `KeHoachKyKet` (chỉ 1 dòng — const đã có :41). KHÔNG key mới. +8. **DbInitializer**: (a) grant `KeHoachKyKet` 4-action cho role đích — dry-run mở: 13/13 role CanRead + Create/Update cho nhóm tham gia (Drafter/ProjectManager/Procurement/CostControl/Director/Admin), Delete cho Drafter/Admin — **grep MỌI site seed-permission trước khi thêm** (S155: >1 site); (b) KHÔNG đụng 7 key `Khkk_*` menu (đã đúng); (c) label nào cần chỉnh = đường labelBackfill (gotcha #11), KHÔNG sửa chuỗi seed suông. +9. **Mã phiếu**: CodeGen service prefix `"KHKK/{YYYY}"` qua `WorkflowAppCodeSequence` (spec cũ #8-CẮT: bảng dùng chung 4 module Office, atomic = transaction SERIALIZABLE trong service, `ContractCodeSequence.cs:5`). +10. **fe-admin Designer type-10**: `ApprovalWorkflowsV2Page.tsx` map typeCode→10 (site `:143` — grep lại lúc làm, cờ spec cũ giữ) + 3 site `ApprovalWorkflowV2AdminFeatures.cs` liệt-kê-thủ-công (`:911/:929` merge chữ-ký-sống · `:971/:992` RetainedOpinionRef · `:1044/:1056` chữ ký mồ côi) — thiếu ⇒ admin xoá Level có chữ ký nổ FK 500. +11. Build + test + commit `[CLAUDE] Domain+Infra+Admin: W1 KHKK schema Mig 69 + policy KeHoachKyKet + Designer type-10`. + +## ③ CHECKLIST + +**A. Gate 5-anchor (mở TRƯỚC):** `ApprovalWorkflow.cs:53-67` (slot 10 trống) · `MenuKeys.cs:41` (const KeHoachKyKet + comment tái dùng) · `MenuKeys.cs:163+` (All chưa có KeHoachKyKet) · `AuthorizePolicyRegressionTests.cs:138-139` (lưới policy-ngoài-All ⇒ 500) · `DbInitializer.cs:1779-1786` (Khkk_* 7 key đã seed). +**B. Acceptance (từ spec cũ W1 `:281-300`, giữ nguyên hiệu lực + bổ sung):** +- [ ] `dotnet ef migrations list` → migration cuối chứa `AddContractSigningPlans` (KHÔNG có tiền tố `69_`). +- [ ] `sqlcmd` local: `SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped=0` → **89 → 96**. +- [ ] `dotnet test` → `AuthorizePolicyRegressionTests` XANH (policy `KeHoachKyKet.*` hợp lệ — All đã chứa key). +- [ ] Test mới tồn tại ĐÚNG TÊN: `ContractSigningPlanSchemaTests.BayBang_MoiBang_CoAuditFields_VaQueryFilter` + sàn `N ≥ 2` test mới, PASS 0 fail (số-sau ≥ số-đo-ngay-trước + 2). +- [ ] Admin tạo được quy trình `type=10` 3 Bước (PMH→CCM→CEO), pin `IsUserSelectable`; xoá 1 Level chưa chữ ký → OK. +- [ ] `dotnet ef migrations script` + **Down chạy sạch trên DB copy**. +- [ ] Policy-grant: user role Drafter gọi `GET /api/contract-signing-plans` (W2 endpoint chưa có — SKIP ở W1, chuyển W2) — thay bằng: `sqlcmd` đếm Permission rows key `KeHoachKyKet` = số role đích × đúng cờ. +- [ ] cicd 3-chân-kiềng sau deploy: CI PASS · `__EFMigrationsHistory` top = Mig 69 (curl API không đo được mig ⇒ dùng sqlcmd qua RDP-người hoặc chấp nhận CI-log làm chứng) · sys.tables 96 · menu KHÔNG đổi (142 giữ — W1 không thêm menu). +**C. Rủi ro tick trước khi đóng wave:** [ ] không quên HasQueryFilter · [ ] UNIQUE filtered có WHERE IsDeleted=0 (gotcha #57) · [ ] 0 SQL tay (gotcha #84). diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w2-crud-nhap-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w2-crud-nhap-29-07-2026.md new file mode 100644 index 0000000..4b6ea6f --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w2-crud-nhap-29-07-2026.md @@ -0,0 +1,35 @@ +# SPEC WAVE-2 — CRUD phiếu nháp + căn cứ b.8-9 + FE 3 page ×2 app (GĐ2 KHKK) + +> Nguồn RATIFIED: spec cũ §2.3 (CQRS) + §2.4 (FE 4-place) + §③ WAVE-2 (`:310-339`). Chạy SAU W1. + +## ① TÍNH NĂNG / ĐỀ BÀI + +BCH tạo được **phiếu nháp "Kế hoạch ký kết HĐ"** từ PE `DaDuyet`: dòng giá đề xuất per-winner (snapshot `PeReferenceAmount`), danh mục căn cứ b.8-9 (mẫu/shopdrawing TVGS per-item status), đính kèm — **chưa trình được** (trình = W3). Người dùng thấy: 6 leaf `Khkk_*` hết ComingSoon, thành trang thật. + +## ② CÁCH IMPLEMENT + +1. **Gate 5-anchor** (§③-A). +2. **BE `Application/ContractSigningPlans/ContractSigningPlanFeatures.cs`** (mega-file khuôn `ProposalFeatures.cs` — CHỈ CRUD/DTO, 🔴 CẤM copy phần duyệt của Proposal, bẫy §2.1 spec cũ): + - `CreateContractSigningPlanCommand(PeId, ApprovalWorkflowId, GhiChu?)`: validator PE tồn tại + `Phase==DaDuyet` (mirror `PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs:53-54` — đường dẫn ĐẦY ĐỦ, file KHÔNG nằm ở Contracts/) + chưa-có-plan-sống (`AnyAsync(p.PeId==x && p.Phase != TuChoi)` → 409; cho tạo lại sau TuChoi) + workflow `ApplicableType==10` (mirror Proposal `:258-267` — copy CẢ guard `:266-267`, đừng chỉ copy phần nạp). Handler auto-sinh Lines từ winners: `pe.Suppliers.Where(IsWinner)`, `PeReferenceAmount = SUM(Quotes WHERE IsSelected)` per-winner (copy phép tính `:56-62,:88-90`) — snapshot 1 lần. Sinh `MaKeHoach` = `KHKK/{YYYY}/{Seq:D3}` (W1 CodeGen). + - `UpdateContractSigningPlanDraftCommand`: guard `Phase ∈ {DangSoanThao, TraLai}` + cho re-pin `ApprovalWorkflowId`. + - `Get`/`List`/`pendingMe`: detail Include Lines+DossierItems+Attachments+LevelOpinions+workflow-tree (khuôn Proposal `:161-228`); **inbox V2 precompute TỪ NGÀY 1** (mirror PE `ResolveV2InboxIdsAsync`) — wire sẵn cho W3. + - `UpsertDossierItemCommand`/`DeleteDossierItemCommand`: chỉ mở `DangSoanThao|TraLai`. Attachment mở mọi phase (triết lý PE S147). + - `DeleteContractSigningPlanCommand`: allow-list `DangSoanThao|TuChoi` — **mở NGAY W2** (spec cũ M6: thiếu nó, tạo nhầm 1 phiếu là PE bị khoá 409 vĩnh viễn). + - Màn nguồn "PE đã duyệt — chờ lập KH": query mirror picker `ListApprovedPurchaseEvaluationsQuery:173` đổi vị ngữ `&& chưa-có-plan-sống`. +3. **Controller `ContractSigningPlansController.cs`** — 🔴 authz 2 TẦNG: class `[Authorize]` + per-action `[Authorize(Policy="KeHoachKyKet.{Read|Create|Update|Delete}")]` (Δ W1: prefix `KeHoachKyKet`, KHÔNG phải `ContractSigningPlans` như spec cũ `:202`). Endpoints: GET list/inbox · GET {id} · POST · PUT {id} · dossier-items CRUD · attachments · DELETE {id}. (transitions = W3.) +4. **FE ×2 app** (duplicate CÓ CHỦ ĐÍCH, 4-place mirror Pattern 16-bis): `pages/khkk/` 3 page (List ~252 + Create ~245 + Detail ~386 + types ~95 — khuôn bộ Proposal) · `App.tsx` routes · `Layout.tsx` staticMap **re-point 6 leaf `Khkk_*` khỏi `/coming-soon`** (List→list · Create→create · Pending→list?filter=ChoDuyet · Approved→list?filter=DaDuyet · Deleted→deleted-view · WfView→matrix type=10) · `lib/menuKeys.ts`. Detail: bảng Lines (đề xuất per-NCC) + bảng DossierItems (chip status + TVGS + scan) + attachments; panel workflow + banner = W3. +5. Build ×2 + test + commit. + +## ③ CHECKLIST + +**A. Gate 5-anchor:** `ProposalFeatures.cs:427-439` (bẫy flatten — để BIẾT mà né) · `CreateContractFromEvaluationFeatures.cs:53-62` (guard+phép tính snapshot) · `ProposalFeatures.cs:258-267` (guard workflow-type trọn) · `DbInitializer.cs:2207` khu staticMap comment `Khkk_*` (leaf đang ComingSoon) · `MenuKeys.cs` All (KeHoachKyKet ĐÃ vào từ W1). +**B. Acceptance (spec cũ W2 `:322-337` + Δ):** +- [ ] `POST /api/contract-signing-plans` từ PE DaDuyet → 201; DB N Lines = số winner (đếm đối chiếu `PurchaseEvaluationSuppliers WHERE IsWinner=1`). +- [ ] `PeReferenceAmount` mỗi Line == SUM Quotes IsSelected đúng NCC (2 câu đếm so khớp). +- [ ] Tạo lần 2 cùng PE → 409. · [ ] PE chưa DaDuyet → 400/409, KHÔNG 500. +- [ ] 🔴 Authz 2 tầng: user thiếu quyền POST → **403** (không 200/401/500); có quyền → 201. Gate rẻ: `grep -c "Authorize(Policy" ContractSigningPlansController.cs` ≥ 4. +- [ ] DELETE ở DangSoanThao → 204; sau đó tạo lại plan cho CÙNG PE → 201 (chứng thoát khoá 409). +- [ ] `dotnet test` ≥ baseline + 2, PASS 0 fail, có tên: 1 test authz-regression controller mới + 1 test guard `Phase!=DaDuyet`. +- [ ] FE build ×2 PASS · SHA256 file mirror IDENTICAL · 6 leaf `Khkk_*` hết trỏ coming-soon: `grep "Khkk_" fe-user/src/components/Layout.tsx | grep -c "coming-soon"` = 0 (cùng lệnh fe-admin). +- [ ] cicd 3-chân-kiềng + byte-marker bundle (control ÂM độc quyền — chọn chuỗi trước khi đo). +**C. Rủi ro:** [ ] HasQueryFilter hoạt động (phiếu xóa mềm không hiện list) · [ ] tạo-lại-sau-xóa không bị chặn oan (gotcha #57). diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w3-duyet-3-tram-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w3-duyet-3-tram-29-07-2026.md new file mode 100644 index 0000000..4b3ca4c --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w3-duyet-3-tram-29-07-2026.md @@ -0,0 +1,37 @@ +# SPEC WAVE-3 — Duyệt 3 trạm + finalize chốt giá (GĐ2 KHKK) 🔴 wave nặng nhất + +> Nguồn RATIFIED: spec cũ §2.1 (bẫy Proposal) + §2.3 service + §③ WAVE-3 (`:341-386`). Chạy SAU W2. Test-before BẮT BUỘC (critical algorithm OR-of-N). + +## ① TÍNH NĂNG / ĐỀ BÀI + +Phiếu KHKK chạy trọn `DangSoanThao → ChoDuyet → (PMH → CCM → CEO) → DaDuyet`, chốt `ApprovedAmount` per-winner tại finalize choke-point. Default dry-run: **đường THƯỜNG duy nhất** — CEO ký thật, KHÔNG bật 2 đường kết thúc sớm (`AllowApproverFinalize`/`CeoApprovalThreshold` — anh từng phán "PHÁ VỠ" S155; plan cha default #1). + +## ② CÁCH IMPLEMENT + +1. **Gate 5-anchor** (§③-A). +2. **Test-before**: viết 6 test sàn (bảng §③-B) RED trước. +3. **`Infrastructure/Services/ContractSigningPlanWorkflowService.cs`** — copy `ContractWorkflowService.ApproveV2Async:217-394` (🔴 CẤM copy ProposalFeatures `:427-439` — flatten phá OR-of-N, comment nó tự thú "assume 1 step"). Giữ nguyên: con-trỏ ĐÔI StepIndex+LevelOrder · GroupBy Order `:246` · OR-of-N `:259-260` · LevelOpinions UPSERT `:292-316` · skipToFinal `:320-352`. Adapt 3 chỗ: + (a) Terminal → `Phase=DaDuyet` + helper **`ApplyApprovedValuesOnFinalize`** (mọi Line `ApprovedAmount ??= ProposedAmount`) tại choke-point DUY NHẤT + RULE comment "mọi nhánh set DaDuyet PHẢI gọi helper" + grep-site tự kiểm (khuôn `PurchaseEvaluationWorkflowService.cs:1008` — comment nêu LUẬT+lệnh grep, CẤM số-đếm site). + (b) 2 nhánh kết thúc sớm: **PORT NHƯNG KHÔNG KÍCH HOẠT** (code path có, workflow seed không bật cờ) — default #1. + (c) Reject → tối thiểu `TraLai` toàn phần (4 return-mode per-level cờ sẵn `Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:116-125` — 🔴 class `ApprovalWorkflowLevel` nằm TRONG `ApprovalWorkflow.cs:95`, KHÔNG có file riêng tên đó). +4. **Ghi lịch sử**: mỗi transition ghi `ContractSigningPlanApprovals` (bảng #7 W1) + `Changelogs` qua `LogWorkflowTransitionAsync` (4 cột đủ từ W1). +5. **Guard trình** = `CreatedBy==actor ∨ DeptManager(cùng DepartmentId phiếu) ∨ Admin` — CỐ Ý khác khuôn role-based Contract để né lớp 403-PMH (spec cũ `:193-196`). +6. **Notify**: mirror `LogTransitionAsync` (`ContractWorkflowService.cs:407-427`) + **THÊM notify đích danh approver Cấp kế** (fix lỗ notify-chỉ-Drafter từ ngày 1). +7. **Endpoint** `POST {id}/transitions` (+ policy `KeHoachKyKet.Update`). +8. **FE ×2**: panel workflow (mirror PE Panel-3 ✓/●/○) + banner "Đến lượt bạn" (khuôn `blockedByV2Level`) + nút Duyệt/Trả lại/Từ chối + ô ý kiến (UPSERT vào LevelOpinions khi duyệt — Q1=1B sync). +9. Build ×2 + test GREEN + UAT 1 lượt thật (2 phiếu song sinh) + commit. + +## ③ CHECKLIST + +**A. Gate 5-anchor:** `ContractWorkflowService.cs:217-394` (nguồn copy) · `:259-260` (OR-of-N) · `:292-316` (UPSERT) · `ApprovalWorkflow.cs:95+:116-125` (return-mode cờ, class trong file này) · `PurchaseEvaluationWorkflowService.cs:1008` (RULE choke-point). +**B. Acceptance (spec cũ W3 `:348-384` — SÀN 6 test, thiếu 1 tên = TRƯỢT):** +- [ ] 🔴 OR-of-N 4 vế: (a) 2 phiếu song sinh — phiếu-1 A duyệt → tiến; phiếu-2 B duyệt → CŨNG tiến; (b) ca ÂM: người ngoài cấp → 403; (c) `LevelOpinions WHERE PlanId=@id` = 1 row; (d) KHÔNG đòi cả A và B. +- [ ] Con-trỏ đôi: workflow 3 Bước → StepIndex 1→2→3, LevelOrder reset về 1 mỗi Bước mới. +- [ ] Finalize: `Lines WHERE PlanId=@id AND ApprovedAmount IS NULL` = 0 (🔴 có `AND PlanId=@id`). +- [ ] Inbox: approver cấp đang chờ thấy phiếu; cấp khác KHÔNG. +- [ ] UPSERT: duyệt 2 lần cùng cấp → 1 row, `SignedByUserId` người thật. +- [ ] Trả lại → TraLai → sửa → gửi lại chạy từ Cấp 1 Bước 1. +- [ ] Notify: approver Cấp kế NHẬN thông báo (test-after được phép — ngoài sàn 6). +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 6, đủ 6 TÊN trong `ContractSigningPlanApprovalTests`: `OrOfN_MotNguoiTrongCap_DuyetDuoc_KhongDoiDuCaHaiKy` · `ConTroDoi_SangBuocMoi_LevelOrder_ResetVe1` · `Finalize_MoiLineCuaDungPhieuDo_CoApprovedAmount` · `Inbox_ChiApproverCapDangCho_ThayPhieu` · `LevelOpinions_DuyetHaiLanCungCap_ChiMotRow` · `TraLai_GuiLai_ChayLaiTuCap1Buoc1`. +- [ ] FE build ×2 PASS; cicd 3-chân-kiềng sau deploy; prod curl: user approver thật vào inbox thấy phiếu test ZZTEST. +**C. Rủi ro:** [ ] copy nhầm Proposal = mất OR-of-N ÂM THẦM (build sạch test cũ xanh — acceptance #1 là lưới duy nhất) · [ ] quên inbox precompute (đã wire W2) = lặp lỗi Contract. diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w5-cau-khkk-hd-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w5-cau-khkk-hd-29-07-2026.md new file mode 100644 index 0000000..fa25307 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w5-cau-khkk-hd-29-07-2026.md @@ -0,0 +1,30 @@ +# SPEC WAVE-5 — Cầu KHKK→Hợp đồng + pin V2 (GĐ2→GĐ3, sửa bridge cũ 1 file) + +> Nguồn: spec cũ §2.5 + §③ WAVE-5 (`:397-403`) + lane-2 §2 (chi tiết nhất). Anh ĐÃ MỞ gate chạm HĐ @S160 (run.md ④). Defaults plan cha #2 (giá KH thắng) + #3 (Q11 cảnh báo mềm). Chạy SAU W3 + W6. + +## ① TÍNH NĂNG / ĐỀ BÀI + +Từ phiếu KHKK `DaDuyet`, tạo HĐ per-winner với **giá = `ApprovedAmount` của đúng NCC** (thay SUM báo giá PE), HĐ **pin workflow V2** để trình được ngay (hết kẹt `ConflictException`). **SỬA BRIDGE CŨ — KHÔNG đẻ endpoint mới** (lane-2 §2c: 2 endpoint cùng nghĩa = lớp lỗi 2-site S155). + +## ② CÁCH IMPLEMENT + +1. **Gate 5-anchor** (§③-A). +2. **`CreateContractFromEvaluationFeatures.cs`** (file DUY NHẤT phía BE logic): + (a) đọc plan `DaDuyet` của PE (`Plans.Where(PurchaseEvaluationId==peId && Phase==DaDuyet)`) — CÓ: `giaTri = line.ApprovedAmount` khớp `w.SupplierId` thay `SUM :88-90`, ghi ContextNote lệch *"giá KH x vs SUM-PE y"* vào changelog (khuôn `:121-140`, bài D4 S134); KHÔNG có: giữ SUM + **cảnh báo mềm** trong response (Q11 default). + (b) `+param ApprovalWorkflowId?` vào command + body record `PurchaseEvaluationsController.cs:379-382` (🔴 record-count controller == command — bài 10-point checklist) → pin V2 cho HĐ tạo ra; không truyền → fallback hiện trạng V1 + warning. + (c) 🆕 ghi ngược `Line.ContractId = contract.Id` per-winner trong vòng lặp `:82-115` (cột C6 sẵn từ W1). + (d) GIỮ nguyên: guard `:53-60` · idempotency `pe.ContractId :145` · atomic codegen `:78-82` · mapping `:101-107`. +3. **FE ×2 app**: màn KHKK Detail (phiếu DaDuyet) thêm nút **"Tạo Hợp đồng"** deep-link gọi CÙNG endpoint `POST /api/purchase-evaluations/{peId}/create-contract` (+field chọn workflow V2 cho HĐ). Màn PE giữ nút cũ (2 đường song song — Q11 default). +4. Test-before 4 test (§③-B) + build ×2 + commit. ⚠️ Đây là commit CHẠM module Contract đầu tiên — cicd verify kỹ (endpoint 401-control-404, khuôn đợt-5). + +## ③ CHECKLIST + +**A. Gate 5-anchor:** `CreateContractFromEvaluationFeatures.cs:53-60` (guard) · `:68-71` (pin V1-only) · `:88-90` (SUM) · `:145` (idempotency) · `ContractWorkflowService.cs:115-116` (ConflictException kẹt V2-less). +**B. Acceptance (lane-2 §7-W7 + spec cũ W5):** +- [ ] 4 test tồn tại ĐÚNG TÊN trong `ContractSigningPlanBridgeTests`: `Bridge_CoPlanDaDuyet_GiaTriHopDong_BangApprovedAmountCuaDungNCC` · `Bridge_KhongCoPlan_GiuSumBaoGiaPE_VaCanhBaoMem` · `Bridge_PinApprovalWorkflowV2_HopDongTrinhDuoc_KhongConConflictException` · `Bridge_GhiNguoc_LineContractId_DungTungWinner`. +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 4; `CreateContractFromEvaluationMultiWinnerTests` (ĐÃ TỒN TẠI trên đĩa) VẪN XANH — chống hồi quy multi-winner. +- [ ] TÍNH-CHẤT: PE có W winner + plan DaDuyet → `POST create-contract` trả đúng W contractId; MỖI HĐ `giaTri == ApprovedAmount` của Line cùng supplierId; tổng W HĐ == SUM(ApprovedAmount) toàn plan. +- [ ] Prod curl (ZZTEST): HĐ vừa tạo → `POST /{id}/transitions {targetPhase:10}` bởi Drafter → 204 (KHÔNG 409); `GET /{id}` → phase=10 + currentApprovalLevelOrder=1. +- [ ] Changelog có ContextNote chứa CẢ 2 số khi giá KH ≠ SUM-PE. +- [ ] FE build ×2 · cicd 3-chân-kiềng. +**C. Rủi ro:** [ ] KHÔNG sửa lệch phép SUM cũ cho PE-không-plan (đường cũ phải y nguyên — test #2 canh) · [ ] body record lệch count controller↔command (silent 400). diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w6-duong-ong-hd-v2-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w6-duong-ong-hd-v2-29-07-2026.md new file mode 100644 index 0000000..6a3d466 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w6-duong-ong-hd-v2-29-07-2026.md @@ -0,0 +1,32 @@ +# SPEC WAVE-6 — Đường ống HĐ V2: view + inbox + vai-trình (GĐ3 sống) + +> Nguồn: lane-2 §1 (bảng SẴN/GAP/PARTIAL re-verify 07-29) + §6-W6. Điều kiện tiên quyết VẬN HÀNH của GĐ3 — không có nó, HĐ pin V2 xong vẫn "vô hình" với approver ("cơ chế đúng, đường đi không có"). Build độc lập W1-W3 (0 đụng file KHKK) — chạy sớm được. + +## ① TÍNH NĂNG / ĐỀ BÀI + +3 lỗ đường ống làm HĐ V2 không vận hành được (đo 07-29): +1. **Approver V2 KHÔNG XEM được HĐ `ChoDuyet`** — view-guard `GetEligiblePhases` (`ContractFeatures.cs:334-351`) 0 entry ChoDuyet cho approver (`:488-489` ném Forbidden). +2. **HĐ ChoDuyet KHÔNG vào inbox** — `PhaseActorRoles` (`:421-430`) 100% legacy phase. +3. **PMH role `Procurement` bị 403 khi trình** — guard trình đòi `Drafter∨DeptManager` (`ContractWorkflowService.cs:73-78`). +Wave này = 0 UI mới, 0 mig; sau deploy: approver thấy + mở + duyệt được HĐ đang chờ mình. + +## ② CÁCH IMPLEMENT + +1. **Gate 5-anchor** (§③-A). +2. **Test-before** 3 test (§③-B) RED. +3. **View**: `GetEligiblePhases`/nhánh view (`ContractFeatures.cs:334-351` + guard `:488-489`) — thêm nhánh V2: user là `ApproverUserId` của BẤT KỲ Level trong workflow đã pin của HĐ (hoặc precompute-set, xem 4) → được xem `ChoDuyet` + `DaPhatHanh` của HĐ đó. Admin giữ thấy-hết (`:298/:440`). KHÔNG mở toang: user ngoài workflow + không Drafter → 403 y cũ. +4. **Inbox**: mirror khuôn PE `ResolveV2InboxIdsAsync` (precompute id-set các HĐ mà con-trỏ Step/Level hiện tại chứa `ApproverUserId==actor`) — GHÉP vào inbox query hiện có thay vì chỉ `PhaseActorRoles` legacy. Giữ nhánh legacy cho HĐ V1 (nếu prod xuất hiện lại data V1). +5. **Guard trình** (`ContractWorkflowService.cs:73-78`): nới thành `Drafter ∨ DeptManager ∨ CreatedBy==actor ∨ Procurement` (dòng tối thiểu đóng lớp 403-PMH; giữ 403 cho role ngoài). +6. (Nếu DTO cần) thêm `currentApproval` info vào detail DTO cho FE panel — chỉ khi FE thiếu dữ liệu hiển thị. +7. Build + test GREEN + commit `[CLAUDE] Contract: W6 duong ong V2 view+inbox+trinh`. cicd verify. + +## ③ CHECKLIST + +**A. Gate 5-anchor:** `ContractFeatures.cs:334-351` · `:421-430` · `:488-489` · `ContractWorkflowService.cs:73-78` · `ContractsController.cs:38-40` (khu inbox/list endpoint). +**B. Acceptance (lane-2 §7-W6):** +- [ ] 3 test ĐÚNG TÊN trong `ContractV2PipelineTests`: `Inbox_ApproverCapDangCho_ThayHopDongChoDuyet` · `Inbox_CapKhac_KhongThayHopDong` (ca ÂM) · `View_ApproverV2_KhongPhaiDrafter_XemDuocHopDongChoDuyet`. +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 3. +- [ ] Prod ③ (sau W5 có HĐ test; trước đó dùng HĐ tạo tay ZZTEST): login approver-V2 KHÔNG-admin → `GET /api/contracts/inbox` ≥1 HĐ ChoDuyet mà mình là Level hiện tại; `GET /{id}` → 200 (trước W6: inbox 0 + 403). +- [ ] Ca ÂM giữ: user ngoài workflow + không Drafter → `GET /{id}` vẫn 403 (chứng KHÔNG mở toang). +- [ ] CI run PASS; 0 mig (diff `*Migrations*` = 0 — khuôn M2 đợt-5). +**C. Rủi ro:** [ ] đổi view-guard làm hở HĐ cho MỌI user (thiếu điều kiện thuộc-workflow) — ca ÂM #4 canh · [ ] inbox V2 quên lọc `IsDeleted` · [ ] đụng nhánh Reject-trước-guard (`:49-66`) = KHÔNG PHẢI wave này (O-C, W8, cần anh gật riêng — đừng "tiện tay sửa"). diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w7-ban-cung-hdc-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w7-ban-cung-hdc-29-07-2026.md new file mode 100644 index 0000000..a2cf333 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w7-ban-cung-hdc-29-07-2026.md @@ -0,0 +1,35 @@ +# SPEC WAVE-7 — Bản cứng `Hdc_*` 7 leaf trang thật (GĐ4, b.19→21, upload-only) + +> Nguồn: lane-2 §3 (option A derive 0-mig — grounding đã SỬA: Hdc = **7 leaf per-ContractType**, KHÔNG phải 6-leaf; KHÔNG có phase "KyCung") + lane-1 F-13/F-14/F-15 (nghiệp vụ ký/dấu/lưu/scan; ⚠️ bảng 6-leaf của F-15 xây trên tiền đề menu SAI — NỘI DUNG cột/field giữ, HÌNH menu theo lane-2) + F-11 (bản cứng sinh ở b.17-18 GĐ3 → GĐ4 hiển thị readonly phần đó). Build song song từ đầu; VERIFY cần HĐ `DaPhatHanh` (sau W5+W6). + +## ① TÍNH NĂNG / ĐỀ BÀI + +7 leaf `Hdc_` ("01. HĐ Thầu phụ - Bảng cứng" … "07.") hết ComingSoon → trang **theo dõi bản cứng** per loại HĐ: list HĐ `phase=9 DaPhatHanh` + trạng thái bản cứng + thao tác 3 trạm b.19/20/21 dạng **upload-only** (ký GĐ → đóng dấu HRA/ISO → phát hành/lưu/scan). **Option A: 0-migration** — trạng thái DERIVE từ attachment; GĐ4 nằm SAU terminal nên KHÔNG thêm phase, KHÔNG hồi sinh `DangDongDau=8 [LEGACY]`. + +Mapping trạm → bằng chứng upload (đảo được sang option B nếu anh muốn field tường minh): +| Trạm (QT) | Ai | Bằng chứng hệ thống | Derive badge | +|---|---|---|---| +| b.19 Ký GĐ/NĐUQ | BOD | upload `ScannedSigned` (scan bản có chữ ký GĐ) | "Đã ký GĐ" = has(ScannedSigned) | +| b.20 Đóng dấu | HRA/ISO | (gộp vào b.21 — option A không tách được 2 mốc; khai lệch so 4-mốc lane-1 F-14) | — | +| b.21 Phát hành + Lưu | PB/BCH + CCM | upload `SealedCopy` (scan bộ đóng dấu) + **note bắt buộc: số bộ gốc · đã giao CCM/NTP · nơi lưu/Filing path** (plan cha default #9) + upload scan cover FO-002.01 (luật (c) lane-1) | "Đã lưu bản cứng" = has(SealedCopy) | +Readonly từ GĐ3 (F-11): khối "Ký nháy & cover b.17-18" hiển thị từ attachments (`InitialSigned` mới) + LevelOpinions HĐ. + +## ② CÁCH IMPLEMENT + +1. **Gate 5-anchor** (§③-A). +2. **BE nhỏ**: (a) enum-extend `AttachmentPurpose` += `InitialSigned = 4` (int column, 0-mig — tiền lệ PE `ComparisonTable=4`; `ContractAttachment.cs:5-11`); (b) `ContractListItemDto` +2 cờ derive `hasScannedSigned`/`hasSealedCopy` trong list query (sửa DTO + projection, 0-mig; DTO hiện 12 field `ContractFeatures.cs:323-327` KHÔNG trả attachments). +3. **FE ×2 app**: page `HardCopiesPage` (khuôn `MyContractsPage.tsx` — ĐÃ đọc đủ `type`/`phase` params `:29,:34-35`): route `/hard-copies?type=N` → list `GET /api/contracts?type=N&phase=9` + cột mã HĐ/NCC/dự án/giá trị + 2 badge derive + panel upload (3 purpose) + note theo default #9 + khối readonly GĐ3. 4-place mirror ×2 app: page + `App.tsx` route + `Layout.tsx` staticMap **re-point 7 dòng `Hdc_`** (`fe-user Layout.tsx:63-69`, mirror fe-admin) khỏi `/coming-soon?stage=4&t=NN` + types. Empty-state khi loại chưa có HĐ phát hành. +4. **Authz**: dùng policy root **`Contracts.{Read,Update}`** (Contracts ∈ All `:168`, policy sống sẵn — `Hdc_*`/`HopDongCung` NGOÀI All CỐ Ý, lane-2 §3; tiền lệ `/deleted` `ContractsController.cs:28-29`). KHÔNG đẻ key/policy mới. +5. Build ×2 + test + commit. cicd: byte-marker + menu KHÔNG đổi (142 giữ — chỉ re-point staticMap FE, seeder không đụng). + +## ③ CHECKLIST + +**A. Gate 5-anchor:** `ContractAttachment.cs:5-11` · `DbInitializer.cs:1862-1867` (7-leaf loop — chỉ ĐỌC, không sửa) · `fe-user/src/components/Layout.tsx:63-69` · `fe-user/src/pages/contracts/MyContractsPage.tsx:29-46` · `ContractsController.cs:16-24` (list params `type`/`phase`). +**B. Acceptance (lane-2 §7-W8 đổi số wave):** +- [ ] Test ĐÚNG TÊN trong `ContractHardCopyTests`: `Danhsach_BangCung_ChiTraHopDong_PhaseDaPhatHanh` · `CoBanCung_Derive_TuAttachment_SealedCopy`. +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 2. +- [ ] FE build ×2 PASS; `grep "Hdc_" fe-user/src/components/Layout.tsx | grep -c "coming-soon"` = **0** (cùng lệnh fe-admin). +- [ ] Prod ②+③: curl-grep bundle chứa route mới (byte-level #77) · `GET /api/contracts?type=1&phase=9` → 200, mọi item `phase==9`. +- [ ] Ca ÂM: leaf loại 0 HĐ phát hành → empty-state, KHÔNG 500. +- [ ] Upload 3 purpose trên HĐ ZZTEST DaPhatHanh → 2 badge lật đúng; note b.21 có đủ {số bộ, giao CCM/NTP, nơi lưu}. +**C. Rủi ro:** [ ] mirror mù 6-leaf (grounding cũ) — menu Hdc là 7-leaf-per-type owner vẽ, KHÔNG tự đẻ key (gotcha #84) · [ ] upload không phase-guard là HÀNH VI HIỆN TRẠNG (up được mọi phase) — W7 không siết (đó là W8-authz nếu chọn) · [ ] khai lệch b.20 gộp b.21 trong UI (tooltip/label) để user không tưởng thiếu trạm. diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w8-authz-29-07-2026.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w8-authz-29-07-2026.md new file mode 100644 index 0000000..6982b64 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w8-authz-29-07-2026.md @@ -0,0 +1,33 @@ +# SPEC WAVE-8 — Authz khoanh vùng Contract (cross-cutting, trước khi mở rộng người bấm) + +> Nguồn: lane-2 §5 (6 nhóm endpoint + 4 option, re-verify service 07-29) + đo @S160 (prod Contracts = 0 — lỗ CHƯA có nạn nhân thật, nhưng dry-run là thứ đưa data vào). Option = REVIEW CHỐT quyết; **O-C cần anh gật riêng** (chạm `ContractWorkflowService` — O-3 "chỗ hợp đồng cứ từ từ" còn hiệu lực cho phần SỬA service). Đề xuất plan cha #11: **O-B**, +O-C chờ anh. + +## ① TÍNH NĂNG / ĐỀ BÀI + +Menu HĐ public 13/13 role (❹ RỦI-RO CÓ Ý THỨC @S159) + `ContractsController` class-trần ⇒ mọi user đăng nhập gọi được **22 endpoint ghi**. 2 ô đỏ nhất (lane-2 §5): (1) nhánh **Reject-trước-guard** `ContractWorkflowService.cs:49-66` — user bất kỳ TuChoi/TraLai BẤT KỲ HĐ ở BẤT KỲ phase (kể cả DaPhatHanh); (2) **Create ăn sequence thật** ngay lúc tạo (`ContractFeatures.cs:113-118`) nếu chọn dự án thật. Wave này thu hẹp **AI bấm được** (O-B) và — nếu anh gật — đóng ca Reject-sau-terminal (O-C). + +**4 option (giữ đủ cho review chốt):** +- **O-A** giữ nguyên + chấp nhận có ý thức (0 file; quy ước ZZTEST + backup DB). +- **O-B** ⬅ đề xuất: per-action policy `Contracts.{Create,Update,Delete}` cho 22 endpoint ghi theo verb (policy sống sẵn — `Contracts` ∈ All `:168`; tiền lệ `/deleted :28-29` + `SuppliersController.cs:67-99`). ⚠️ policy chặn theo MENU-permission ⇒ thu hẹp AI, không thu hẹp BẤM VÀO ĐÂU. +- **O-C** = O-B + vá tối thiểu Reject: thêm điều kiện `fromPhase==ChoDuyet` cho nhánh Reject (`:49`) — đóng đúng ca "TuChoi HĐ DaPhatHanh", không đổi hành vi hợp lệ. **CHẠM SERVICE HĐ — chỉ làm khi anh gật.** +- **O-D** khoanh data — ĐÃ GIẢI @S160: đo được 0 HĐ thật ⇒ moot hôm nay; ghi lại làm mốc. + +## ② CÁCH IMPLEMENT (theo option O-B; O-C thêm bước 4) + +1. **Gate 5-anchor** (§③-A). +2. **`ContractsController.cs`**: gắn `[Authorize(Policy=...)]` per-action — GET giữ class-trần (đọc mở như hiện trạng, trừ `/deleted` đã có Read) · POST → `Contracts.Create` · PUT/PATCH → `Contracts.Update` · DELETE → `Contracts.Delete` · `POST {id}/transitions` → `Contracts.Update`. Neo TÍNH-CHẤT: MỌI action HttpPost/Put/Delete có policy — KHÔNG neo con số 22. +3. **Seeder grant** (gotcha #84 — CÙNG COMMIT): cấp `Contracts` Create/Update cho role tham gia dry-run (Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin), Delete cho Drafter/Admin — thiếu grant = 403 im lặng giết chính dry-run (gotcha #44). Grep MỌI site seed-permission (S155 >1 site) + để revoker S92-đã-gỡ yên. +4. *(chỉ khi anh gật O-C)* `ContractWorkflowService.cs:49`: nhánh Reject thêm guard `contract.Phase == ContractPhase.ChoDuyet` (trước mọi side-effect); test ca ÂM đi kèm. +5. Build + test + commit + deploy; **chạy NGAY phép 403/201 sau deploy** (đừng để người dry-run tự phát hiện bị chặn). + +## ③ CHECKLIST + +**A. Gate 5-anchor:** `ContractsController.cs:13` (class-trần) · `:28-29` (tiền lệ policy) · `SuppliersController.cs:67` (khuôn Master) · `MenuKeys.cs:163-181` (Contracts ∈ All) · `ContractWorkflowService.cs:49-66` (nhánh Reject — CHỈ đọc nếu O-B, sửa nếu O-C). +**B. Acceptance (lane-2 §7-W9 đổi số wave):** +- [ ] `AuthorizePolicyRegressionTests` (ĐÃ TỒN TẠI) VẪN XANH. +- [ ] Test mới `ContractsControllerAuthzTests`: `MoiEndpointGhi_CoAuthorizePolicy` (reflection: mọi HttpPost/Put/Delete có `[Authorize(Policy=...)]` — tính-chất, không đếm 22) + `User_ThieuQuyen_GoiPost_Tra403` (ca ÂM — 403, KHÔNG 500/200). +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 2. +- [ ] Prod ③ sau deploy: user đã-cấp POST /api/contracts → 201; user chưa-cấp → 403. Chạy NGAY sau deploy. +- [ ] (O-C) test `Reject_KhiHopDongDaPhatHanh_BiChan` + prod verify HĐ ZZTEST DaPhatHanh không đổi phase khi user thường bắn transitions Reject. +- [ ] cicd 3-chân-kiềng: restart-proof + grant seeder ăn (đếm Permission rows key `Contracts` per-role qua log seeder/`sqlcmd` RDP) + control ÂM `Hrm/Off/Personal` non-Admin vẫn 0 (revoker còn sống, không bị seeder mới lật). +**C. Rủi ro:** [ ] 403 nhầm người đang dry-run (thiếu grant seeder — gotcha #44) · [ ] grant mới vô tình nâng row `false` cố ý của S92 (seeder grant CHỈ key `Contracts`, không đụng Hrm/Off/Personal) · [ ] tự ý sửa Reject khi anh CHƯA gật (O-3). diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-investigator-codebase-2.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-investigator-codebase-2.md index ec0a732..441af74 100644 --- a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-investigator-codebase-2.md +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-investigator-codebase-2.md @@ -58,3 +58,125 @@ | ⚠️ đường tắt | PE → Contract KHÔNG QUA KHKK vẫn sống (Q11 mặc định = cảnh báo mềm) | dry-run phải chấp nhận 2 đường song song cho tới khi owner nâng cứng Q11 | **Pattern tái dùng cite được:** (1) pull-model picker `ListApprovedPurchaseEvaluationsQuery:173` — nhân bản đổi vị ngữ `&& chưa-có-plan-sống` cho màn "PE chờ lập KH" (W2 spec cũ); picker tạo-HĐ đổi nguồn thành plan `DaDuyet` (hoặc giữ PE + banner có/không plan — Q11). (2) Multi-entity create + self-committing codegen atomic `:78-82` comment + loop. (3) Auto-Lines từ winners `:56-62,:82-91`. + +--- + +## §3 — Q3: GĐ4 `Hdc_*` trang thật cần gì + +**🔴 SỬA GROUNDING đề bài (2 chỗ):** +1. **`Hdc_*` KHÔNG phải 6-leaf {WfView/List/Create/Pending/Approved/Deleted}** — đó là cấu trúc `Khkk_*` (`DbInitializer.cs:1779-1786`). `Hdc_*` thật = **7 leaf theo ContractType**, 1 leaf/loại, seed loop `DbInitializer.cs:1862-1867` (`tree.Add(("Hdc_"+code, label+" - Bảng cứng", MenuKeys.HopDongCung, ...))`), label "01. HĐ Thầu phụ - Bảng cứng"…"07." (`:1980-1986`), staticMap → `/coming-soon?stage=4&t=01..07` (`fe-user Layout.tsx:63-69`). Owner tự vẽ cấu trúc này @S159-đợt4 — KHÔNG mirror mù bộ 6-leaf. +2. **Enum KHÔNG có "KyCung"** — grep `KyCung` toàn `src` = **0 hit**. Phase 8 = `DangDongDau` `[LEGACY]` (`ContractPhase.cs:23`), 9 = `DaPhatHanh` terminal SỐNG (`:24`). "Bản cứng" chưa từng là phase. + +**Đã có gì cho GĐ4 (đo):** `DaPhatHanh` = terminal V2 gen-mã (`ContractWorkflowService.cs:366-385`) · `AttachmentPurpose.ScannedSigned=2/SealedCopy=3` (`ContractAttachment.cs:8-9`) upload không phase-guard ⇒ up ĐƯỢC sau terminal · `Contract.cs` **0 field bản-cứng**. + +**Đề xuất TỐI THIỂU cho dry-run (options — review chốt):** +- **(A) 0-migration DERIVE** ⬅ đề xuất chính: mỗi leaf `Hdc_` → trang list `GET /contracts?type=N&phase=9` (param `phase` CÓ SẴN `ContractsController.cs:20`, FE đã chứng minh dùng được: leaf `Ct_*_Approved` → `/my-contracts?type=N&phase=9` `Layout.tsx:141`); trạng thái bản cứng = DERIVE `attachments.Any(Purpose==SealedCopy)` → badge "Đã lưu bản cứng / Chưa"; hành động = upload `ScannedSigned`/`SealedCopy` + enum-extend `InitialSigned=4` ký-nháy (int column, 0-mig — tiền lệ PE `ComparisonTable=4`). List DTO hiện KHÔNG trả attachments (`ContractListItemDto` 12 field `ContractFeatures.cs:323-327`) ⇒ cần +1 cột derive `hasSealedCopy` trong query (sửa DTO, 0-mig). +- **(B) field mỏng trên Contract**: `HardCopyStatus int?` (+`HardCopyNote`) — 1 mig AddColumn (Mig 70+, bảng giữ 89) khi owner muốn trạng thái TƯỜNG MINH (in/ký/đóng dấu/lưu/chuyển phát per bước b.19-21) thay vì derive từ attachment. +- **(C) entity `ContractHardCopy` riêng** — KHÔNG khuyến nghị cho dry-run: b.19-21 là 3 thao tác trên 1 HĐ đã phát hành, không có vòng đời duyệt riêng (giấy đã ký xong); bảng mới = màn admin + FK + configuration thừa. + +**Bind 7 leaf (phương án A):** `Hdc_` → `/hard-copies?type=` (page mới) hoặc rẻ nhất tái dùng `/my-contracts?type=N&phase=9&hdc=1` biến thể hiển thị. Route mới = 4-place mirror ×2 app (App.tsx route `fe-user/src/App.tsx:52-65` khuôn + Layout staticMap `:63-69` đổi khỏi coming-soon + page + types) — khuôn `MyContractsPage.tsx` ĐÃ đọc đủ `type`/`phase`/`deleted` params (`:29,:34-35`) + tự rẽ `/contracts/deleted` (`:41-46`) ⇒ **mirror được, page ~300-400 LOC/app theo khuôn**. + +**Mirror khuôn 6-leaf Ct_* được không?** — Về CƠ CHẾ (regex resolvePath `Layout.tsx:132-143` + query-param page) = ĐƯỢC và NÊN; về HÌNH DẠNG menu = KHÔNG (owner đã chốt 7-leaf-per-type cho Hdc). Nếu sau này owner muốn Pending/Approved trong Hdc thì nới regex `Hdc_([^_]+)_(...)` — hiện KHÔNG tự đẻ key mới (gotcha #84 seeder-only + đổi cấu trúc menu = hỏi owner). + +**Authz lớp menu:** `Hdc_*` leaf + root `HopDongCung` đều **NGOÀI `MenuKeys.All`** (`MenuKeys.cs:37-42` — CỐ Ý, comment ghi rõ) ⇒ 0 policy `Hdc_*`/`HopDongCung.*` tồn tại; per-action authz cho trang Hdc phải dùng policy root **`Contracts.{Action}`** (root ∈ All `:168`) — tiền lệ đúng khuôn: `GET /contracts/deleted` dùng `Contracts.Read` (`ContractsController.cs:29`). + +--- + +## §4 — Q4: "DRY-RUN" về mặt kỹ thuật (prod đang có 37 NCC / 17 user / 62 dự án seed / 7 HĐ V1 thật) + +**Đề xuất: phương án LAI "dự án test riêng", KHÔNG cờ IsDryRun.** + +| Phương án | Đánh giá | Chứng | +|---|---|---| +| **(i) Dự án test riêng `ZZTEST` + 1-2 NCC test** ⬅ đề xuất cho GĐ3+GĐ4 | Mã HĐ dry-run nằm CHUỖI SEQUENCE RIÊNG — prefix codegen = `{ProjectCode∨Year}/{typeCode}/SOL&{SupplierCode}` (`ContractCodeGenerator.cs:33`), seq per-prefix (`:40-53`) ⇒ KHÔNG ăn số của dự án thật. 2 loại framework scope=Year vẫn tách nhờ SupplierCode test. Dự án test NGOÀI danh sách seed ⇒ soft-delete xong **KHÔNG resurrect** (#75/#76 chỉ áp per-code trong seed `SeedRealMasterDataAsync` ungated). Tạo qua UI CRUD, 0 code | `ContractCodeGenerator.cs:33,:40-53` | +| **(ii) Cờ `IsDryRun`** — KHÔNG khuyến nghị | +1 mig + PHẢI sửa MỌI list/inbox/picker query (blast hàng chục handler — đúng lớp lỗi cardinality S87/S88 "sửa write-site sót read-site") + FE filter ×2 app. Đắt hơn giá trị 1 đợt dry-run | bài học `feedback_cardinality_change_grep_consumers` | +| **(iii) Cứ dùng thật** | ĐÚNG cho **KHKK GĐ2** (7 bảng mới 0-data — mỗi phiếu đầu đều là "thử", xóa mềm sạch); **SAI cho Contract GĐ3** (7 HĐ V1 prod thật chạy nhánh V1 `ContractWorkflowService.cs:97` comment + 37 NCC thật trong picker + mã ăn sequence thật nếu chọn dự án thật) | — | + +**Rollback path (soft-delete sẵn có + 2 hố):** +- Cơ chế nền: soft-delete qua interceptor (Remove→IsDeleted, lớp #81-EXT indirect) + global `HasQueryFilter(!IsDeleted)` — bằng chứng Contract có filter: `ListDeleted` phải `IgnoreQueryFilters().Where(IsDeleted)` (`ContractFeatures.cs:372-373` + LUẬT comment `:356-360`). Màn "Đã xóa" chỉ-xem ĐÃ CÓ cả Contract (arc-2) lẫn PE (S155). +- 🔴 **HỐ 1 — HĐ dry-run chạy trọn vòng KHÔNG XÓA ĐƯỢC qua API:** guard `DeleteContractCommand` = `Phase >= DangInKy` (`ContractFeatures.cs:632-633`) là so sánh SỐ trên enum ⇒ chặn cả `DaPhatHanh=9`, `ChoDuyet=10`, `TraLai=98`, `TuChoi=99` (chỉ xóa được phase 2/3/4). Đường vòng tồn tại nhưng xấu: Admin manual override set `targetPhase=DangSoanThao` (`ContractWorkflowService.cs:191-198`) rồi DELETE. ⇒ dry-run phải CHẤP NHẬN HĐ test DaPhatHanh nằm lại (lọc bằng dự án ZZTEST) hoặc dùng đường vòng admin — ghi vào checklist wave cuối, review chốt. +- HỐ 2 — phiếu KHKK/PE `DaDuyet` cũng không xóa (KHKK spec allow-list `DangSoanThao|TuChoi` mirror PE `:1404`) — cùng cách xử lý. +- Sequence KHÔNG rollback — gap số chấp nhận được, chính comment repo nói vậy (`ContractFeatures.cs:113-115` "wasted... Acceptable"). +- Notification dry-run bắn cho user THẬT (in-app `LogTransitionAsync→NotifyAsync` `ContractWorkflowService.cs:407-427`) — không tắt được per-record; dặn trước team. + +**Rủi ro lẫn data thật (liệt kê để checklist):** (1) record test hiện CHUNG list với data thật (IDOR theo role; admin thấy hết) → convention "mọi record dry-run thuộc ZZTEST" + tên phiếu tiền tố; (2) CẤM đặt Code dự án/NCC test TRÙNG danh sách seed (twin FLOCK-01 S148 — seed re-fill theo Code); (3) `pe.ContractId` idempotency — mỗi vòng thử cần PE test MỚI (`:59-60`); (4) file upload thật nằm `wwwroot/uploads` — delete attachment có cleanup, HĐ xóa mềm thì file ở lại (rác chấp nhận); (5) menu đã public 13/13 role + authz trần ⇒ trong lúc dry-run ai cũng bấm được nút ghi trên HĐ THẬT — sang §5. +**Ràng buộc đo đạc:** SSH prod chết khi mở SQL-client (S134/S148 re-confirm) ⇒ phép đo prod trong §7 ưu tiên **curl API** làm chân chính; sqlcmd chỉ chạy local/Dev hoặc đưa lệnh cho người có RDP. + +--- + +## §5 — Q5: AUTHZ khoanh vùng cho dry-run (options — REVIEW CHỐT, không tự quyết) + +**Hiện trạng lộ (menu public 13/13 role ❹ + `[Authorize]` trần):** bất kỳ user đăng nhập nào gọi được 22 endpoint ghi của `ContractsController` (`:13` class-trần; ngoại lệ duy nhất `/deleted` có policy `:28-29`). Phân nhóm theo độ nguy hiểm: + +| Nhóm | Endpoint | Nguy hiểm THẬT (đã re-verify service) | +|---|---|---| +| 🔴 Workflow | `POST {id}/transitions` (`:61-66`) | **Reject-trước-guard** `ContractWorkflowService.cs:49-66`: nhánh Reject KHÔNG check role, KHÔNG check phase-nguồn ⇒ user bất kỳ TraLai/TuChoi **bất kỳ HĐ ở bất kỳ phase — kể cả HĐ THẬT `DaPhatHanh`** (đặt TuChoi=99 lên HĐ đã phát hành). Nhánh Approve NGƯỢC LẠI có guard (OR-of-N 403 `:259-265`; trình đòi Drafter/DeptManager `:73-78`) | +| 🔴 CRUD | `POST /` · `PUT {id}` · `DELETE {id}` | Create = tạo HĐ rác **+ ĂN SEQUENCE thật ngay lúc tạo** (gen mã tại create `ContractFeatures.cs:113-118`) nếu chọn dự án thật; Update draft KHÔNG check drafter (`:151-157`) ⇒ sửa `GiaTri` nháp người khác; Delete không role-check (chỉ phase-guard `:632-633`) | +| 🟠 Attachment | `POST/DELETE {id}/attachments` | Không phase-guard (§1) ⇒ thay/xóa file scan trên HĐ THẬT đã phát hành; delete cleanup file vật lý | +| 🟠 Details | 15 endpoint add/update/delete details (`:123-226`) | Sửa ruột chi tiết 7 loại HĐ mọi lúc (handler có lock-edit 17 handler phase≠DangSoanThao? — lock S8/S9 áp Contract handlers cũ; details HĐ đã qua guard này, mức rủi ro còn lại là nháp người khác) | +| 🟡 Comment | `POST {id}/comments` | Spam/ghi đè ngữ cảnh — nhẹ | +| 🟡 Form-export | `FormsController` (`api/forms`, cũng `[Authorize]` trần `:11`): render/export-pdf (`:25,:32`) = STATELESS đọc-render, rủi ro thấp; `POST templates` (`:42`) = ghi template dùng chung — cùng lỗ class-trần, NGOÀI ContractsController | đo 07-29 | + +**Khuôn per-action CÓ SẴN trong repo (đối chiếu):** (a) Master: `SuppliersController.cs:67-99` 4× `[Authorize(Policy="Suppliers.Update")]`; (b) chính ContractsController: `/deleted` dùng `Contracts.Read` `:28-29`; (c) S155 kết luận cho PE: root-policy dùng được NGAY vì root ∈ `MenuKeys.All` — áp y nguyên cho Contract (`Contracts` ∈ All `MenuKeys.cs:168` ⇒ 4 policy `Contracts.{Read,Create,Update,Delete}` ĐÃ đăng ký runtime, 0 key mới, 0 mig). 🔴 **Sửa grounding đề bài #3:** "khuôn PE controller per-action 3 tầng" KHÔNG tồn tại — `PurchaseEvaluationsController` cũng class-trần (S155 đo `:15`); khuôn per-action thật nằm ở Master + `/deleted`. + +**OPTIONS cho review chốt (chi phí ↑ dần):** +- **O-A GIỮ NGUYÊN + chấp nhận có ý thức** (đúng O-3 "hợp đồng cứ từ từ" + tinh thần dry-run mở): rủi ro tập trung 2 ô 🔴; giảm nhẹ bằng quy ước ZZTEST + backup DB trước đợt dry-run (`scripts/backup-sql.ps1`). 0 file. +- **O-B PER-ACTION ROOT-POLICY 22 endpoint ghi** (`Contracts.Create/Update/Delete` theo verb): 1 file controller, 0 key mới, tiền lệ `/deleted`. ⚠️ kèm bắt buộc: cấp CanCreate/CanUpdate/CanDelete cho role tham gia dry-run QUA SEEDER (gotcha #84, KHÔNG SQL tay) TRƯỚC deploy — không thì 403 im lặng (gotcha #44) làm chết chính dry-run. Chú ý: policy chặn theo MENU-permission, user được cấp vẫn đụng được HĐ THẬT ⇒ O-B thu hẹp AI bấm, không thu hẹp BẤM VÀO ĐÂU. +- **O-C = O-B + vá tối thiểu Reject-trước-guard**: thêm 1 điều kiện `fromPhase==ChoDuyet` cho nhánh Reject (`ContractWorkflowService.cs:49`) — đóng đúng ca "TuChoi HĐ DaPhatHanh", không đổi hành vi hợp lệ nào (Reject hợp lệ chỉ có nghĩa khi đang ChoDuyet). Chạm service HĐ ⇒ **cần anh gật riêng** (O-3). +- **O-D khoanh DATA:** trước dry-run, đo phase 7 HĐ V1 thật (curl admin `GET /api/contracts` — SSH-SQL không dùng được) — nếu cả 7 đã DaPhatHanh thì O-C đóng được ~hết mặt tấn công transitions lên chúng. **UNCERTAIN hiện tại: phase thật của 7 HĐ V1 chưa đo được từ đây** — khai đúng trạng thái, không đoán. + +--- + +## §6 — Q6: WAVE-STRUCTURE KỸ THUẬT GĐ3+GĐ4 (nối W1-W5 cũ; đánh số tiếp W6→W9) + +> Nguyên tắc giữ nguyên spec cũ: mỗi wave **deploy độc lập**, không để nửa tính năng chết trên prod. Đơn vị độ lớn = **số file + có/không mig** (KHÔNG ước giờ). Số bảng nền hôm nay: 89 `sys.tables` (88 app + history); mig cuối trên đĩa = `20260727033522_AddPeAllowApproverDelete` ⇒ **mig kế tiếp = 69** (W1 KHKK dùng), GĐ3/GĐ4 dưới đây **0 mig** trừ W8 nếu owner chọn (B). + +| Wave | Tên | Deploy riêng? | Người dùng THẤY gì | Mig? | Reuse gì | Độ lớn (file) | +|---|---|---|---|---|---|---| +| **W6** | **Đường ống HĐ V2** (điều kiện tiên quyết GĐ3) | ✅ có — 0 UI mới, HĐ V2 hết "vô hình" | Approver V2 **xem được + thấy trong inbox** HĐ đang chờ mình (nay: không) | ❌ 0 | mở rộng `GetEligiblePhases` (`ContractFeatures.cs:334-351`) + inbox `PhaseActorRoles` (`:421-430`) theo khuôn PE `ResolveV2InboxIdsAsync`; guard trình `:73-78` nới cho `Procurement`/drafter-của-HĐ (lớp 403-PMH) | **2-3 file BE** (ContractFeatures + service guard [+ DTO nếu trả `currentApproval`]) | +| **W7** | **Cầu KHKK→HĐ + pin V2** (= W5 spec cũ, đổi tên cho liền mạch) 🔴 CHẶN owner (chạm Contract, Q6/Q11) | ✅ có | Nút "Tạo HĐ" từ KHKK `DaDuyet`; HĐ sinh ra chạy trạm V2 thật (hết kẹt `ConflictException :115-116`) | ❌ 0 | `CreateContractFromEvaluationFeatures.cs` (+param `ApprovalWorkflowId`, giá từ `ApprovedAmount`, ContextNote lệch, ghi `Line.ContractId`) + body record `PurchaseEvaluationsController.cs:379-382` + 1 nút FE ×2 app | **2 BE + 2 FE** | +| **W8** | **GĐ4 bản cứng `Hdc_*`** (7 leaf hết ComingSoon) | ✅ có | 7 trang "…- Bảng cứng" list HĐ `phase=9` + badge trạng thái bản cứng + upload ScannedSigned/SealedCopy/InitialSigned | ❌ 0 nếu (A); ✅ 1 mig AddColumn nếu owner chọn (B) `HardCopyStatus` | param `phase` (`ContractsController.cs:20`) + khuôn `MyContractsPage.tsx:29-46` + enum-extend `AttachmentPurpose` (`ContractAttachment.cs:5-11`) + staticMap `Layout.tsx:63-69` re-point | **~6-8 FE** (page+types+App.tsx+Layout ×2 app) **+1-2 BE** (DTO thêm cờ derive) | +| **W9** | **Authz khoanh vùng** (theo option §5 review chốt) | ✅ có | (không thấy gì nếu làm đúng) — hoặc 403 sạch thay vì cho phép âm thầm | ❌ 0 | khuôn `/deleted :28-29` + `SuppliersController.cs:67-99`; **+ seeder cấp quyền** (gotcha #84) | **1 BE controller + 1 DbInitializer** (+1 service nếu O-C) | + +**Thứ tự & phụ thuộc:** W6 phải TRƯỚC W7 (không thì HĐ pin V2 xong vẫn không ai duyệt được — đúng lỗi "cơ chế đúng, đường đi không có"). W8 độc lập hoàn toàn với W6/W7 (chỉ đọc HĐ `DaPhatHanh`) ⇒ **chạy song song được** — hữu ích cho dry-run vì owner thấy GĐ4 sớm. W9 nên đi **trước hoặc cùng** buổi mở rộng người bấm; nếu review chọn O-A thì W9 = 0 file (ghi quyết định vào plan cha). +**Kết nối W1-W5 cũ:** W1-W4 (KHKK) không đụng Contract ⇒ chạy trước/song song thoải mái. W5 cũ ≡ **W7** ở đây (đổi nhãn để khỏi 2 tên cho 1 việc — plan cha nên chọn 1 cách gọi). + +--- + +## §7 — Q7: ACCEPTANCE ĐO ĐƯỢC per wave (khuôn H5: pin `N ≥ x` + TÊN test cụ thể; neo TÍNH-CHẤT tránh literal tự-lão-hoá) + +> ⚠️ Neo số test: KHÔNG viết "562 + N" như spec cũ (mốc 562 tự lão hoá + `N` thả tự do = 0 bit). Viết: **`dotnet test SolutionErp.slnx` PASS 0 fail, và số test SAU ≥ số test TRƯỚC + x, và x test dưới đây tồn tại ĐÚNG TÊN**. Baseline đo NGAY TRƯỚC wave bằng chính lệnh đó. +> Phép prod theo khuôn 3-chân-kiềng (cicd): ① CI run PASS · ② artifact/bundle prod verify byte-level (curl-grep, #77) · ③ dữ liệu prod đúng (curl API — **KHÔNG sqlcmd qua SSH**, chết client S134/S148). + +**W6 — Đường ống HĐ V2** (≥4) +1. Test-before, class `ContractV2PipelineTests` (`tests/SolutionErp.Infrastructure.Tests/Application/`), ≥3 tên: `Inbox_ApproverCapDangCho_ThayHopDongChoDuyet` · `Inbox_CapKhac_KhongThayHopDong` (ca ÂM) · `View_ApproverV2_KhongPhaiDrafter_XemDuocHopDongChoDuyet` (nay ForbiddenException `ContractFeatures.cs:488-489`). +2. `dotnet test` PASS 0 fail; số test ≥ baseline + 3. +3. Phép prod ③: đăng nhập user approver-V2 (không Admin) → `curl GET /api/contracts/inbox` trả **≥1** HĐ đang `ChoDuyet` mà user là Level hiện tại; cùng user `GET /api/contracts/{id}` → **200** (trước W6: inbox 0 + detail 403). +4. Ca ÂM giữ nguyên đường cũ: user KHÔNG thuộc Level nào và không Drafter → `GET /{id}` vẫn **403** (chứng không mở toang bằng cách bỏ guard). +5. Phép prod ①: CI run xanh trên commit W6. + +**W7 — Cầu KHKK→HĐ + pin V2** (≥4) +1. Test-before `ContractSigningPlanBridgeTests`: `Bridge_CoPlanDaDuyet_GiaTriHopDong_BangApprovedAmountCuaDungNCC` · `Bridge_KhongCoPlan_GiuSumBaoGiaPE_VaCanhBaoMem` · `Bridge_PinApprovalWorkflowV2_HopDongTrinhDuoc_KhongConConflictException` · `Bridge_GhiNguoc_LineContractId_DungTungWinner`. +2. `dotnet test` PASS 0 fail; số test ≥ baseline + 4; test cũ `CreateContractFromEvaluationMultiWinnerTests` (đã tồn tại trên đĩa) **vẫn xanh** — chống hồi quy multi-winner. +3. TÍNH-CHẤT đo được (thay literal): với 1 PE có W winner và plan `DaDuyet`, `POST /{peId}/create-contract` trả **đúng W** contractId, và **mỗi** HĐ có `giaTri == ApprovedAmount` của Line cùng `supplierId`; tổng W HĐ == `SUM(ApprovedAmount)` toàn plan. +4. Phép prod ③ (curl, dự án ZZTEST): HĐ vừa tạo → `POST /{id}/transitions {targetPhase:10, decision:1}` bởi Drafter → **204** (không 409 `ConflictException`); đọc lại `GET /{id}` thấy `phase=10` + `currentApprovalLevelOrder=1`. +5. Changelog có ContextNote so-lệch khi giá KH ≠ SUM-PE (đọc `GET /{id}/changelogs`, chuỗi chứa cả 2 số). + +**W8 — GĐ4 bản cứng** (≥4) +1. Test `ContractHardCopyTests`: `Danhsach_BangCung_ChiTraHopDong_PhaseDaPhatHanh` · `CoBanCung_Derive_TuAttachment_SealedCopy` (+ nếu chọn (B): `HardCopyStatus_MacDinh_Null_KhongVoLuoc`). +2. `dotnet test` PASS 0 fail; số test ≥ baseline + 2 (hoặc +3 nếu (B)). +3. FE: `npm run build` PASS **×2 app**; 7 leaf `Hdc_*` KHÔNG còn trỏ `/coming-soon` — grep `Hdc_` trong `fe-user/src/components/Layout.tsx` + `fe-admin/.../Layout.tsx` trả **0 hit chuỗi `coming-soon`** trên các dòng `Hdc_`. +4. Phép prod ②+③: sau deploy, curl-grep bundle prod chứa route mới (byte-level, #77 — cache≠deploy); rồi curl `GET /api/contracts?type=1&phase=9` trả list (200) và mọi item có `phase==9`. +5. Ca ÂM: leaf Hdc của loại KHÔNG có HĐ phát hành nào → trang rỗng có empty-state, KHÔNG lỗi 500. + +**W9 — Authz** (≥4; nếu review chọn O-A thì wave này = ghi quyết định, các acceptance dưới KHÔNG áp) +1. `AuthorizePolicyRegressionTests` (ĐÃ TỒN TẠI `tests/SolutionErp.Infrastructure.Tests/Api/`) **vẫn xanh** — nó bắt ca policy trỏ menu-key ngoài `MenuKeys.All` (⇒ chứng `Contracts.*` hợp lệ, không đẻ 500). +2. Test mới `ContractsControllerAuthzTests`: `MoiEndpointGhi_CoAuthorizePolicy` (reflection đếm, neo TÍNH-CHẤT: **mọi** action `[HttpPost|HttpPut|HttpDelete]` của `ContractsController` có `[Authorize(Policy=...)]` — không neo con số 22) + `User_ThieuQuyen_GoiPost_Tra403` (ca ÂM, KHÔNG 500/200). +3. `dotnet test` PASS 0 fail; số test ≥ baseline + 2. +4. Phép prod ③ sau deploy: user dry-run ĐÃ được seeder cấp quyền → `POST /api/contracts` **201**; user chưa cấp → **403** (không 500). ⚠️ chạy ngay sau deploy: 403 nhầm cho người đang dry-run = tự chặn chính mình (gotcha #44). +5. Nếu chọn O-C: `Reject_KhiHopDongDaPhatHanh_BiChan` (ca ÂM cho lỗ `ContractWorkflowService.cs:49-66`) + verify HĐ thật không đổi phase. + +**Gate 5-anchor (ràng buộc run.md) — anchor gợi ý cho mỗi wave:** W6 = `ContractFeatures.cs:334-351` · `:421-430` · `:488-489` · `ContractWorkflowService.cs:73-78` · `ContractsController.cs:38-40`. W7 = `CreateContractFromEvaluationFeatures.cs:53-60` · `:68-71` · `:88-90` · `:145` · `ContractWorkflowService.cs:115-116`. W8 = `ContractAttachment.cs:5-11` · `DbInitializer.cs:1862-1867` · `Layout.tsx:63-69` · `MyContractsPage.tsx:29-46` · `ContractsController.cs:16-24`. W9 = `ContractsController.cs:13` · `:28-29` · `SuppliersController.cs:67` · `MenuKeys.cs:163-181` · `ContractWorkflowService.cs:49-66`. + + + diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-4.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-4.md new file mode 100644 index 0000000..a98dadc --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-4.md @@ -0,0 +1,19 @@ +# sub-reviewer-4 — LANE D: NHẤT QUÁN spec-cũ-ratified + QUYỀN OWNER (S160) + +> Lane D / reviewer ensemble. Đích soi: `plan-cha-dry-run-toan-trinh-29-07-2026.md` + `spec-wave-w5-cau-khkk-hd-29-07-2026.md` + `spec-wave-w8-authz-29-07-2026.md`. +> Đối chiếu: `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md` (RATIFIED) + `run.md` S160. +> NOTE naming: task LƯỢT-1 gọi file `sub-reviewer-owner-consistency-s160.md`, khối RUN-TRACE + ISOLATION chốt `sub-reviewer-4.md` — chọn theo ISOLATION (1 file duy nhất). + +VERDICT: (đang chạy — sẽ ghi ở cuối) + +## 0. Trạng thái ghi-đĩa +- [x] khung rỗng (lượt 1) +- [ ] đọc 3 file đích +- [ ] đọc spec-cũ ratified + run.md +- [ ] đối chiếu 12-default +- [ ] chốt F-O1..n + SEV + +## 1. Findings (append dần) + +(chưa có) + diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-A-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-A-s160.md new file mode 100644 index 0000000..6b9c83e --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-A-s160.md @@ -0,0 +1,15 @@ +# sub-reviewer-anchor-A-s160 — LĂNG KÍNH: verify ANCHOR/CLAIM vs ĐĨA (cụm A) + +## VERDICT-pending + +(chưa chốt — sẽ ghi ở lượt cuối) + +## FINDINGS + +(append từng finding F-A1..n ngay khi có) + +## EVIDENCE-LOG + +(append lệnh đo + output ngay khi chạy) + +END-placeholder (chưa hoàn tất) diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-B-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-B-s160.md new file mode 100644 index 0000000..fd4606f --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-B-s160.md @@ -0,0 +1,13 @@ +# sub-reviewer-anchor-B-s160 — LĂNG KÍNH: verify ANCHOR/CLAIM vs ĐĨA (cụm B) + +> Run: 2026-07-29-S160-khkk-dryrun-plan · vai: reviewer (adversarial) · 3 file đích: +> `spec-wave-w3-duyet-3-tram-29-07-2026.md` · `spec-wave-w5-cau-khkk-hd-29-07-2026.md` · `spec-wave-w6-duong-ong-hd-v2-29-07-2026.md` +> Quy ước: mỗi finding = F-B + SEV ∈ {HIGH, MED, LOW, INFO} + {spec-file:dòng} + {bằng-chứng ĐĨA file:line hoặc lệnh đo}. + +## VERDICT (dòng-1) +(đang chạy — chốt ở cuối file) + +## FINDINGS +(append từng cái ngay khi đo xong) + +## END diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-C-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-C-s160.md new file mode 100644 index 0000000..2d06602 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-anchor-C-s160.md @@ -0,0 +1,14 @@ +# sub-reviewer — LANE C (anchor/claim vs ĐĨA) — S160 KHKK dry-run plan + +> Lăng kính: verify ANCHOR/CLAIM vs ĐĨA. Đích: `spec-wave-w7-ban-cung-hdc` + `spec-wave-w8-authz` + `plan-cha §3-§5` (baseline + data + bảng 12 default). +> Trạng thái: ĐANG CHẠY (khung rỗng lượt 1, append trong lúc làm — chống #53). + +## 0. Verdict (điền cuối) + +## 1. Bảng anchor đo được + +## 2. FLAG F-C1..n + +## 3. Bảng 12-default — default nào NGƯỢC đĩa + +## 4. Ghi chú đo lường / lệnh đã chạy diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-security-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-security-s160.md new file mode 100644 index 0000000..3e31151 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-security-s160.md @@ -0,0 +1,12 @@ +# sub-reviewer (lane 5) — LĂNG KÍNH SECURITY + DATA-RISK — S160 dry-run KHKK→HĐ + +> Ghi-đĩa-trong-lúc-làm (chống #53). Verdict dòng cuối. Append-only. +> Đích: `spec-wave-w6-duong-ong-hd-v2` + `spec-wave-w8-authz` + `plan-cha §4`. + +## 0. Trạng thái +- [ ] Đọc 3 file đích +- [ ] Verify đĩa ContractFeatures.cs +- [ ] Verify authz controller + seeder +- [ ] Grep secret trong artifact +- [ ] Chốt F-S1..n + SEV + diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-wave-logic-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-wave-logic-s160.md new file mode 100644 index 0000000..f883dc0 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-wave-logic-s160.md @@ -0,0 +1,41 @@ +# sub-reviewer (lane 3) — LĂNG KÍNH: LOGIC WAVE + ACCEPTANCE — S160 + +> Run: `2026-07-29-S160-khkk-dryrun-plan` · Vai: reviewer (adversarial, READ-only) · Ngày: 2026-07-29 +> Đích soi: `plan-cha-dry-run-toan-trinh-29-07-2026.md` (sâu) + `spec-wave-w1-schema-designer` (sâu) + `spec-wave-w3-duyet-3-tram` (sâu); w2/w5/w6/w7/w8 liếc đối chiếu đồ thị. +> NOTE tên file: TASK lượt-1 chỉ định tên này; phần boilerplate RUN-TRACE ghi `sub-reviewer-3.md` — ghi theo TASK, khai lệch trong return. + +**VERDICT (dòng-1):** _(chốt ở cuối — đang đo)_ + +--- + +## 0. Nhật ký đo (ghi TRONG lúc làm — chống #53) + +- [t1] scaffold file này. + +## 1. Đồ thị phụ thuộc (plan-cha §2) + +_(đang đo)_ + +## 2. Deploy-độc-lập per wave + +_(đang đo)_ + +## 3. E2E dry-run tới GĐ4 — phủ b.13→21 + +_(đang đo)_ + +## 4. Defer W4/W9 có phá E2E? + +_(đang đo)_ + +## 5. Acceptance 0-bit / không đo được / literal tự-lão-hoá + +_(đang đo)_ + +## 6. Gate-5-anchor per-wave (đủ 5? anchor có thật?) + +_(đang đo)_ + +## 7. FLAG list (F-W1..n + SEV) + +_(đang đo)_