diff --git a/.claude/WAL.md b/.claude/WAL.md index 891af14..a262b47 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -1,9 +1,20 @@ # WAL — auto-generated, không sửa tay -updated: 2026-07-29T15:45+07:00 | session: S160 (phiên-LOGIC L8, window 2) | branch: main +updated: 2026-07-29T16:30+07:00 | session: S160 (phiên-LOGIC L8, window 2) | branch: main -goal: DRY-RUN TOÀN TRÌNH ĐẾN HỢP ĐỒNG CỨNG (lệnh anh @S160 — verbatim đầy đủ trong run.md). Spec KHKK cũ RATIFIED ("Spec vừa rồi build đúng rồi đó") = nền, KHÔNG viết lại. Pipeline anh chỉ định: 2×/fable-real invest (bổ sung spec cũ → dry-run tới HĐ cứng) → PLAN CHA + spec-chi-tiết-cách-làm + checklist CHO TỪNG WAVE → /fable-clone review → /fable-real review CHỐT CUỐI → hmw Opus-5-MAX chạy từng wave theo Plan cha + kiểm luôn. +goal: DRY-RUN TOÀN TRÌNH ĐẾN HỢP ĐỒNG CỨNG (lệnh anh @S160 — verbatim run.md). Spec KHKK cũ RATIFIED = nền. Pipeline: 2×/fable-real invest → PLAN CHA + spec/checklist per-wave → /fable-clone review → /fable-real review CHỐT → hmw Opus-5-MAX chạy+kiểm từng wave. chain: - [x] closeout arc-2 + chốt đầu việc (commit 1d3d167 local; push chờ squash session-end) - [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 +- [x] PLAN CHA + 7 SPEC-WAVE (plan-cha + w1/w2/w3/w5/w6/w7/w8; W4/W9 defer; 12 default; 1 mig duy nhất 69/W1) + harvest diary investigator-codebase (real-fable-S160) +- [x] /fable-clone reviewer 6 lane (`wf_0eb994aa-a63`) — 5/5 verdict SUA-TRUOC-THI-CONG; synthesis + lead-verify 5 claim 5/5 CONFIRMED đĩa → `review-synthesis-clone-s160.md` +- [!] CHỜ ANH (surface qua AskUser): W8 authz posture (đụng "hợp đồng cứ từ từ" — không tự quyết) + heads-up 6 tên test ratified +- [ ] fix ~18 correctness NHÓM-2 → /fable-real reviewer chốt cuối trên spec ĐÃ SỬA → hmw thi công + +next: CHỜ anh trả W8-posture → lead fix NHÓM-2 (seeder UPGRADE-pattern chống 403-no-op · W3 bỏ port early-finalize + changelog KHKK-riêng chống FK-547 + 2-write-path finalize · W7 +param type · W6 view phase-9 đúng-cách + W7 depends W6-view · grant +HrAdmin/AuthorizedSigner · plan §4 dựng workflow 3-trạm Designer · attachment IDOR/phase authz · tên test khuôn-repo · baseline định-nghĩa · forge-negative W5) → /fable-real reviewer chốt → hmw. Disposition đầy đủ: `review-synthesis-clone-s160.md`. + +verify: +- 🔴 Review THẬT có răng — anchor spec 24-28/28 đúng NHƯNG 5 lỗ cấu-trúc lead-verify CONFIRMED đĩa: (1) seeder-grant no-op ⇒ 403 giết dry-run (`DbInitializer.cs:2242-2245` skip-existing) (2) workflow HĐ seed **1 trạm** ≠ 3 E2E hứa (`:239-265`) (3) `GetEligiblePhases` 0 phase-9 ⇒ bản-cứng rỗng+403 (`ContractFeatures.cs:334-350`) (4) `GET /contracts` 0 param `type` (`ContractsController.cs:16-24`) (5) changelog Contract FK-547 nếu truyền plan.Id +- prod state: Contracts 0 · menu 142 · test 562 · Mig 68/89 bảng · bundle admin D0sXA0fe/user Y6dW_5CM +- 🔴 GATE 5-anchor per-wave đã nhúng mọi spec-wave §③-A (reviewer xác nhận 15/15 đúng W3/W5/W6) +- 🔴 O-3 "hợp đồng cứ từ từ" + ❹ STATUS:13 tái-khẳng-định = W8 KHÔNG default tự chạy diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md index 2aa2c78..f64b2a8 100644 --- a/.claude/agent-memory/reviewer/MEMORY.md +++ b/.claude/agent-memory/reviewer/MEMORY.md @@ -17,6 +17,7 @@ ## 📁 Area memory (L2 on-demand — Read khi review vùng tương ứng) - [S62 PE budget soft-warning](project_s62_pe_budget_soft_warning.md) — PASS: hard-block→soft-warning; submit-guard intact + validator giữ `BudgetPeriodAmount>0`, row8 negative-safe (additive-only). Validator class `PurchaseEvaluationFeatures.cs:317`. - [Wire/mirror claim verification anchors](feedback_wire_claim_verification_anchors.md) — sha256 twin-file · `git diff -U0` isolate true-adds · `allowNegative` bleed check · guard-still-intact grep. +- **S160 (07-29) review SPEC-BỘ dry-run KHKK→HĐ-cứng [engine `/fable-clone` 6 lane opus + `/fable-real` chốt fable; `runs/2026-07-29-S160-khkk-dryrun-plan/review-synthesis-clone-s160.md`]:** 6 lane (anchor-A rỗng #StructuredOutput-fail nhưng ghi-đĩa-lượt-1-2 cứu · B/C/wave-logic/owner-consist/security). **5/5 verdict SUA-TRUOC-THI-CONG, hội tụ cao** ⇒ lead-verify 5 claim load-bearing 5/5 CONFIRMED đĩa. Bài đắt: (1) **review SPEC ≠ review CODE — bắt lỗ ở acceptance + default-lấn-quyền + hố-chưa-khai, KHÔNG chỉ anchor** (anchor 24-28/28 đúng dòng nhưng spec vẫn hỏng cấu-trúc); (2) **seeder-grant no-op = 403 giết-feature IM LẶNG** — row Permission đã tồn tại CanRead-only ⇒ grant kiểu insert rơi skip-existing `continue` (`DbInitializer.cs:2242-2245`), phải UPGRADE-if-exists (`SeedProcurementMasterAccessAsync:2367-2374`); **acceptance đếm ROW = 0-bit, phải đo CỜ CanCreate/CanUpdate**; (3) **owner-authority finding** — lane owner-consistency bắt lead đặt default lấn đúng chỗ owner phán "để yên" (O-3 "hợp đồng cứ từ từ" + STATUS:13 tái-khẳng-định) ⇒ surface AskUser thay vì tự quyết → anh chọn O-A gỡ hẳn wave; (4) **"204 KHÔNG 409" PASS SẴN** = acceptance 0-bit nửa-vế vì nhánh trình không đọc workflow (`ContractWorkflowService.cs:70-89`) — vế răng = `currentApprovalLevelOrder=1` phải chạy tới lượt DUYỆT; (5) **FK-547 compiler-không-chặn**: gọi `LogWorkflowTransitionAsync(Guid contractId)` với plan.Id = enum/Guid cast được nên build xanh, nổ runtime — dùng bảng changelog RIÊNG của module mới; (6) **choke-point "DUY NHẤT" sai** — admin-override gán Phase QUA BIẾN (`:190-193`) nên literal-grep miss (lớp #81), phải kê 2 write-path. **anchor-B census tên-test: repo 0/460 tên thuần-Việt ⇒ khuôn = English-predicate** (owner chốt đổi). Ensemble ĐÓNG GÓI C2 ăn: 6 lane ≤3-file + khung-rỗng-lượt-1-2 ⇒ 0 lane chết dù 1M+ token. Tag `[s160, review-spec-bo, seeder-grant-no-op-403, acceptance-dem-row-0bit, owner-authority-surface, 204-khong-409-pass-san, fk-547-compiler-mien, choke-point-2-write-path, test-name-census-repo]` - **S89 trio** ([sectionB](project_s89_pe_sectionb_review.md) · [endsBeforeCeo](project_s89_pe_endsbeforeceo_plan_review.md) · [finalize-note](project_s89_finalize_note_plan_review.md)) — adjustMut share race + authz-lens ≠ concurrency-lens · FE 4 render-sites plan-missed-2 · ground-truth-accurate-nhưng-gaps. - **S101 H18/H19 WF2 + LANE-C — PWC/PASS:** content-hash ≠ commit-SHA (hex strip/no-strip) · exact-token sweep misses paraphrase → widen concept-phrase. → `archive/2026-07.md`. - [S103 H20+crystallized-backfill review](project_s103_h20_crystallized_backfill_review.md) — WF2 PASS; 6/6 hash recompute MATCH + 4 already-met verified. Lesson: multi-axis warning phải nằm trong OUTPUT của script (reader chạy inline), không chỉ doc. @@ -97,9 +98,5 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod - **S145-curate (self lane-4, 2026-07-22, hook 22.6KB→~17.7KB):** concurrent 5-lane /fable-clone session (mỗi lane +~1.6KB fresh entry). Moved 4 fresh verbatim (S145 Axis-D-self·Axis-E·C7-lane·S145b) → `archive/2026-07.md` @S145-curate marker + verify `grep=4` moved-not-cut → collapsed L1 to pointers. S145a/S143 đã digested (linter). Residual ~17.7KB (>17.1 soft) defer — không cut-without-move mid-return (S140 precedent). - **Prev curates (digest):** S140 (S139·S134b→@S140) · S134 (8 verbatim→@S134, seal 6/6+2) · S128 em-main (md5 e5218d2f) · **S102 recovery: cut-not-moved ×5 @`f229b07` — Lesson: L1-shrink kèm "→archive" = BẮT BUỘC grep moved-not-cut trước khi trust** · S116·S113·S109·S80·S71·S70·S40. -## S149 (2026-07-24) — gate hội-tụ-bookend [LEAD GHI ON-BEHALF @S150 — M9/B3 theo H2-F4 (1 invocation thật → 0 entry); nguồn: `runs/2026-07-24-S149-hoi-tu-bookend/sub-reviewer-hoi-tu.md` 15.733B] - -- `/fable-real reviewer` gate spec hội-tụ-bookend: **GO-WITH-FIXES 1C/6M/4m** (CLEAN 218K, 0 garble) — spec v1→v2 trước wave implement 5-lane Opus (5/5 done 946K 0-garble). Lưu ý sổ: lượt gate adap-backlog `wf_f4e4c006` KHÔNG tính invocation vai này (reviewer-lane **skeleton-ruột-rỗng** — khung 274B, 0 append, return+resume đều fail → em-main-solo re-do; xem sổ garble sub-class @S150). - ## S152 (2026-07-25) — R1 retention-note ranh NEVER [M9 on-behalf lead] - **NEVER Edit/Write vào code-tree, NEVER commit, NEVER push** — vai = verdict-only (PASS/FAIL + issue file:line), sửa là việc em-main/implementer. KHAI THẬT 2 nấc: (a) ranh commit/push/code-tree **chưa từng vỡ**; (b) Write/Edit runtime CÓ cấp và ĐÃ dùng hợp lệ cho sub-file run-folder + diary-self-write (AS-10 VERIFY-KEEP, nhân-chứng S143) — tức "NEVER" áp cho ĐÍCH (code/commit/push), không phải cho tool-tồn-tại; containment thật = git-diff lead. Tag `[s152, r1-retention, never-edit-write-commit-push-target-scoped]` diff --git a/.claude/agent-memory/reviewer/archive/2026-07.md b/.claude/agent-memory/reviewer/archive/2026-07.md index bd4677f..5111225 100644 --- a/.claude/agent-memory/reviewer/archive/2026-07.md +++ b/.claude/agent-memory/reviewer/archive/2026-07.md @@ -162,3 +162,9 @@ ## @S159-curate — moved from L1 (S155-đợt2 verbatim, moved-not-cut) - **S155-đợt2 (07-27) PE delete-approver DIFF-review #2 (12 FLAG 2H/4M/6L · ĐỪNG-DEPLOY):** 🔴 class MỚI **`hợp-đồng-đứt-giữa-2-bờ`** — FE type chép tay khai `allowApproverDelete: boolean`, BE record chỉ 8-member KHÔNG có cờ ⇒ nút CHẾT mà `tsc` + `dotnet build` + 561-test đều XANH (mọi thước đo đo chỗ khác) ⇒ **phép rẻ nhất = đếm member DTO vs khoá FE + `grep -c ` ở file dựng DTO**. · policy-gate `X.Delete` chặn ĐÚNG nhóm cần dùng (đo DB: 11/13 vai `CanDelete=0`, 2 seeder KHÔNG BAO GIỜ nâng) ⇒ owner gỡ policy; **verify "gỡ authz" = truy TỪNG dòng chặn thành bảng + grep `.First()` fallback + grep `Admin` trong chính file** (gỡ policy sinh LOW mới: endpoint thành máy-dò 4-loại-phản-hồi cho mọi tài khoản). · **mã ĐỔI giữa lượt soi ⇒ re-đo đĩa TRƯỚC verdict**; test đổi chiều (`==X` → `==null`) = **ĐỔI-SPEC hợp lệ ≠ nới-assert** vì 2 mệnh đề loại trừ nhau (nới = mệnh đề mới SUY RA được từ cũ). · cascade `Add con → Remove cha → 1 SaveChanges`: chứng "site duy nhất" bằng liệt-kê-vét-cạn 34 `Remove` → lọc 8 → xét cha-con từng cái. Tag `[s155-dot2, hop-dong-dut-2-bo, policy-gate-do-DB, re-do-dia-truoc-verdict]` + +## Moved @S160 (2026-07-29) — verbatim from L1 (moved-not-cut) + +## S149 (2026-07-24) — gate hội-tụ-bookend [LEAD GHI ON-BEHALF @S150 — M9/B3 theo H2-F4 (1 invocation thật → 0 entry); nguồn: `runs/2026-07-24-S149-hoi-tu-bookend/sub-reviewer-hoi-tu.md` 15.733B] + +- `/fable-real reviewer` gate spec hội-tụ-bookend: **GO-WITH-FIXES 1C/6M/4m** (CLEAN 218K, 0 garble) — spec v1→v2 trước wave implement 5-lane Opus (5/5 done 946K 0-garble). Lưu ý sổ: lượt gate adap-backlog `wf_f4e4c006` KHÔNG tính invocation vai này (reviewer-lane **skeleton-ruột-rỗng** — khung 274B, 0 append, return+resume đều fail → em-main-solo re-do; xem sổ garble sub-class @S150). diff --git a/.claude/agent-memory/reviewer/archive/_INDEX.md b/.claude/agent-memory/reviewer/archive/_INDEX.md index 7d5cb3b..3168016 100644 --- a/.claude/agent-memory/reviewer/archive/_INDEX.md +++ b/.claude/agent-memory/reviewer/archive/_INDEX.md @@ -74,3 +74,4 @@ 2026-07-12 · security/governance lane · S112 PE sign-off approach GO-WITH-ADJ; EDGE-5 terminal-lock bỏ sót admin-override SVC:290 → guard ĐỈNH-method; A1 opens new CEO-skip path · substring:"S112 PE sign-off approach-review PRE-fan-out" → `2026-07.md` 2026-07-12 · BE+ADJUST lane · S112 Supplier import PRE-deploy GO-WITH-ADJ 6-axis; bake 30 ExpectedHeaderTokens; CAUGHT Unicode NFC/NFD self-flip-test ≠ external-file-accept · substring:"S112 Supplier Excel-import close-review PRE-deploy, BE+ADJUST" → `2026-07.md` 2026-07-12 · FINAL pre-commit+deploy · S112 BÓ 2-feature PE-signoff+import VERDICT GO 0-must-fix; both adjust applied; Case-9 self-adjust PASS ≠ real==expected must byte-diff · substring:"S112 FINAL pre-commit+deploy review — BÓ 2-feature" → `2026-07.md` +- 2026-07-24 · S149 gate hội-tụ-bookend (rename-ratify + 4 máy PS1 + hình-B) · `2026-07.md` · substring:"S149 (2026-07-24) — gate hội-tụ-bookend" 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 index 5dcff1d..4231411 100644 --- 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 @@ -24,23 +24,26 @@ | **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Đ) | +| ~~W8~~ | ~~Authz khoanh vùng Contract~~ | cross | — | — | `spec-wave-w8-*.md` (= biên bản quyết định) | ⛔ **GỠ KHỎI DRY-RUN — anh chốt O-A @S160** (để mở, không siết; đúng ❹ "hiển thị hết để góp ý" + O-3 "hợp đồng cứ từ từ"). Rủi ro (Reject-trước-guard + attachment IDOR + Create ăn số) = **chấp nhận có ý thức**, khoanh bằng ZZTEST + backup DB (§4) | | ~~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, +W6 ───────────┘ (đường ống HĐ V2: mở view+inbox ChoDuyet (GĐ3 approver) VÀ view + DaPhatHanh phase-9 (GĐ4 actor bản-cứng) — PHẢI trước W5/W7 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) +W7 (build song song từ đầu; 🔴 VERIFY PHỤ THUỘC W6-view — không có W6 mở phase-9 thì trang + Bảng cứng RỖNG + 403 cho HRA/BOD/CCM non-admin: F-C2/F-01) +W8: GỠ (O-A — anh chốt để mở @S160) ``` **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). +🔴 **CẠNH ĐỒ THỊ THÊM (review F-C2/F-01/F-06):** W7-verify đòi W6-đã-mở-view-phase-9; b.14-16 (soạn/góp ý/đàm phán) = trạng thái Nháp HĐ + comment + update-draft trong W5/W6 (KHÔNG wave riêng — default #13). **Cần dựng 1 ApprovalWorkflow type=3 THẬT 3-trạm qua Designer** (seed chỉ 1 trạm — §4 bước mới). ## 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. +- 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; anh chốt O-A (để mở) ⇒ **W8 GỠ**, rủi ro khoanh bằng ZZTEST + backup. - 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). @@ -51,12 +54,14 @@ W8 (trước khi mở rộng người bấm — đề xuất ngay sau W6) 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`). +2-bis. 🔴 **DỰNG WORKFLOW type=3 3-TRẠM (review F-02 — bắt buộc E2E):** seed prod chỉ có `QT-HD-V2-001` = **1 trạm CCM** (`DbInitializer.cs:239-265`, approver binh.le). E2E hứa 3 trạm (PRO→CCM→CEO) + b.19 BOD ⇒ **admin phải tự dựng 1 ApprovalWorkflow `ApplicableType=3` 3 Bước qua Designer** (0 code — Designer type-3 đã có) TRƯỚC khi chạy W5. Tương tự KHKK type=10 cần 1 workflow 3-trạm (W1 Designer). Ghi là **bước setup thủ công**, không phải code wave. 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`). +7. 🔴 **HỐ AUTHZ CHẤP NHẬN CÓ Ý THỨC (O-A anh chốt @S160 — review F-S2/F-O1) — disclose để không ai tưởng đã kín:** vì để mở, trong dry-run **mọi user đăng nhập**: (i) TraLai/TuChoi **bất kỳ HĐ bất kỳ phase** (Reject-trước-guard `ContractWorkflowService.cs:49-66`); (ii) **upload/xóa/tải attachment** (kể cả scan đã ký) của HĐ bất kỳ — 3 handler `ContractAttachmentFeatures.cs:56-152` **0 IDOR + 0 phase-guard**; (iii) Create HĐ ăn sequence thật nếu chọn dự án thật. ⇒ **CHỈ chạy dry-run trên ZZTEST + backup trước**; đây là rủi ro anh nhận, KHÔNG phải lỗ chưa biết. *(Khác KHKK GĐ2: module MỚI vẫn build authz 2-tầng đúng #82 — "để mở" chỉ áp module Contract sẵn có.)* -## 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) +## 5. BẢNG DEFAULT 13 QUYẾT ĐỊNH (dry-run chạy bằng default ĐẢO ĐƯỢC — anh veto lúc nào cũng kịp; #11 ĐÃ RESOLVED O-A) | # | Câu treo | DEFAULT dry-run | Đảo thế nào | Nguồn | |---|---|---|---|---| @@ -70,8 +75,9 @@ W8 (trước khi mở rộng người bấm — đề xuất ngay sau W6) | 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 | +| 11 | Authz Contract | ✅ **RESOLVED — anh chốt O-A @S160: ĐỂ MỞ, không siết. W8 GỠ.** Rủi ro disclose §4.7 | (anh đổi ý lúc nào cũng kịp — bật lại W8 = 1-2 file) | owner @S160 | +| 12 | Grant quyền KHKK (W1) | 🔴 **UPGRADE-if-exists** (`SeedProcurementMasterAccessAsync:2367-2374` khuôn), chạy SAU revoke — **KHÔNG insert** (row `KeHoachKyKet`/`Khkk_*` seed CanRead-only sẵn ⇒ insert = skip-existing no-op ⇒ 403 giết dry-run, review F-S1/F-O3/F-O4). Cấp CanCreate/CanUpdate cho **13/13 role** (đúng O-A "hiển thị hết") + Delete cho Drafter/Admin | siết nhóm hẹp hơn nếu muốn | review 4-lane hội tụ | +| 13 | b.14-16 (soạn/góp ý/đàm phán HĐ) | Nháp HĐ + comment thread + update-draft trong W5/W6 — KHÔNG wave riêng | thêm Bước workflow admin-config (0 code) nếu muốn trạm thật | lane-1 F-19 + review F-06 | ## 6. GATE 5-ANCHOR (bắt buộc TRƯỚC MỖI WAVE — HANDOFF, reviewer đề @S157, gotcha-proof) @@ -79,7 +85,7 @@ Trước thi công wave: mở **5 anchor** của wave đó (danh sách trong t ## 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). +- Test: `dotnet test SolutionErp.slnx` PASS 0 fail · **baseline = số test đo NGAY TRƯỚC wave bằng chính lệnh đó** (KHÔNG literal "562" — nó tự lão-hoá; review F-B8) · số-sau ≥ baseline + N · **tên test pin cụ thể theo KHUÔN REPO** (anh chốt @S160: English-predicate + danh-từ-VN-không-dấu, vd `ApproveV2_LastLevel_TransitionsToDaPhatHanh` — KHÔNG vị-ngữ-thuần-Việt; census repo 0/460 tên kiểu cũ, review F-B7). - 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. diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/review-synthesis-clone-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/review-synthesis-clone-s160.md new file mode 100644 index 0000000..9951df7 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/review-synthesis-clone-s160.md @@ -0,0 +1,63 @@ +# REVIEW SYNTHESIS — /fable-clone reviewer 6 lane (S160, verdict hội tụ SUA-TRUOC-THI-CONG) + +> Engine: `/fable-clone reviewer` ensemble tier-opus, run `wf_0eb994aa-a63`. 6 lane: anchor-A (rỗng — fail StructuredOutput, sub-file đĩa còn) · anchor-B · anchor-C · wave-logic · owner-consistency · security. +> **5/5 lane trả verdict = SUA-TRUOC-THI-CONG.** Anchor spec ĐỨNG vững (24-28/28 đúng dòng, 0 anchor-trôi sau arc-2) — lỗi nằm ở **mô-tả tại neo + acceptance + default lấn quyền + hố chưa khai**, KHÔNG ở số dòng. +> Lead verify độc lập 5 claim load-bearing @S160 (dưới) — **5/5 CONFIRMED trên đĩa**. Đây là review THẬT có răng. + +## Lead verify độc lập (chống rubber-stamp ngược — 5/5 confirmed) + +| Claim | Lane | Verify đĩa | Verdict | +|---|---|---|---| +| Seeder-grant no-op: row `Contracts`/`KeHoachKyKet`/`Khkk_*` seed CanRead-only + nhánh non-Pe skip-existing `continue` | S1·O3·O4·04 | `DbInitializer.cs:2204-2213` (read-only concat) + `:2242-2245` (`continue`) + khuôn UPGRADE `:2367-2374` | ✅ THẬT | +| Seed workflow type=3 = **1 trạm** (binh.le CCM), không phải 3 | 02 | `DbInitializer.cs:239-265` "1 Bước Phòng CCM × 1 Cấp" | ✅ THẬT | +| `GetEligiblePhases` KHÔNG có `DaPhatHanh(9)`/`ChoDuyet(10)` | C2·01 | `ContractFeatures.cs:334-350` (dừng ở DangDongDau) | ✅ THẬT | +| `GET /api/contracts` KHÔNG có param `type` | C1·05 | `ContractsController.cs:16-24` (7 param, 0 `type`) | ✅ THẬT | +| `LogWorkflowTransitionAsync` = hàm Contract có FK vật lý ⇒ truyền plan.Id = FK-547 | B2 | `IChangelogService.cs:28` signature contractId-typed | ✅ THẬT (plausible→confirmed sig) | + +## DISPOSITION per finding (nhóm theo hành động) + +### 🔴 NHÓM 1 — CHECKPOINT OWNER (không tự quyết, surface cho anh) + +| # | Finding | Vì sao chờ anh | +|---|---|---| +| **O1·O2** | W8 authz O-B đặt làm DEFAULT tự chạy | O-3 `SC:49` nêu ĐÍCH DANH 2 vật (`ContractWorkflowService.cs:48-66` + controller class-trần) = "NGOÀI SCOPE, treo chủ đích, wave chạm phải hỏi anh"; `STATUS.md:13` anh **tái khẳng định** ❹ 1 ngày trước; `run.md:17`④ chính lời tôi "KHÔNG tự coi được sửa". ⇒ **FIX = demote W8 về fully owner-gated, default O-A do-nothing**; câu hỏi posture surface cho anh. Reviewer KHÔNG có thẩm quyền mở lại (O2). | +| **B7 (6 tên ratified)** | 6 tên test W3 từ `SC:377-382` ratified lệch khuôn repo (English-predicate, census 0/460) | Đổi tên = sửa spec ĐÃ ratified ⇒ heads-up cho anh (12 tên W5/W6/W7 tôi tự sửa được vì mới). | + +### 🟠 NHÓM 2 — FIX correctness (safe direction, lead tự sửa spec) + +| # | Finding | Fix | +|---|---|---| +| **S1·O3·O4·04** | Grant no-op ⇒ 403 giết dry-run (W1 KeHoachKyKet + W8 Contracts) | Đổi MỌI grant site sang khuôn **UPGRADE-if-exists** (`SeedProcurementMasterAccessAsync:2367-2374`), chạy SAU revoke; acceptance đo **CanCreate/CanUpdate CỜ**, KHÔNG đếm row. | +| **B3** | W3 "PORT không kích hoạt" = containment GIẢ (Designer type=10 render checkbox admin-opt-in ⇒ 1 tick bật đường "PHÁ VỠ") | W3 **KHÔNG port** 2 nhánh kết-thúc-sớm; W1 Designer type=10 **ẩn/tắt** 2 checkbox `AllowApproverFinalize`/`SkipToFinal` (hoặc khai rõ + gate owner). | +| **B2** | Changelog: dùng hàm Contract FK-vật-lý = FK-547 runtime | W3 ghi vào **`ContractSigningPlanChangelogs`** (bảng W1 #6) qua method KHKK-riêng, KHÔNG `LogWorkflowTransitionAsync` của Contract. | +| **B1** | choke-point "DUY NHẤT" sai — admin-override gán qua biến (lớp #81) | W3 kê **2 write-path** finalize + test admin-override. | +| **C1·05** | `GET /contracts` 0 param `type` ⇒ 7 leaf Hdc trùng list | W7: thêm `type` vào `ListContractsQuery` (BE nhỏ) — KHÔNG lọc client-side trần-100. | +| **C2·01** | eligiblePhases 0 phase-9 ⇒ HRA/BOD/CCM thấy rỗng + 403 | W6 mở view phase-9 cho actor bản-cứng (đúng cách: membership-workflow-của-HĐ, KHÔNG role-blanket); W7 phụ thuộc W6-view (thêm cạnh đồ thị). +LIST test + ca ÂM pin ROLE. | +| **C3·03** | grant-list thiếu `HrAdmin` + `AuthorizedSigner` (b.19/b.20 signer) | Thêm 2 vai vào grant W1+W8. | +| **02** | Seed workflow HĐ 1 trạm, E2E hứa 3 trạm | Plan §4: thêm bước **dựng workflow type=3 3-trạm qua Designer** (data setup), KHÔNG dựa seed. | +| **S2** | Attachment 3 handler 0 IDOR/0 phase-guard; O-B `DELETE attachments`→`Contracts.Delete`⇒ Drafter xóa scan HĐ người khác | W8: thêm mục attachment authz (IDOR + phase-guard) vào scope; plan §4 khai hố. | +| **B5** | `ConflictException` định vị sai ⇒ "204 KHÔNG 409" PASS SẴN 0-bit | W5 acceptance chạy tới **lượt duyệt Cấp-1** (`currentApprovalLevelOrder=1`), không dừng ở trình. | +| **B6** | Pin `ApprovalWorkflowId` thiếu guard `ApplicableType==Contract` (forge) | W5: +ca ÂM forge workflow type 1/2/10. | +| **B4** | mô-tả khuôn `PEWS:1008` sai (khuôn đúng = `ContractFeatures.cs:356-360` LUẬT+grep) | W3 sửa trỏ khuôn. | +| **B8·13/13 tên mới** | "baseline" vô định + tên test lệch khuôn repo (English-predicate) | Định nghĩa baseline = "số-đo-ngay-trước-wave"; đổi 12 tên mới sang khuôn repo. | +| **C5** | `AuthorizePolicyRegressionTests` hardcode 4 action `ApprovalWorkflowsV2Controller` ⇒ 0-bit cho Contracts | W8 test mới phải tự phủ ContractsController, không dựa test cũ. | +| **B13·S4** | W6 vá inbox bỏ nhánh admin; ca ÂM view không pin role | thêm nhánh admin + pin role ca ÂM. | +| **06·F-B10..16·C6·F-07·F-09** | LOW: b.14-16 default, anchor lệch-biên, badge proxy `ScannedSigned`, cover FO-002.01 thiếu purpose | gom sửa lúc thi công wave tương ứng. | + +## Điểm DƯƠNG (reviewer tự xác nhận — chống rubber-stamp) + +- Gate 5-anchor W3/W5/W6 = **15/15 đúng** · `ApprovalWorkflow.cs:95+:116-125` đúng class-trong-file + 4 cờ · 3 claim chẩn W6 đo đúng · `CreateContractFromEvaluationMultiWinnerTests` có thật · `ResolveV2InboxIdsAsync` có thật `:817` · `w3:30` giữ đúng `AND PlanId=@id` · baseline plan §3 6/6 khớp (kể cả prod curl Contracts=0) · 28 neo 0 anchor-trôi. + +## ✅ OWNER-DECISION @S160 (AskUser) + FIX ĐÃ ÁP + +- **W8 authz = O-A (để mở, không siết).** W8 GỠ khỏi dry-run → `spec-wave-w8` rewrite thành biên bản O-A + disclose 3 rủi ro (Reject-guard · attachment-IDOR · Create-seq) khoanh ZZTEST+backup. Hệ quả: grant-list HrAdmin/AuthorizedSigner (F-C3) + AuthorizePolicyRegression-Contracts (F-C5) + O-B seeder (F-S1 phần Contracts) đều **MOOT** — không siết thì không cần grant Contract. +- **Tên test = khuôn repo** (anh chốt đổi). Đã đổi 6 W3 + 5 W5 + 5 W6 + 3 W7 + 2 W2 sang English-predicate. + +**FIX NHÓM-2 đã áp (13 spec-edit):** plan §2/§4/§5/§7 (W8→O-A · dựng workflow 3-trạm F-02 · grant upgrade #12 · disclose §4.7 · baseline+tên-test §7 · cạnh W7⊃W6) · W1 (grant UPGRADE-if-exists F-S1 + đo CỜ + ẩn 2 checkbox early-finalize type=10 F-B3 + Designer site :144-148) · W2 (tên + grant-ăn) · W3 (2-write-path finalize F-B1 + changelog KHKK-riêng chống FK-547 F-B2 + bỏ port F-B3 + khuôn-trỏ F-B4 + tên) · W5 (acceptance tới Cấp-1 F-B5 + forge-negative F-B6 + tên) · W6 (view per-HĐ 3-site F-S3/F-C2 + LIST test F-01 + role-pin ÂM F-S4 + kiểm-xóa-mềm + tên) · W7 (param `type` F-C1 + badge InitialSigned F-C6 + cover purpose F-09 + O-A authz + tên + vai-thật-verify). + +## Kế tiếp +1. ✅ Owner-decision + 13 fix NHÓM-2 — XONG. +2. ⏳ `/fable-real reviewer` chốt cuối trên spec ĐÃ SỬA (verify fix landed + leftover LOW). +3. hmw Opus-5-MAX thi công từng wave theo Plan cha + kiểm. + + 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 f2b9720..a2fe85c 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,12 @@ | 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 — 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` | [!] | +| 5 | Review ensemble — 6 lane reviewer `tier:'opus'` (run `wf_0eb994aa-a63`) | `/fable-clone reviewer` | [x] | +| 5b | Synthesis + lead-verify 5 claim (5/5 CONFIRMED đĩa) → `review-synthesis-clone-s160.md`; disposition: 2 owner-gate + ~18 fix | lead | [x] | +| 5c | Owner-decision (AskUser): **W8=O-A để mở** + **đổi tên test khớp repo** | anh @S160 | [x] | +| 5d | Fix NHÓM-2 (13 spec-edit: W8→O-A biên bản · seeder UPGRADE · W3 2-write-path+changelog-KHKK+bỏ-port · W6 view-per-HĐ-3-site · W7 param-type · tên test khuôn repo · dựng WF 3-trạm · disclose §4.7) | lead | [x] | +| 6 | `/fable-real reviewer` chốt cuối trên spec ĐÃ SỬA | `/fable-real` | [!] | +| 7 | hmw Opus-5-MAX thi công từng wave + kiểm | hmw | [ ] | | 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) | [ ] | 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 index f48fd2a..abfd85f 100644 --- 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 @@ -19,9 +19,10 @@ 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. +8. 🔴 **DbInitializer grant — UPGRADE-IF-EXISTS, KHÔNG INSERT (review F-S1/F-O3/F-O4 — 4 lane hội tụ, lead verify đĩa CONFIRMED):** row Permission key `KeHoachKyKet` + `Khkk_*` **ĐÃ tồn tại CanRead-only cho mọi role** (`DbInitializer.cs:2204-2213` nhánh read-only), và nhánh grant key-non-Pe là **skip-existing `continue`** (`:2242-2245`) ⇒ thêm grant kiểu **insert = NO-OP im lặng** ⇒ CanCreate/Update giữ `false` ⇒ **W2/W3 lên prod 403 cho 13/13 role, chỉ Admin qua = GIẾT dry-run** (gotcha #44). **Fix:** viết method mới khuôn **`SeedProcurementMasterAccessAsync:2367-2374`** (`if (existing.TryGetValue(key,out row)) { if(!row.CanCreate){row.CanCreate=true;…} }`), chạy **SAU** `RevokeTemporarilyHidden…` (mirror `:2098-2101`) để thắng. Cấp CanCreate/CanUpdate cho **13/13 role** (đúng O-A "hiển thị hết để góp ý") + CanDelete cho Drafter/Admin, trên `KeHoachKyKet` + 6 leaf `Khkk_*`. 🔴 **CẤM SQL tay** (gotcha #84 — vết `DbInitializer.cs:2287-2296` Run #423 grant tay bị revoker lật 447/494). (b) KHÔNG đụng menu-row `Khkk_*` (đã seed). (c) label chỉnh = labelBackfill (gotcha #11). 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. +10. **fe-admin Designer type-10**: `ApprovalWorkflowsV2Page.tsx` map `TYPE_CODE_TO_INT` (site thực `:144-148`, KHÔNG phải comment `:143` — review chéo) + 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. + 🔴 **Ẩn 2 checkbox early-finalize cho type=10 (review F-B3 — containment THẬT):** Designer V2 render checkbox `AllowApproverFinalize`/`AllowApproverSkipToFinal` (`ApprovalWorkflowsV2Page.tsx:1267-1268`) = per-Level admin opt-in. Anh phán 2 đường này **"PHÁ VỠ"** (S155). W3 KHÔNG port code path ⇒ để checkbox sống mà backend không xử = cờ chết đánh lừa admin. **Fix:** ẩn/disable 2 checkbox khi `typeCode==10` (hoặc render read-only + tooltip "chưa hỗ trợ cho quy trình KHKK"). Đây là cách làm default #1 ("đường THƯỜNG") thành **containment thật** thay vì "port-nhưng-không-kích-hoạt" (giả). 11. Build + test + commit `[CLAUDE] Domain+Infra+Admin: W1 KHKK schema Mig 69 + policy KeHoachKyKet + Designer type-10`. ## ③ CHECKLIST @@ -34,6 +35,6 @@ - [ ] 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ờ. +- [ ] 🔴 Policy-grant ĐO CỜ, KHÔNG đếm ROW (review F-04: row đã có sẵn ⇒ đếm row = 0-bit): sau seed, `sqlcmd`/curl xác nhận Permission key `KeHoachKyKet` có **CanCreate=1 ∧ CanUpdate=1** cho ≥1 role non-Admin (vd Drafter). Row đã tồn tại từ trước ⇒ chỉ CỜ mới chứng upgrade ăn. - [ ] 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 index 4b6ea6f..7dbb7a2 100644 --- 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 @@ -29,7 +29,8 @@ BCH tạo được **phiếu nháp "Kế hoạch ký kết HĐ"** từ PE `DaDuy - [ ] 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`. +- [ ] `dotnet test` ≥ baseline + 2, PASS 0 fail, tên khuôn repo (English-predicate): `ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy` + `CreateContractSigningPlan_FromNonApprovedPe_Throws`. +- [ ] 🔴 Grant ăn (review F-04): sau seed, Permission `KeHoachKyKet` có **CanCreate=1** cho role đích (đo CỜ — row đã tồn tại nên đếm-row 0-bit); user role đó `POST` → 201, user không-cấp → 403 (KHÔNG 500 — chứng policy `KeHoachKyKet.*` sinh đúng, khớp key ∈ All). - [ ] 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 index 4b3ca4c..6e9b887 100644 --- 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 @@ -10,11 +10,11 @@ Phiếu KHKK chạy trọn `DangSoanThao → ChoDuyet → (PMH → CCM → CEO) 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. +3. **`Infrastructure/Services/ContractSigningPlanWorkflowService.cs`** — copy `ContractWorkflowService.ApproveV2Async:217-394` (🔴 CẤM copy ProposalFeatures — flatten phá OR-of-N, comment tự thú "assume 1 step" ở `:424`, cơ chế `:427-439`). 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) 🔴 **Finalize choke-point — 2 WRITE-PATH, KHÔNG "DUY NHẤT" (review F-B1, lớp #81):** nguồn copy có ≥2 nhánh set terminal — nhánh duyệt-cấp-cuối VÀ nhánh **admin-override** (`ContractWorkflowService.cs:190-193` gán `contract.Phase = targetPhase` QUA BIẾN ⇒ literal-grep KHÔNG thấy). Helper `ApplyApprovedValuesOnFinalize` (mọi Line `ApprovedAmount ??= ProposedAmount`) phải gọi ở **CẢ HAI**. Khuôn RULE-comment ĐÚNG = `ContractFeatures.cs:356-360` (LUẬT + `grep -rn` + "không đếm số") — **KHÔNG** phải `PEWS:1008` (review F-B4: dòng đó CÓ số-đếm "4 site" mà thực 5 call-site, tự mâu thuẫn; và `:1009` là helper NS khác helper GIÁ `:973-1002`). + (b) 🔴 **KHÔNG PORT 2 nhánh kết thúc sớm** (review F-B3 — anh phán "PHÁ VỠ" S155, "port-nhưng-không-kích-hoạt" = containment GIẢ vì Designer type=10 render checkbox thật). W3 chỉ đường THƯỜNG. Containment thật = W1 ẩn 2 checkbox cho type=10. (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). +4. 🔴 **Ghi lịch sử — DÙNG BẢNG KHKK RIÊNG, KHÔNG hàm Contract (review F-B2 = FK-547 runtime):** mỗi transition ghi `ContractSigningPlanApprovals` (#7) + `ContractSigningPlanChangelogs` (#6) qua **method KHKK-riêng** (`LogSigningPlanTransition` tự viết, ghi `_db.ContractSigningPlanChangelogs`). 🔴 **CẤM gọi `IChangelogService.LogWorkflowTransitionAsync`** — signature `(Guid contractId,…)` ghi `ContractChangelogs` có **FK vật lý `ContractId` Cascade** (`ContractChangelogConfiguration.cs:22-26`) ⇒ truyền `plan.Id` = **FK violation 547** (enum cast được nên compiler KHÔNG chặn). 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`). @@ -27,11 +27,11 @@ Phiếu KHKK chạy trọn `DangSoanThao → ChoDuyet → (PMH → CCM → CEO) **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`). +- [ ] Finalize: `Lines WHERE PlanId=@id AND ApprovedAmount IS NULL` = 0 (🔴 có `AND PlanId=@id`) — TEST **CẢ 2 write-path**: duyệt-cấp-cuối THƯỜNG + admin-override (F-B1). - [ ] 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`. +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 6, đủ 6 TÊN theo **khuôn repo** (anh chốt @S160 đổi khỏi tên thuần-Việt spec cũ `:377-382`; English-predicate + danh-từ-VN-không-dấu) trong `ContractSigningPlanApprovalTests`: `ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures` (OR-of-N ca (d)) · `ApproveV2_NewStep_ResetsLevelOrderToOne` (con-trỏ đôi) · `Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan` (choke-point `AND PlanId=@id`) · `Inbox_OnlyPendingLevelApprover_SeesPlan` (ca ÂM cấp khác) · `ApproveV2_TwiceSameLevel_UpsertsSingleOpinionRow` (UPSERT + SignedByUserId) · `SendBack_Resubmit_RestartsFromStepOneLevelOne` (vòng trả-lại). *(Ý nghĩa 1:1 với 6 acceptance #1-#6; đổi tên KHÔNG đổi phủ.)* - [ ] 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 index fa25307..3f8ac0b 100644 --- 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 @@ -11,7 +11,7 @@ Từ phiếu KHKK `DaDuyet`, tạo HĐ per-winner với **giá = `ApprovedAmount 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. + (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. 🔴 **GIỮ guard `ApplicableType==Contract` (review F-B6):** cửa-1 chống forge đã có `ContractFeatures.cs:84-86` + comment `:77-78` (chặn attacker forge PE-workflow-id type 1/2); pin ở bridge phải qua CÙNG guard đó — CẤM pin thẳng workflow type 1/2/10 vào HĐ. Test canh `CreateContractCommandApplicableTypeTests` (đã có) + ca ÂM mới. (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). @@ -21,10 +21,10 @@ Từ phiếu KHKK `DaDuyet`, tạo HĐ per-winner với **giá = `ApprovedAmount **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. +- [ ] 5 test tồn tại ĐÚNG TÊN (khuôn repo English-predicate) trong `ContractSigningPlanBridgeTests`: `CreateContract_WithApprovedPlan_SetsGiaTri_ToApprovedAmountPerSupplier` · `CreateContract_NoPlan_KeepsSumOfPeQuotes_AndWarnsSoft` · `CreateContract_PinsV2Workflow_ContractReachesLevelOneOnSubmit` · `CreateContract_WritesBackLineContractId_PerWinner` · `CreateContract_ForgePeWorkflowId_ThrowsApplicableTypeGuard` (ca ÂM forge — F-B6). +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 5; `CreateContractFromEvaluationMultiWinnerTests` + `CreateContractCommandApplicableTypeTests` (ĐÃ TỒN TẠI) VẪN XANH — chống hồi quy multi-winner + forge. - [ ] 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. +- [ ] 🔴 Prod curl (ZZTEST) — CHẠY TỚI LƯỢT DUYỆT, KHÔNG dừng ở trình (review F-B5: nhánh trình `ContractWorkflowService.cs:70-89` KHÔNG đọc workflow ⇒ "204 KHÔNG 409" PASS SẴN, 0-bit): HĐ tạo với `ApprovalWorkflowId` V2 → `GET /{id}` có **`currentApprovalLevelOrder=1`** (vế có răng — chứng pin V2 ăn); → Cấp-1 approver `POST /{id}/transitions` duyệt → tiến Bước 2. HĐ tạo KHÔNG workflow → trình vẫn 204 (đường cũ) nhưng `currentApprovalLevelOrder=null`. - [ ] 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 index 6a3d466..45d2607 100644 --- 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 @@ -14,7 +14,7 @@ Wave này = 0 UI mới, 0 mig; sau deploy: approver thấy + mở + duyệt đư 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ũ. +3. 🔴 **View — ĐÚNG CÁCH per-HĐ, KHÔNG role-blanket (review F-S3/F-C2/F-01):** `GetEligiblePhases(userRoles)` (`ContractFeatures.cs:334-350`) chữ ký **chỉ nhận role** ⇒ nếu chỉ nhét `ChoDuyet`/`DaPhatHanh` vào tập role thì mở toang cho MỌI user mang role đó (kể cả HĐ xóa mềm ở `:381`). **Fix:** thêm nhánh V2 **membership-theo-HĐ** ở CẢ 3 call-site của `GetEligiblePhases` (`:301` List · `:381` ListDeleted · `:486` Detail — review F-S3 chỉ ra gate cũ thiếu 2 site) — user thấy HĐ `ChoDuyet` **nếu là `ApproverUserId` của Level trong workflow pin CỦA CHÍNH HĐ đó**, và HĐ `DaPhatHanh` **nếu thuộc workflow HĐ đó HOẶC mang vai bản-cứng** (Director/AuthorizedSigner/HrAdmin/CostControl — actor b.19/20/21, cho GĐ4/W7). Admin giữ thấy-hết. KHÔNG mở toang: user ngoài cả hai điều kiện → 403 y cũ. *(Đây là điều kiện W7 phụ thuộc — F-C2.)* 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ị. @@ -24,9 +24,10 @@ Wave này = 0 UI mới, 0 mig; sau deploy: approver thấy + mở + duyệt đư **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). +- [ ] 5 test ĐÚNG TÊN (khuôn repo) trong `ContractV2PipelineTests`: `Inbox_PendingLevelApprover_SeesChoDuyetContract` · `Inbox_OtherLevel_DoesNotSeeContract` (ca ÂM) · `View_V2Approver_NotDrafter_CanSeeChoDuyetContract` · `List_HardCopyActor_NotAdmin_SeesDaPhatHanhContract` (🔴 review F-01: LIST test cho actor bản-cứng — không wave nào đo LIST trước) · `View_UserOutsideWorkflow_WithUnrelatedRole_Gets403` (🔴 review F-S4: ca ÂM **pin ROLE cụ thể** như CostControl/Procurement, KHÔNG chọn HrAdmin cho pass dễ). +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 5. +- [ ] Prod ③: 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; login actor bản-cứng non-admin (vd Director) → `GET /api/contracts?type=1&phase=9` ≥1 dòng (trước W6: 0 dòng + 403). +- [ ] Ca ÂM giữ (pin role): user role KHÔNG liên quan + không thuộc workflow HĐ nào → `GET /{id}` vẫn 403 (chứng KHÔNG mở toang qua role-blanket). +- [ ] 🔴 Kiểm hở xóa-mềm: HĐ `ChoDuyet` đã soft-delete → user V2 KHÔNG thấy ở `:381` ListDeleted (fix membership không được rò sang màn Đã-xóa). - [ ] 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"). +**C. Rủi ro:** [ ] đổi view-guard làm hở HĐ cho MỌI user (thiếu điều kiện per-HĐ) — ca ÂM pin-role + kiểm-xóa-mềm canh · [ ] inbox V2 quên lọc `IsDeleted` · [ ] SỬA CHỈ 1/3 call-site `GetEligiblePhases` (F-S3: gate cũ thiếu `:301`+`:381`) — grep 3 site trước khi đóng · [ ] đụng nhánh Reject-trước-guard (`:49-66`) = O-A anh chốt ĐỂ MỞ, ĐỪ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 index a2cf333..eebc175 100644 --- 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 @@ -17,19 +17,20 @@ Readonly từ GĐ3 (F-11): khối "Ký nháy & cover b.17-18" hiển thị từ ## ② 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). +2. **BE nhỏ**: (a) enum-extend `AttachmentPurpose` += `InitialSigned = 4` (ký nháy b.17-18) **+ `CoverChecklist = 5`** (scan cover FO-002.01, luật (c) — review F-09: thiếu purpose cho cover) — int column, 0-mig (tiền lệ PE `ComparisonTable=4`; `ContractAttachment.cs:5-11`); (b) 🔴 **thêm param `type` vào `ListContractsQuery` + controller (review F-C1/F-05 — `GET /api/contracts` HIỆN 0 CÓ `type`, `ContractsController.cs:16-24` chỉ phase/supplierId/projectId):** thêm `[FromQuery] ContractType? type` + filter handler `ContractFeatures.cs:305-307` — KHÔNG lọc client-side `pageSize:100` (trần 100/loại). (c) `ContractListItemDto` +2 cờ derive `hasSealedCopy`/`hasInitialSigned` trong projection (DTO hiện 12 field `:323-327` KHÔNG trả attachments; 🔴 badge "Đã ký GĐ" derive từ **`InitialSigned`/`SealedCopy`**, KHÔNG từ `ScannedSigned` — review F-C6: `ScannedSigned=2` nghĩa "scan có chữ ký NCC ở DangInKy", proxy-predicate sai vai b.19 GĐ, gotcha #71). 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. +4. **Authz**: 🔴 anh chốt **O-A (để mở)** ⇒ Contract giữ `[Authorize]` trần, **KHÔNG gắn policy mới** cho trang Hdc/upload — mọi user auth thao tác được (đúng "hiển thị hết để góp ý"; rủi ro attachment-IDOR disclose plan §4.7). *(Nếu sau này anh bật O-B thì mới dùng `Contracts.{Read,Update}` — Contracts ∈ All `:168`, policy sống sẵn.)* 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. +- [ ] Test ĐÚNG TÊN (khuôn repo) trong `ContractHardCopyTests`: `ListHardCopies_ReturnsOnlyContracts_InDaPhatHanhPhase` · `ListHardCopies_FiltersByContractType` (🔴 review F-C1 — chứng param `type` LỌC thật, không hiện trùng 7 leaf) · `HardCopyBadge_DerivesFromSealedCopyAttachment`. +- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 3. - [ ] 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`. +- [ ] 🔴 Prod ③ — 2 loại KHÁC NHAU ra 2 danh sách KHÁC NHAU (chứng `type` lọc, review F-C1): `GET /api/contracts?type=1&phase=9` ≠ `?type=2&phase=9` (khi có ≥1 HĐ mỗi loại); mọi item `phase==9`. Byte-marker bundle #77. +- [ ] 🔴 Prod ③ — VAI THẬT thấy được (review F-C2/F-01, phụ thuộc W6): login actor bản-cứng non-admin (Director b.19 / HrAdmin b.20 / CostControl b.21) → trang Bảng cứng ≥1 dòng, KHÔNG rỗng/403. - [ ] 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 index 6982b64..9f35b45 100644 --- 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 @@ -1,33 +1,34 @@ -# SPEC WAVE-8 — Authz khoanh vùng Contract (cross-cutting, trước khi mở rộng người bấm) +# WAVE-8 — Authz Contract: ✅ BIÊN BẢN QUYẾT ĐỊNH O-A (GỠ KHỎI DRY-RUN) -> 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. +> 🔴 **KHÔNG PHẢI WAVE THI CÔNG.** Anh chốt @S160 (AskUser): **O-A — để mở, không siết.** File này giữ lại làm biên bản + disclose rủi ro, KHÔNG có code. -## ① TÍNH NĂNG / ĐỀ BÀI +## Quyết định -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). +Review ensemble (`review-synthesis-clone-s160.md` F-O1/F-O2/F-S1) bắt: plan bản đầu đặt **O-B (siết per-action)** làm default tự chạy — **lấn quyền owner**. Vì: +- `SC:49` (O-3, ratified): *"Lỗ hổng an ninh `ContractWorkflowService.cs:48-66` + controller HĐ class-trần: NGOÀI SCOPE, treo có chủ đích. Wave nào chạm module Contract phải hỏi lại anh."* +- `docs/STATUS.md:13` (S159, 1 ngày trước): anh **tái khẳng định** ❹ *"đang phát triển → cho hiển thị hết để mọi người góp ý"*. +- `run.md:17` ④ (chính lời lead): *"riêng LỖ HỔNG authz S156 … KHÔNG tự coi là được sửa hay được bỏ qua"*. -**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. +⇒ Lead surface câu hỏi cho anh. **Anh chọn O-A: để mở, không siết** (đúng tinh thần "hiển thị hết để góp ý"). -## ② CÁCH IMPLEMENT (theo option O-B; O-C thêm bước 4) +## Hệ quả -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). +- **KHÔNG** gắn per-action policy lên `ContractsController` (giữ `[Authorize]` trần). +- **KHÔNG** vá nhánh Reject-trước-guard (O-C bị bỏ — anh không chọn). +- **KHÔNG** seeder-grant gì cho Contract (không cần — class-trần = mọi user auth ghi được). +- W8 **không tiêu 1 dòng code**; dry-run chạy W1+W2+W3+W6+W5+W7. -## ③ CHECKLIST +## 🔴 Rủi ro CHẤP NHẬN CÓ Ý THỨC (disclose — anh nhận, KHÔNG phải lỗ chưa biết; = plan-cha §4.7) -**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). +Trong dry-run, **mọi user đăng nhập**: +1. TraLai/TuChoi **bất kỳ HĐ ở bất kỳ phase** (kể cả DaPhatHanh) — `ContractWorkflowService.cs:49-66` Reject chạy trước mọi guard. +2. Upload/xóa/tải **attachment** (kể cả scan đã ký/đóng dấu) của HĐ bất kỳ — `ContractAttachmentFeatures.cs:56-152` 3 handler **0 IDOR + 0 phase-guard** (review F-S2). +3. Create HĐ **ăn sequence mã thật** nếu chọn dự án thật — `ContractFeatures.cs:113-118`. + +**Khoanh bằng:** CHỈ chạy trên dự án **ZZTEST** + **backup DB** trước mỗi đợt (`scripts/backup-sql.ps1`) + dặn team. Prod hiện **0 HĐ thật** nên bề mặt tấn công lên data thật = rỗng cho tới khi có người nhập HĐ thật ngoài dry-run. + +## Bật lại sau này (nếu anh đổi ý) + +O-B (siết per-action) = ~1 file controller + grant **UPGRADE-pattern** (không insert — xem plan §5 #12). O-C thêm vá Reject `:49` = +1 điều kiện `fromPhase==ChoDuyet` trong service. Cả hai vẫn cần anh gật vì chạm module Contract (O-3). + + 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 index a98dadc..8ef72a5 100644 --- 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 @@ -1,19 +1,120 @@ # 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: SUA-TRUOC-THI-CONG** (4 HIGH · 3 MED · 4 LOW · 5 điểm củng cố). Chặn: **W8 chạy bằng default O-B** + **Δ1 policy-prefix**. W5 (trừ F-O7) và bảng default 5-câu-owner ĐỨNG. -VERDICT: (đang chạy — sẽ ghi ở cuối) +> Đí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, viết tắt **SC** = spec cũ) + `run.md` S160 + đĩa + curl prod. +> NOTE naming: task LƯỢT-1 gọi `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). ## 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 +- [x] khung rỗng (lượt 1) · [x] 3 file đích · [x] SC + run.md · [x] đo đĩa 9 lệnh · [x] curl prod · [x] chốt F-O1..F-O11 -## 1. Findings (append dần) +--- -(chưa có) +## 1. HIGH +### F-O1 [HIGH] — O-B vá ĐÚNG NỬA SAU của thứ O-3 tuyên "NGOÀI SCOPE", nhưng plan xếp làm DEFAULT tự chạy +- **SC:49 (O-3), verbatim hệ quả:** *"Lỗ hổng an ninh `ContractWorkflowService.cs:48-66` **+ controller HĐ class-trần**: **NGOÀI SCOPE**, treo có chủ đích. Wave nào chạm module Contract phải hỏi lại anh."* → nêu ĐÍCH DANH **2 vật**. +- **SC:410-411** ("Ngoài mọi wave — KHÔNG làm trong run này") liệt lại đúng 2 vật, "anh chốt *từ từ*". +- **`docs/STATUS.md:13` (canonical, S159 — 1 ngày trước):** *"❹-risk CÓ Ý THỨC (verbatim anh): «đang phát triển → cho hiển thị hết để mọi người góp ý, còn duyệt NCC thì cứ để như cũ» — menu HĐ public 13/13 role trong khi authz HĐ còn `[Authorize]` trần (lỗ hổng S156 treo chủ-đích) — **đã cảnh báo, anh tái khẳng định**."* ⇒ owner ĐÃ được cảnh báo đúng lỗ này và TÁI KHẲNG ĐỊNH giữ nguyên. +- **plan:73** (§5 row 11): DEFAULT = **O-B** = gắn `[Authorize(Policy)]` 22 endpoint ghi + seeder grant ⇒ chính là vá vật thứ 2. Chỉ **O-C** (vật thứ 1) bị đánh dấu "cần anh gật riêng". +- **Mâu thuẫn nội bộ:** `run.md:17` ④ lead tự viết *"riêng LỖ HỔNG authz S156 ... **KHÔNG tự coi là được sửa hay được bỏ qua**"* ⟂ plan:73 biến nó thành default tự chạy. +- **Hệ quả nghiệp vụ (không chỉ hình thức):** siết authz = có thể 403 đúng những người owner muốn "bấm thử để góp ý" (xem F-O3). Tức O-B đi ngược MỤC ĐÍCH owner phát biểu, không chỉ ngược thủ tục. +- **Sửa:** chuyển O-B về cùng nhóm "chờ anh gật" với O-C, **hoặc** giữ default nhưng plan phải có 1 dòng Δ tường minh + 1 câu hỏi owner: *"siết quyền ghi HĐ trước dry-run — đồng ý không, biết rằng siết sai grant = 403 người bấm thử?"*. + +### F-O2 [HIGH] — quyền quyết O-B bị chuyển từ OWNER sang REVIEW-ENSEMBLE (uỷ quyền sai cửa) +- `w8:3`: *"Option = **REVIEW CHỐT quyết**"*; plan:73 *"(O-A fallback **nếu review bác**)"*. Vật này do **owner** đóng ở O-3/`STATUS.md:13`, reviewer không có thẩm quyền mở lại. +- Phụ: **plan:27** ghi *"option **O-A/O-B mặc định**"* ⟂ plan:73 chọn hẳn O-B ⇒ 2 option cùng làm "mặc định" = **0 bit** (không biết đang ở đâu thì không có gì để đảo). +- **Sửa:** plan §5 row 11 ghi 1 giá trị default duy nhất + ghi rõ "quyết định thuộc owner, review chỉ khuyến nghị". + +### F-O3 [HIGH] — W8 bước 3 (seeder grant) KHÔNG THỂ ĂN trên prod ⇒ O-B deploy = 403 toàn bộ, giết đúng dry-run +- `w8:19` bước 3: *"Seeder grant (gotcha #84 — CÙNG COMMIT): cấp `Contracts` Create/Update cho role tham gia dry-run"*. +- **Đĩa bác:** `DbInitializer.cs:2233-2243` — với key **non-PE**, nếu row đã tồn tại thì `continue` (**skip-existing**, comment nguyên văn `:2242` *"Key non-Pe: skip-existing (giữ nguyên như cũ)"*). Nhánh insert `:2247-2253` đặt `CanRead=true, CanCreate=isPe` (⇒ **false** cho `Contracts`), `CanUpdate=false`, `CanDelete=false`. +- Row `Contracts`/`Ct_*` **đã tồn tại prod** (grant "hiển thị hết" S159 — `DbInitializer.cs:2209-2213` comment: *"nhánh grant key-thường là **skip-existing** ⇒ trên prod row false **KHÔNG được nâng ở đây** — re-grant prod đã chạy SQL trực tiếp cùng ngày"*). +- ⇒ O-B thêm `[Authorize(Policy="Contracts.Create/Update/Delete")]` **có răng ngay**, còn grant **no-op** ⇒ **403 cho 13/13 role non-Admin trên 22 endpoint ghi** (`MenuPermissionHandler.cs:41-50` khớp `p.MenuKey==req.MenuKey` + cờ tương ứng; chỉ Admin bypass `:27-31`). +- Bài học ĐÃ CÓ trong chính SC mà w8 không mang theo: **SC:271 (NOTE @S160)** *"ĐỔI nhãn/parent/CanRead của key ĐÃ TỒN TẠI phải đi đường **update-backfill kiểu `labelBackfill` đợt-5**, KHÔNG phải insert; và CẤM SQL tay"*. Thêm vết đắt: `DbInitializer.cs:2287-2296` ghi Run #423 — grant SQL tay bị revoker lật, *"447/494 row rơi, 11/13 role về 0"*. +- Acceptance `w8:32` (đếm Permission rows per-role + restart-proof) CÓ răng — nhưng chỉ bắt **SAU deploy** (`w8:21` "chạy NGAY phép 403/201 sau deploy"). +- **Sửa:** bước 3 phải ghi rõ **nhánh upgrade riêng cho key non-PE** (đọc row tồn tại → set CanCreate/CanUpdate → SaveChanges), idempotent, và nghiệm thu **bằng restart THẬT**; nếu không có nhánh này thì O-B **cấm deploy**. + +### F-O4 [HIGH] — Δ1 (plan §5 row 12) `All += KeHoachKyKet`: sinh policy nhưng 0 grant ⇒ #44 silent-403; đồng thời mất hạt per-leaf và nới Read thành default-OPEN +Đo đĩa (5 mệnh đề, tất cả verify được bằng lệnh trong file này): +1. `MenuKeys.cs:41` `public const string KeHoachKyKet = "KeHoachKyKet";` — **NGOÀI `All`** (mảng `All` `:163-183`, không chứa) ⇒ plan:48 nói đúng, 0 policy hiện tại. +2. Menu row root có thật: `DbInitializer.cs:1774` `(MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, ...)`; 6 leaf + G1 dùng key **`Khkk_*`** (`:1779-1786`), G1 có parent = `MenuKeys.KeHoachKyKet`. +3. Grant: `:2206` `.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })` với comment `:2204-2205` *"2 root placeholder GĐ2/GĐ4 — **CanRead-only** mọi role (IsPeKey=false ⇒ nhánh read-only)"* ⇒ `CanCreate/Update/Delete = false`, và skip-existing chặn nâng (F-O3). +4. `MenuPermissionHandler.cs:41` `p.MenuKey == req.MenuKey` — **khớp ĐÚNG-KHOÁ, KHÔNG kế thừa root** ⇒ CanRead trên `Khkk_List`/`Khkk_Create` (13/13 role, `:2177-2180`) **KHÔNG cứu** policy `KeHoachKyKet.Create`. +5. ⇒ W2/W3 lên prod: `POST /api/contract-signing-plans` **403 im lặng cho 13/13 role**, chỉ Admin qua (`:27-31`) — đúng gotcha #44, đúng thứ dry-run cần nhất ("mọi người bấm thử"). +Hai hệ quả nữa **chưa được khai** trong row 12: +- **Mất hạt:** SC §2.4 `:210-212` ratified *"+4 const `ContractSigningPlans`/`_List`/`_Create`/`_Inbox` **VÀ đưa CẢ 4 vào `All`**"*. Plan chỉ đưa **1 root** ⇒ mọi endpoint chung 1 tiền tố; 6 leaf `Khkk_*` (đã seed + granted) nằm ngoài `All` ⇒ 0 policy, quyền leaf thành **trang trí**. +- **Nới quyền đọc:** root `KeHoachKyKet` đang CanRead **mọi role** ⇒ `KeHoachKyKet.Read` = **default-OPEN** cho toàn module (đọc `ProposedAmount`/`ApprovedAmount` per-NCC), trong khi SC `:224` seed **admin-permission** = default-CLOSED. Owner-verbatim "hiển thị hết" (`STATUS.md:13`) nói về **MENU**, không nói về **quyền đọc số tiền qua API**. +- **Trả lời câu hỏi của đề bài ("Δ1 cần anh gật lại không?"):** **đổi TÊN KEY = KHÔNG cần** (hệ quả kỹ thuật của skeleton `Khkk_*` mà chính owner chốt @S159; SC:269 còn CẤM đẻ bộ key mới). **Đổi mức mặc định đọc (closed→OPEN) + bỏ hạt per-leaf = CẦN 1 dòng cho anh** — nó đổi *ai đọc được số tiền*, không phải chi tiết đặt tên. + +--- + +## 2. MED + +### F-O5 [MED] — nhãn "12 default ĐẢO ĐƯỢC, anh veto lúc nào cũng kịp" mâu thuẫn với chính §4 của plan +- plan:59 (tiêu đề §5): *"dry-run chạy bằng default **ĐẢO ĐƯỢC** — anh veto lúc nào cũng kịp"*. +- plan:55 (§4.4) tự khai: HĐ ≥ phase 5 **KHÔNG xoá được** qua API (`ContractFeatures.cs:632-633`); phiếu KHKK/PE `DaDuyet` **không xoá**; **sequence không rollback**. plan:54 (§4.3): **KHÔNG cờ IsDryRun**. +- ⇒ "đảo được" đúng cho **MÃ**, sai cho **DỮ LIỆU đã sinh trên prod**. Nặng nhất: default #2 (Q6 — giá nào vào HĐ, sai thì HĐ đã phát hành mang số sai) và #6 (format mã KHKK **đóng băng vào Mig 69** — chính SC:306-308 đã cảnh báo *"'không chặn' chỉ đúng nghĩa không-chờ-trả-lời, KHÔNG đúng nghĩa không có quyết định nào bị khoá"*). +- **Sửa:** hạ câu tiêu đề §5 xuống *"đảo được ở mã; dữ liệu/HĐ đã sinh thì không"* + đánh dấu #2/#6 là **default một-chiều**. + +### F-O6 [MED] — O-3 bị THU HẸP mà không khai Δ tường minh (W6/W7 chạm Contract nhưng không có cửa hỏi) +- SC:49: *"**Wave nào chạm module Contract** phải hỏi lại anh"* (phạm vi = MỌI wave). +- plan:90 rút còn *"O-3 còn hiệu lực cho **phần SỬA service**"*; `w8:3` lặp lại y vậy ⇒ **cắt mất vế controller** và vế "mọi wave". +- Hệ quả trên bảng wave: **W6** (plan:25 "sẵn chạy") nằm TRỌN trong module Contract (view-guard + inbox + vai-trình); **W7** (plan:26 "sẵn chạy"); **W8-O-B** (plan:73 default). 3 wave chạm Contract, 0 cửa hỏi. (Điểm sáng: `w6:32` có tự rào *"đụng nhánh Reject-trước-guard = KHÔNG PHẢI wave này (O-C, cần anh gật riêng — đừng «tiện tay sửa»)"*.) +- **Suy luận công bằng:** lệnh anh @S160 (`run.md:8-15`) *"dry-run luôn cho đến hợp đồng cứng"* **có** phủ W5/W6/W7 (đó là ĐƯỜNG ĐI của dry-run). Nhưng **W8 không nằm trên đường đi** — nó là đổi hành vi bảo mật ⇒ không suy ra được từ câu đó. +- **Sửa:** plan §1 (hoặc §5 row 11) thêm 1 dòng Δ: *"O-3 :49 được owner mở @S160 cho phần **chạm-để-chạy** (W5/W6/W7); phần **chạm-để-đổi-hành-vi-bảo-mật** (W8 O-B/O-C) VẪN đóng, chờ gật."* + +### F-O7 [MED] — O-B không phủ CỬA TẠO HĐ THẬT mà dry-run dùng (bridge endpoint) +- HĐ dry-run sinh qua `POST /api/purchase-evaluations/{peId}/create-contract` (`w5:17` ghi rõ FE gọi CÙNG endpoint này). +- Đĩa: endpoint ở `PurchaseEvaluationsController.cs:339`; class chỉ `[Authorize]` trần (`:15`). `grep -c "Authorize(Policy"` = 3 nhưng **2/3 là COMMENT** (`:147`, `:161` — `:161` nguyên văn *"🔴 CỐ Ý KHÔNG có `[Authorize(Policy=...)]` — owner chốt 2026-07-27..."*), attribute thật chỉ `:188` (`PurchaseEvaluations.Read`). ⇒ **bridge endpoint = 0 policy**. +- ⇒ Sau O-B: `POST /api/contracts` bị siết, nhưng cửa **tạo HĐ per-winner + ăn sequence thật** vẫn mở cho mọi user đăng nhập. `w8:7` chỉ khoanh *"(2) Create ăn sequence thật (`ContractFeatures.cs:113-118`)"* = nửa cửa. Acceptance `w8:28` `MoiEndpointGhi_CoAuthorizePolicy` (reflection trên `ContractsController`) **cũng không bắt** cửa này. +- ⚠️ Cộng hưởng: **W5 (`w5:14`) THÊM param vào chính body endpoint chưa có khoá** ⇒ mở rộng cửa trước khi khoá. +- **Sửa:** w8 §① liệt cửa thứ 3 = bridge endpoint; hoặc khai tường minh "cố ý để mở, lý do X" (kiểu comment `:161`) để không tưởng đã phủ hết. + +--- + +## 3. LOW + +### F-O8 [LOW-MED] — nhãn 2 đời: dùng ĐÚNG, nhưng file RATIFIED chưa được stamp errata +- ĐÚNG: `plan:13` + `w1:11` khai Δ tường minh; đĩa chứng `DbInitializer.cs:1774` "Kế hoạch ký kết HĐ" + `:1779` "1. Kế hoạch ký kết HĐ (NCC-TP)"; owner-verbatim `docs/STATUS.md:13` *"❷ **Kế hoạch ký kết HĐ** (đời trước «Đề xuất ký kết hợp đồng» — superseded)"* + `docs/HANDOFF.md:9` cùng nội dung. W5/W8 không dùng nhãn ⇒ 0 rủi ro. +- Tồn dư: SC **:27** vẫn ghi *"OWNER CHỐT NHÃN @S159: tên phiếu/menu = «Đề xuất ký kết hợp đồng» — **mọi label VN trong spec đọc theo tên này**"* và SC **:223** vẫn dạy seed nhãn cũ, dù **CÙNG FILE** đã được chèn NOTE @S160 tại `:268-271` ⇒ pass bảo trì **bất đối xứng**. Ai đọc §2.4 trước w1 sẽ seed sai nhãn. +- **Sửa:** stamp errata 1 dòng tại SC:27 + SC:223 (không viết lại spec — đúng luật "chỉ BỔ SUNG"). + +### F-O9 [LOW · CỦNG CỐ] — 5 câu owner cũ: default KHỚP trạng thái treo, KHÔNG lấn +SC:488 khai *"5 câu owner (O-Q1 · O-Q2 · Q3 · Q6 · Q11) vẫn nguyên"*; plan không đâu tuyên "đã trả lời". Từng dòng: +| plan §5 | Câu | Đối chiếu SC | Phán | +|---|---|---|---| +| #1 | Q3 | SC:364 *"W3 mặc định chỉ làm đường THƯỜNG; 2 đường kia chờ anh"* + phán "PHÁ VỠ" S155 | **KHỚP** — plan chọn bảo thủ; `w3:7` nhất quán (0 bật `AllowApproverFinalize`/`CeoApprovalThreshold`) | +| #2 | Q6 | SC:400 *"mặc định thiết kế: **KH thắng** + audit lệch"* | **KHỚP** (nhưng xem F-O5: hệ quả DỮ LIỆU không đảo được) | +| #3 | Q11 | SC:401 hỏi "chặn hay cảnh báo", không nêu default | plan chọn **mềm** = hướng đảo-được (nâng 409 sau) — hợp lý, không lấn | +| #4 | O-Q1 | SC:62 *"cảnh báo mềm (tiền lệ S62) — O-Q1 owner quyết mềm/cứng"* | **KHỚP** nghiêng sẵn của SC | +| #5 | O-Q2 | SC:391-393 | plan **DEFER W4, 0 dev** = giữ hiện trạng ⇒ không tiêu quyền nào | +⇒ 5/5 nằm TRONG biên SC đã ghi. Đây là điểm **củng cố** (cân bằng với 4 HIGH ở trên). + +### F-O10 [LOW] — baseline "0 HĐ" ĐÚNG (tôi đo lại độc lập) nhưng thiếu 1 mệnh đề nhân-quả + 1 điều kiện env +- **Curl độc lập (2026-07-29, token admin 468B):** `GET /api/contracts?page=1&pageSize=1` → `{"items":[],"total":0,...}` · `GET /api/contracts/deleted?...` → `{"items":[],"total":0,...}` ⇒ **plan:43 ĐỨNG** (measured-label gate PASS: claim có phép phản chứng và phép đó đã chạy). +- Nguồn "7 HĐ V1" của sổ S156 = **demo seeder**, không phải HĐ nghiệp vụ: `docs/changelog/recently-done-archive-2026-04.md:22` *"SeedDemoContractsAsync — 7 demo HĐ varied phases ... 7 HĐ covering 7 ContractType"*. Plan gọi "STALE" đúng KẾT LUẬN nhưng thiếu vế nguyên nhân (0 ≠ "ai đó xoá 7 HĐ thật"). +- Điều kiện env: `DbInitializer.cs:134` bọc `SeedDemoContractsAsync` trong `if (!demoSeedDisabled)`; path tạo-mới `:892` chạy khi **không còn** HĐ `[DEMO]` ⇒ **env không bật cờ (Dev/staging) sẽ tái sinh 7 HĐ demo** ⇒ quy ước lọc `ZZTEST` của plan §4.1 phải cộng bộ lọc `[DEMO]`. + +### F-O11 [LOW] — literal SC:331 đã tự-lão-hoá (không lây sang bộ mới) +SC:331 (acceptance W2) dạy đối chứng *"`grep -c "Authorize(Policy" ContractsController.cs` = **0 hit toàn file**"*. Đĩa hôm nay = **1** (`/deleted` thêm @S159 đợt-5: `ContractsController.cs:28-29`). Bộ spec MỚI không chép literal này (grep "0 hit toàn file" trên `plan-cha`+`spec-wave-*` = 0 hit) và `w8:11` dẫn đúng tiền lệ `/deleted` ⇒ **đã tránh được**; chỉ cần errata ở SC. + +--- + +## 4. ĐIỂM CỦNG CỐ (đo được, chống Smart-Friend một chiều) +1. **5/5 anchor §③-A của W5 HIT trên đĩa:** `CreateContractFromEvaluationFeatures.cs:53-60` (guard `Phase!=DaDuyet` + winners + `pe.ContractId`) · `:68-71` (pin V1: `WorkflowDefinitions ... IsActive`) · `:88-90` (`SUM(q.ThanhTien)` đúng 3 dòng) · `:145` (`pe.ContractId = contracts[0].Id`) · `ContractWorkflowService.cs:115-116` (`ConflictException("HĐ chưa pin workflow definition...")`). +2. **W5 (c) khớp SC:143-144** — `ContractId` nằm trên **`Lines`** (per-NCC), không phải header; `w5:16` gọi đúng "cột C6 sẵn từ W1". Không có drift cardinality. +3. **W8 §③-A anchor đúng:** `ContractsController.cs:13` `[Authorize]` class-trần · `:28-29` `[HttpGet("deleted")]`+`[Authorize(Policy="Contracts.Read")]` · `MenuKeys.cs:168` `Contracts, Forms, Reports,` (⇒ 4 policy `Contracts.*` sống thật). +4. **plan:48 đúng** — `KeHoachKyKet` có const, ngoài `All`. +5. **Q3 default ⟂ w3:7 không mâu thuẫn** (SC:362-365 tự khai xung đột và đã tự giải theo hướng plan chọn). + +## 5. YÊU CẦU SỬA TRƯỚC THI CÔNG (acceptance cho lượt vá) +- **A1 (F-O1/F-O2/F-O6):** plan §5 row 11 + `w8:3` — O-B chuyển thành **"CHỜ ANH GẬT"** ngang O-C, hoặc thêm dòng Δ tường minh vs SC:49 + `STATUS.md:13` và ghi rõ quyết định thuộc owner. Bỏ cụm "O-A/O-B mặc định" (0 bit). +- **A2 (F-O3):** `w8:19` phải mô tả **nhánh upgrade grant cho key non-PE**; acceptance thêm 1 dòng: *"sau restart THẬT, `SELECT COUNT(*) FROM Permissions WHERE MenuKey='Contracts' AND CanCreate=1` ≥ số role dry-run"*. +- **A3 (F-O4):** plan §5 row 12 bổ sung: (i) bước cấp `Create/Update/Delete` cho key mới qua nhánh upgrade; (ii) khai hệ quả **Read default-OPEN** + hỏi owner 1 dòng; (iii) chốt còn giữ hạt per-leaf `Khkk_*` hay không. +- **A4 (F-O5):** sửa tiêu đề §5 + đánh dấu #2/#6 là default **một chiều**. +- **A5 (F-O7):** `w8:7` liệt cửa `POST /api/purchase-evaluations/{id}/create-contract` (hoặc khai cố-ý-để-mở). +- **A6 (F-O8/F-O11):** stamp errata SC:27 · SC:223 · SC:331 (1 dòng/chỗ, không viết lại). + + diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-5.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-5.md new file mode 100644 index 0000000..6db3872 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-5.md @@ -0,0 +1,27 @@ +# sub-reviewer-5 — CON TRỎ (lane 5: SECURITY + DATA-RISK) + +**VERDICT: SUA-TRUOC-THI-CONG** — 3 HIGH / 6 MED / 5 LOW / 1 PASS-dương · 0 anchor sai. + +Full-detail nằm ở file cùng thư mục theo tên lane mà spec-task chỉ định: +👉 `.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-security-s160.md` + +(2 tên file do task-block gọi `sub-reviewer-security-s160.md` còn boilerplate RUN-TRACE gọi `sub-reviewer-5.md`; file này giữ để cả 2 đường trỏ đều tới nội dung, tránh em main đọc hụt.) + +## Index 15 finding +| # | SEV | Tóm | +|---|---|---| +| F-S1 | HIGH | W8 seeder-grant dễ thành NO-OP im lặng (row `Contracts` đã tồn tại, nhánh non-Pe = `continue` skip-existing) ⇒ 403 giết dry-run | +| F-S2 | HIGH | 3 handler attachment 0-authz 0-phase-guard (download scan ký/dấu bởi mọi user); W8 cố ý để GET trần; plan §4 chưa khai | +| F-S3 | HIGH | W6 gate 5-anchor thiếu 2/3 call-site `GetEligiblePhases` (`:301` list, `:381` đã-xoá); hàm role-only ⇒ vá rẻ = mở toang | +| F-S4 | HIGH→MED | W6 không có test ÂM cho VIEW; ca ÂM prod không pin ROLE ⇒ 0-bit | +| F-S5 | MED | W8 grant theo ROLE ⟂ V2 duyệt theo USER-ID; 6/13 role ngoài list (gồm HrAdmin/AuthorizedSigner) | +| F-S6 | MED | Map "POST→Create" nuốt comments/attachments/details; test tính-chất không bắt map sai | +| F-S7 | MED | `DeleteContractCommandHandler` 0 owner-check | +| F-S8 | MED | W8 verify bằng `sqlcmd` RDP mâu thuẫn plan §7 (prod verify = curl) | +| F-S9 | MED | W6 nới guard trộn vế hẹp (`CreatedBy`) + vế rộng (`Procurement`); tên field lệch `DrafterUserId` | +| F-S10 | MED-LOW | `FormsController:11` class-trần, 0 policy per-action — hố chưa khai | +| F-S11 | LOW | Guard `>= DangInKy` chặn cả ChoDuyet/TraLai/TuChoi, plan chỉ khai DaPhatHanh | +| F-S12 | LOW | Q11 2-đường-song-song ⇒ dry-run không chứng minh được cổng KHKK giữ | +| F-S13 | PASS+INFO | 0 mật khẩu trong plan/spec/sub-file; INFO: `Admin@123456` ở 25 file tracked khác | +| F-S14 | PASS | "22 endpoint ghi" đúng; O-B/O-C phân vai đúng; revoker S92 không xung đột | +| F-S15 | LOW | Nhãn đo `plan §3` "curl 6 phase" dưới-phủ enum 12 giá trị (vật chịu lực là "tổng") | 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 index fd4606f..e14ed87 100644 --- 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 @@ -3,11 +3,147 @@ > 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}. +> Đĩa đo tại commit `950054e` (wal: flush 20260729T1553). ## VERDICT (dòng-1) -(đang chạy — chốt ở cuối file) -## FINDINGS -(append từng cái ngay khi đo xong) +**SUA-TRUOC-THI-CONG** — 2 HIGH + 4 MED + 3 LOW-MED + 4 LOW. +Neo dòng (line-anchor) của 3 spec **rất tốt**: 28 neo đo, **0 neo trỏ mã không tồn tại** (không có anchor-trôi sau arc-2), 24/28 khớp từng dòng. Lỗi KHÔNG nằm ở số dòng mà ở **mô-tả nội dung tại neo** và ở **acceptance** — 2 lỗi HIGH đủ để mất số chốt giá im lặng (F-B1) và nổ FK runtime + mâu thuẫn W1 (F-B2). + +--- + +## A. BẢNG NEO — đo từng cái + +| # | Spec | Neo | Đĩa | Phán | +|---|---|---|---|---| +| 1 | w3:13,26 | `ContractWorkflowService.cs:217-394` (ApproveV2Async) | :217 `private async Task ApproveV2Async(` … :394 `}` | ✅ khớp 2 đầu | +| 2 | w3:13 | `:246` GroupBy Order | `var levelGroups = currentStep.Levels.OrderBy(l => l.Order).GroupBy(l => l.Order).ToList();` | ✅ | +| 3 | w3:13,26 | `:259-260` OR-of-N | `allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet();` + `if (!allowedUserIds.Contains(actorUserId.Value))` | ✅ | +| 4 | w3:13,26 | `:292-316` LevelOpinions UPSERT | `db.ContractLevelOpinions.FirstOrDefaultAsync(...)` → if/else Add/update | ✅ | +| 5 | w3:13 | `:320-352` skipToFinal | `if (skipToFinal) { … }` | ✅ | +| 6 | w3:19 | `:407-427` LogTransitionAsync | method = :396-428; :407-427 = **khối notify Drafter** (thiếu :405 `changelog.LogWorkflowTransitionAsync`) | ⚠️ lệch biên (LOW, F-B14) | +| 7 | w3:16,26 | `ApprovalWorkflow.cs:95` class `ApprovalWorkflowLevel` trong file | :95 `public class ApprovalWorkflowLevel : BaseEntity` | ✅ (cảnh báo "KHÔNG có file riêng" ĐÚNG) | +| 8 | w3:16,26 | `:116-125` 4 return-mode | :116 OneLevel · :119 OneStep · :122 ToAssignee · :125 ToDrafter | ✅ đúng 4 cờ | +| 9 | w3:14,26 | `PurchaseEvaluationWorkflowService.cs:1008` RULE | dòng tồn tại — **nội dung ≠ mô-tả spec** | ❌ F-B4 | +| 10 | w3:13 | `ProposalFeatures.cs:427-439` | flatten :427-429 + `ElementAtOrDefault` :433 + match-1-người :439 ✅; **câu "assume 1 step" ở :424 NGOÀI range** | ⚠️ F-B10 | +| 11 | w5:22 | `CreateContractFromEvaluationFeatures.cs:53-60` guard | 3 guard: DaDuyet :53-54 · winners :57-58 · ContractId :59-60 | ✅ | +| 12 | w5:22 | `:68-71` pin V1-only | `db.WorkflowDefinitions…ContractType==… && IsActive` | ✅ (đúng: V1, 0 dòng V2) | +| 13 | w5:22 | `:88-90` SUM | `pe.Details.SelectMany(d=>d.Quotes).Where(q=>…IsSelected).Sum(q=>q.ThanhTien)` | ✅ | +| 14 | w5:16,22 | `:145` idempotency | `pe.ContractId = contracts[0].Id;` | ✅ (check nằm :59-60, write :145 — cả 2 phủ) | +| 15 | w5:15 | `:82-115` vòng lặp per-winner | :82 khai list, :83 `foreach (var w in winners)` … :115 `}` | ✅ | +| 16 | w5:16 | `:101-107` mapping giữ nguyên | TenHopDong/NoiDung/Bypass/DraftData/BudgetManual* | ✅ (cố ý loại GiaTri :100 + Workflow :108 — 2 chỗ W5 sửa) | +| 17 | w5:16 | `:78-82` atomic codegen | :78-81 = **COMMENT**, :82 = khai list; codegen thật ở :113, atomic thật = 1 `SaveChangesAsync` :147 | ⚠️ F-B15 | +| 18 | w5:13 | `:121-140` khuôn ContextNote | ContractChangelog :121-130 + PeChangelog :131-140 | ✅ | +| 19 | w5:14 | `PurchaseEvaluationsController.cs:379-382` body record | `CreateContractFromEvaluationBody(ContractType, TenHopDong, Bypass=false)` = 3 field == command 3 field (trừ route Id) | ✅ | +| 20 | w5:22 | `ContractWorkflowService.cs:115-116` ConflictException | `if (def == null \|\| def.Steps.Count == 0) throw new ConflictException("HĐ chưa pin workflow definition…")` | ✅ neo đúng — nhưng §1/§27 định vị SAI ý nghĩa (F-B5) | +| 21 | w6:8,17,25 | `ContractFeatures.cs:334-351` GetEligiblePhases | method trọn; 7 phase legacy, **0 entry ChoDuyet** | ✅ claim đúng | +| 22 | w6:9,25 | `:421-430` PhaseActorRoles | 7 key legacy, 0 ChoDuyet | ✅ claim "100% legacy" đúng | +| 23 | w6:8,25 | `:488-489` Forbidden | `if (!isDrafter && !isEligibleByRole) throw new ForbiddenException("Bạn không có quyền xem HĐ này.")` | ✅ | +| 24 | w6:17 | `:298` / `:440` admin thấy-hết | :298 = List handler · :440 = Inbox handler — **KHÔNG phải handler đang sửa** (GetContractQuery admin-bypass = :482-483) | ⚠️ F-B11 | +| 25 | w6:10,19,25 | `ContractWorkflowService.cs:73-78` guard trình | `!Drafter && !DeptManager → ForbiddenException` | ✅ | +| 26 | w6:25 | `ContractsController.cs:38-40` inbox | `[HttpGet("inbox")] … GetMyInboxQuery()` | ✅ | +| 27 | w6:32 | `:49-66` Reject-trước-guard | khớp `ContractWorkflowService.cs:49-66` — **thiếu tên file** | ⚠️ F-B12 | +| 28 | w6:18 | khuôn PE `ResolveV2InboxIdsAsync` | có thật: `PurchaseEvaluationFeatures.cs:817` (decl) + `:748` (call) | ✅ (thiếu file:line — nên neo) | + +**Tổng:** 24 ✅ khớp từng dòng · 4 ⚠️ lệch biên/nhầm handler · 1 ❌ mô-tả-sai-nội-dung (#9) · **0 anchor trỏ mã đã biến mất**. + +--- + +## B. FINDINGS + +### F-B1 — HIGH — "choke-point DUY NHẤT" SAI: nguồn copy có ≥2 đường tới terminal, đường thứ 2 gán QUA BIẾN (đúng bug #81 đã trả giá ở S133) +- **Spec:** `spec-wave-w3-...:14` — *"helper `ApplyApprovedValuesOnFinalize` … 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"*. +- **Đĩa (nguồn copy):** `ContractWorkflowService.cs:190-199` — sau nhánh V2 còn nhánh **Admin manual override**: `if (isAdmin) { contract.Phase = targetPhase; … }` (:191-193). Đây là đường **thứ 2** tới phase terminal, **gán qua BIẾN** ⇒ mọi phép `grep "Phase = …DaDuyet"` literal **KHÔNG thấy**. +- **Tiền lệ đã trả giá (nguyên văn đĩa):** `PurchaseEvaluationWorkflowService.cs:308-311` — `// [S133 site 5 — reviewer catch] Override thẳng → DaDuyet cũng PHẢI chốt snapshot NS (RULE helper; assignment qua BIẾN targetPhase nên literal-grep 4-site không thấy — #81).` +- **Hệ quả:** acceptance W3 `:30` (`Lines WHERE PlanId=@id AND ApprovedAmount IS NULL = 0`) chỉ chạy đường thường ⇒ phiếu DaDuyet qua admin-override để `ApprovedAmount` NULL **im lặng**, đúng loại lỗi phải vá lại 1 lần rồi. +- **Acceptance đề nghị (không phải sửa code):** spec kê **đủ 2 write-path** (V2-terminal + admin-override) và bổ 1 ca test admin-override→DaDuyet cũng phải có `ApprovedAmount != NULL`. + +### F-B2 — HIGH — `LogWorkflowTransitionAsync` là hàm CỦA HỢP ĐỒNG (FK vật lý) ⇒ W3 §4 bất khả thi + mâu thuẫn thẳng W1 +- **Spec:** `w3:17` — *"mỗi transition ghi `ContractSigningPlanApprovals` (bảng #7 W1) + `Changelogs` **qua `LogWorkflowTransitionAsync`** (4 cột đủ từ W1)"*. +- **Đĩa:** `IChangelogService.cs:28-33` = `LogWorkflowTransitionAsync(Guid contractId, ContractPhase fromPhase, ContractPhase toPhase, string? comment, ct)`; impl `ChangelogService.cs:72-89` ghi thẳng `_db.ContractChangelogs`; cấu hình `ContractChangelogConfiguration.cs:22-26` có **FK VẬT LÝ**: `HasOne(x => x.Contract).WithMany(c => c.Changelogs).HasForeignKey(x => x.ContractId).OnDelete(Cascade)`. +- ⇒ truyền `plan.Id` = **FK violation runtime (SQL 547)**, không phải lỗi biên dịch. Tham số `ContractPhase` ≠ `ContractSigningPlanPhase` (W1 `:16`) **cast được** ⇒ compiler KHÔNG chặn. +- **Mâu thuẫn W1:** `spec-wave-w1-...:16` bảng #6 = `Changelogs` RIÊNG của KHKK *"[ĐỦ 4 cột EntityType/EntityId/UserName/FieldChangesJson]"* — tức W1 đã dựng bảng riêng, W3 lại chỉ sang service của HĐ. +- **Acceptance đề nghị:** W3 nêu rõ ghi qua đường changelog **của KHKK** (bảng #6 W1) + khai Δ nếu cố ý tái dùng. + +### F-B3 — MED-HIGH — "PORT NHƯNG KHÔNG KÍCH HOẠT (workflow seed không bật cờ)" = containment GIẢ + lấn quyền owner +- **Spec:** `w3:15` — *"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"*; `w3:7` dẫn lời anh *"PHÁ VỠ" S155*. +- **Đĩa:** 2 cờ là **per-Level admin opt-in**, không phải hằng seed: `ApprovalWorkflow.cs:143` `AllowApproverSkipToFinal`, `:154` `AllowApproverFinalize` (comment :151-153: *"AUTO theo cấu hình quy trình … admin per-level designation = cổng duy nhất"*). Designer V2 **dùng chung** render checkbox thật: `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:1267-1268` (`checked={entry.allowApproverFinalize}` + `onChange`). +- **Ghép với W1:** `spec-wave-w1-...:24` mở chính Designer đó cho `type=10` ⇒ **bất kỳ admin tick 1 ô là bật đường anh đã phán "PHÁ VỠ"**, seed không chặn được gì. +- **Quyền owner:** spec cũ ratified `:363-367` để ngỏ *"2 đường kia **chờ anh xác nhận còn muốn không**"* — tức chờ quyết định CÓ/KHÔNG. W3 tự quyết **PORT** (viết code) rồi mới nói không kích hoạt ⇒ default lấn phần chờ anh. + +### F-B4 — MED — W3 mô-tả SAI nội dung khuôn `PurchaseEvaluationWorkflowService.cs:1008` (và số trong khuôn đó đã lệch tuỳ thước) +- **Spec:** `w3:14` — *"(khuôn `PurchaseEvaluationWorkflowService.cs:1008` — comment nêu **LUẬT + lệnh grep**, **CẤM số-đếm site**)"*. +- **Đĩa :1008 nguyên văn:** `// RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay TRƯỚC assignment (reviewer grep-check 4 site).` ⇒ **KHÔNG có lệnh grep**; **CÓ số-đếm "4 site"** — đúng thứ spec bảo cấm. +- **Đo số "4":** `grep -c "Phase = PurchaseEvaluationPhase.DaDuyet"` = **4** (:877 :916 :949 :1154) nhưng call-site của helper = **5** (:311 :876 :915 :948 :1153). Cùng 1 comment, 2 thước ra 2 số ⇒ số trong chú thích tự-lan sai (đúng lớp F-10/S159). +- **Khuôn ĐÚNG có thật trên đĩa** (LUẬT + lệnh grep + không đếm): `ContractFeatures.cs:356-360` — *"🔴 LUẬT (không đếm số …) … Tự kiểm: grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs"*. +- Phụ: helper tại :1009 là `ApplyBudgetSnapshotOnFinalizeAsync` (ngân sách); khuôn **giá** mà spec cũ `:65` trỏ là `ApplyApprovedPriceOnFinalize` `:973-1002` — W3 rút còn 1 neo và rút nhầm cái. + +### F-B5 — MED — W5 định vị SAI `ConflictException` ⇒ acceptance prod 0-bit ở nửa vế + tên test mã hoá cùng ngộ nhận +- **Spec:** `w5:7` *"HĐ **pin workflow V2 để trình được ngay** (hết kẹt `ConflictException`)"*; `w5:27` *"`POST /{id}/transitions {targetPhase:10}` bởi Drafter → **204 (KHÔNG 409)**"*; tên test `w5:24` `Bridge_PinApprovalWorkflowV2_HopDongTrinhDuoc_KhongConConflictException`. +- **Đĩa:** nhánh **trình** `ContractWorkflowService.cs:70-89` KHÔNG đọc workflow, chỉ check role (:73-78) rồi set `Phase=ChoDuyet` (:80) — **204 kể cả HĐ pin V1 hoặc không workflow nào**. Handler `ContractFeatures.cs:216-238` không thêm guard. `ConflictException` `:115-116` chỉ nổ ở nhánh **DUYỆT** (`fromPhase==ChoDuyet && decision==Approve`). +- ⇒ vế "204 KHÔNG 409" **PASS SẴN trước khi sửa** = 0 bit. Vế CÓ răng = `currentApprovalLevelOrder=1` (đo được: DTO `ContractDtos.cs:55`, projection `ContractFeatures.cs:605`, chỉ set khi `ApprovalWorkflowId != null` — `ContractWorkflowService.cs:84`). +- **Acceptance đề nghị:** phải chạy tới **lượt duyệt Cấp-1 bởi approver V2** (2xx + con-trỏ đổi + 1 row LevelOpinion) mới chứng pin V2 sống; giữ vế pointer, bỏ/hạ vế 204. + +### F-B6 — MED-HIGH — W5 mở cửa thứ 2 pin `ApprovalWorkflowId` mà KHÔNG nhắc guard `ApplicableType == Contract` (cửa 1 có, và có test canh) +- **Spec:** `w5:14` — *"+param `ApprovalWorkflowId?` vào command + body record … → pin V2 cho HĐ tạo ra"* (0 chữ về ApplicableType). +- **Đĩa cửa 1:** `ContractFeatures.cs:84-86` `if (aw.ApplicableType != …Contract) throw …`, comment `:77-78` nguyên văn *"BE guard chặn attacker forge POST với PE workflow ID (ApplicableType=1/2)"*; test sống `Create_PinApprovalWorkflowId_ApplicableType_DuyetNcc_Throws` (`tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs`). +- W5 `:7` tự nêu luật *"2 endpoint cùng nghĩa = lớp lỗi 2-site S155"* rồi mở cửa 2 thiếu đúng guard ấy ⇒ pin HĐ vào workflow type 1/2/**10 (KHKK)**. Cần acceptance: ca ÂM forge `approvalWorkflowId` của type≠3 → 4xx. + +### F-B7 — MED — 13/13 tên test đề pin LỆCH khuôn đặt tên của repo (tiền lệ 0/460) +- **Đo:** 460 method `public async Task` trong `tests/**/*Tests.cs`. Grep vị-ngữ thuần Việt (`Duoc|Ve1|ChiMot|_Cua|Bang[A-Z]|Thay|Xem|GuiLai|ChayLai|DungTung`) = **0 hit**. +- **Khuôn đĩa** = vị-ngữ tiếng Anh + danh từ VN không dấu: `ApproveV2_LastLevel_FinalStep_TransitionsToDaPhatHanh_GeneratesMaHopDong`, `Create_TwoWinners_CreatesTwoContracts_EachGiaTriIsThatWinnerQuoteTotal`, `T21_DeleteByApprover_ChoDuyetPe_MovesFourAccumulatorsInFourDirections`. +- **Spec pin 13 tên thuần Việt:** w3:35 (6) · w5:24 (4) · w6:27 (3). +- **Provenance (công bằng):** 6 tên W3 đến từ spec cũ **ratified** `:377-382`; 7 tên W5/W6 từ lane-2 `sub-investigator-codebase-2.md:152,:159`. Lệch có gốc thượng nguồn — nhưng 3 spec biến nó thành **cổng cứng** (`w3:27` *"thiếu 1 tên = TRƯỢT"*) ⇒ khoá vĩnh viễn deviation vào cây test. Cần anh/owner chốt: giữ tên ratified hay chuẩn hoá về khuôn repo (đổi tên = phải sửa cả spec cũ, không được sửa lén). + +### F-B8 — MED — "baseline" trong acceptance đếm test KHÔNG định nghĩa ở W3/W5/W6, trong khi W1 dùng thước chặt hơn +- `w3:35` *"số-sau ≥ baseline + 6"* · `w5:25` *"≥ baseline + 4"* · `w6:28` *"≥ baseline + 3"* — "baseline" đo lúc nào, ai đo, không nói ⇒ đo sau khi thêm test vẫn "đạt". +- Đối chiếu **cùng bộ**: `spec-wave-w1-...:34` *"số-sau ≥ **số-đo-ngay-trước** + 2"* (đúng thước, chống tự-lão-hoá); spec cũ ratified `:369` pin literal **"562 + N"** (literal tự lão hoá — `docs/STATUS.md:6` hiện ghi 562 PASS 45D+517I, sẽ lệch ngay commit sau). +- ⇒ 3 spec nên dùng đúng 1 idiom "số-đo-ngay-trước + N" như W1. + +### F-B9 — LOW-MED — W5 sửa acceptance của nền RATIFIED mà không khai Δ (W1 có mục Δ, W3/W5/W6 không) +- Ratified `:402`: *"HĐ tạo từ PE có KH DaDuyet → `GiaTri == SUM(ApprovedAmount)`"*. +- W5 `:26`: *"MỖI HĐ `giaTri == ApprovedAmount` của Line cùng supplierId; tổng W HĐ == SUM(ApprovedAmount) toàn plan"*. +- W5 **đúng hơn** (nền multi-winner: `CreateContractFromEvaluationFeatures.cs:83-115` tạo N HĐ), nhưng đây là **sửa nền ratified im lặng**. W1 `:9-11` đã lập khuôn khai Δ ⇒ W5 (và W3 ở F-B3) nên có mục Δ tương ứng. + +### F-B10 — LOW — W3 trích câu "assume 1 step" nằm NGOÀI range đã neo +- `w3:13` *"CẤM copy ProposalFeatures `:427-439` … comment nó **tự thú** 'assume 1 step'"*. +- Đĩa: câu tự thú ở **`:424`** (`// Multi-step workflow: traverse step-by-step. Lite version: assume 1 step per workflow`). Trong range :427-439 có đủ bằng chứng khác (flatten :427-429 · `ElementAtOrDefault(p.CurrentApprovalLevelOrder-1)` :433 · match 1-người :439). Range nên là **:423-440**. + +### F-B11 — LOW — W6 "Admin giữ thấy-hết (`:298/:440`)" trỏ 2 handler KHÁC handler đang sửa +- W6 `:17` sửa view-guard trong `GetContractQueryHandler` (`:488-489`), nhưng admin-bypass của **chính handler đó** nằm ở `ContractFeatures.cs:482-483` (`var isAdmin = currentUser.Roles.Contains(AppRoles.Admin); if (!isAdmin) {…}`). `:298` = `ListContractsQueryHandler`; `:440` = `GetMyInboxQueryHandler`. Giữ nguyên :298/:440 **không bảo toàn** gì cho :488-489. + +### F-B12 — LOW — W6 `:49-66` là anchor TRẦN (không tên file), dễ đọc nhầm sang controller +- `w6:32` *"đụng nhánh Reject-trước-guard (`:49-66`)"*. Nội dung khớp `ContractWorkflowService.cs:49-66` (nhánh Reject chạy trước mọi guard — đúng lỗ S156/gotcha #82). Nhưng file gần nhất được nêu trong cùng §③-A là `ContractsController.cs:38-40` ⇒ mơ hồ. Ghi rõ tên file. + +### F-B13 — MED — W6 vá inbox chỉ theo actor-là-approver, BỎ nhánh admin ⇒ admin vẫn mù HĐ `ChoDuyet` sau W6 +- Đĩa `ContractFeatures.cs:440-441`: `eligiblePhases = isAdmin ? PhaseActorRoles.Keys.ToList() : …` mà dict `:421-430` **không có ChoDuyet** ⇒ admin gọi `/api/contracts/inbox` vẫn ra 0 HĐ ChoDuyet kể cả sau khi ghép precompute-set V2 cho actor. +- Acceptance W6 `:29` chỉ đo **approver-V2 KHÔNG-admin** ⇒ không bắt được lỗ này. Bổ 1 vế: admin gọi /inbox thấy HĐ ChoDuyet (hoặc khai rõ CỐ Ý không cho admin thấy). + +### F-B14 — LOW — neo notify `:407-427` cắt mất chính dòng ghi changelog `:405` +- `w3:19` *"Notify: mirror `LogTransitionAsync` (`ContractWorkflowService.cs:407-427`)"* trong khi `w3:17` lại yêu cầu ghi changelog — dòng làm việc đó là `:405` `await changelog.LogWorkflowTransitionAsync(...)`, **ngoài range**. Method trọn = `:396-428`. + +### F-B15 — LOW — neo "atomic codegen `:78-82`" trỏ vào COMMENT, trong khi W5 lại thêm ghi-vào-vòng-lặp +- Đĩa `:78-81` = comment giải thích, `:82` = `var contracts = new List();`. Bất-biến atomic THẬT = codegen tự-commit `:113` + **duy nhất 1** `await db.SaveChangesAsync(ct)` `:147`. +- W5 `:15` thêm ghi ngược `Line.ContractId` **trong vòng lặp** ⇒ rủi ro chính là ai đó chèn `SaveChanges` vào loop; "giữ nguyên :78-82" (comment) không canh được. Nên neo :113 + :147. + +### F-B16 — LOW — W5 §2(a)/(c) chưa chốt cardinality + chế độ đọc plan +- `w5:13` *"đọc plan `DaDuyet` của PE (`Plans.Where(PurchaseEvaluationId==peId && Phase==DaDuyet)`)"* — không nói **1 PE có tối đa mấy plan DaDuyet** (spec cũ `:157` xác nhận tra ngược bằng đúng query này nhưng cũng không chốt 1-1; W1 `:16` chỉ UNIQUE `(PlanId,SupplierId)` ở Lines, không có ràng buộc 1-plan-DaDuyet/PE) ⇒ 2 plan = lấy cái nào, không định nghĩa. +- `w5:15` ghi ngược `Line.ContractId` cần đọc **tracked** (không `AsNoTracking`) để `SaveChanges` :147 flush — spec im lặng. + +--- + +## C. XÁC NHẬN DƯƠNG (chống rubber-stamp ngược — những chỗ spec ĐÚNG và chắc) +- Gate 5-anchor W3 (`:217-394`, `:246`, `:259-260`, `:292-316`, `:320-352`) — **5/5 đúng từng dòng**, kể cả 2 đầu method. +- `ApprovalWorkflow.cs:95` + `:116-125` — đúng cả class-trong-file lẫn đúng 4 cờ return-mode; cảnh báo "KHÔNG có file riêng tên đó" là **thật**. +- Gate 5-anchor W5 (`:53-60`, `:68-71`, `:88-90`, `:145`, `CWS:115-116`) — 5/5 đúng; `:121-140` khuôn changelog đúng; `PurchaseEvaluationsController.cs:379-382` đúng 3 field. +- Gate 5-anchor W6 (`:334-351`, `:421-430`, `:488-489`, `CWS:73-78`, `ContractsController.cs:38-40`) — 5/5 đúng; 3 claim chẩn đoán (`0 entry ChoDuyet` · `100% legacy phase` · `guard trình đòi Drafter∨DeptManager`) đều **đo đúng**. +- `w5:25` *"`CreateContractFromEvaluationMultiWinnerTests` (ĐÃ TỒN TẠI trên đĩa)"* — đúng: `tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromEvaluationMultiWinnerTests.cs` (6 test). +- `w6:18` khuôn `ResolveV2InboxIdsAsync` — có thật `PurchaseEvaluationFeatures.cs:817` (+ call :748). +- `w3:18` guard trình `CreatedBy==actor` — khả thi: `BaseEntity.CreatedBy` là `Guid?` và được set thật ở `AuditingInterceptor.cs:44-45` (state Added). `DepartmentId` của phiếu có trong W1 `:16` (C5). +- `w3:30` acceptance finalize CÓ `AND PlanId=@id` (giữ đúng bản vá của spec cũ `:359`) — không tái phạm FAIL-giả toàn bảng. +- `w3:28` (c) `LevelOpinions WHERE PlanId=@id = 1 row` khớp UNIQUE `(PlanId,LevelId)` W1 `:16` (#4). + +## D. GHI CHÚ CHÉO (ngoài cụm B, để lane khác không sót) +- W1 `:24` neo `ApprovalWorkflowsV2Page.tsx:143` cho `typeCode→10`: đĩa `:143` là **dòng comment**, map thật `const TYPE_CODE_TO_INT` ở **`:144-148`** (3 entry: DuyetNcc/DuyetNccPhuongAn/Contract). W1 tự hedge "grep lại lúc làm" ⇒ INFO. ## 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 index 2d06602..7c8e185 100644 --- 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 @@ -1,14 +1,145 @@ # 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). +> Lăng kính: verify ANCHOR/CLAIM vs ĐĨA. Đích: `spec-wave-w7-ban-cung-hdc-29-07-2026.md` + `spec-wave-w8-authz-29-07-2026.md` + `plan-cha-dry-run-toan-trinh-29-07-2026.md` §3-§5. +> Mọi số dưới đây tự đo lại từ đĩa/prod ngày 2026-07-29, KHÔNG chép từ spec. -## 0. Verdict (điền cuối) +## 0. VERDICT -## 1. Bảng anchor đo được +**SUA-TRUOC-THI-CONG** — 3 HIGH (F-C1, F-C2, F-C3) đều nằm ở tiền đề dữ liệu/quyền của W7-W8: sửa 3 dòng spec là đủ, nhưng nếu thi công y nguyên thì W7 ra trang rỗng cho đúng những người phải bấm, và W8 khoá cửa đúng 2 vai đó. -## 2. FLAG F-C1..n +- Anchor ĐO ĐƯỢC: **24/26 ĐÚNG** (2 sai: 1 claim param không tồn tại, 1 nhãn file sai). +- Baseline plan §3: **6/6 khớp** `docs/STATUS.md:6` + prod curl. -## 3. Bảng 12-default — default nào NGƯỢC đĩa +## 1. BẢNG ANCHOR ĐO ĐƯỢC -## 4. Ghi chú đo lường / lệnh đã chạy +| # | Anchor spec | Phán | Bằng chứng đĩa | +|---|---|---|---| +| 1 | `ContractAttachment.cs:5-11` enum | ✅ ĐÚNG | enum `AttachmentPurpose` chiếm đúng :5-11 (DraftExport=1, ScannedSigned=2, SealedCopy=3, Other=99) ⇒ chèn `=4` an toàn | +| 2 | tiền lệ PE `ComparisonTable=4` | ✅ ĐÚNG | `PurchaseEvaluationAttachment.cs:10` | +| 3 | enum-extend là BẮT BUỘC (không phải tuỳ chọn) | ✅ ĐÚNG (spec chưa nói) | `ContractAttachmentFeatures.cs:51` `RuleFor(x => x.Purpose).IsInEnum()` ⇒ hôm nay `purpose=4` bị 400 | +| 4 | `DbInitializer.cs:1862-1867` loop 7-leaf | ✅ ĐÚNG | `:1862` `foreach (var code in MenuKeys.ContractTypeCodes)` … `:1867` `tree.Add(("Hdc_" + code, …))`; `MenuKeys.cs:34-35` = 7 mã | +| 5 | `ContractListItemDto` `:323-327` 12-field | ✅ ĐÚNG | projection `:323-327` liệt kê đúng **12** giá trị, KHÔNG có attachments | +| 6 | `MyContractsPage.tsx:29-46` | ✅ ĐÚNG (kết luận suy ra thì SAI — F-C1) | `:29` typeFilter · `:34` phaseFilter · `:35` showDeleted · `:45` gửi `type` **chỉ khi** `showDeleted` · `:60` lọc type CLIENT-side · `:44` `pageSize:100` | +| 7 | `fe-user/…/Layout.tsx:63-69` | ✅ ĐÚNG | đúng 7 dòng `Hdc_*` → `/coming-soon?stage=4&t=01..07` | +| 8 | mirror fe-admin | ✅ ĐÚNG + CẦN THẬT | `fe-admin/…/Layout.tsx:40-46` cũng 7 dòng; `:178-180` `isAdminHidden` chỉ chặn `key.startsWith('Ct_')` ⇒ `Hdc_*` KHÔNG bị ẩn | +| 9 | `ContractsController.cs:13` class-trần | ✅ ĐÚNG | `[Authorize]` tại `:13`; grep `Authorize` cả file = 3 hit (:13 class, :27 comment, :29 deleted) | +| 10 | `ContractsController.cs:16-24` "list params type/phase" | ❌ **SAI** | `:16-24` khai 7 param: page,pageSize,search,sortDesc,**phase**,supplierId,projectId — **KHÔNG có `type`** → F-C1 | +| 11 | `ContractsController.cs:28-29` tiền lệ policy | ✅ ĐÚNG | `:28` `[HttpGet("deleted")]` · `:29` `[Authorize(Policy = "Contracts.Read")]`; `type` chỉ có ở đây (`:31` + `ListDeletedContractsQuery` `ContractFeatures.cs:364`) | +| 12 | "22 endpoint ghi" | ✅ ĐÚNG (đếm được 22) | `[HttpPost|Put|Delete]` tại 46,53,61,68,75,85,110,123,130,137,144,151,158,165,172,179,186,193,200,207,214,221 = **22**, **0/22** có policy | +| 13 | `SuppliersController.cs:67-99` khuôn Master | ✅ ĐÚNG (khuôn là HỖN HỢP) | `:67`/`:78`/`:86`/`:99` `[Authorize(Policy="Suppliers.Update")]`; nhưng `:57` DELETE dùng `[Authorize(Roles="Admin,CatalogManager")]` ⇒ W8 chọn policy thuần cho DELETE là QUYẾT ĐỊNH, nên khai | +| 14 | `MenuKeys.cs:163-181` `Contracts` ∈ All | ✅ ĐÚNG | mảng `All` = `:163-181`, `Contracts` ở `:168`; `Actions` `:183` | +| 15 | `MenuKeys.cs:41` `KeHoachKyKet` const + "comment dặn tái dùng" + ngoài All | ✅ ĐÚNG | `:41` const; comment `:37-40` "CỐ Ý NGOÀI `All` … Wave-1 GĐ2 phải TÁI DÙNG/thay root này" | +| 16 | `ContractWorkflowService.cs:49-66` Reject-trước-guard | ✅ ĐÚNG | `:49` `if (decision == ApprovalDecision.Reject)` → set Phase → log → `SaveChanges` → `return` `:65`; trước đó `:44-46` chỉ gán biến, 0 guard | +| 17 | `ContractWorkflowService.cs:191-198` admin-override | ✅ ĐÚNG (điều kiện tới được thì SAI — F-C8) | `:191` `if (isAdmin) { contract.Phase = targetPhase; … return; }` | +| 18 | `ContractFeatures.cs:632-633` delete-guard numeric | ✅ ĐÚNG (phạm vi hụt — F-C8) | `:632` `if (entity.Phase >= ContractPhase.DangInKy)` | +| 19 | `ContractFeatures.cs:298` admin-bypass (list) | ✅ ĐÚNG | `:298` `if (!currentUser.Roles.Contains(AppRoles.Admin))` + `:302` IDOR where | +| 20 | `ContractFeatures.cs:440` admin-bypass (inbox) | ✅ ĐÚNG | `:440` `var eligiblePhases = isAdmin ? PhaseActorRoles.Keys.ToList() : …` | +| 21 | `ContractFeatures.cs:113-118` gen mã tại create + `:113-115` comment gap | ✅ ĐÚNG | `:113-115` "Trade-off: nếu HĐ bị TuChoi → mã bị 'wasted' (gap trong sequence). Acceptable"; `:118` `GenerateAsync(...)` | +| 22 | `ContractCodeGenerator.cs:25` MB | ✅ ĐÚNG | `ContractType.HopDongMuaBan => "MB"` | +| 23 | `ContractCodeGenerator.cs:33,:40-53` seq-per-prefix | ✅ ĐÚNG | `:33` `prefix = $"{scope}/{typeCode}/SOL&{supplierCode}"`; `:40-53` lookup/insert/increment/return trong tx SERIALIZABLE `:37` | +| 24 | `pe.ContractId` `:59-60,:145` idempotency | ✅ ĐÚNG | `CreateContractFromEvaluationFeatures.cs:59-60` `if (pe.ContractId is not null) throw ConflictException` · `:145` `pe.ContractId = contracts[0].Id` | +| 25 | `AuthorizePolicyRegressionTests` "ĐÃ TỒN TẠI" | ✅ CÓ THẬT (phủ sóng thì 0-bit — F-C5) | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs`; phép "policy-name có thật" `:120-143` **hardcode 4 action của `ApprovalWorkflowsV2Controller` `:123-129`** | +| 26 | plan §5 #1 nguồn "W3 `:362-365`" | ❌ **SAI FILE** | `spec-wave-w3-duyet-3-tram-29-07-2026.md` = **37 dòng**; đoạn thật ở `spec-ke-hoach-ky-ket-hd-28-07-2026.md:362-365` | + +**Anchor spec-cũ khác trong plan — ĐÚNG HẾT:** `:27` = "OWNER CHỐT NHÃN @S159 … ranh GĐ2 b.7→12 · GĐ3 b.13→18 · GĐ4 b.19→21 upload-only · b.7 email BỎ (Q8 ĐÓNG)" ✅ · `:67-71` = "Rẽ nhánh b.12 — 4 đường ĐỀU CÓ SẴN … Q3 owner" ✅ · `:128` = row CẮT `ContractSigningPlanCodeSequences`, tái dùng `WorkflowAppCodeSequence` ✅ (đĩa: `IApplicationDbContext.cs:136` + dùng thật ở `LeaveOtApprovalFeatures.cs:35-39`). + +## 2. FLAG F-C1..F-C12 + +### F-C1 [HIGH] `GET /api/contracts` KHÔNG nhận `type` — cả 7 leaf `Hdc_*` sẽ hiện CÙNG một danh sách +- Spec: W7 `:21` "route `/hard-copies?type=N` → list `GET /api/contracts?type=N&phase=9`" · `:27` anchor "list params `type`/`phase`" · `:32` acceptance "`?type=1&phase=9` → 200, mọi item `phase==9`". +- Đĩa: `ContractsController.cs:16-24` không khai `type`; handler `ContractFeatures.cs:305-307` chỉ lọc `Phase`/`SupplierId`/`ProjectId`. `type` CHỈ sống ở `/deleted` (`:31`). +- Model-binding ASP.NET bỏ qua param lạ ⇒ **200 + không lọc**. Acceptance `:32` **0-bit trên trục type** (đúng-bằng-xây-dựng). +- Prod đo 2026-07-29 (token admin, field `accessToken`): `GET /api/contracts?page=1&pageSize=1&type=1&phase=9` → **HTTP 200, `total:0`** — y hệt khi không truyền `type` (prod rỗng nên phép này còn không phân biệt nổi 2 thế giới). +- Khuôn được chỉ định (`MyContractsPage.tsx`) né bằng cách lọc CLIENT-side `:60` trên `pageSize:100` `:44` ⇒ copy nguyên khuôn thì "thầm đúng" nhưng trần 100 dòng/loại. +- Sửa: hoặc thêm `type` vào `ListContractsQuery` (BE, ~3 dòng), hoặc spec khai rõ "lọc client-side + trần 100"; acceptance đổi thành phép PHÂN BIỆT (2 loại HĐ khác nhau, gọi `type=1` chỉ trả loại 1). + +### F-C2 [HIGH] Nguồn dữ liệu `phase=9` bị IDOR-filter chặn đúng những người phải thao tác +- Đĩa `ContractFeatures.cs:298-303`: non-admin chỉ thấy `DrafterUserId == userId || eligiblePhases.Contains(Phase)`. +- `GetEligiblePhases` `:334-350` liệt kê DangSoanThao/DangDamPhan/DangInKy/DangGopY/DangKiemTraCCM/DangTrinhKy/DangDongDau — **KHÔNG có `DaPhatHanh`(9)**, cũng **không có `ChoDuyet`(10)**. +- ⇒ Trang Bảng cứng (list `phase=9`) **RỖNG** với mọi non-admin không phải người soạn — gồm HrAdmin (đóng dấu b.20), Director/AuthorizedSigner (ký b.19), CostControl (b.21). +- Acceptance W7 `:32` chạy bằng token admin (bypass `:298`) ⇒ PASS mà không lộ. Thiếu hẳn ca "user KHÔNG phải drafter mở được trang". +- Cùng lớp ở inbox: `:440-452` lọc theo `PhaseActorRoles` (legacy-phase) ⇒ tập hiển thị của HĐ V2 chưa ai sửa. Plan §2 `:36` khai W7 chỉ cần W5/W6 "đẻ HĐ DaPhatHanh", KHÔNG khai phụ thuộc W6 phải mở lens `DaPhatHanh`/`ChoDuyet` ⇒ đồ thị phụ thuộc thiếu 1 cạnh. + +### F-C3 [HIGH] Grant-list W8 bỏ sót đúng 2 vai mà W7 cần: `HrAdmin` + `AuthorizedSigner` +- W8 `:19` cấp `Contracts` Create/Update cho Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin (7 vai). +- W7 `:12-14` giao b.19 cho **BOD** (Director ∪ NĐUQ = `AuthorizedSigner`), b.20 cho **HRA/ISO**, b.21 cho **PB/BCH + CCM**. +- Đĩa: `AppRoles.cs:15` `AuthorizedSigner`, `:16` `HrAdmin` — cả 2 có thật, và chính hệ thống gán `AuthorizedSigner`→DangTrinhKy, `HrAdmin`→DangDongDau (`ContractFeatures.cs:348-349`) = đúng vai ký/đóng dấu. +- W8 `:18` gắn POST → `Contracts.Create`; upload là `[HttpPost("{id:guid}/attachments")]` (`ContractsController.cs:85`) ⇒ sau W8, HrAdmin/AuthorizedSigner **403 khi upload** = giết b.19/b.20 của W7 (gotcha #44 tái diễn — đúng rủi ro W8 `:33` tự nêu mà grant-list không đóng). +- Phụ: luật "theo verb" ở `:18` tự mâu thuẫn (POST→Create nhưng `POST {id}/transitions`→Update) ⇒ 8 POST `details/*` (`:123-165`) + POST comments (`:68`) + POST attachments (`:85`) bỏ ngỏ cho implementer đoán. Nên phát biểu theo NGỮ NGHĨA: chỉ `POST /api/contracts` (`:46`) là Create, còn lại Update. + +### F-C4 [MEDIUM] Acceptance 403 của W8 không có chủ thể; tài khoản UAT duy nhất nằm TRONG tập được cấp +- W8 `:30` "user đã-cấp → 201; user chưa-cấp → 403" không nêu tài khoản. `AppRoles.cs:5-21` = 13 vai; cấp 7 ⇒ còn 6 (Finance/Accounting/Equipment/AuthorizedSigner/HrAdmin/CatalogManager). Tài khoản UAT sẵn có `nv.test` = Drafter ⇒ **thuộc tập được cấp** ⇒ vế ÂM không chạy được nếu không tạo tài khoản mới. Ghi tên tài khoản + vai vào acceptance. + +### F-C5 [MEDIUM] Test mới W8 không khoá "policy-name có thật"; lưới hiện có 0-bit cho ContractsController +- `AuthorizePolicyRegressionTests:120-143` là phép duy nhất chặn bẫy "policy trỏ menu key ngoài `MenuKeys.All` ⇒ Program.cs không sinh ⇒ 500 lúc chạy" — nhưng `:123-129` hardcode 4 action của `ApprovalWorkflowsV2Controller`. ⇒ W8 `:27` "VẪN XANH" là **đúng mà vô nghĩa** cho wave này. +- W8 `:28` mô tả test mới chỉ kiểm "có `[Authorize(Policy=...)]`", thiếu vế `MenuKeys.All.Should().Contain(parts[0])` (`:138-139`) + `MenuKeys.Actions` (`:140`). Hôm nay `Contracts` ∈ All (`MenuKeys.cs:168`) nên an toàn, nhưng acceptance không khoá được điều đó cho lần sửa sau. + +### F-C6 [MEDIUM] Badge "Đã ký GĐ" derive từ `ScannedSigned` = trùng nghĩa với chữ ký NCC (gotcha #71) +- Đĩa `ContractAttachment.cs:8`: `ScannedSigned = 2, // Scan HĐ có chữ ký NCC ở phase DangInKy`. +- W7 `:12` gán chính value đó cho "b.19 Ký GĐ/NĐUQ … 'Đã ký GĐ' = has(ScannedSigned)" ⇒ file up theo nghĩa cũ (chữ ký NCC) làm badge GĐ sáng SAI, và không có cách phân biệt về sau. +- Bất đối xứng: W7 `:20` sẵn sàng đẻ `InitialSigned=4` cho ký-nháy b.17-18 nhưng lại tái dùng value mơ hồ cho b.19. Thêm `DirectorSigned=5` vẫn 0-mig. + +### F-C7 [MEDIUM] "sửa DTO + projection" (số ít) — thực tế 3 site dựng `ContractListItemDto` +- `ContractFeatures.cs:323` (list) · `:401` (deleted) · `:454` (inbox). Record positional ⇒ compile-caught, KHÔNG im lặng; nhưng nếu 2 site kia điền `false` cho đủ chữ ký thì badge ở màn "Đã xóa"/inbox **luôn tắt** = sai-im-lặng. Ước lượng "BE nhỏ" (W7 `:20`) / "FE nặng + 1-2 BE" (plan §2 `:26`) hụt. + +### F-C8 [MEDIUM] plan §4.4: đúng chữ, hụt phạm vi; và công thức admin-override không chạy cho HĐ đang `ChoDuyet` +- `ContractPhase.cs`: DangInKy=**5**, DaPhatHanh=**9**, ChoDuyet=**10**, TraLai=**98**, TuChoi=**99**. Guard `ContractFeatures.cs:632` `Phase >= DangInKy` so sánh SỐ ⇒ chặn xoá **mọi phiếu đã trình (10) / trả lại (98) / từ chối (99)**, không chỉ "HĐ test DaPhatHanh nằm lại" như §4.4 mô tả. Dry-run sẽ đẻ nhiều HĐ dở-dang không xoá được. +- Lối thoát §4.4 (`ContractWorkflowService.cs:191-198`) chỉ tới được nếu rơi khỏi các nhánh trước. Nhánh APPROVE `:92` `if (fromPhase == ChoDuyet && decision == Approve)` bắt TRƯỚC → `ApproveV2Async` `:100` ⇒ admin bấm "duyệt về Nháp" trên HĐ `ChoDuyet` sẽ **ĐẨY TỚI** chứ không lùi. Từ `DaPhatHanh`(9) thì `:92` không khớp ⇒ override chạy đúng. ⇒ công thức rollback đúng cho phase 9, **sai cho phase 10**. + +### F-C9 [LOW-MED] plan §5 #7: đường-đảo "map MuaBan→NCC" không có căn cứ trong RG-001 v02 +- Vế khẳng định của #7 ✅ ĐÚNG: `ContractCodeGenerator.cs:25` sinh "MB"; `src-RG-001-v02-ma-so-hd.txt:12-16` chỉ định đúng 4 viết tắt HĐTP/HĐGK/NCC/HĐDV — **không có MB**. +- Nhưng RG-001 `:33-42` + TABLE 3 `:57` có **format PO riêng** `A/PO/SOL&C/D`, và comment repo `ContractCodeGenerator.cs:18` tự khai "HĐ Mua bán: dùng PO format" ⇒ đường đảo đúng theo nguồn là **PO**, không phải NCC. Đảo sang NCC còn trộn chuỗi số với HĐ NCC thật (dùng chung prefix `{Project}/NCC/SOL&{NCC}` — `:33,:40-50`). + +### F-C10 [LOW-MED] plan §5 #12 thiếu nghĩa vụ đồng bộ canonical mà chính mã dặn +- `MenuKeys.cs:37-40` + `:149-151` nói thẳng: thêm key vào `All` sẽ đổi 2 số canonical `Menu keys`/`Policies` ở `docs/STATUS.md` (Policies = |All| × |Actions|, DERIVED). +- Default #12 "All += KeHoachKyKet" không kèm bước cập nhật `docs/STATUS.md`; §7 `:85` chỉ nói "Menu/permission CHỈ qua seeder". ⇒ drift canonical ngay wave 1. + +### F-C11 [LOW] plan §5 #1 anchor `W3 :362-365` trỏ ra ngoài file +- `spec-wave-w3-duyet-3-tram-29-07-2026.md` = **37 dòng** (`wc -l`). Văn bản thật ("2 trong 3 đường … anh phán **'PHÁ VỠ'** ở S155 ⇒ **W3 mặc định chỉ làm đường THƯỜNG**; 2 đường kia chờ anh xác nhận") nằm ở `spec-ke-hoach-ky-ket-hd-28-07-2026.md:362-365`. **Nội dung default #1 ĐÚNG** (không lấn quyền owner) — chỉ nhãn nguồn sai file. + +### F-C12 [LOW] Acceptance grep của W7 có răng nhưng dễ Goodhart; note b.21 không có chốt máy +- Phép `grep "Hdc_" Layout.tsx | grep -c "coming-soon"` hiện = **7** ở CẢ 2 app (`fe-user:63-69`, `fe-admin:40-46`) ⇒ đích "=0" KHÔNG tautology ✅. Nhưng trỏ 7 leaf về BẤT KỲ chuỗi nào khác cũng ra 0 ⇒ nên kèm vế dương: 7 dòng chứa `/hard-copies?type=`. +- Note b.21: BE không ràng buộc gì ngoài `RuleFor(x => x.Purpose).IsInEnum()` (`ContractAttachmentFeatures.cs:51`); `Note` tự do ⇒ badge "Đã lưu bản cứng" bật kể cả note rỗng ⇒ default #9 (số bộ gốc / nơi lưu) không có ai canh. Nếu giữ option A thì acceptance phải là phép NGƯỜI đọc, đừng ghi như phép máy. + +## 3. BẢNG 12-DEFAULT — default nào NGƯỢC ĐĨA? + +| # | Phán | Bằng chứng | +|---|---|---| +| 1 | ✅ nội dung đúng · ❌ nhãn nguồn | F-C11 | +| 2 | ⚪ ngoài tầm đo lane C (chưa có bảng KHKK trên đĩa) | — | +| 3 | ⚪ ngoài tầm đo | — | +| 4 | ⚪ ngoài tầm đo | — | +| 5 | ⚪ ngoài tầm đo | — | +| 6 | ✅ ĐÚNG | `WorkflowAppCodeSequence` có thật (`IApplicationDbContext.cs:136`, dùng ở `LeaveOtApprovalFeatures.cs:35-39`); spec cũ `:128` đúng là row quyết định tái dùng | +| 7 | ✅ vế khẳng định · ⚠️ đường-đảo sai nguồn | F-C9 | +| 8 | ✅ ĐÚNG khả thi | enum `:5-11` + `IsInEnum` `:51` + tiền lệ PE `:10`; ⚠️ ngữ nghĩa value F-C6 | +| 9 | ✅ đúng "option A không có field" · ⚠️ 0 chốt máy | F-C12 | +| 10 | ✅ ĐÚNG | `ContractFeatures.cs:113-118` + comment gap `:113-115` | +| 11 | ✅ ĐÚNG số 22 + class-trần · ⚠️ grant-list thiếu 2 vai | F-C3 / F-C4 / F-C5 | +| 12 | ✅ tiền đề đúng · ⚠️ thiếu bước sync canonical | F-C10 | + +**Không phát hiện default nào lấn quyền owner:** #1 giữ đúng chốt "PHÁ VỠ" S155 (spec cũ `:362-365`), #11 để O-C chờ anh gật (W8 `:12`, `:20`, plan `:90`), #8 để option B chờ anh. O-3 "chỗ hợp đồng cứ từ từ" được tôn trọng ở phần SỬA service. + +## 4. BASELINE §3 — ĐO LẠI 6/6 KHỚP + +| Claim plan §3 | Đo lại | Khớp | +|---|---|---| +| prod Contracts = 0 active + 0 deleted | curl admin 2026-07-29: `/api/contracts` → `total:0`; `/api/contracts/deleted` → `total:0` | ✅ | +| Mig 68 · 89 bảng · 562 test (45D+517I) | `docs/STATUS.md:6` | ✅ | +| Menu 142 (root 12) · 21 key `Ct_*` | `docs/STATUS.md:6` | ✅ | +| `Khkk_*` = 7 key (G1 + 6 leaf) | `DbInitializer.cs:1779` (G1) + `:1781-1786` (6 leaf) | ✅ | +| `Hdc_*` = 7 leaf per-ContractType | `DbInitializer.cs:1862-1867` × `MenuKeys.cs:34-35` (7 mã) | ✅ | +| bundle admin `D0sXA0fe`/`DWDbm5As` · user `Y6dW_5CM`/`6YIAufJR` | `docs/STATUS.md:6` | ✅ | +| `KeHoachKyKet` ngoài `All` ⇒ 0 policy | `MenuKeys.cs:41` vs `All :163-181` | ✅ | +| `scripts/backup-sql.ps1` (§4.6) | file tồn tại | ✅ | +| seed-permission ">1 site" (W8 `:19`) | 7 site `db.Permissions.Add` trong `DbInitializer.cs` (:2060,:2246,:2378,:2437,:2511,:2567,:2640) | ✅ (số thật = 7) | + +## 5. LỆNH ĐÃ CHẠY + +- `curl -X POST https://api.solutions.com.vn/api/auth/login` (admin, field `accessToken`, len 468) → 3 GET: `/api/contracts`, `/api/contracts/deleted`, `/api/contracts?type=1&phase=9` (HTTP 200, total 0). +- `grep -nE "\[Http(Post|Put|Delete|Patch)" ContractsController.cs` → 22 dòng. +- `grep -n "Authorize" ContractsController.cs` → 3 dòng (:13, :27 comment, :29). +- `grep -rn "new ContractListItemDto\(" src/Backend` → 3 site. +- `wc -l` toàn bộ spec run-dir + spec cũ S157 (496 dòng). +- `grep -rn "WorkflowAppCodeSequence" src/Backend` → có thật. +- `grep -n "public const string" AppRoles.cs` → 13 vai. diff --git a/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-chot-cuoi-s160.md b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-chot-cuoi-s160.md new file mode 100644 index 0000000..3a08cbd --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-chot-cuoi-s160.md @@ -0,0 +1,41 @@ +# sub-reviewer-chot-cuoi-s160 — CỔNG CHỐT CUỐI /fable-real + +> Vai: reviewer adversarial · Engine-label "real-fable-S160" (em-main stamp @P3) +> Nhiệm vụ: xác minh 13 fix clone-review ĐÃ LAND + ĐÚNG, không đẻ lỗi mới, HIGH đã đóng thật. +> Giả định lead sửa ẩu — tìm ra. + +## VERDICT-pending + +(chưa chốt — append trong lúc làm) + +## KIỂM-13-FIX + +Checklist rút từ `review-synthesis-clone-s160.md:56` (13 spec-edit) + NHÓM-2 table + 2 owner-decision (O-A W8 · tên-test khuôn-repo). Sẽ chấm từng dòng {landed-đúng / thiếu / sai}: + +1. plan: W8→O-A (§2/§5#11) — synthesis khớp plan `:27` `:78` ✅ ĐÃ THẤY plan-cha (đọc lần 1) +2. plan: dựng workflow 3-trạm F-02 (§2-cạnh + §4.2-bis) — ✅ THẤY plan `:42` `:57` +3. plan: grant upgrade #12 (§5) — ✅ THẤY plan `:79` +4. plan: disclose §4.7 hố authz O-A — ✅ THẤY plan `:62` +5. plan: baseline+tên-test §7 — ✅ THẤY plan `:88` +6. plan: cạnh W7⊃W6 — ✅ THẤY plan `:37-38` `:42` +7. W1: grant UPGRADE-if-exists + đo CỜ + ẩn 2 checkbox type=10 + Designer site :144-148 — PENDING đọc spec-w1 +8. W2: tên test + grant-ăn — PENDING +9. W3: 2-write-path finalize F-B1 + changelog KHKK-riêng F-B2 + bỏ port F-B3 + khuôn-trỏ F-B4 + tên — PENDING +10. W5: acceptance tới Cấp-1 F-B5 + forge-negative F-B6 + tên — PENDING +11. W6: view per-HĐ 3-site F-S3/F-C2 + LIST test F-01 + role-pin ÂM F-S4 + kiểm-xóa-mềm + tên — PENDING +12. W7: param type F-C1 + badge InitialSigned F-C6 + cover purpose F-09 + O-A authz + tên + vai-thật-verify — PENDING +13. W8: rewrite biên bản O-A 0-code + disclose 3 rủi ro — PENDING + +Trục-2 code-anchor cần chấm đĩa: DbInitializer:2367-2374 · CWS:190-193 · ContractChangelogConfiguration:22-26 · ContractAttachmentFeatures:56-152 · ApprovalWorkflowsV2Page:1267-1268 · ContractFeatures:334-350 + :301/:381/:486 · ContractsController:16-24 · DbInitializer:239-265. + +## LỖI-MỚI + +(appending...) + +## LEFTOVER + +(appending...) + +## END + +(pending) 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 index 3e31151..8614037 100644 --- 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 @@ -1,12 +1,210 @@ # 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`. +**VERDICT: SUA-TRUOC-THI-CONG** — 3 HIGH (F-S1/F-S2/F-S3) phải sửa spec trước khi hmw chạy W6/W8; 6 MED; 5 LOW; 1 PASS-dương. +Đích: `spec-wave-w6-duong-ong-hd-v2-29-07-2026.md` + `spec-wave-w8-authz-29-07-2026.md` + `plan-cha §4`. +Mọi finding = {spec-file:dòng} + {bằng chứng ĐĨA file:line}. -## 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 +--- +## 0. Anchor audit — spec nói vs đĩa (đo 2026-07-29, git HEAD `febe6b1`) + +| Anchor spec | Đĩa | Verdict | +|---|---|---| +| `ContractFeatures.cs:334-351` GetEligiblePhases | `:334 internal static List GetEligiblePhases(IReadOnlyList userRoles)` → body tới `:351` | ✅ ĐÚNG | +| `ContractFeatures.cs:421-430` PhaseActorRoles | `:421 private static readonly Dictionary PhaseActorRoles` (7 entry, hết ở `:429`) | ✅ ĐÚNG | +| `ContractFeatures.cs:488-489` Forbidden | `:489 throw new ForbiddenException("Bạn không có quyền xem HĐ này.")` | ✅ ĐÚNG | +| `ContractFeatures.cs:298` admin bypass | `:298 if (!currentUser.Roles.Contains(AppRoles.Admin))` | ✅ ĐÚNG | +| `ContractFeatures.cs:113-118` gen mã create-time | `:118 entity.MaHopDong = await codeGenerator.GenerateAsync(...)` | ✅ ĐÚNG | +| `ContractFeatures.cs:632-633` delete guard | `:632 if (entity.Phase >= ContractPhase.DangInKy)` | ✅ ĐÚNG | +| `ContractWorkflowService.cs:49-66` Reject-trước-guard | `:49 if (decision == ApprovalDecision.Reject)` … `:65 return; :66 }` — 0 role-check, 0 phase-check | ✅ ĐÚNG (lỗ THẬT) | +| `ContractWorkflowService.cs:73-78` guard trình | `:73-75 if (!isAdmin && !isSystem && !actorRoles.Contains(Drafter) && !actorRoles.Contains(DeptManager))` | ✅ ĐÚNG | +| `ContractWorkflowService.cs:191-198` admin override | `:191 if (isAdmin) { contract.Phase = targetPhase; … }` | ✅ ĐÚNG | +| `ContractsController.cs:13` class-trần | `:13 [Authorize]` (trần, 0 policy) | ✅ ĐÚNG | +| `ContractsController.cs:28-29` tiền lệ | `:28 [HttpGet("deleted")] :29 [Authorize(Policy="Contracts.Read")]` | ✅ ĐÚNG | +| `ContractsController.cs:38-40` inbox | `:38 [HttpGet("inbox")]` | ✅ ĐÚNG | +| `SuppliersController.cs:67` khuôn Master | `:67 [Authorize(Policy="Suppliers.Update")]` | ✅ ĐÚNG | +| `MenuKeys.cs:41` KeHoachKyKet | `:41 public const string KeHoachKyKet = "KeHoachKyKet";` + `:38-40` comment "CỐ Ý NGOÀI All" | ✅ ĐÚNG | +| `MenuKeys.cs:168` Contracts ∈ All | `:163 All = [` … `:168 Contracts, Forms, Reports,` | ✅ ĐÚNG | +| `AuthorizePolicyRegressionTests` "ĐÃ TỒN TẠI" | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs` | ✅ ĐÚNG | +| plan `scripts/backup-sql.ps1` | `scripts/backup-sql.ps1` có mặt | ✅ ĐÚNG | +| W8 "22 endpoint ghi" | đếm tay `ContractsController.cs`: POST 11 (`:46,61,68,85,123,130,137,144,151,158,165`) + PUT 8 (`:53,179,186,193,200,207,214,221`) + DELETE 3 (`:75,110,172`) = **22** | ✅ ĐÚNG (F-S14 dương) | + +⇒ **0 anchor sai**. Lỗi của bộ spec này KHÔNG nằm ở anchor — nằm ở **anchor THIẾU** + **ngữ nghĩa seeder** + **hố chưa khai**. + +--- + +## F-S1 [HIGH] W8 §2 bước 3 "seeder grant" — RẤT DỄ THÀNH NO-OP IM LẶNG trên prod ⇒ 403 giết chính dry-run + +**Spec:** `spec-wave-w8-authz-29-07-2026.md:19` — "Seeder grant (gotcha #84 — CÙNG COMMIT): cấp `Contracts` Create/Update cho role tham gia dry-run… Grep MỌI site seed-permission (S155 >1 site)". + +**Đĩa:** hàng `Permission` cho key `Contracts` **ĐÃ TỒN TẠI SẴN cho mọi role** — `DbInitializer.cs:2156-2170 ContractMenuKeys()` yield `MenuKeys.Contracts` + `Ct_*` vào `reviewKeys` của `SeedAllRolesReviewReadPermissionsAsync` (`:2136`), và nhánh tạo mới ở `:2247-2254` set `CanRead=true, CanCreate=isPe(=false), CanUpdate=false, CanDelete=false`. + +🔴 Chỗ chết: `DbInitializer.cs:2231-2245` — với key **non-Pe** (tức `Contracts`), nhánh "row đã có" là: +``` +// Key non-Pe: skip-existing (giữ nguyên như cũ). +continue; +``` +Nghĩa là: nếu implementer cắm grant Create/Update vào chính `ContractMenuKeys()`/`SeedAllRolesReviewReadPermissionsAsync` (nơi tự nhiên nhất vì key `Contracts` đang sống ở đó), thì **trên prod nó chạy vào `continue` và KHÔNG nâng gì cả** — row `Contracts` đã có từ đợt S159 với `CanCreate=false/CanUpdate=false`. Build xanh, test xanh, log seeder im, deploy xong → **mọi POST/PUT bị 403 im lặng** (đúng gotcha #44 mà spec đang định tránh). + +Khuôn ĐÚNG có sẵn trong cùng file: `SeedProcurementMasterAccessAsync` `:2367-2374` (`if (g.R && !row.CanRead) {…} if (g.C && !row.CanCreate) {…}` = **upgrade-if-exists**), chạy SAU revoke để thắng (`:2098-2101` comment). + +**Spec không hề nói** row đã tồn tại, không nói phải dùng ngữ nghĩa UPGRADE, không trỏ method-khuôn. "Grep MỌI site" là lời dặn tìm site, không phải lời dặn về **ngữ nghĩa ghi**. + +**Acceptance cần thêm (mô tả, không phải code):** spec phải (a) khai rằng row `Contracts` per-role ĐÃ tồn tại với 3 cờ false, (b) pin ngữ nghĩa **upgrade-if-exists** + trỏ khuôn `SeedProcurementMasterAccessAsync:2367-2374`, (c) acceptance đo **CanCreate/CanUpdate** chứ không đo "có row" (đếm row = 0-bit vì row đã có sẵn). + +--- + +## F-S2 [HIGH] Attachment 3 handler = KHÔNG có authz nào ngoài `[Authorize]` trần — W8 CỐ Ý để hở, plan §4 KHÔNG khai + +**Spec:** `spec-wave-w8-authz-29-07-2026.md:18` — "GET giữ class-trần (đọc mở như hiện trạng, trừ `/deleted`)". `plan-cha §4` (dòng 51-57) không có mục nào về attachment. + +**Đĩa `ContractAttachmentFeatures.cs`:** +- `:56-90 UploadContractAttachmentCommandHandler` — ctor chỉ `(IApplicationDbContext, IFileStorage, IChangelogService)`; **0 `ICurrentUser`, 0 check Drafter, 0 check role, 0 check Phase**. Load HĐ bằng Id rồi ghi thẳng (`:63-64` → `:84-85`). +- `:111-127 DownloadContractAttachmentQueryHandler` — **0 IDOR guard**. Bất kỳ user đăng nhập nào biết `{contractId}/{attId}` → tải được **bản scan ký/đóng dấu** của HĐ bất kỳ. Đây chính là vật GĐ4 (`spec-wave-w7` bản cứng) sẽ upload. +- `:130-152 DeleteContractAttachmentCommandHandler` — **0 IDOR, 0 phase-guard**; xoá được attachment của HĐ `DaPhatHanh` (file storage xoá best-effort `:150-151`). + +Đối chiếu: `GetContractQueryHandler` CÓ IDOR guard (`ContractFeatures.cs:482-489`) — tức lá chắn tồn tại ở endpoint HĐ nhưng **không lan sang endpoint con attachment** (`ContractsController.cs:85, :103, :110`). + +**Hệ quả cho dry-run:** dữ liệu thật đưa vào ở GĐ4 là **scan hợp đồng đã ký + đóng dấu**. Quyết định "GET giữ class-trần" của W8 giữ nguyên lỗ đọc này. Sau O-B, `DELETE {id}/attachments/{attId}` rơi vào `Contracts.Delete` → grant cho Drafter ⇒ **mọi user role Drafter xoá được scan đã ký của HĐ bất kỳ**. + +**Cần:** W8 §① phải liệt attachment như **ô đỏ thứ 3** (hoặc khai tường minh "chấp nhận có ý thức" như O-A), và `plan-cha §4` phải có 1 gạch đầu dòng data-risk cho attachment. Hiện tại là **hố CHƯA KHAI** — đúng câu hỏi (3) của lane. + +--- + +## F-S3 [HIGH] W6 gate 5-anchor THIẾU 2/3 call-site của chính hàm nó bảo sửa ⇒ vá 1 site sót site cùng lớp + +**Spec:** `spec-wave-w6:25` gate 5-anchor liệt `ContractFeatures.cs:334-351 · :421-430 · :488-489 · ContractWorkflowService.cs:73-78 · ContractsController.cs:38-40`. + +**Đĩa — `GetEligiblePhases` có ĐÚNG 3 call-site:** +- `:301` `ListContractsQueryHandler` (danh sách HĐ) — **KHÔNG trong anchor list** +- `:381` `ListDeletedContractsQuery` (màn "Đã xoá" S159-đợt5) — **KHÔNG trong anchor list** +- `:486` `GetContractQueryHandler` (detail) — có, qua `:488-489` + +**Chỗ chết cấu trúc:** chữ ký là `GetEligiblePhases(IReadOnlyList userRoles)` — **chỉ nhận role, không nhận userId, không nhận contract**. Nó **không thể** biểu diễn được điều kiện W6 yêu cầu ("user là `ApproverUserId` của Level trong workflow đã pin **của HĐ đó**"). Cách rẻ nhất mà implementer sẽ làm là *thêm `ChoDuyet` vào tập role* → khi đó `x.c.Phase == ChoDuyet` khớp ở **cả `:301` (list) lẫn `:381` (danh sách đã xoá) lẫn `:486`** ⇒ mở toang toàn bộ HĐ `ChoDuyet` cho mọi user mang role đó. + +`spec-wave-w6:17` viết "(hoặc precompute-set, xem 4)" — đặt phương án AN TOÀN làm **lựa chọn ngang hàng trong ngoặc**, không phải bắt buộc. Với đường mòn "sửa hàm 1 chỗ", default sẽ trôi về phương án hở. + +**Cần:** (a) anchor bổ sung `:298-302` + `:378-381`; (b) spec CHỐT phương án precompute-set (hoặc overload nhận `contractId`/`userId`) là BẮT BUỘC, cấm mở rộng tập role trả về từ `GetEligiblePhases`; (c) nêu rõ `:381` là màn "Đã xoá" — mở nhầm ở đây = lộ HĐ đã xoá mềm. + +--- + +## F-S4 [HIGH→MED] W6: ca ÂM chống-mở-toang KHÔNG có test tự động, và ca ÂM thủ công 0-bit vì không pin ROLE + +**Spec:** `spec-wave-w6:27` liệt 3 test: `Inbox_ApproverCapDangCho_ThayHopDongChoDuyet` · `Inbox_CapKhac_KhongThayHopDong` (ÂM, **inbox**) · `View_ApproverV2_KhongPhaiDrafter_XemDuocHopDongChoDuyet` (**DƯƠNG**, view). +⇒ **0 test ÂM cho VIEW**. Rủi ro lớn nhất (`spec-wave-w6:32` mục C-1 "đổi view-guard làm hở HĐ cho MỌI user") chỉ được canh bằng 1 dòng prod thủ công `:30`. + +**`:30` 0-bit:** "user ngoài workflow + không Drafter → `GET /{id}` vẫn 403". Guard là **role-keyed** (`ContractFeatures.cs:486` truyền `currentUser.Roles`). Nếu người kiểm chọn 1 user role `HrAdmin` (chỉ có `DangDongDau` trong `GetEligiblePhases:348`) thì 403 vẫn đúng **kể cả khi** lỗ đã mở toang cho role `CostControl`/`Procurement`. Ca ÂM **không pin role của user thử** ⇒ pass được mà không mang thông tin. + +**Cần:** thêm test ÂM cho VIEW (tên cụ thể) + ca ÂM prod phải pin **role của user thử = đúng role bị nghi mở** (hoặc quét 13/13 role). Kèm 1 phép ÂM cho `:381` (list đã xoá). + +--- + +## F-S5 [MED] W8 grant theo ROLE ⟂ W6/V2 duyệt theo USER-ID — 6/13 role ngoài danh sách ⇒ chuỗi duyệt V2 có thể chết giữa đường + +**Spec:** `spec-wave-w8:19` grant cho 7 role: Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin. `spec-wave-w8:18` map `POST {id}/transitions → Contracts.Update`. + +**Đĩa:** `ContractWorkflowService.cs:255-266 ApproveV2Async` match approver **thuần theo `ApproverUserId`**, KHÔNG hề đọc role: +``` +var allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet(); +if (!allowedUserIds.Contains(actorUserId.Value)) throw new ForbiddenException(...) +``` +⇒ Designer V2 gán được **user bất kỳ** làm Cấp duyệt. Nếu user đó mang role ∉ 7-role-list (còn lại: Finance, Accounting, Equipment, HrAdmin, AuthorizedSigner, CatalogManager…), họ **403 ngay ở tầng policy** trước khi tới `ApproveV2Async` → HĐ kẹt ở `ChoDuyet`. +Ngoài ra nhánh legacy: 2 transition CUỐI đời HĐ V1 thuộc `AuthorizedSigner` (`DangTrinhKy→DangDongDau`) và `HrAdmin` (`DangDongDau→DaPhatHanh`) — `ContractFeatures.cs:426-427` (`PhaseActorRoles`) — **cả hai đều ngoài grant-list**. + +**Giảm nhẹ (đo được):** workflow V2 seed sẵn `QT-HD-V2-001` chỉ có **1 approver = `binh.le@solutions.com.vn` (Lê Văn Bình, CCM)** — `DbInitializer.cs:230-233, :256-262`. CCM ↔ role `CostControl` **CÓ** trong grant-list ⇒ đường mặc định sống. Rủi ro chỉ bật khi dry-run tạo workflow mới qua Designer (W5 pin V2 cho phép chọn). + +**Cần:** spec khai luật "grant phải phủ **role của mọi user được gán làm Level** trong workflow được pin", và acceptance thêm 1 bước đo: liệt approver của workflow sẽ dùng → đối chiếu role vs grant-list TRƯỚC deploy. + +--- + +## F-S6 [MED] W8 map "POST → Contracts.Create" nuốt cả comment / attachment / details ⇒ chặn nhầm người có Update mà không có Create; và test tính-chất KHÔNG bắt được map sai + +**Spec:** `spec-wave-w8:18` — "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." + +**Đĩa:** trong 11 POST có `{id}/comments` (`ContractsController.cs:68`), `{id}/attachments` (`:85`), và 7 `{id}/details/*` (`:123,130,137,144,151,158,165`). Về ngữ nghĩa đây là **sửa HĐ đang có**, không phải tạo HĐ. Map thành `Contracts.Create` ⇒ role được cấp Update-only (ví dụ approver chỉ cần góp ý) **không comment được, không upload được**. + +**Chỗ chết của acceptance:** `spec-wave-w8:28` test `MoiEndpointGhi_CoAuthorizePolicy` chỉ khẳng định "có `[Authorize(Policy=...)]`" — **map SAI vẫn PASS**. Test tính-chất đúng hướng (tránh Goodhart số 22) nhưng **mù về nội dung map**. + +**Cần:** spec chốt bảng map per-endpoint (ít nhất tách `comments`/`attachments`/`details` sang `Contracts.Update`), và acceptance thêm 1 assert đối chiếu map mong đợi cho ≥3 endpoint đại diện. + +--- + +## F-S7 [MED] `DeleteContractCommandHandler` KHÔNG có check chủ sở hữu — plan §4 chỉ khai chiều "khó xoá", bỏ chiều "ai cũng xoá được" + +**Đĩa `ContractFeatures.cs:625-639`:** handler nhận `(IApplicationDbContext db)` — **0 `ICurrentUser`, 0 check `DrafterUserId`, 0 check role**. Chỉ có guard phase `:632`. +**Plan:** `plan-cha:55` (§4 mục 4) chỉ nói "HĐ ≥ phase 5 KHÔNG xóa được qua API" — khai đúng nhưng **chỉ chiều bất tiện rollback**; chiều nguy hiểm (bất kỳ ai có quyền Delete xoá HĐ nháp của **người khác**) không được khai. Hiện trạng: 13/13 role gọi được (class-trần `:13`). Sau O-B: mọi user role Drafter. + +**Cần:** khai vào §4 (hoặc W8 §C) + cân nhắc ràng "chỉ Drafter của HĐ hoặc Admin". + +--- + +## F-S8 [MED] W8 acceptance nói verify grant bằng `sqlcmd` RDP — MÂU THUẪN plan cha §7 ("sqlcmd chỉ để local/Dev") + +`spec-wave-w8:32`: "grant seeder ăn (đếm Permission rows key `Contracts` per-role qua log seeder/**`sqlcmd` RDP**)". +`plan-cha:84` (§7): "data prod verify **bằng curl API** (SSH-SQL client chết S134/S148; **sqlcmd chỉ để local/Dev**)". +⇒ Acceptance của W8 chỉ định một phương tiện mà plan cha vừa tuyên là không dùng được ở prod. Người thi công sẽ hoặc bỏ qua bước này, hoặc tự chế cách khác ⇒ nghiệm thu trôi. +Ghi chú kỹ thuật: "đếm row" cũng sai trục (xem F-S1) — phải đo **cờ CanCreate/CanUpdate**, và có đường curl hợp lệ: `GET /api/permissions` (role-matrix) hoặc `GET /api/menus/me` bằng chính user dry-run. + +--- + +## F-S9 [MED] W6 nới guard trình: trộn 1 vế PHẠM-VI-HẸP với 1 vế PHẠM-VI-RỘNG trong cùng một dòng, và tên field không khớp đĩa + +`spec-wave-w6:19`: nới thành `Drafter ∨ DeptManager ∨ CreatedBy==actor ∨ Procurement`. +- `CreatedBy==actor` = hẹp, đóng đúng triệu chứng "PMH tự trình phiếu của mình". +- `Procurement` = **rộng theo role**: mọi user Cung ứng trình được HĐ nháp của **người khác**. Không có ca ÂM nào trong `spec-wave-w6:26-31` canh vế này. +- Tên field: đĩa dùng `Contract.DrafterUserId` cho mọi guard chủ-sở-hữu (`ContractFeatures.cs:302`, `:485`); `CreatedBy` là field của `BaseEntity` (audit). Spec ghi `CreatedBy` ⇒ implementer có thể so nhầm field, lệch với guard IDOR đang chạy. + +**Cần:** tách 2 vế, nêu vế nào là bắt buộc; sửa tên field về `DrafterUserId` (hoặc khai rõ vì sao dùng `CreatedBy`); thêm ca ÂM nếu giữ vế `Procurement`. + +--- + +## F-S10 [MED-LOW] `FormsController` class-trần, 0 policy per-action — hố chưa khai (đúng giả thuyết lane) + +`src/Backend/SolutionErp.Api/Controllers/FormsController.cs:11` = `[Authorize]`; grep `Authorize` trong file trả **đúng 1 dòng** ⇒ không có `[Authorize(Policy=...)]` nào. `MenuKeys.Forms` ∈ All (`MenuKeys.cs:168`) nên policy `Forms.*` có sẵn — tức đây là lỗ **có sẵn thuốc mà chưa uống**. Ngoài phạm vi W8 (W8 chỉ `ContractsController`), và **không xuất hiện** trong `plan-cha §4`. Dry-run có đụng export/template HĐ ⇒ nên ít nhất khai. + +--- + +## F-S11 [LOW] plan §4 mục 4: so sánh SỐ chặn nhiều phase hơn plan mô tả + +`ContractFeatures.cs:632` `entity.Phase >= ContractPhase.DangInKy` với enum (`ContractPhase.cs:17-27`): `DangInKy=5`, `DaPhatHanh=9`, **`ChoDuyet=10`**, **`TraLai=98`**, **`TuChoi=99`**. +⇒ HĐ đang `ChoDuyet`, bị **Trả lại**, hoặc bị **Từ chối** đều KHÔNG xoá được. `plan-cha:55` chỉ nói tới "HĐ test DaPhatHanh nằm lại". Dry-run chắc chắn sinh HĐ TraLai/TuChoi (3 trạm) ⇒ rác nhiều hơn plan liệu trước; đường thoát vẫn là admin-override `:191-198` nhưng phải khai. + +--- + +## F-S12 [LOW] Q11 "2 đường song song" = cổng KHKK bị né ĐƯỢC, và dry-run KHÔNG chứng minh được cổng giữ + +`plan-cha:65` (§5 dòng 3): Q11 default "**cảnh báo mềm** — 2 đường song song (PE→HĐ tắt vẫn sống)". +Đây là quyết định nghiệp vụ hợp lệ và **đảo được**, nhưng hệ quả kiểm-soát chưa khai: trong suốt dry-run, `POST /api/contracts` (`ContractsController.cs:46`) tạo HĐ **không cần** phiếu KHKK đã duyệt ⇒ giá chốt của W3 finalize có thể bị bỏ qua. Nghĩa là **vòng dry-run không sinh được bằng chứng "cổng 3 trạm giữ"** — nó chỉ chứng minh đường thuận chạy. Nên khai ở §4 (data-risk) hoặc ghi vào acceptance E2E rằng ca ÂM "tạo HĐ không có KHKK" là ca **cố ý để mở**. + +--- + +## F-S13 [PASS + INFO] Mật khẩu trong artifact + +- **Trong phạm vi lane:** `grep -riE "Admin@1234|TestUser@|password|mật khẩu|matkhau"` trên **toàn bộ** `runs/2026-07-29-S160-khkk-dryrun-plan/` (12 file) và `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/` → **0 hit**. ✅ Plan cha + 7 spec-wave + 2 sub-investigator + run.md **KHÔNG lộ mật khẩu**. +- **INFO ngoài phạm vi (không phải lỗi của bộ spec này):** `git grep -lI "Admin@123456"` = **25 file tracked**, gồm `README.md`, `docs/STATUS.md`, `docs/CLAUDE.md`, `.claude/agents/reviewer.md`, `.claude/agents/cicd-monitor.md`, `.claude/skills/iis-deploy-runbook/SKILL.md`, 5 file `.claude/agent-memory/*/MEMORY.md`. Mật khẩu admin prod nằm trong repo tracked từ lâu — quyết định của owner, chỉ nêu để không bị "vắng mặt trông giống sạch". + +--- + +## F-S14 [PASS dương] Những chỗ chịu được soi + +- "22 endpoint ghi" — đếm tay khớp 100% (11 POST + 8 PUT + 3 DELETE). +- W8 §① nói đúng vai của từng option: O-B *thu hẹp ai bấm*, O-C mới *đóng* ca Reject-sau-terminal (`:7`, `:12`). Không có claim "O-B đóng 2 ô đỏ" ⇒ **không phải lỗi**. Cảnh báo còn lại: sau O-B, 7 role được grant **vẫn** Reject/TraLai được HĐ ở mọi phase kể cả `DaPhatHanh` (`ContractWorkflowService.cs:49-66`) — spec `:12` có nói, plan `:73` có nói "O-C cần anh gật riêng" ⇒ **default KHÔNG lấn quyền owner** ở điểm này. +- Rủi ro "grant mới lật row false cố ý S92" (`spec-wave-w8:33`) — đo đĩa: revoker `RevokeTemporarilyHiddenModulesAsync` (`DbInitializer.cs:2278`, gọi ở `:2096`) nay **chỉ còn** `Hrm*`/`Off*`/`Personal` (`:2291-2303`, nhánh S92 cho Contracts/Master đã GỠ @S159). ⇒ grant `Contracts` **không đụng** vùng revoker; control-ÂM mà spec đề (`:32`) là đúng thứ cần đo. **Không phải lỗ.** +- W6 rủi ro "inbox V2 quên lọc IsDeleted" (`:32`) — `db.Contracts.AsNoTracking()` ở inbox (`ContractFeatures.cs:447`) vẫn ăn global query filter; chỉ vỡ nếu ai đó thêm `IgnoreQueryFilters` ⇒ giữ dòng cảnh báo là hợp lý, không thừa. + +--- + +## F-S15 [LOW] Nhãn đo trong `plan-cha §3` — "curl admin 6 phase" dưới-phủ enum 12 giá trị + +`plan-cha:43`: "Prod: Contracts = 0 active + 0 deleted (curl admin **6 phase** + tổng + /deleted)". `ContractPhase` có 12 giá trị (`ContractPhase.cs:17-27`: 2,3,4,5,6,7,8,9,10,98,99 + DangChon). Phép "6 phase" **không phủ** enum; vật chịu lực thật là **"tổng"** (list không filter, admin bypass `ContractFeatures.cs:298` ⇒ trả mọi phase) và `/deleted`. Kết luận "0 active" vẫn ĐÚNG nhờ "tổng", nhưng câu chữ khiến người sau tưởng phép đo là enumeration-per-phase. Nên viết lại thành "tổng không-filter (phủ mọi phase) + /deleted; 6 phase chỉ là cross-check". + +--- + +## 3. Trả lời trực tiếp 4 câu của lane + +1. **W6 có làm hở HĐ cho user ngoài workflow không?** — **CÓ RỦI RO CAO như spec đang viết** (F-S3): hàm bị sửa là role-only, 2/3 call-site không nằm trong gate, và phương án an toàn để trong ngoặc. Ca ÂM trong spec **chưa đủ** (F-S4: 0 test ÂM cho view; ca ÂM prod không pin role). **PE data thật lộ thêm gì?** — W6 tự nó không chạm PE. Đường lộ gián tiếp là qua **attachment** (F-S2): hồ sơ NCC/scan đính vào HĐ sinh từ KHKK tải được bởi mọi user đăng nhập, không cần thuộc workflow. +2. **O-B đóng đúng 2 ô đỏ không?** — **Không, và spec KHÔNG nói sai** (F-S14): `spec-wave-w8:7,:12` nói rõ O-B chỉ thu hẹp, O-C mới đóng Reject; plan `:73`/`:90` giữ O-C cho anh gật. **Seeder-grant có lật row false S92 không?** — **Không** (revoker sau S159 chỉ còn Hrm/Off/Personal). Nhưng seeder-grant có **lỗi ngược lại nguy hiểm hơn**: no-op im lặng (F-S1). **"Policy chặn theo menu-permission, user được cấp vẫn đụng HĐ THẬT" — spec khai đủ chưa?** — `spec-wave-w8:11` CÓ khai ("⚠️ thu hẹp AI, không thu hẹp BẤM VÀO ĐÂU"). Nhưng **thiếu** hệ quả cụ thể: 7 role được cấp vẫn Reject/xoá/xoá-attachment HĐ của người khác (F-S2/F-S7). +3. **plan-cha §4 còn hố nào chưa khai?** — **4 hố**: attachment upload/download/delete 0-authz 0-phase-guard (F-S2) · delete-HĐ 0 owner-check (F-S7) · `FormsController` class-trần (F-S10) · phase-guard chặn cả TraLai/TuChoi/ChoDuyet chứ không chỉ DaPhatHanh (F-S11). Cộng 1 hố "mềm": Q11 2-đường-song-song khiến dry-run không chứng minh được cổng (F-S12). +4. **Mật khẩu trong artifact?** — **0 hit** trong toàn bộ plan + 7 spec + sub-file của cả 2 run-folder ✅. Ghi nhận INFO: `Admin@123456` tồn tại ở 25 file tracked khác của repo (F-S13). + + 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 index f883dc0..71fa7cf 100644 --- 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 @@ -1,41 +1,175 @@ # 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. +> Đích soi sâu: `plan-cha-dry-run-toan-trinh-29-07-2026.md` + `spec-wave-w1-schema-designer` + `spec-wave-w3-duyet-3-tram`; liếc đối chiếu đồ thị: w2/w5/w6/w7/w8 + `sub-investigator-codebase-1.md` §F-10. +> NOTE tên file: TASK lượt-1 chỉ định tên này; 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)_ +**VERDICT (dòng-1): SUA-TRUOC-THI-CONG** — 4 HIGH (2 chặn E2E im lặng: W1 grant no-op trên prod · W7 danh sách GĐ4 rỗng với đúng vai; 2 rơi bước: workflow HĐ 3 trạm không ai dựng · b.14-17 rơi khỏi bảng default). W2/W3/W5/W6 = GO-WITH-FIXES. Bộ plan **grounding tốt bất thường**: 26/28 neo tôi chấm lại trên đĩa là ĐÚNG. --- ## 0. Nhật ký đo (ghi TRONG lúc làm — chống #53) - [t1] scaffold file này. +- [t2] Đọc: plan-cha (92 dòng) · w1 (39) · w3 (37) · w5 (30) · w6 (32) · w2 (35) · w7 (35) · w8 (33) · sub-investigator-codebase-1 (215). +- [t3] Grep `ApproveV2Async` toàn `src/Backend` ⇒ **CÓ** `ContractWorkflowService.cs:217`. Nghi-vấn "W3 copy sai file" (CLAUDE.md §S158 ghi ApproveV2Async ở `ContractFeatures.cs`): **BÁC BỎ** — CLAUDE.md mới là chỗ mô-tả lệch, spec đúng. +- [t4-t8] Chấm 28 neo trên đĩa (bảng §6). Bắt 4 HIGH + 5 MED + 5 LOW. +- [t9] Viết file này TRƯỚC khi return. -## 1. Đồ thị phụ thuộc (plan-cha §2) +--- -_(đang đo)_ +## 1. Đồ thị phụ thuộc (plan-cha §2 `:30-39`) -## 2. Deploy-độc-lập per wave +**Cạnh ĐÚNG (verify được):** +- `W1→W2→W3` — W2 §②-2 sinh `MaKeHoach` bằng CodeGen của W1, W3 §③-A neo bảng `Approvals`/`Changelogs` của W1 ⇒ thứ tự bắt buộc, đúng. +- `W3→W5` — W5 §②-2a đọc `plan Phase==DaDuyet`; trạng thái đó chỉ tồn tại sau W3. Đúng. +- **`W6 trước W5` (về VẬN HÀNH) = ĐÚNG và có căn cứ đĩa**: không có W6 thì HĐ pin V2 vào `ChoDuyet=10` là *vô hình* — `GetEligiblePhases` (`ContractFeatures.cs:334-351`) **0 entry cho ChoDuyet lẫn DaPhatHanh**, `PhaseActorRoles` (`:421-430`) 100% phase legacy. Đồng thời **W5 KHÔNG bị W6 chặn về mặt acceptance**: người trình là Drafter tự tạo ⇒ qua guard `:73-78`, và `GET /{id}` qua được nhờ nhánh `isDrafter` (`:485-489`). Tức plan gọi đây là ràng buộc *vận hành* chứ không phải *biên dịch* — mô tả CHÍNH XÁC. +- `W7 build song song, VERIFY sau W5+W6` — đúng về nguyên tắc, nhưng thiếu 2 cạnh (dưới). -_(đang đo)_ +**CẠNH THIẾU (F-01, F-02, F-03 — chi tiết §7):** +1. **`W7 → (view-guard cho vai KHÔNG-approver)`**: W7 liệt HĐ `phase=9`. Bộ lọc IDOR ở `ContractFeatures.cs:302` = `DrafterUserId==userId || eligiblePhases.Contains(Phase)`; `GetEligiblePhases` **không có DaPhatHanh(9)** ⇒ HRA (đóng dấu b.20) và PB/BCH (phát hành b.21) thấy **danh sách rỗng**, `GET /{id}` 403. W6 chỉ mở cho **approver V2 của chính HĐ đó** (W6 §②-3) — HRA/BCH thường KHÔNG nằm trong workflow ⇒ không wave nào cấp. Cạnh này KHÔNG có trong đồ thị. +2. **`W5/W6 → (dựng ApprovalWorkflow V2 type=3 ba trạm)`**: E2E `:39` hứa "3 trạm HĐ". Đĩa chỉ có seed `QT-HD-V2-001` = **1 Bước × 1 Cấp** (`DbInitializer.cs:239-265`, approver duy nhất `binh.le@` CCM), và seeder tự tắt nếu đã tồn tại workflow type-3 (`:226-228`). Không wave nào, không mục nào của §4 "chiến lược data" giao việc dựng workflow HĐ 3 trạm PMH/PRO→CCM→CEO. Đây là cạnh chuẩn-bị-dữ-liệu bắt buộc. +3. **`W8 → W7` (thứ tự nguy hiểm)**: W8 gắn `Contracts.Update` cho **mọi** HttpPost/Put/Delete của `ContractsController` — bao gồm `POST {id}/attachments` mà W7 dùng để upload 3 purpose. Danh sách role được grant của W8 (§②-3) = Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin — **KHÔNG có HrAdmin (HRA)**, chính là người làm b.20 đóng dấu (F-10 `:123`). W8 chạy trước W7-UAT ⇒ HRA 403 khi upload. -## 3. E2E dry-run tới GĐ4 — phủ b.13→21 +**Thứ tự đề nghị (giữ tinh thần plan, thêm 2 nút):** `W1 → W2 → W3` ∥ `W6` → **[dựng WF V2 type=3 3 trạm]** → `W5` → `W7(+view-guard vai cứng)` → `W8(+HrAdmin vào grant)`. -_(đang đo)_ +--- + +## 2. Mỗi wave có THẬT deploy-độc-lập không? ("người dùng thấy gì" đứng vững?) + +| Wave | Người dùng thấy gì khi đẩy riêng | Đứng vững? | +|---|---|---| +| W1 | 0 thay đổi cho user thường (menu giữ, leaf vẫn `/coming-soon`); **admin** dựng được quy trình type-10 | ✅ đứng — nhưng xem F-04 (grant có thể no-op ⇒ "thấy" mà không "gọi" được ở W2) | +| W2 | 6 leaf `Khkk_*` hết ComingSoon → trang thật, tạo/sửa/xoá phiếu nháp | ✅ đứng | +| W3 | Phiếu trình được, 3 trạm duyệt, chốt giá | ✅ đứng (phụ thuộc W2, khai rõ) | +| W5 | Nút "Tạo Hợp đồng" trên phiếu KHKK DaDuyet | ✅ đứng | +| W6 | Approver V2 **thấy + mở + duyệt** HĐ đang chờ mình | ✅ đứng — và tôi xác nhận đường tạo HĐ-tay để test W6 trước W5 là KHẢ THI thật: `ContractCreatePage.tsx:340` lấy bucket `applicableType===3`, `ContractFeatures.cs:109` nhận `ApprovalWorkflowId` ⇒ pin V2 được từ UI. (Điểm cộng cho spec.) | +| W7 | 7 leaf `Hdc_*` thành trang theo dõi bản cứng | ⚠️ **KHÔNG đứng vững cho đúng vai** — F-01 (list rỗng/403 với HRA, BCH, CCM) + F-05 (filter `type` không tồn tại ⇒ 7 leaf hiển thị y hệt nhau) | +| W8 | Thu hẹp ai bấm được | ✅ đứng (acceptance prod 201/403 là phép đo thật) — trừ F-03 (thiếu HrAdmin) | + +--- + +## 3. E2E tới GĐ4 có ĐỦ qua W1+W2+W3+W6+W5+W7? — đối chiếu bảng b.13→21 (`sub-investigator-codebase-1.md:114-124`) + +| b | Việc (QT) | Wave phủ | Verdict | +|---|---|---|---| +| 13 | Lựa chọn NTP/NCC | GĐ1 PE + W2 (winners→Lines) | phủ gián tiếp; plan không khai ánh xạ — LOW | +| 14 | Soạn thảo HĐ (7 ngày) | W5 tạo HĐ nháp; **soạn nội dung** = `PUT /contracts/{id}` sẵn có | ⚠️ **RƠI khỏi bảng default** (F-06) | +| 15 | Góp ý 6 phòng (7 ngày) | không wave nào; F-19 `:202` đề "comment thread" | ⚠️ RƠI (F-06) | +| 16 | Đàm phán (7 ngày) | không có máy | ⚠️ RƠI (F-06) | +| 17 | In HĐ + **số HĐ** + ký nháy + cover khối ĐỀ XUẤT | số HĐ: default #10 ✓ (verify `ContractFeatures.cs:113-118` gen tại create — ĐÚNG); **ký nháy + cover: chỉ được HIỂN THỊ ở W7, KHÔNG có đường UPLOAD nào trong GĐ3** | ⚠️ RƠI một nửa (F-07) | +| 18 | CCM kiểm tra + ký nháy + cover khối CCM | trạm V2 — nhưng workflow 3 trạm chưa ai dựng (F-02) | ⚠️ phụ thuộc F-02 | +| 19 | BOD/NĐUQ ký duyệt | W7 (upload `ScannedSigned`) | ✅ — nhưng TRÙNG trạm CEO của V2 (F-08 semantics) | +| 20 | Đóng dấu HRA/ISO | W7 khai **gộp vào b.21** (`spec-w7:13`) | ✅-with-note (khai thật, chấp nhận) — nhưng F-03 chặn HRA upload nếu W8 chạy trước | +| 21 | Phát hành + lưu (scan, 01 bản gốc CCM, Filing) | W7 (`SealedCopy` + note) | ⚠️ **scan cover FO-002.01 không có purpose** (F-09) | + +**Kết luận (3):** chuỗi wave đủ để *máy chạy* từ PE→KHKK→HĐ→bản-cứng, nhưng **4 bước nghiệp vụ (b.14/15/16 + nửa b.17) không có ô nào trong bảng default §5 và không wave nào nhận**, còn b.18-21 vướng F-02/F-03/F-09. Nói cách khác: E2E hiện *đo được* ở tầng phase-machine, KHÔNG đo được ở tầng "9 bước ISO". + +--- ## 4. Defer W4/W9 có phá E2E? -_(đang đo)_ +- **W9 defer: KHÔNG phá** ✅ (đúng như F-19 `:200`). +- **W4 defer: KHÔNG phá luồng**, nhưng nhãn default sai một nửa (F-10): plan §2 `:23` ghi W4-defer ⇒ dry-run chạy "comment tự do **+ cảnh báo mềm**, 0 dev". F-18 `:184` chỉ chứng minh **comment tự do = 0 dev**; "cảnh báo mềm khi thiếu căn cứ TvgsDuyet" (default #4, §5 `:66`) **không wave nào code** — W3 §② không có bước nào đọc `DossierItems` lúc trình. Vậy hành vi thật = **không kiểm gì cả**, không phải "cảnh báo mềm". Default đang tự khen là có lưới trong khi lưới rỗng. +- Hệ quả kèm: luật (c) FO-002.01 (cover phải lưu kèm bộ gốc) mà F-18.4 `:187` đề thành **acceptance GĐ4** — W4 defer + F-09 ⇒ không chỗ nào giữ. -## 5. Acceptance 0-bit / không đo được / literal tự-lão-hoá +--- -_(đang đo)_ +## 5. Acceptance: cái nào 0-bit / không đo được / literal tự-lão-hoá -## 6. Gate-5-anchor per-wave (đủ 5? anchor có thật?) +| # | Chỗ | Loại | Bằng chứng | +|---|---|---|---| +| A1 | W1 `:33` "`dotnet test` → `AuthorizePolicyRegressionTests` XANH (policy `KeHoachKyKet.*` hợp lệ — All đã chứa key)" | **0-BIT** | Test tại `AuthorizePolicyRegressionTests.cs:120-143` chỉ soi **`ApprovalWorkflowsV2Controller`** (+ HrmConfigs/Employees ở dưới). Ở W1 **chưa có controller nào dùng policy `KeHoachKyKet.*`** (controller ra ở W2) ⇒ test XANH **bất kể** có thêm key vào `All` hay không. Phép đo không thể trượt ⇒ 0 bit. | +| A2 | W1 `:35` "Admin tạo quy trình type-10 …; **xoá 1 Level CHƯA chữ ký** → OK" | **0-bit với rủi ro đã nêu** | Chính W1 §②-10 cảnh báo 3 site `ApprovalWorkflowV2AdminFeatures.cs` (`:911/:929`, `:971/:992`, `:1044/:1056`) — cả 3 chỉ chạy khi Level **CÓ** chữ ký. Ca duy nhất acceptance kiểm là ca KHÔNG chạm 3 site đó. Ở W1 chưa thể có chữ ký type-10 ⇒ rủi ro tự khai chỉ lộ ở W3. | +| A3 | W7 `:32` "`GET /api/contracts?type=1&phase=9` → 200, mọi item `phase==9`" | **0-BIT trên trục `type`** | `ListContractsQuery` = `(Phase, SupplierId, ProjectId)` — **KHÔNG có Type** (`ContractFeatures.cs:282-285`); controller List `:16-24` cũng không nhận `type`. Query-param lạ bị bỏ im lặng ⇒ assertion "mọi item phase==9" PASS **dù filter loại chết**. | +| A4 | W1 `:37` "sqlcmd đếm Permission rows key `KeHoachKyKet` = **số role đích × đúng cờ**" | **không đo được** | không có số kỳ vọng, không có tên cờ ⇒ mọi kết quả đều "khớp". So sánh: W8 `:30` ("user đã-cấp POST→201; chưa-cấp→403") là phép đo THẬT — cùng bộ spec mà 2 chuẩn khác nhau. | +| A5 | W1 `:32` "`sys.tables` **89 → 96**" · `:38` "menu KHÔNG đổi (**142** giữ)" · W7 `:23` "142 giữ" | literal tự-lão-hoá | plan §7 `:82` tự đặt luật "neo TÍNH-CHẤT, CẤM literal" cho test-count nhưng lại literal cho bảng/menu. Rủi ro thấp (1 mig) nhưng nên viết "= số-đo-ngay-trước + 7" / "= số-đo-ngay-trước". | +| A6 | Mã phiếu `KHKK/{YYYY}/{Seq:D3}` (default #6, W1 §②-9) | **không có acceptance nào** | W1 §③ không có dòng nào chạm CodeGen; W2 §③ chỉ "POST → 201". Một quyết-định owner-visible (format mã in trên phiếu) đi qua 2 wave mà 0 phép đo. | +| A7 | W3 `:30` "`Lines WHERE PlanId=@id AND ApprovedAmount IS NULL` = 0" | ✅ TỐT | có `AND PlanId=@id` (chống bẫy S134 đếm-toàn-bảng) — giữ nguyên. | +| A8 | W3 `:28` OR-of-N 4 vế (2 phiếu song sinh + ca ÂM + 1 row + không-đòi-cả-hai) | ✅ TỐT NHẤT bộ | đúng lưới cho rủi ro "copy nhầm `ProposalFeatures.cs:427-439` mất OR-of-N âm thầm". | -_(đang đo)_ +--- -## 7. FLAG list (F-W1..n + SEV) +## 6. Gate 5-anchor: đủ 5? có thật? (chấm lại 28 neo trên đĩa) -_(đang đo)_ +| Wave | Neo | Trên đĩa | Verdict | +|---|---|---|---| +| W1 | `ApprovalWorkflow.cs:53-67` slot 10 trống | enum dừng `TravelRequest = 9` (`:66`), `}` `:67` | ✅ ĐÚNG | +| W1 | `MenuKeys.cs:41` const + comment tái dùng | `:41 public const string KeHoachKyKet` + comment `:37-40` "Wave-1 GĐ2 … phải TÁI DÙNG" | ✅ ĐÚNG | +| W1 | `MenuKeys.cs:163+` All chưa có KeHoachKyKet | `All` `:163-181`, không có | ✅ ĐÚNG | +| W1 | `AuthorizePolicyRegressionTests.cs:138-139` | `MenuKeys.All.Should().Contain(parts[0], …)` đúng 2 dòng đó | ✅ ĐÚNG (nhưng xem A1) | +| W1 | `DbInitializer.cs:1779-1786` Khkk_* 7 key | `:1779 Khkk_G1` + `:1781-1786` 6 leaf | ✅ ĐÚNG | +| W2 | `ProposalFeatures.cs:427-439` bẫy flatten · `:258-267` guard type · `CreateContractFromEvaluationFeatures.cs:53-62` | (2 cái Proposal không chấm — ngoài ngân sách lượt; cái thứ 3 ✅ `:53-62` đúng guard+SUM) | ⚪ 1/3 chấm | +| W2 | `DbInitializer.cs:2207` "khu **staticMap** comment Khkk_*" | `:2207` là comment trong khối **grant CanRead** (`reviewKeys` `:2202-2208`), KHÔNG phải staticMap. staticMap thật ở `fe-user/src/components/Layout.tsx:59-62,79-80` | ⚠️ **NHÃN SAI** (F-11) | +| W3 | `ContractWorkflowService.cs:217-394` nguồn copy | `ApproveV2Async` `:217`, kết thúc `:394` | ✅ ĐÚNG (kể cả cận trên) | +| W3 | `:259-260` OR-of-N · `:292-316` UPSERT · (`:246` GroupBy, `:320-352` skipToFinal) | `:259 allowedUserIds` `:260 Contains` ✓ · `:292-316` UPSERT ContractLevelOpinions ✓ · `:246` GroupBy ✓ · `:320` if(skipToFinal) … `:352` ✓ | ✅ 4/4 ĐÚNG (chính xác đến dòng) | +| W3 | `ApprovalWorkflow.cs:95` class Level + `:116-125` 4 return-mode | class `:95` ✓; `:116/119/122/125` = 4 cờ ✓ (cảnh báo "class nằm TRONG ApprovalWorkflow.cs, không có file riêng" ĐÚNG) | ✅ ĐÚNG | +| W3 | `PurchaseEvaluationWorkflowService.cs:1008` khuôn RULE choke-point — spec mô tả *"comment nêu LUẬT + **lệnh grep**, CẤM số-đếm site"* | `:1008` = `// RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay TRƯỚC assignment (**reviewer grep-check 4 site**).` — **không có lệnh grep** và **CÓ số-đếm "4 site"** | ⚠️ **MÔ TẢ NGƯỢC** (F-12) | +| W5 | `:53-60` guard · `:68-71` pin V1 · `:88-90` SUM · `:145` idempotency-write · `ContractWorkflowService.cs:115-116` Conflict | 5/5 khớp từng dòng | ✅ 5/5 | +| W6 | `ContractFeatures.cs:334-351` · `:421-430` · `:488-489` · `ContractWorkflowService.cs:73-78` · `ContractsController.cs:38-40` | 5/5 khớp (inbox endpoint đúng `:38-40`) | ✅ 5/5 | +| W7 | `ContractAttachment.cs:5-11` enum · `DbInitializer.cs:1862-1867` loop 7 leaf · `Layout.tsx:63-69` 7 dòng Hdc_ · `MyContractsPage.tsx:29-46` | 4/4 khớp (Layout `:63-69` đúng CHÍNH XÁC 7 dòng) | ✅ 4/4 | +| W7 | `ContractsController.cs:16-24` "list params **type**/phase" | `:16-24` có `phase/supplierId/projectId`, **KHÔNG có `type`**; `type` chỉ ở `/deleted` `:31` | ❌ **SAI** (F-05) | +| W8 | `ContractsController.cs:13` class-trần · `:28-29` tiền lệ policy · `MenuKeys.cs:163-181` Contracts∈All (`:168`) · `ContractWorkflowService.cs:49-66` Reject | 4/4 khớp (`SuppliersController.cs:67` không chấm) | ✅ 4/4 chấm | + +**Đếm:** 7/7 wave có đủ 5 mục anchor về HÌNH THỨC. **Về THỰC CHẤT: 26 đúng / 2 sai-hoặc-nhãn-sai** (W7 `type`, W2 "staticMap") **+ 1 mô tả ngược** (W3 khuôn `:1008`). +**Khiếm khuyết cấu trúc (F-13, LOW):** W3 đếm 5 neo nhưng 3 trong số đó **lồng nhau** (`:259-260` ⊂ `:292-316` ⊂ `:217-394`) ⇒ chỉ 3 sự-kiện độc lập; W1 có 2 neo cùng file `MenuKeys.cs`; W6 3/5 cùng `ContractFeatures.cs`. "5 anchor" đang đếm KHOẢNG-DÒNG chứ không đếm mệnh-đề độc lập. + +--- + +## 7. FLAG (F-01 → F-16) + SEV + +### HIGH + +**F-04 (W1) — grant `KeHoachKyKet` 4-action nhiều khả năng NO-OP trên prod (403 im lặng, gotcha #44 + #84).** +Trên đĩa, key này **đã có row Permission cho mọi role** qua nhánh read-only: `DbInitializer.cs:2204-2206` `.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })` (comment: "CanRead-only mọi role … Prod đã INSERT trực tiếp"). Ngay dưới, `:2211-2212` cảnh báo nguyên văn: *"nhánh grant key-thường là **skip-existing** ⇒ trên prod row false **KHÔNG được nâng** ở đây"*. ⇒ W1 §②-8(a) thêm Create/Update/Delete theo đường seeder thường sẽ **bị bỏ qua vì row đã tồn tại**; W2/W3 deploy xong thì user dry-run POST → **403 im lặng**, và acceptance A4 (đếm ROW, không đếm CỜ) không bắt được. +*Acceptance đề nghị:* dùng đúng khuôn W8 `:30` — sau deploy, user role đích gọi `POST /api/contract-signing-plans` phải 201 và user không-cấp phải 403; cộng thêm phép đọc CỜ (`CanCreate/CanUpdate/CanDelete`) chứ không đếm row. + +**F-01 (W7, kèm W6) — GĐ4 không vận hành được cho đúng vai: danh sách rỗng + 403.** +`ContractFeatures.cs:302` lọc `DrafterUserId==userId || eligiblePhases.Contains(Phase)`; `GetEligiblePhases` `:342-349` **không chứa `DaPhatHanh(9)`** (cũng không chứa `ChoDuyet(10)`). ⇒ HRA (b.20), PB/BCH + CCM (b.21), BOD (b.19) nếu không phải Drafter/Admin sẽ thấy **0 dòng** ở mọi leaf `Hdc_*` và 403 khi mở chi tiết. W6 §②-3 chỉ mở cho **approver V2 của chính HĐ đó**, và W6 acceptance chỉ đo `inbox` + `GET /{id}` — **không wave nào đo LIST**. W7 §③ prod-check lại không nói ai gọi (admin gọi thì luôn xanh vì `:298`). +*Acceptance đề nghị:* thêm ca "user vai HRA/BCH KHÔNG-admin KHÔNG-drafter gọi `GET /api/contracts?phase=9` ≥1 dòng" + ca ÂM giữ 403 cho người ngoài. + +**F-02 (plan §4 + W5/W6) — không ai dựng ApprovalWorkflow V2 `type=3` ba trạm; seed chỉ có 1 trạm.** +`DbInitializer.cs:226-228` bỏ qua nếu đã có workflow type-3; `:239-265` seed `QT-HD-V2-001` = **1 Bước (CCM) × 1 Cấp**, approver duy nhất `binh.le@solutions.com.vn`. E2E `:39` hứa "3 trạm HĐ"; W6 §① hứa "3 trạm PRO→CCM→CEO". §4 "chiến lược data" chỉ liệt ZZTEST + NCC + PE — **thiếu hẳn mục dựng workflow HĐ**. Nếu chạy bằng seed: GĐ3 chỉ 1 trạm CCM, **b.19 (BOD ký) không có trạm web**, và default #1 "CEO ký thật" không thể xảy ra. +*Acceptance đề nghị:* thêm bước-0 của W5/W6 "admin tạo WF type-3 ≥3 Bước, `IsUserSelectable=true`, approver = user dry-run thật" + acceptance đếm `Steps>=3` trước khi bấm E2E. + +**F-03 (W8×W7) — grant W8 thiếu `HrAdmin` ⇒ khoá đúng người làm b.20.** +W8 §②-2 gắn policy cho MỌI HttpPost/Put/Delete của `ContractsController` (bao gồm `POST {id}/attachments`); §②-3 grant cho Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin — **không có HrAdmin**, trong khi b.20 đóng dấu là HRA/ISO (`sub-investigator-codebase-1.md:123`) và W7 yêu cầu HRA upload bằng chứng. Cùng lớp với F-04 (403 im lặng giết chính dry-run — W8 tự nêu rủi ro này ở §③-C mà lại rơi đúng vào nó). + +### MEDIUM + +**F-05 (W7) — `type` filter không tồn tại ở endpoint list ⇒ 7 leaf hiển thị TRÙNG nhau; anchor sai; acceptance 0-bit.** +`ContractsController.cs:16-24` + `ListContractsQuery` `:282-285` không có `Type`. Khuôn W7 chọn (`MyContractsPage.tsx:41-46,:60`) lách bằng **lọc client-side sau khi lấy `pageSize:100`** ⇒ nếu copy nguyên, trang bản-cứng phân trang SAI (server phân trang trước khi lọc loại) và trần 100. Cần hoặc thêm `Type` vào query BE (nhỏ, 0-mig) hoặc khai rõ lọc client + bỏ phân trang. + +**F-06 (plan §5 + W5) — b.14/15/16 rơi khỏi bảng 12 default.** +`sub-investigator-codebase-1.md:202` có default rõ ("Nháp + comment thread + update draft; muốn trạm thật thì thêm Bước trong workflow, 0 code") nhưng plan-cha KHÔNG chép vào §5 và không wave nào nhận. E2E `:39` nhảy thẳng "HĐ per-winner pin V2 → 3 trạm HĐ" ⇒ 21 ngày SLA nghiệp vụ (7+7+7) biến mất khỏi kế hoạch mà không có dòng khai. + +**F-07 (W7 §① + F-11 lane-1) — ký nháy/cover b.17-18 chỉ có đường ĐỌC, không có đường GHI.** +W7 khai "readonly từ GĐ3 … hiển thị từ attachments (`InitialSigned` mới)" nhưng chính W7 mới là chỗ thêm enum `InitialSigned`, và panel upload của W7 nằm ở trang GĐ4 (sau `DaPhatHanh`). ⇒ hoặc GĐ3 không có nút upload ký-nháy (dữ liệu luôn rỗng, khối readonly vô nghĩa), hoặc phải upload ở GĐ4 rồi gọi là "của b.17" (nghịch thời gian). Cần chốt 1 trong 2 và khai. + +**F-09 (W7) — scan cover FO-002.01 không có purpose để gắn.** +`AttachmentPurpose` = `{DraftExport=1, ScannedSigned=2, SealedCopy=3, Other=99}` (`ContractAttachment.cs:5-11`); W7 chỉ thêm `InitialSigned=4`. §① lại đòi "upload scan cover FO-002.01 (luật (c))" và §③ chỉ đo "3 purpose". ⇒ cover rơi vào `Other` hoặc rơi hẳn; luật (c) (bắt buộc lưu kèm bộ gốc) không có phép đo nào. + +**F-10 (plan §2 `:23` + §5 #4) — default "cảnh báo mềm" không có chủ.** +W4 deferred = 0 dev, W3 §② không có bước kiểm căn cứ ⇒ hành vi thật là **không kiểm gì**. Nhãn "cảnh báo mềm" đang mô tả một lưới không tồn tại (lớp Goodhart: default trông như đã che rủi ro). + +### LOW + +- **F-08 (semantics, cross-wave):** terminal V2 của HĐ = `DaPhatHanh=9` "Đã duyệt (gen mã + phát hành) — terminal thành công" (`ContractPhase.cs:24`, set tại `ContractWorkflowService.cs:379`). Nhưng "phát hành" theo QT là **b.21**, tức trạng thái hệ thống nói "đã phát hành" **trước khi** GĐ4 làm b.19/20/21. W7 §① có ý thức ("GĐ4 nằm SAU terminal") nhưng plan-cha không khai mâu thuẫn NHÃN. Rẻ: đổi nhãn hiển thị GĐ3-terminal thành "Đã duyệt HĐ" ở UI HĐ. +- **F-11 (W2 anchor):** `DbInitializer.cs:2207` bị gọi là "khu staticMap" — thực tế là khối grant CanRead; site staticMap thật (`fe-user Layout.tsx:59-62,79-80` + mirror fe-admin) **không nằm trong gate-5 của W2** dù W2 phải sửa đúng chỗ đó. +- **F-12 (W3):** spec mô tả khuôn `PurchaseEvaluationWorkflowService.cs:1008` là "LUẬT + lệnh grep, CẤM số-đếm" — đĩa cho thấy ngược: có "(reviewer grep-check **4 site**)", không có lệnh grep. Worker copy khuôn sẽ copy đúng cái con-số mà spec đang cấm. +- **F-13:** gate-5-anchor đếm khoảng-dòng lồng nhau (W3: `:259-260` ⊂ `:292-316` ⊂ `:217-394`) ⇒ "5" phóng đại độ phủ; đề nghị đếm mệnh-đề độc lập. +- **F-14 (W2 vs W5):** tên field FK lệch giữa 2 spec — W2 `p.PeId` (`spec-w2:13`) vs W5 `Plans.Where(PurchaseEvaluationId==peId)` (`spec-w5:13`); W1 §②-2 không chốt tên cột. Chốt 1 tên ở W1 để W5 không phải sửa lại. +- **F-15 (W1):** thêm `KeHoachKyKet` vào `All` **buộc** cập nhật 2 số canonical — chính mã dặn: `MenuKeys.cs:150-151` *"Thêm vào `All` sẽ đổi 2 số canonical `Menu keys`/`Policies` ở `docs/STATUS.md` (Policies = |All| × |Actions|, DERIVED)"*. Checklist W1 không có dòng cập nhật `docs/STATUS.md` (B1 drift). +- **F-16 (plan §4.4, xác nhận ĐÚNG):** "HĐ ≥ phase 5 không xoá được" — `ContractFeatures.cs:632-633` `Phase >= DangInKy` ✓; đường thoát admin-override `ContractWorkflowService.cs:191-198` ✓ tồn tại thật. Lưu ý phụ: `ChoDuyet=10`/`TraLai=98`/`TuChoi=99` đều ≥5 ⇒ **mọi phiếu HĐ đã trình đều không xoá được** bằng API — plan chỉ nói "≥ phase 5", đúng nhưng dễ đọc nhầm là chỉ HĐ cuối luồng. + +--- + +## 8. Điểm ĐỨNG VỮNG trước soi (ghi để không chỉ kể lỗi) + +- 26/28 neo chấm lại **đúng đến dòng**, kể cả các cận trên khó (`:217-394`, `Layout.tsx:63-69` đúng 7 dòng, `DbInitializer.cs:1779-1786` đúng 7 key). +- W3 §③-B (OR-of-N 4 vế + ca ÂM + `AND PlanId=@id`) là bộ acceptance mạnh nhất bộ plan — đủ răng để bắt đúng rủi ro tự khai ("copy nhầm Proposal mất OR-of-N âm thầm"). +- W6 §③ có **ca ÂM tường minh** ("user ngoài workflow vẫn 403") — đúng bài chống mở-toang. +- Đường tạo HĐ-tay để test W6 trước W5 là khả thi thật (`ContractCreatePage.tsx:340` + `ContractFeatures.cs:109`) — không phải hứa suông. +- Nghi vấn nặng nhất tôi mang vào (W3 copy sai file vì CLAUDE.md nói `ApproveV2Async` ở `ContractFeatures.cs`) đã bị đĩa BÁC BỎ: spec đúng, CLAUDE.md lệch. + +