wal: flush 20260727T1224
This commit is contained in:
@ -42,7 +42,10 @@ chain:
|
||||
[!] 🔴 A-LỖI-KHOANH-PHẠM-VI CỦA LEAD (nguồn F3): spec CÓ sẵn **F-7** *"Designer thêm nút Sửa (khác Nhân bản)"* nhưng lead liệt wave-1 gồm F-1..F-6/F-10/F-11 và **BỎ SÓT F-7** — đúng cái dây nối 2 nửa. Build+549 test đều KHÔNG bắt được (test gọi thẳng handler; 2 đầu build sạch). Chỉ soi đường nút→endpoint mới thấy
|
||||
[x] A-owner chốt lượt 4: **"vá trọn rồi đẩy"** (bác 2 phương án đẩy-sớm)
|
||||
[!] wf: A-wave5 hmw — run-id `wf_07fd9304-74e` · BE vá F1 (bọc transaction) + F2 (thu hẹp purge = chỉ khi phiếu cha KHÔNG CÒN ROW; phiếu xoá-mềm ⇒ CHẶN; bỏ vế `o.IsDeleted`) +2 test nghiệm thu · FE làm F-7 (tách "Sửa tại chỗ"→PUT ⟂ "Tạo phiên bản mới"→POST)
|
||||
[!] A-wave4-bis reviewer RESUME (SendMessage) làm nốt 4 trục F8+ (validator lách · migration an-toàn-prod · test xanh-giả · regression) + dòng khuyến nghị deploy
|
||||
[x] A-wave4-bis reviewer XONG TRỌN — 47.113B · **14 FLAG** · verdict **`DEPLOY-CÓ-RỦI-RO`**. CLEAN 4 trục sau thách-phá thật: F9 migration (4 phép) · F10 validator T1/T2 (11 đường lách, tắc hết) · F12 test còn lại (3 phép) · F13 hồi quy (3 phép)
|
||||
[ ] 🔴🔴 **A-F8 CỔNG DEPLOY — LUẬT COMMIT, ĐỌC TRƯỚC KHI PUSH**: 2/3 file migration đang **UNTRACKED** (`20260727033522_AddPeAllowApproverDelete.cs` + `.Designer.cs`) trong khi `ApplicationDbContextModelSnapshot.cs` **đã tracked**. `git commit -a` / `git add -u` ⇒ nạp snapshot+entity NHƯNG BỎ file migration ⇒ prod `MigrateAsync()` không thấy gì để áp ⇒ cột không được tạo ⇒ mọi truy vấn `ApprovalWorkflowLevels` ném `Invalid column name` ⇒ **HẠ TOÀN BỘ 7 MODULE** (PE + HĐ + 5 module Văn phòng số). ⇒ **BẮT BUỘC `git add` ĐÍCH DANH** 3 path (2 migration + `tests/.../UpdateAwDefinitionTests.cs`); trước commit `git status --porcelain | grep '^??'` phải RỖNG ở vùng `Migrations/` + `tests/`; sau commit `git show --stat HEAD | grep AddPeAllowApproverDelete` phải ra **2 dòng**
|
||||
[ ] A-F11 (MED) test XANH GIẢ `F16_…SafeUpdate_Succeeds_AndWritesNoPeChangelog` `:753-754`: assert `PeChangelogs.Count==0` nhưng seed **0 phiếu PE** ⇒ đúng ở CẢ 2 thế giới (còn cổng / gỡ cổng) ⇒ không đo gì. Vá: seed 1 PE ghim đúng workflow Proposal đó rồi mới assert. Nghiệm thu: tạm gỡ cổng `def.ApplicableType is …` `:790-792` ⇒ ca này PHẢI ĐỎ
|
||||
[~] A-wave4-bis reviewer RESUME (SendMessage) làm nốt 4 trục F8+ (validator lách · migration an-toàn-prod · test xanh-giả · regression) + dòng khuyến nghị deploy
|
||||
[~] A-wave4 reviewer — spawn TRỰC TIẾP qua Agent-tool (KHÔNG qua hmw: wave-2 hmw-reviewer sinh 0 file). Ưu tiên: 2 mục lead tự viết · vùng wave-3 vừa vá · 🔴 nghi ngờ riêng của lead: `ExecuteDelete` commit NGAY, nếu SaveChanges sau đó fail thì chữ ký đã xoá CỨNG mà phần còn lại rollback ⇒ mất dữ liệu không hoàn tác
|
||||
[~] A-vá-lỗi wave-3 gốc: F14 lộ bug THẬT — guard F-5 không chặn trước `db.ApprovalWorkflowLevels.Remove(gone)` ⇒ EF ném "association ... severed" thay vì ConflictException lịch sự. Tức đúng cái F-14 sinh ra để bắt
|
||||
[ ] A-DEPLOY (sau khi wave-3 xanh + reviewer PASS)
|
||||
|
||||
@ -1,13 +1,8 @@
|
||||
# Reviewer Agent — Persistent Memory
|
||||
|
||||
- **S155 (07-27) SPEC-review tiền-hmw (PE xoá phiếu ở màn duyệt + lệnh Update workflow) — PASS-WITH-FLAGS 4H/8M/5L; spec trích-dẫn SẠCH nhưng 4 lỗ chí-mạng:** 🔴 **H1 class MỚI `nới-authz-mà-không-ai-có-quyền`** — spec gắn `[Authorize(Policy="PurchaseEvaluations.Delete")]` lên endpoint đang **KHÔNG có policy**; sqlcmd Dev đo: CanDelete=1 chỉ `Admin`+`DeptManager`, **11/13 role = 0** (`DbInitializer.cs:2515` `canDelete = roleName==DeptManager`, seeder-2 chỉ nâng Read/Create + skip-if-exists ⇒ prod không tự vá) ⇒ vừa **regression** (Drafter mất xoá-nháp đang chạy) vừa **feature chết** (Procurement 403). `MenuPermissionHandler.cs:41-51` khớp MenuKey **CHÍNH XÁC, 0 kế-thừa** — inheritance của `GetMyMenuTreeQuery` chỉ là display. **LUẬT: đóng API = phải đo bảng `Permissions` xem AI còn qua được; test-403 một-chiều luôn xanh-giả.** · **H3** `IgnoreQueryFilters()` **gỡ** filter chứ không đảo ⇒ list "Đã xoá" thiếu `.Where(IsDeleted)` + thiếu tái-lập IDOR `:596-617` = lộ toàn hệ. · **H2** cờ trên `ApprovalWorkflowLevel` = **1 row = 1 NGƯỜI**, repo có SẴN 2 idiom cho cờ anh-em F5 (`Service:859` per-row ⟂ `Features:1182` `g.Any()` per-Cấp) ⇒ "per-Cấp" trong spec = N người xoá được (owner muốn 1). · **H4** lệnh Update workflow không khai policy mà chính nó là **cổng cấp quyền** (thêm-người + bật-cờ). · **M1 phản-ví-dụ cho luật "AN TOÀN ⟺ giữ 2 tập Order"**: cờ `AllowApproverFinalize` đổi ĐIỂM KẾT THÚC phiếu đang chờ (`:859-878`) · `pendingLevelGroup.First()` (`:733-734`, tie **không có tie-breaker**) → thêm/bớt row cùng Order đổi slot chữ-ký Admin · thêm người vào Cấp ĐÃ QUA → `ComputeLevelStatus` thuần-con-trỏ (`:1135-1148`) hiện "Done" dù chưa ký. **⇒ con-trỏ-không-dịch ≠ phiếu-không-hỏng.** · **M3** soft-delete ⇒ cascade không chạy ⇒ `LevelOpinion` mồ-côi vẫn Restrict Level ⇒ nợ hoãn (N1) **ăn thẳng** vào luật mới F-5 cùng spec. · **M4** phiếu ChoDuyet chỉ vào **2/4** phép cộng ⇒ test "cả 4 giảm" bất-khả-xanh. · **M6** thêm key `Pe_*` **KHÔNG** đổi `Policies`/`Menu keys` (derived từ `MenuKeys.All`, `Pe_*` sinh factory) ⇒ spec bảo sửa số canonical = hỏng detector. **Cách phá F.0 (6 đường, tắc cả 6)** ⇒ mệnh-đề trung-tâm ĐỨNG, chỉ tiêu-đề rộng quá. Tag `[s155, spec-review-pre-impl, authz-widen-nobody-has-it, ignorequeryfilters-not-inverted, order-set-rule-underfit]`
|
||||
|
||||
- **[verbatim → git 8b6df0a closeout] S146 (07-22) PRE-COMMIT spec-v2-applied (governance 20 file) — 2 MAJOR THẬT (cả hai của LEAD); nấc `verified` tự-chạy-lệnh không-tin-lead:** F1 `session-end.md` còn `hook-3-path` GẠCH-NỐI dù worker CE đã chỉ đích-danh (class "residual có người chỉ tận nơi vẫn rớt") + bất-đối-xứng nội-bộ (`pause.md` đã 4-path). F2 phép quét cardinality của lead siết LỆCH-TRỤC (co-occurrence CÙNG-DÒNG + 1 mẫu-chữ) → re-run HEAD tiền-vá lọt 4 ca thật gồm chính F1. F3 HELD (git show da7ee8a: hook 4-path + `-File` re-đọc mỗi Stop). 🔴 Bài rút: lead sửa 1 phép quét 4 lần/phiên vì mã-hoá ý-định NGỮ-NGHĨA vào regex; nấc đúng = lưới-soát-cho-người + allowlist CÓ TÊN + use⟂mention; lần-4 = citation-trap (dò bắt chính nốt-vá vì nó trích giá-trị cũ).
|
||||
|
||||
- **[→ archive/2026-07.md @S145-curate] S145 Axis-E lane — FAIL 2C/1M/4m:** cross-cut seam catch (hook 3→4 path sessions/ orphan phá Sàn-3 tiep.md; session_ctx_kb=64 nghịch owner + ghost-wire #H18). LESSON: bất-biến chia-đôi giữa 2 axis nằm ĐÚNG SEAM → chỉ cross-cut sweep bắt.
|
||||
- **[→ archive/2026-07.md @S145-curate] S145 lane-4 (Axis-D) — PWF 2M/1m:** phép-4 LANDMINE (`agent-memory/ring1-audit/` EMPTY, auto-fires close ⇒ phép-4 FAIL). LESSON: 'vai-có-sổ' acceptance ⇒ MUST `ls agent-memory/<role>/` (folder≠sổ).
|
||||
- **[→ archive/2026-07.md @S145-curate] S145 C7-lane (Axis A) — PWF 1C:** spec "4 surface→2" stale-fix FALSE (4 caller THẬT); grep-"4 surface"=0-hit = ANTI-TEETH Goodhart. LESSON: stale-fix verify caller-trên-đĩa; 2-script-same-suffix = conflation-vector.
|
||||
- **[→ archive/2026-07.md @S145-curate] S145b (07-22) GATE roster #2/#3 ring2-audit+ring4-audit (18→20) — PASS 1m:** implementer vá ĐÚNG mọi S145a-finding + tự sweep 2 NUMERIC site ngoài checklist. 1 MINOR measured-label: `README:249` "17 folder" falsified by ls=18 (ring1-audit RỖNG). LESSON: đếm VALID_ROLES = per-file grep bidirectional KHÔNG regex (hmw.js `[2]`/`[S110` comment lừa parser).
|
||||
- **[→ archive/2026-07.md @S155-curate] S155 ×2 (07-27) PE delete-approver — DIFF-review đợt 1 tiền-deploy (14 FLAG 4H/6M/3L/1I, DEPLOY-CÓ-RỦI-RO) + SPEC-review (PWF 4H/8M/5L):** 🔴 class MỚI **`HIGH nằm ở git chứ không ở mã`** — 2/3 file migration UNTRACKED mà snapshot ĐÃ tracked ⇒ `commit -a` nạp model-có-cột nhưng bỏ migration ⇒ `Invalid column name` cả 7 module, build+test vẫn xanh ⇒ **gate deploy phải chấm `git status ^??` cho `Migrations/`** · `ExecuteDelete` ngoài transaction TRƯỚC `SaveChanges` = mất dữ liệu không hoàn tác · purge xoá CỨNG chữ ký phiếu XOÁ MỀM ⇒ **test đang KHOÁ chiều ngược, vá xong test đỏ ≠ hồi quy, CẤM nới assert** · ~600 dòng BE = mã chết (`grep api.put`=0) · **xanh-giả bắt bằng phép 2-thế-giới** (còn-gate ⟂ bỏ-gate cùng cho 0 ⇒ 0 thông tin) · CLEAN chứng bằng **liệt kê vét cạn** write-site + sweep gián-tiếp (#81-EXT) · 2 lỗi TỰ BẮT: trích line-number **từ diff** (diff không mang số dòng file) · `grep -v Snapshot` ăn nhầm tên migration · **file bị sửa TRONG LÚC soi ⇒ neo bằng NỘI DUNG DÒNG + ghi bản-đồ-trôi**.
|
||||
- **[→ archive/2026-07.md @S155-curate] S146·S147·S152-D2 (digest):** S146 lead sửa 1 phép quét 4 lần/phiên vì mã-hoá ý-định NGỮ-NGHĨA vào regex ⇒ nấc đúng = lưới-soát-cho-người + allowlist CÓ TÊN + use⟂mention · S147 **decouple 1 gate ⇒ truy xem prop dùng chung đã mang carve-out chưa** (`itemsReadOnly` ≠ raw `readOnly`); "same fix" cho surface anh-em có thể trúng gate KHÁC · S152-D2 census thay spot 307/307; **3 THƯỚC-HỎNG** (md5 disk vs `git show` = FAIL giả do CRLF⟂LF · `grep -c` dính citation-trap · `grep -iF` MSYS trả 0-hit IM LẶNG trên UTF-8) + máy-trích mù đuôi `{.yml,.json,.sql}`.
|
||||
- **[→ archive/2026-07.md @S145-curate] S145 4-lane (Axis-A/D/E + S145b GATE) — digest:** bất-biến chia-đôi giữa 2 axis nằm ĐÚNG SEAM ⇒ chỉ cross-cut sweep bắt (hook 3→4 path orphan; `session_ctx_kb` ghost-wire #H18) · 'vai-có-sổ' acceptance ⇒ MUST `ls agent-memory/<role>/` (folder≠sổ, ring1-audit RỖNG) · stale-fix phải verify caller-TRÊN-ĐĨA — grep-"4 surface"=0-hit là ANTI-TEETH Goodhart; 2-script-same-suffix = conflation-vector · đếm VALID_ROLES = per-file grep bidirectional KHÔNG regex (comment lừa parser).
|
||||
- **[→ archive/2026-07.md @S145b-curate] S145a·S143 (digest, archived):** S145a NUMERIC "roster 17" hardcode trốn name-enum sweep ⇒ sweep repo-wide CẢ numeric LẪN name-enum (superseded S145b PASS) · S143 CLASS `bằng-chứng-tự-huỷ-sau-squash` (cite outward = commit SỐNG-sau-squash) + số neo-mốc + errata CẤM sửa file đã-stamp + #53 ghi-đĩa-trong-lúc-làm.
|
||||
- **[→ archive/2026-07.md @S140/@S134] S139·S134b·S133 (digest, archived):** S139 re-measure MỌI acceptance từ git HEAD + no-self-exempt (chấm cả file CHÍNH VAI MÌNH); "measured" sống CHỈ KHI falsify-path CHẠY ∧ đứng · S134b insertion-only + `grep '^-'`=0 chắc hơn diff-mắt · S133 "mọi nhánh set X" phải grep CẢ assignment-qua-BIẾN (#81).
|
||||
|
||||
@ -82,8 +77,6 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
|
||||
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` + `archive/2026-07.md` via `archive/_INDEX.md`)
|
||||
|
||||
- **S147 (07-23) DIAGNOSIS-review PE attach TraLai (no diff — verify root-cause + stress fix) — Diagnosis PASS 5/5, fix CORE-safe/EXT-unsafe:** 5 claims verified from disk; BE-no-guard CONFIRMED **intentional** by S78 changelog L9 ("handler KHÔNG guard drafter-only → approver upload no-403") + sole pipeline behavior=ValidationBehavior. 🔴 **MAJOR blind-spot:** `attachEditable=isDrafter&&isEditablePhase` SAFE for GeneralAttachmentsSection (:2013 readOnly=raw, no carve-out) but UNSAFE for SupplierAttachmentsCell (:2758) whose readOnly=**itemsReadOnly** (`readOnly&&!approverEditMode`, Mig28-F3 ChoDuyet approver-edit) → naive REPLACE breaks approver QuoteDocument edit in ChoDuyet; SAME gap in optional BE guard (carve-out {ChoDuyet+ApprovalAttachment} misses ChoDuyet+QuoteDocument). **Scope trap:** isDrafter@:140 in MAIN scope, OUT-of-scope at :2013/:2758 (child comps take only {ev,readOnly}); changing :421 over-broadly hits HoSoLinkRow:2017. **Completeness:** WORKSPACE (readOnly=false, picker editableOnly∋TraLai) already edits comparison-doc in TraLai → bug is LIST-detail-only (:574/:669 hardcode). LESSON: **decouple a gate → trace if shared prop already carries a carve-out (itemsReadOnly ≠ raw readOnly); "same fix" for sibling surface may hit a DIFFERENT gate source.**
|
||||
|
||||
- **[→ archive/2026-07.md @S126/@S134] S123·S124·S125·S129·S131·S131b digest (all archived):** S131b follow-up phải GIỮ con-số source · S131 số-atomic-đúng vẫn fencepost → hero-ratio re-derive từ raw ([topic](project_s131_h24_h22_3gap_outward_gate.md)) · S129 writer-only = NỬA-VÁ + quick-test tự-chạy = chứng rẻ nhất · S124/125 do-token trap → re-Read full + sibling-test 2-CHIỀU (MATCH⇒file-bệnh·FAIL⇒verifier-bệnh) · S123 🔴 #53 ghi diary TRƯỚC return + vá-1-lớp grep MỌI matcher cùng-lớp ([topic](project_s123_governance_4change_review.md)).
|
||||
- **Digest S93→S117 (verbatim → `archive/2026-07.md`; 1 mệnh-đề/entry):** S93 code-gate re-derived denom · S97 default-flip byte-mirror SHA ×2 · S98 Windows byte-verify = .NET (MSYS strip
|
||||
báo sai) · S100 HELD stale run-id NOT-stamped · S101 persist-claim cần reader-side · S108 outward-claim "đã sửa X" phải cat X trước gate · S109 gist meta-count ≠ disk · S110 claim-về-code grep lại kể cả reword · S111 nâng-nấc-quên-sync cùng-diff · S115 verify EACH regex-alt by-hand · S116 persist-claim = tracked + 0-drift · S117 2-tier guard compare SUM-EXPRESSION.
|
||||
@ -104,12 +97,5 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
|
||||
- `/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-26) — D2 Fidelity-gate 6 gist "CỤM BỔ SUNG" (coverage-diff) — **PASS_WITH_FIXES** (A/B/C/D PASS · E FAIL)
|
||||
- **Census thay spot:** 307/307 trích có trong verbatim CẶP (275 exact + 32 sau khi hoàn `'`→`"`), 307/307 chứa ĐÚNG token gắn nhãn, 307/307 token thật-sự VẮNG khỏi gist tiền-append (0 gap thổi-phồng). `ref`=46 khớp đúng claim lead. Trích = cửa-sổ ~150 ký-tự cố-định, token ở ~27% → cắt giữa từ ở 2 biên nhưng payload (file:line/finding) còn nguyên ⇒ điều-hướng được, KHÔNG rác.
|
||||
- 🔴 **E FAIL — class-gap hệ-thống, chữ-ký sạch:** máy-trích emit file-ref CHỈ cho `{.cs 101/.tsx 45/.ts 16/.ps1 8}` = 170 token, **ZERO cho `{.yml,.json,.slnx,.csproj,.sql,.config}`** ⇒ 13 token infra vắng ở 5/6 cặp. Nặng nhất: `deploy.yml` (chính file pipeline, trong gist của vai CI/CD!) · `appsettings.Production.json` (16 lần trong verbatim) · 2× `s59-*.sql` (chạy TAY prod ngoài pipeline) · `memory-budget.json` (single-source canonical CLAUDE.md). Header cụm TỰ đặt chuẩn "file-ref phải sống trong gist" mà KHÔNG khai N/A ⇒ trượt bằng chính thước mình.
|
||||
- 🔴 **3 THƯỚC-HỎNG (bug ở phép đo, không ở vật):** (1) md5 disk vs `git show` — spec gate ghi vậy — báo FAIL GIẢ 3/6 verbatim do worktree CRLF ⟂ blob LF (autocrlf=true + 188 file legacy `i/lf w/crlf`); đúng phải `git diff HEAD --quiet` hoặc strip-CR 2 phía. (2) `grep -c distill-gen` vô-dụng vì CHÍNH header cụm chứa chữ "distill-gen giữ" (citation-trap nhẹ) → phải so VALUE `distill-gen:\s*\d+`. (3) `grep -iF` trên MSYS trả **0-hit im-lặng** cho pattern ASCII trong file UTF-8 (`Mig42`: `-F`=1 nhưng `-iF`=0) = bẫy vắng-mặt-trông-giống-sạch S146 — thoát nhờ đối-chiếu Python.
|
||||
- **Tự bắt 2 lỗi của CHÍNH mình trước khi báo:** "Mig 47/50/42/46 MISS" = artifact chuẩn-hoá của tao (`Mig42` liền không dấu cách, có ĐỦ 2 phía) · context-regex `.{70}` trả RỖNG đọc nhầm thành sạch → phải `.{0,70}`. ⇒ **LUẬT: mọi MISS phải literal-grep XÁC-NHẬN 2 phía bằng công-cụ THỨ HAI trước khi vào report.**
|
||||
- D re-ground từ đĩa: `docs/gotchas.md` count=83/max=83 ⇒ 0 nhãn `gotcha #N>83`; soi NGƯỢC 24 dòng gotcha ≤83 tìm mirror-error (run-number đội lốt gotcha) → 0 ca, mọi #N đều là gotcha thật trong ngữ-cảnh nguồn. Tag `[s152, d2-fidelity-gate, coverage-diff, extractor-ext-blindspot, thuoc-hong-x3]`
|
||||
|
||||
## 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]`
|
||||
|
||||
@ -130,3 +130,29 @@
|
||||
- 🔴 **3 THƯỚC-HỎNG (bug ở phép đo, không ở vật):** (1) md5 disk vs `git show` — spec gate ghi vậy — báo FAIL GIẢ 3/6 verbatim do worktree CRLF ⟂ blob LF (autocrlf=true + 188 file legacy `i/lf w/crlf`); đúng phải `git diff HEAD --quiet` hoặc strip-CR 2 phía. (2) `grep -c distill-gen` vô-dụng vì CHÍNH header cụm chứa chữ "distill-gen giữ" (citation-trap nhẹ) → phải so VALUE `distill-gen:\s*\d+`. (3) `grep -iF` trên MSYS trả **0-hit im-lặng** cho pattern ASCII trong file UTF-8 (`Mig42`: `-F`=1 nhưng `-iF`=0) = bẫy vắng-mặt-trông-giống-sạch S146 — thoát nhờ đối-chiếu Python.
|
||||
- **Tự bắt 2 lỗi của CHÍNH mình trước khi báo:** "Mig 47/50/42/46 MISS" = artifact chuẩn-hoá của tao (`Mig42` liền không dấu cách, có ĐỦ 2 phía) · context-regex `.{70}` trả RỖNG đọc nhầm thành sạch → phải `.{0,70}`. ⇒ **LUẬT: mọi MISS phải literal-grep XÁC-NHẬN 2 phía bằng công-cụ THỨ HAI trước khi vào report.**
|
||||
- D re-ground từ đĩa: `docs/gotchas.md` count=83/max=83 ⇒ 0 nhãn `gotcha #N>83`; soi NGƯỢC 24 dòng gotcha ≤83 tìm mirror-error (run-number đội lốt gotcha) → 0 ca, mọi #N đều là gotcha thật trong ngữ-cảnh nguồn. Tag `[s152, d2-fidelity-gate, coverage-diff, extractor-ext-blindspot, thuoc-hong-x3]`
|
||||
|
||||
|
||||
<!-- ===== @S155-curate (2026-07-27) moved from MEMORY.md L1 ===== -->
|
||||
|
||||
- **S155 (07-27) DIFF-review đợt 1 tiền-DEPLOY (PE delete-approver, 14 FLAG 4H/6M/3L/1I) — DEPLOY-CÓ-RỦI-RO:** 🔴 **class MỚI `HIGH nằm ở git chứ không ở mã`** — 2/3 file migration UNTRACKED (`?? …AddPeAllowApproverDelete.{cs,Designer.cs}`) trong khi **snapshot đã tracked** ⇒ `git commit -a`/`add -u` nạp model-có-cột mà bỏ migration ⇒ `DbInitializer.cs:64 MigrateAsync()` không thấy gì để áp ⇒ `Invalid column name` cho **cả 7 module** dùng schema V2. Build+test đều xanh, local đã áp mig ⇒ **0 dấu vết**. **LUẬT: gate deploy phải chấm `git status ^??` cho `Migrations/`, không chỉ chấm diff.** · **H `ExecuteDelete` ngoài transaction TRƯỚC `SaveChanges`** = mất chữ ký không hoàn tác (đếm được **4 nguồn ném** nằm giữa 2 mốc ⇒ đường nổ có thật, không giả định) · **H purge xoá CỨNG chữ ký của phiếu mới XOÁ MỀM** ⇒ phá thẳng "khôi phục phiếu" của chính đợt 2; và **test `:607-609` đang KHOÁ chiều ngược** (`Should().Be(0)`) ⇒ cảnh báo trước cho wave vá: đỏ ≠ hồi quy, **cấm nới assert** · **H ~600 dòng BE = mã chết**: `grep api.put` = **0 hit**, Designer chỉ POST ⇒ đúng mục tiêu §F.0 (cứu phiếu đang treo) KHÔNG dùng được trong UAT.
|
||||
- **XANH-GIẢ bắt được bằng phép 2-thế-giới:** `F16_…NoPeChangelog` assert `PurchaseEvaluationChangelogs.Count==0` nhưng test seed **0 phiếu PE** ⇒ còn-gate và bỏ-gate **cùng cho 0** ⇒ phép đo không phân biệt được 2 thế giới = 0 thông tin. **Cách bắt: truy MỌI write-site của bảng được assert, rồi hỏi "bỏ guard đi thì quan sát có đổi không?"**
|
||||
- **CLEAN có răng (không "đọc thấy ổn"):** bất-biến Version/IsActive/Id chứng bằng **liệt kê vét cạn** `grep "def\.[A-Za-z]* *="` = đúng **3** write (Name/Description/UpdatedAt) **+ sweep gián-tiếp** (`= def;`/`SetValues`/`Entry(`/`CurrentValues`) = 0 hit ghi (hit duy nhất là READ trong string nội suy) — đúng lớp gotcha #81-EXT · **11 đường lách thử, tắc cả 11** · double-insert `DbSet.Add`+`nav.Add` = 1 entity, **và đã có test bắt nếu sai** (F12 assert đúng 2 row) · bán-kính đo NGƯỢC từ config: `grep OnDelete` ra **đúng 7** FK Restrict = khớp 7 bảng handler quét.
|
||||
- **2 lỗi CỦA CHÍNH TAO tự bắt:** (i) lần đầu trích line-number **từ diff** thay vì từ đĩa ⇒ sai toàn bộ, phải re-grep sửa lại (**LUẬT: `git diff` không mang số dòng file — mọi anchor phải grep từ đĩa**); (ii) `ls | grep -v Snapshot` **ăn nhầm** `AddPeApprovedBudgetSnapshot` ⇒ tưởng thiếu file migration — thoát nhờ đối chiếu `__EFMigrationsHistory` (S152 luật "MISS phải xác nhận bằng công-cụ THỨ HAI" ăn thật lần nữa).
|
||||
- **File bị sửa TRONG LÚC soi** (mtime lệch 71s — wave vá song song): anchor ≤`:659` giữ nguyên, sau `:660` trôi **+21**. **LUẬT: review file đang-được-sửa ⇒ ghi bản-đồ-trôi + neo bằng NỘI DUNG DÒNG, đừng chỉ số.** Tag `[s155, diff-review-pre-deploy, untracked-migration-gate, executedelete-no-tx, vacuous-test-two-worlds, anchor-drift]`
|
||||
|
||||
- **S155 (07-27) SPEC-review tiền-hmw (PE xoá phiếu ở màn duyệt + lệnh Update workflow) — PASS-WITH-FLAGS 4H/8M/5L; spec trích-dẫn SẠCH nhưng 4 lỗ chí-mạng:** 🔴 **H1 class MỚI `nới-authz-mà-không-ai-có-quyền`** — spec gắn `[Authorize(Policy="PurchaseEvaluations.Delete")]` lên endpoint đang **KHÔNG có policy**; sqlcmd Dev đo: CanDelete=1 chỉ `Admin`+`DeptManager`, **11/13 role = 0** (`DbInitializer.cs:2515` `canDelete = roleName==DeptManager`, seeder-2 chỉ nâng Read/Create + skip-if-exists ⇒ prod không tự vá) ⇒ vừa **regression** (Drafter mất xoá-nháp đang chạy) vừa **feature chết** (Procurement 403). `MenuPermissionHandler.cs:41-51` khớp MenuKey **CHÍNH XÁC, 0 kế-thừa** — inheritance của `GetMyMenuTreeQuery` chỉ là display. **LUẬT: đóng API = phải đo bảng `Permissions` xem AI còn qua được; test-403 một-chiều luôn xanh-giả.** · **H3** `IgnoreQueryFilters()` **gỡ** filter chứ không đảo ⇒ list "Đã xoá" thiếu `.Where(IsDeleted)` + thiếu tái-lập IDOR `:596-617` = lộ toàn hệ. · **H2** cờ trên `ApprovalWorkflowLevel` = **1 row = 1 NGƯỜI**, repo có SẴN 2 idiom cho cờ anh-em F5 (`Service:859` per-row ⟂ `Features:1182` `g.Any()` per-Cấp) ⇒ "per-Cấp" trong spec = N người xoá được (owner muốn 1). · **H4** lệnh Update workflow không khai policy mà chính nó là **cổng cấp quyền** (thêm-người + bật-cờ). · **M1 phản-ví-dụ cho luật "AN TOÀN ⟺ giữ 2 tập Order"**: cờ `AllowApproverFinalize` đổi ĐIỂM KẾT THÚC phiếu đang chờ (`:859-878`) · `pendingLevelGroup.First()` (`:733-734`, tie **không có tie-breaker**) → thêm/bớt row cùng Order đổi slot chữ-ký Admin · thêm người vào Cấp ĐÃ QUA → `ComputeLevelStatus` thuần-con-trỏ (`:1135-1148`) hiện "Done" dù chưa ký. **⇒ con-trỏ-không-dịch ≠ phiếu-không-hỏng.** · **M3** soft-delete ⇒ cascade không chạy ⇒ `LevelOpinion` mồ-côi vẫn Restrict Level ⇒ nợ hoãn (N1) **ăn thẳng** vào luật mới F-5 cùng spec. · **M4** phiếu ChoDuyet chỉ vào **2/4** phép cộng ⇒ test "cả 4 giảm" bất-khả-xanh. · **M6** thêm key `Pe_*` **KHÔNG** đổi `Policies`/`Menu keys` (derived từ `MenuKeys.All`, `Pe_*` sinh factory) ⇒ spec bảo sửa số canonical = hỏng detector. **Cách phá F.0 (6 đường, tắc cả 6)** ⇒ mệnh-đề trung-tâm ĐỨNG, chỉ tiêu-đề rộng quá. Tag `[s155, spec-review-pre-impl, authz-widen-nobody-has-it, ignorequeryfilters-not-inverted, order-set-rule-underfit]`
|
||||
|
||||
|
||||
- **[verbatim → git 8b6df0a closeout] S146 (07-22) PRE-COMMIT spec-v2-applied (governance 20 file) — 2 MAJOR THẬT (cả hai của LEAD); nấc `verified` tự-chạy-lệnh không-tin-lead:** F1 `session-end.md` còn `hook-3-path` GẠCH-NỐI dù worker CE đã chỉ đích-danh (class "residual có người chỉ tận nơi vẫn rớt") + bất-đối-xứng nội-bộ (`pause.md` đã 4-path). F2 phép quét cardinality của lead siết LỆCH-TRỤC (co-occurrence CÙNG-DÒNG + 1 mẫu-chữ) → re-run HEAD tiền-vá lọt 4 ca thật gồm chính F1. F3 HELD (git show da7ee8a: hook 4-path + `-File` re-đọc mỗi Stop). 🔴 Bài rút: lead sửa 1 phép quét 4 lần/phiên vì mã-hoá ý-định NGỮ-NGHĨA vào regex; nấc đúng = lưới-soát-cho-người + allowlist CÓ TÊN + use⟂mention; lần-4 = citation-trap (dò bắt chính nốt-vá vì nó trích giá-trị cũ).
|
||||
|
||||
|
||||
- **S147 (07-23) DIAGNOSIS-review PE attach TraLai (no diff — verify root-cause + stress fix) — Diagnosis PASS 5/5, fix CORE-safe/EXT-unsafe:** 5 claims verified from disk; BE-no-guard CONFIRMED **intentional** by S78 changelog L9 ("handler KHÔNG guard drafter-only → approver upload no-403") + sole pipeline behavior=ValidationBehavior. 🔴 **MAJOR blind-spot:** `attachEditable=isDrafter&&isEditablePhase` SAFE for GeneralAttachmentsSection (:2013 readOnly=raw, no carve-out) but UNSAFE for SupplierAttachmentsCell (:2758) whose readOnly=**itemsReadOnly** (`readOnly&&!approverEditMode`, Mig28-F3 ChoDuyet approver-edit) → naive REPLACE breaks approver QuoteDocument edit in ChoDuyet; SAME gap in optional BE guard (carve-out {ChoDuyet+ApprovalAttachment} misses ChoDuyet+QuoteDocument). **Scope trap:** isDrafter@:140 in MAIN scope, OUT-of-scope at :2013/:2758 (child comps take only {ev,readOnly}); changing :421 over-broadly hits HoSoLinkRow:2017. **Completeness:** WORKSPACE (readOnly=false, picker editableOnly∋TraLai) already edits comparison-doc in TraLai → bug is LIST-detail-only (:574/:669 hardcode). LESSON: **decouple a gate → trace if shared prop already carries a carve-out (itemsReadOnly ≠ raw readOnly); "same fix" for sibling surface may hit a DIFFERENT gate source.**
|
||||
|
||||
|
||||
## S152 (2026-07-26) — D2 Fidelity-gate 6 gist "CỤM BỔ SUNG" (coverage-diff) — **PASS_WITH_FIXES** (A/B/C/D PASS · E FAIL)
|
||||
- **Census thay spot:** 307/307 trích có trong verbatim CẶP (275 exact + 32 sau khi hoàn `'`→`"`), 307/307 chứa ĐÚNG token gắn nhãn, 307/307 token thật-sự VẮNG khỏi gist tiền-append (0 gap thổi-phồng). `ref`=46 khớp đúng claim lead. Trích = cửa-sổ ~150 ký-tự cố-định, token ở ~27% → cắt giữa từ ở 2 biên nhưng payload (file:line/finding) còn nguyên ⇒ điều-hướng được, KHÔNG rác.
|
||||
- 🔴 **E FAIL — class-gap hệ-thống, chữ-ký sạch:** máy-trích emit file-ref CHỈ cho `{.cs 101/.tsx 45/.ts 16/.ps1 8}` = 170 token, **ZERO cho `{.yml,.json,.slnx,.csproj,.sql,.config}`** ⇒ 13 token infra vắng ở 5/6 cặp. Nặng nhất: `deploy.yml` (chính file pipeline, trong gist của vai CI/CD!) · `appsettings.Production.json` (16 lần trong verbatim) · 2× `s59-*.sql` (chạy TAY prod ngoài pipeline) · `memory-budget.json` (single-source canonical CLAUDE.md). Header cụm TỰ đặt chuẩn "file-ref phải sống trong gist" mà KHÔNG khai N/A ⇒ trượt bằng chính thước mình.
|
||||
- 🔴 **3 THƯỚC-HỎNG (bug ở phép đo, không ở vật):** (1) md5 disk vs `git show` — spec gate ghi vậy — báo FAIL GIẢ 3/6 verbatim do worktree CRLF ⟂ blob LF (autocrlf=true + 188 file legacy `i/lf w/crlf`); đúng phải `git diff HEAD --quiet` hoặc strip-CR 2 phía. (2) `grep -c distill-gen` vô-dụng vì CHÍNH header cụm chứa chữ "distill-gen giữ" (citation-trap nhẹ) → phải so VALUE `distill-gen:\s*\d+`. (3) `grep -iF` trên MSYS trả **0-hit im-lặng** cho pattern ASCII trong file UTF-8 (`Mig42`: `-F`=1 nhưng `-iF`=0) = bẫy vắng-mặt-trông-giống-sạch S146 — thoát nhờ đối-chiếu Python.
|
||||
- **Tự bắt 2 lỗi của CHÍNH mình trước khi báo:** "Mig 47/50/42/46 MISS" = artifact chuẩn-hoá của tao (`Mig42` liền không dấu cách, có ĐỦ 2 phía) · context-regex `.{70}` trả RỖNG đọc nhầm thành sạch → phải `.{0,70}`. ⇒ **LUẬT: mọi MISS phải literal-grep XÁC-NHẬN 2 phía bằng công-cụ THỨ HAI trước khi vào report.**
|
||||
- D re-ground từ đĩa: `docs/gotchas.md` count=83/max=83 ⇒ 0 nhãn `gotcha #N>83`; soi NGƯỢC 24 dòng gotcha ≤83 tìm mirror-error (run-number đội lốt gotcha) → 0 ca, mọi #N đều là gotcha thật trong ngữ-cảnh nguồn. Tag `[s152, d2-fidelity-gate, coverage-diff, extractor-ext-blindspot, thuoc-hong-x3]`
|
||||
|
||||
|
||||
@ -209,4 +209,319 @@ Thêm nữa, FE **đang** hiển thị tên người duyệt cho chính người
|
||||
|
||||
---
|
||||
|
||||
<!-- WIP — còn F8+ (validator/migration/test/regression). File này sẽ được ghi tiếp. -->
|
||||
## 🔴 F8 — HIGH (cổng deploy, không phải cổng mã) — 2/3 file migration đang UNTRACKED; commit kiểu `-a`/`add -u` sẽ bỏ chúng lại và **hạ toàn bộ 7 module**
|
||||
|
||||
**Bằng chứng `git status --porcelain`:**
|
||||
|
||||
```
|
||||
?? src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260727033522_AddPeAllowApproverDelete.Designer.cs
|
||||
?? src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260727033522_AddPeAllowApproverDelete.cs
|
||||
?? tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs
|
||||
M src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs
|
||||
```
|
||||
|
||||
**Điều sai.** Trong bộ 3 file của gotcha #17, **file thứ ba (snapshot) đã tracked** còn **hai file kia thì chưa**. `git commit -a` và `git add -u` chỉ nạp file đã tracked. Kết quả nếu lỡ tay:
|
||||
|
||||
- Snapshot + entity (`ApprovalWorkflow.cs`) + config (`ApprovalWorkflowConfiguration.cs`) **vào commit** ⇒ model của EF **có** cột `AllowApproverDelete`;
|
||||
- File migration **không vào commit** ⇒ `DbInitializer.cs:64` `await db.Database.MigrateAsync()` chạy trên prod và **không thấy gì để áp** ⇒ cột không bao giờ được tạo;
|
||||
- Mọi truy vấn chạm `ApprovalWorkflowLevels` ném `Invalid column name 'AllowApproverDelete'`.
|
||||
|
||||
Blast radius = **toàn bộ** người dùng schema V2: PE, Hợp đồng, và 5 module Văn phòng số (Đề xuất / Nghỉ phép / Tăng ca / Công tác / Xe). Không phải một tính năng hỏng — là **duyệt phiếu ngừng hoạt động toàn hệ**.
|
||||
|
||||
Đây là sai lầm **rẻ nhất để phạm và đắt nhất để lãnh** trong toàn bộ đợt này, vì nó không để lại dấu vết nào ở local (local đã áp migration rồi, xem F9) và build/test đều xanh.
|
||||
|
||||
**Đề xuất.** Trước khi commit: `git add` **đích danh** 3 đường dẫn trên, rồi `git status --porcelain | grep '^??'` phải **rỗng** cho vùng `Migrations/` và `tests/`. Sau khi commit: `git show --stat HEAD | grep AddPeAllowApproverDelete` phải ra **2 dòng**.
|
||||
|
||||
---
|
||||
|
||||
## 🟠 F9 — CLEAN (migration) + 1 LOW về tên — *thách-CLEAN: tao thử phá bằng 4 phép, trượt cả 4*
|
||||
|
||||
**Kết luận: bản migration này AN TOÀN để chạy trên prod có dữ liệu thật.** Không phải "đọc thấy ổn" — dưới đây là 4 cách tao đã thử phá:
|
||||
|
||||
**Phép 1 — 3-file rule (gotcha #17).** Đủ cả ba: `20260727033522_AddPeAllowApproverDelete.cs`, `…Designer.cs`, `ApplicationDbContextModelSnapshot.cs`. (Vấn đề *tracked hay chưa* tách riêng ở F8 — đó là lỗi git, không phải lỗi migration.)
|
||||
|
||||
**Phép 2 — Designer ⟂ Snapshot có lệch không?** So từng dòng, hai khối **trùng khít**:
|
||||
|
||||
```csharp
|
||||
b.Property<bool>("AllowApproverDelete")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
```
|
||||
|
||||
(`…Designer.cs:198-201` ⟂ `ApplicationDbContextModelSnapshot.cs`, cùng nội dung.) Lệch snapshot là nguồn "migration kế tiếp sinh ra lệnh lạ" — không có ở đây.
|
||||
|
||||
**Phép 3 — đã chạy thật trên SQL Server chưa, hay mới chỉ là ý định?** Đo bằng `sqlcmd` trên LocalDB Dev, không tin file:
|
||||
|
||||
```
|
||||
__EFMigrationsHistory (3 bản mới nhất):
|
||||
20260727033522_AddPeAllowApproverDelete ← ĐÃ ÁP
|
||||
20260717032812_AddPeApprovedBudgetSnapshot
|
||||
20260714035636_AddPeApprovedPriceRange
|
||||
|
||||
INFORMATION_SCHEMA.COLUMNS / ApprovalWorkflowLevels:
|
||||
AllowApproverDelete bit NO (CONVERT([bit],(0)))
|
||||
```
|
||||
|
||||
Cột ra đúng hình dạng **y hệt 8 cờ anh em** đang sống (`AllowReturnOneLevel`… `AllowApproverFinalize` đều `bit NOT NULL DEFAULT CONVERT([bit],(0))`, riêng `AllowReturnToDrafter` default `(1)`). Tức nó không phải cấu trúc mới lạ, mà là dòng thứ 9 của một khuôn đã chạy 4 lần trước đó (Mig 29/30/31/58).
|
||||
|
||||
**Phép 4 — có làm khoá bảng / rewrite trên prod không?** `AddColumn<bool>(nullable: false, defaultValue: false)` là **hằng số**, nên SQL Server 2012+ xử lý như thao tác **chỉ-metadata**, không viết lại bảng. Cộng thêm bảng cực nhỏ (`SELECT COUNT(*) FROM ApprovalWorkflowLevels` = **6** trên Dev; prod cùng bậc độ lớn — quy trình duyệt chỉ vài chục dòng Cấp). Không có nguy cơ timeout/khoá kéo dài.
|
||||
|
||||
**`Down()` reversible:** có — `DropColumn`. Lưu ý đúng mức: rollback làm **mất giá trị cờ đã tick**, nhưng vì default là `false` nên rollback đưa hệ về đúng trạng thái trước tính năng. Chấp nhận được.
|
||||
|
||||
**Migration chạy tự động lúc khởi động** (`DbInitializer.cs:64`), nên deploy không cần bước tay. Mặt trái: migration hỏng sẽ chặn app khởi động — bản này không thể hỏng vì lý do ở Phép 4.
|
||||
|
||||
> 🟢 **F9-LOW (tên gọi).** `AddPeAllowApproverDelete` — tiền tố `Pe` gây hiểu nhầm: cột nằm trên `ApprovalWorkflowLevels`, bảng **dùng chung ≥7 module**, không riêng PE. Người sau grep "ai sở hữu cột này" sẽ bị dẫn sai hướng. Không đáng sửa trước deploy (đổi tên migration đã áp còn tệ hơn), chỉ nên ghi 1 dòng vào changelog.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 F10 — CLEAN (Validator T1/T2 + bất biến Version/IsActive/Id) — *thách-CLEAN: 11 đường lách đã thử, tắc cả 11; cộng 1 phép quét gián tiếp*
|
||||
|
||||
### (a) Handler có đúng KHÔNG tăng `Version` / KHÔNG đụng `IsActive` / KHÔNG đổi `Id` không?
|
||||
|
||||
Tao **không** trả lời bằng cách đọc lướt. Tao liệt kê **toàn bộ** phép ghi vào aggregate root trong thân handler:
|
||||
|
||||
```
|
||||
$ sed -n '501,830p' ApprovalWorkflowV2AdminFeatures.cs | grep -n "def\.[A-Za-z]* *="
|
||||
197: def.Name = request.Name;
|
||||
198: def.Description = request.Description;
|
||||
280: def.UpdatedAt = dateTime.UtcNow;
|
||||
```
|
||||
|
||||
**Đúng 3 phép ghi, hết.** Không có `def.Version`, `def.IsActive`, `def.Id`, `def.Code`, `def.ApplicableType`, `def.CeoApprovalThreshold`, `def.IsUserSelectable`. Đây là phép **liệt kê vét cạn**, không phải kiểm điểm.
|
||||
|
||||
Rồi tao quét thêm **đường ghi gián tiếp** (đúng lớp gotcha #81-EXT — sửa qua biến trung gian nên grep trực tiếp không thấy):
|
||||
|
||||
```
|
||||
$ sed -n '501,830p' … | grep -n "= def;|SetValues|Entry(|CurrentValues|\.Version|\.IsActive|\.IsUserSelectable|\.Code *=|ApplicableType *="
|
||||
316: Summary = $"Sửa quy trình duyệt tại chỗ: {def.Code} v{def.Version} (giữ nguyên phiên bản)"
|
||||
```
|
||||
|
||||
Hit duy nhất là một phép **ĐỌC** trong chuỗi nội suy. Không có alias, không có `SetValues`, không có `Entry().CurrentValues`. ⇒ Ba bất biến **được chứng minh**, không phải được tin.
|
||||
|
||||
### (b) 11 đường lách đã thử — tắc cả 11
|
||||
|
||||
| # | Cách phá | Chốt chặn | Vị trí (đã mở kiểm) |
|
||||
|---|---|---|---|
|
||||
| 1 | Hoán vị Order giữa 2 dòng (giữ nguyên TẬP) | Order từng dòng bất biến | `:575` |
|
||||
| 2 | Thêm Cấp mới (Order chưa có) | `SetEquals` cuối vòng | `:621` |
|
||||
| 3 | Rút hết người khỏi một Cấp | nhánh `lost` trong cùng khối | `:621-628` |
|
||||
| 4 | Chuyển người sang Bước khác | `unknown` (dict theo từng Bước) | `:565` |
|
||||
| 5 | Gửi trùng Id dòng Cấp | `Distinct().Count()` | `:560` |
|
||||
| 6 | Bỏ sót / thêm Bước | `SetEquals` Id Bước | `:539` |
|
||||
| 7 | Đảo thứ tự Bước (giữ TẬP) | so `reqStep.Order != dbStep.Order` | `:554` |
|
||||
| 8 | Đổi ngưỡng CEO | echo nghiêm (null ≠ bỏ qua) | `:529` |
|
||||
| 9 | Bật/tắt F5 (và F2 ngay dưới) | echo, lệch ⇒ 409 | `:580` (+`:584`) |
|
||||
| 10 | Người MỚI mang sẵn cờ F2/F5 | chặn riêng nhánh `else` | `:594` |
|
||||
| 11 | Bỏ người **đang giữ** cờ F2/F5 | quét `removedHere` | `:609` |
|
||||
|
||||
Cộng thêm: NV được trỏ tới phải **tồn tại thật** (`:643`, chặn FK 547 hoá 500), và người có chữ ký sống thì **không bỏ được / không đổi người được** (`:659`). Tao không tìm được đường thứ 12.
|
||||
|
||||
### 🟠 F10a — MED — đổi **Mô tả** quy trình: ghi thẳng, KHÔNG sinh vết
|
||||
|
||||
`:698` `def.Description = request.Description;` — gán vô điều kiện, và **không** có `changes.Add` tương ứng (đối chiếu `:696` cho `Name` thì CÓ). Hệ quả kép:
|
||||
|
||||
- Đổi mô tả ⇒ `changes` rỗng ⇒ khối changelog `:790-821` **không chạy** ⇒ phiếu đang chạy **không có vết nào**;
|
||||
- Mô tả là thứ người duyệt **đọc trên phiếu**, nên đây không phải trường trang trí.
|
||||
|
||||
**Đề xuất:** thêm `if (def.Description != request.Description) changes.Add(...)` — một dòng, đối xứng với `Name`.
|
||||
|
||||
### 🟠 F10b — MED — thêm người vào **Cấp ĐÃ QUA** không bị chặn, và màn hình sẽ hiển thị người đó là "đã duyệt"
|
||||
|
||||
Handler cho phép thêm dòng vào bất kỳ Cấp nào miễn Order đã tồn tại — kể cả Cấp mà con trỏ phiếu **đã đi qua**. Phía hiển thị, `ComputeLevelStatus` là **số học con trỏ thuần**, không hề tra chữ ký:
|
||||
|
||||
```csharp
|
||||
// PurchaseEvaluationFeatures.cs:1135-1147
|
||||
if (stepIdx0 < currentIdx.Value) return "Done";
|
||||
if (stepIdx0 == currentIdx.Value) {
|
||||
if (levelOrder < currentLevel.Value) return "Done";
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
⇒ Admin thêm ông A vào Bước 1 Cấp 1 trong khi phiếu đã ở Bước 2 ⇒ luồng duyệt hiển thị Cấp đó **"Done"** ⇒ người đọc hiểu là **ông A đã ký**, trong khi ông A chưa từng chạm phiếu. Trên chứng từ tài chính, đó là **chữ ký ma trên màn hình**.
|
||||
|
||||
Đã nêu ở lượt soi spec (mục M1) và diff này **chưa xử lý**. Không chặn deploy đợt 1 (cần thao tác admin bất thường mới lộ), nhưng phải vào sổ trước khi mở tính năng cho nhiều admin.
|
||||
|
||||
**Đề xuất:** hoặc chặn thêm người vào Cấp đã qua trong `toAdd`, hoặc để `ComputeLevelStatus` đối chiếu `LevelOpinions` thay vì thuần con trỏ.
|
||||
|
||||
### 🟢 F10c — LOW — cho sửa `Step.DepartmentId` là AN TOÀN, đây là cách tao kiểm
|
||||
|
||||
Comment ở `:412` khẳng định "BE chỉ dùng làm hint, không khớp người duyệt". Tao không tin lời khẳng định, tao grep toàn `src/Backend`:
|
||||
|
||||
- `PurchaseEvaluationWorkflowService.cs:1117-1134` và `ContractWorkflowService.cs:131-148` — **đều nằm trong nhánh V1 legacy** (`matchByDeptLevel`, đối tượng là `WorkflowStep` của Mig 21), **không phải** đường V2. Đường V2 khớp người **chỉ** bằng `ApproverUserId` (`PurchaseEvaluationWorkflowService.cs:702`).
|
||||
- `PurchaseEvaluationFeatures.cs:1187-1188` / `:1240-1241` — thuần **DTO hiển thị** (map sang tên phòng).
|
||||
|
||||
⇒ Khẳng định đúng. Ghi chú nhỏ: `changes.Add` cho trường này chỉ ghi *"đổi phòng ban phụ trách"* (`:704`) mà **không ghi từ phòng nào sang phòng nào** — vết yếu hơn các trường khác.
|
||||
|
||||
---
|
||||
|
||||
## 🟠 F11 — MED — một ca test **XANH GIẢ**: `F16_…SafeUpdate_Succeeds_AndWritesNoPeChangelog`
|
||||
|
||||
**File:** `tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs:753-754`
|
||||
|
||||
```csharp
|
||||
(await h.Db.PurchaseEvaluationChangelogs.AsNoTracking().CountAsync())
|
||||
.Should().Be(0, "quy trình module Đề xuất KHÔNG được đẻ vết vào changelog của PE");
|
||||
```
|
||||
|
||||
**Điều sai — ca này không mang thông tin nào.** Cách tao chứng minh (không phải đọc thấy nghi ngờ): truy **mọi** phép ghi vào `PurchaseEvaluationChangelogs` trong handler → chỉ có **một**, ở `:807`, nằm trong `foreach (var pe in affected)`, mà `affected` đến từ `db.PurchaseEvaluations.Where(p => p.ApprovalWorkflowId == def.Id …)` (`:794-800`). Test này seed **0 phiếu PE** (chỉ có một `Proposal`, `:730-740`). Vậy:
|
||||
|
||||
- **Thế giới A** (còn cổng `def.ApplicableType is DuyetNcc or DuyetNccPhuongAn`): `affected` rỗng ⇒ count = 0.
|
||||
- **Thế giới B** (xoá cổng đi): `affected` **vẫn** rỗng ⇒ count = 0.
|
||||
|
||||
Hai thế giới, cùng một quan sát ⇒ phép đo **không phân biệt được** chúng ⇒ xoá cổng module đi test vẫn xanh. Nó đang đo *"không có phiếu PE nào tồn tại"*, không đo *"cổng module hoạt động"*.
|
||||
|
||||
**Đề xuất.** Cho ca này răng bằng cách seed **một phiếu PE ghim đúng workflow ProposalGeneral đó** (`SeedPeAsync(h.Db, wf.Id, …)` — hàm đã có sẵn ở `:108`), rồi mới assert `Count == 0`. Khi đó gỡ cổng ⇒ test đỏ.
|
||||
|
||||
**Tiêu chí nghiệm thu:** tạm gỡ điều kiện `def.ApplicableType is …` khỏi `:790-792`, chạy lại ca này — **phải ĐỎ**. Hiện tại nó xanh.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 F12 — CLEAN (chất lượng các ca test còn lại) — *thách-CLEAN: 3 phép thử, gồm cả phép trả lời thẳng câu "double-insert"*
|
||||
|
||||
**Phép 1 — assert có phải chỉ là tiếng vọng của bộ nhớ không?** Cả file dùng **chung một** `h.Db`. Nếu `ResolveLikeRunningPeAsync` mà tracking thì nó sẽ trả về **chính những đối tượng handler vừa sửa trong RAM** ⇒ mọi assert "phiếu thấy thay đổi" thành vô nghĩa. Nó dùng `AsNoTracking()` (`:143`) ⇒ EF bỏ qua identity map, đọc lại từ DB thật. **Đúng.** Đây là chỗ dễ hỏng nhất và nó không hỏng.
|
||||
|
||||
**Phép 2 — trả lời trục "`db.ApprovalWorkflowLevels.Add` + `step.Levels.Add` có sinh double-insert?"** Không. Cùng **một tham chiếu đối tượng** được đưa vào DbSet và vào collection điều hướng của cha đang tracked ⇒ EF thấy **một** entity ở trạng thái `Added`, không phải hai. Và điều quan trọng hơn: **đã có test bắt được nếu sai** — `F12` (`:262-263`) assert đúng **2** dòng ở Cấp 1:
|
||||
|
||||
```csharp
|
||||
live.Steps[0].Levels.Where(l => l.Order == 1).Select(l => l.ApproverUserId)
|
||||
.Should().BeEquivalentTo(new[] { alice.Id, bob.Id });
|
||||
```
|
||||
|
||||
Double-insert sẽ ra **3** dòng ⇒ đỏ. Đây là bằng chứng, không phải suy luận.
|
||||
|
||||
**Phép 3 — các ca 409 có mù trước lỗi "ném SAU khi đã ghi" không?** Không. Mọi ca `F13*` assert **hai vế**: có ném **và** DB y nguyên. Mạnh nhất là `F13b_OmitCeoApprovalThreshold` (`:443`) — nó gửi kèm `Name = "Tên mới"` rồi assert tên **vẫn là tên cũ**, tức chứng minh **không phần nào** của lệnh lọt xuống trước khi ném.
|
||||
|
||||
Ngoài ra `F12` đo bằng **hành vi thật** (trước: `ForbiddenException`; sau: cùng người, cùng lời gọi, con trỏ tiến sang Cấp 2) chứ không bằng "DB có row mới" — đúng thứ spec cần. `F14` có khối chứng-nhân FK thật (đã tự dọn tracker để đo đúng tầng DB). `F16_Proposal…Blocked409` chạm module Đề xuất thật.
|
||||
|
||||
> 🔴 **F12-CẢNH BÁO CHO WAVE 5 (đang vá F2) — đọc trước khi sửa:**
|
||||
> `UpdateAwDefinitionTests.cs:607-609` đang **khoá hành vi hiện tại**:
|
||||
> ```csharp
|
||||
> (await h.Db.PurchaseEvaluationLevelOpinions.IgnoreQueryFilters()
|
||||
> .CountAsync(o => o.ApprovalWorkflowLevelId == lvlBob.Id)).Should().Be(0);
|
||||
> ```
|
||||
> Ca này **chốt rằng purge PHẢI xoá sạch chữ ký của phiếu xoá mềm** — tức đúng **chiều ngược lại** với đề xuất vá F2. Khi vá F2, ca này **sẽ ĐỎ**. Đó **không phải hồi quy**: phải viết lại chủ đích của ca (đổi thành "chữ ký của phiếu xoá mềm được GIỮ, và lệnh bị 409"), **không được** nới assert cho nó xanh. Nếu ai đó làm nó xanh bằng cách bỏ dòng assert thì F2 coi như chưa vá.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 F13 — CLEAN (hồi quy) — *thách-CLEAN: 3 phép, và phép thứ 3 lòi ra 1 chỗ vá NỬA*
|
||||
|
||||
**Phép 1 — PUT mới có đụng client cũ không? KHÔNG.** Bảng route của controller sau khi thêm: `GET` (không template) · `POST` (không template) · `PUT {id:guid}` ← mới · `PATCH {id:guid}/user-selectable` · `DELETE {id:guid}`. Cặp (verb, template) của PUT **chưa từng tồn tại**, và không có `[HttpPut]` nào khác trong file. Phía client: `grep -rn "approval-workflows-v2" fe-admin/src fe-user/src` ra **29 hit**, phần ghi đúng **3** (`api.delete:286`, `api.patch:297`, `api.post:589`) — **không client nào** gọi PUT. Không thể hồi quy thứ chưa ai gọi.
|
||||
|
||||
**Phép 2 — đổi message 403 có làm đỏ test nào không? KHÔNG.** Tao grep chuỗi `"danh sách NV duyệt"` toàn repo trên `*.cs|*.ts|*.tsx` → đúng **3 hit**: 2 service + đúng **1** test là `ContractWorkflowServiceApproveV2Tests.cs:457`:
|
||||
|
||||
```csharp
|
||||
.WithMessage("*Bước 1*Cấp 1: bạn không có trong danh sách NV duyệt*");
|
||||
```
|
||||
|
||||
Nó dùng **ký tự đại diện** và dừng **trước** phần `({names})` — tức không hề chạm khúc bị sửa. Quan trọng hơn: nó đo `ContractWorkflowService`, file **không nằm trong diff**. Hai lý do độc lập ⇒ không thể đỏ.
|
||||
|
||||
**Phép 3 — module nào có nguy cơ vỡ?** Tao kiểm bán kính bằng cách đếm ngược từ **cấu hình FK** chứ không từ danh sách trong mã:
|
||||
|
||||
```
|
||||
$ grep -A1 "HasForeignKey(x => x.ApprovalWorkflowLevelId)" …/Configurations/
|
||||
→ 7 cấu hình, TẤT CẢ OnDelete(DeleteBehavior.Restrict):
|
||||
PurchaseEvaluationLevelOpinion · ContractLevelOpinion · ProposalLevelOpinion
|
||||
LeaveRequestLevelOpinion · OtRequestLevelOpinion · TravelRequestLevelOpinion
|
||||
VehicleBookingLevelOpinion
|
||||
```
|
||||
|
||||
**Đúng 7 — khớp tuyệt đối** với 7 bảng mà `CountLiveOpinionsAsync:830-884` và `PurgeOrphanOpinionsAsync:886-918` liệt kê. Nếu có bảng thứ 8 thì phép grep này phải ra 8; nó ra 7. Đồng thời `Level→Step` là **Cascade** và `Level→User` là **Restrict** (`ApprovalWorkflowConfiguration.cs:65-75`), nên `Remove(level)` chỉ vướng đúng 7 bảng đó. ⇒ Không module nào vỡ vì thiếu quét.
|
||||
|
||||
### 🟠 F13a — MED — vá NỬA: câu 403 sinh đôi ở `ContractWorkflowService` **chưa được vá**
|
||||
|
||||
**File:** `src/Backend/SolutionErp.Infrastructure/Services/ContractWorkflowService.cs:264`
|
||||
|
||||
```csharp
|
||||
var names = string.Join(", ", allowedUserIds); // ← vẫn in GUID trần
|
||||
throw new ForbiddenException(
|
||||
$"Bước {currentIdx + 1} ({currentStep.Name}) — Cấp {currentLevelOrder}: bạn không có trong danh sách NV duyệt ({names}).");
|
||||
```
|
||||
|
||||
Đây là **cùng một câu, cùng một lỗi, cùng một schema V2**, chỉ khác module. F-6 vá `PurchaseEvaluationWorkflowService.cs:709-716` mà bỏ lại đây ⇒ người duyệt Hợp đồng vẫn nhận một dãy GUID không đọc được. Đúng lớp lỗi "được chỉ đích danh 1 chỗ thì phải grep cả lớp".
|
||||
|
||||
Không chặn deploy (đợt 1 là PE), nhưng nên gộp vào cùng commit vì chi phí bằng 0 và để lâu sẽ thành nợ vô hình.
|
||||
|
||||
**Đề xuất.** Bê nguyên khối `:709-716` sang, **kèm** sửa F6 (`?? u.Id.ToString()` ba nấc) cho cả hai chỗ một lượt.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ GHI CHÚ TRÔI DÒNG — file bị sửa TRONG LÚC soi (wave 5 vá song song)
|
||||
|
||||
Lúc kiểm lại anchor cuối cùng, `ApprovalWorkflowV2AdminFeatures.cs` có mtime **12:18:13** trong khi tao kiểm lúc **12:19:24** — tức file vừa đổi 71 giây trước. Đây là wave 5 đang vá F1/F2/F3 đúng như lead báo. Tao **không** soi lại bản vá đó (ngoài phạm vi lượt này, và lead đã nói sẽ tự verify bằng build + test + grep), nhưng ghi lại hai sự thật đo được để lượt sau không bị lạc:
|
||||
|
||||
**(1) F1 có dấu hiệu ĐÃ được vá đúng hướng.** Đo bằng grep, không phải suy đoán:
|
||||
|
||||
```
|
||||
695: await using var tx = await context.Database.BeginTransactionAsync(ct);
|
||||
704: purgedOrphans = await PurgeOrphanOpinionsAsync(…);
|
||||
843: await db.SaveChangesAsync(ct);
|
||||
847: await tx.CommitAsync(ct);
|
||||
```
|
||||
|
||||
Hình dạng đúng: transaction mở **trước** purge, commit **sau** `SaveChanges`, và `await using` bảo đảm rollback khi ném. Đây là phương án (a) tao đề xuất ở F1. **Chưa** kiểm sâu (chưa soi `context` được lấy từ đâu, chưa soi tương tác với `IApplicationDbContext`), nên đừng đọc dòng này là "F1 đã PASS review".
|
||||
|
||||
**(2) Bản đồ trôi dòng.** Mọi anchor **≤ `:659`** vẫn ĐÚNG NGUYÊN (đã kiểm lại: `:529`, `:539`, `:554`, `:575` không đổi) — nghĩa là **toàn bộ bảng 11 đường lách ở F10 vẫn mở được**. Các anchor **sau `:660`** trôi **+21 dòng**:
|
||||
|
||||
| Mốc | Số cũ trong báo cáo này | Số hiện tại |
|
||||
|---|---|---|
|
||||
| gọi `PurgeOrphanOpinionsAsync` | `:683` | `:704` |
|
||||
| `def.Description = …` | `:698` | `:719` |
|
||||
| `changes.Add` đổi phòng ban | `:704` | `:725` |
|
||||
| `SaveChangesAsync` của handler | `:822` | `:843` |
|
||||
| thân `PurgeOrphanOpinionsAsync` | `:886` | `:911` |
|
||||
| `DetachTrackedOpinionsOn` | `:936` | `:961` |
|
||||
| khối changelog (gate module) | `:790` | `:811` |
|
||||
|
||||
Các FLAG **F4, F5, F10a, F10c, F11** trỏ vào vùng sau `:660` ⇒ cộng **+21** khi mở file ở bản hiện tại. Bản chất phát hiện **không đổi** (tao đã đối chiếu bằng nội dung dòng, không chỉ bằng số).
|
||||
|
||||
---
|
||||
|
||||
# TỔNG KẾT
|
||||
|
||||
## Điểm làm ĐÚNG, đáng giữ nguyên
|
||||
|
||||
1. **`CountLiveOpinionsAsync` viết `!p.IsDeleted` TƯỜNG MINH thay vì dựa global query filter.** Tao kiểm `grep HasQueryFilter` toàn thư mục Configurations: chỉ PE và Contract có filter; **5 bảng Office KHÔNG có**. Nếu tác giả dựa vào filter thì PE đúng còn 5 module kia sai âm thầm. Đây là chi tiết dễ sai nhất trong cả file và nó **đúng**.
|
||||
2. **Bảy bảng opinion được quét đủ**, khớp tuyệt đối với 7 cấu hình FK Restrict (F13 Phép 3).
|
||||
3. **Hai cờ đổi-đường-đi xử lý kiểu ECHO** (`bool?`, lệch ⇒ 409, **không bao giờ ghi**) — cộng ca test `F13b_OmittedFinalizeEcho` chốt rằng bỏ trống thì **giữ nguyên**, không bị `?? false` xoá ngầm.
|
||||
4. **`CeoApprovalThreshold` cố tình KHÔNG dùng "null = bỏ qua"**, kèm lý do viết ngay tại chỗ (`:416-422`): null **là** một giá trị có nghĩa. Đây là chỗ 9/10 lần người ta làm sai.
|
||||
5. **Phép thử T1 siết mạnh hơn set-equality** (Order từng dòng bất biến), và có test `F13_SwapStepOrders` chứng minh set-equality một mình sẽ lọt.
|
||||
6. **`F12` đo bằng hành vi thật** với nửa contrast (trước Forbidden / sau duyệt được) — chống xanh-rỗng đúng cách.
|
||||
7. **Migration là dòng thứ 9 của một khuôn đã chạy 4 lần**, không phải cấu trúc mới (F9 Phép 3).
|
||||
8. **Comment tự khai giới hạn** (`:786-789`: 5 module Office chưa có changelog) thay vì giấu. Khai giới hạn đúng chỗ đáng giá hơn một đoạn mã đẹp.
|
||||
|
||||
## Bảng FLAG
|
||||
|
||||
| # | Mức | Nội dung | Vị trí |
|
||||
|---|---|---|---|
|
||||
| F1 | HIGH | `ExecuteDelete` ngoài transaction, trước `SaveChanges` ⇒ mất chữ ký không hoàn tác | `AdminFeatures.cs:683` ⟂ `:822` |
|
||||
| F2 | HIGH | Purge xoá CỨNG chữ ký của phiếu mới **xoá mềm** ⇒ phá "khôi phục phiếu" (đợt 2) | `AdminFeatures.cs:889-915` |
|
||||
| F3 | HIGH | Endpoint PUT **không có dây FE** ⇒ mục tiêu §F.0 không dùng được trong UAT | `Controller.cs:45` ⟂ `…V2Page.tsx:589` |
|
||||
| F4 | MED | 7 cờ ghi tuyệt đối ⇒ payload thiếu field = **tắt cờ im lặng** | `AdminFeatures.cs:424-440`, `:720-726` |
|
||||
| F5 | MED | `DetachTrackedOpinionsOn` quét theo tên cột: bắt thừa + có thể nuốt `Added` | `AdminFeatures.cs:936-949` |
|
||||
| F6 | LOW | `FullName ?? Email` ra null ⇒ 403 lòi dấu phẩy trống / liệt kê thiếu người | `PeWorkflowService.cs:709-714` |
|
||||
| F7 | INFO | Lộ tên người duyệt: **không phải rò rỉ mới** (GET đã công khai tên+email) | `Controller.cs:21-26` |
|
||||
| **F8** | **HIGH** | **2/3 file migration UNTRACKED ⇒ commit `-a` hạ toàn bộ 7 module** | `git status` |
|
||||
| **F9-LOW** | LOW | Tên migration `…Pe…` gây hiểu nhầm (cột dùng chung ≥7 module) | migration filename |
|
||||
| **F10a** | MED | Đổi **Mô tả** quy trình: ghi thẳng, không sinh vết changelog | `AdminFeatures.cs:698` |
|
||||
| **F10b** | MED | Thêm người vào Cấp **đã qua** ⇒ màn hình hiện "Done" cho người chưa ký | `PeFeatures.cs:1135-1147` |
|
||||
| **F10c** | LOW | Vết đổi phòng ban không ghi từ-đâu-sang-đâu | `AdminFeatures.cs:704` |
|
||||
| **F11** | MED | Ca test `F16_…NoPeChangelog` **xanh giả** (0 phiếu PE ⇒ không phân biệt được) | `UpdateAwDefinitionTests.cs:753` |
|
||||
| **F13a** | MED | Vá NỬA — `ContractWorkflowService.cs:264` vẫn in GUID trần | `ContractWorkflowService.cs:264` |
|
||||
|
||||
**TOTAL = 14 FLAG (4 HIGH · 6 MED · 3 LOW · 1 INFO).**
|
||||
|
||||
## Khuyến nghị deploy
|
||||
|
||||
Đánh giá **trên giả định F1/F2/F3 đã được vá xong** (wave 5), và lead tự xác minh lại bằng build + test + grep trước khi đẩy.
|
||||
|
||||
Ngoài F1/F2/F3, **còn đúng một thứ tao coi là chặn deploy: F8** — nó không phải lỗi mã nên vá mã bao nhiêu cũng không chạm tới nó, mà hậu quả thì nặng nhất bảng (mọi module dùng schema V2 ngừng duyệt). Nó chỉ tốn một lệnh `git add` đích danh cộng một lệnh kiểm chứng sau commit.
|
||||
|
||||
Các flag còn lại (F4, F5, F6, F10a, F10b, F11, F13a) **không chặn** đợt 1: F4 và F5 chỉ nổ qua đường PUT mà hiện chưa client nào gọi; F6/F10c/F13a là chất lượng thông điệp; F10b cần thao tác admin bất thường; F11 là nợ test chứ không phải lỗi chạy.
|
||||
|
||||
**DEPLOY-CÓ-RỦI-RO** — mã sẽ an toàn sau khi vá F1/F2/F3, nhưng rủi ro lớn nhất còn lại nằm ở **thao tác commit**: phải `git add` đích danh 2 file migration đang untracked (F8), nếu không prod sẽ có model chờ một cột mà DB không bao giờ được tạo.
|
||||
|
||||
<!-- END reviewer-diff-dot1 · TOTAL=14 FLAG -->
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
# sub-implementer-backend-5 — WAVE 5 BE: vá F1 + F2 (2 FLAG HIGH mất dữ liệu)
|
||||
|
||||
> Ghi TRONG LÚC LÀM, flush từng phần, tự `wc -c` kiểm byte thật.
|
||||
|
||||
- Ngày: 2026-07-27 · run `2026-07-27-S155-pe-delete-approver`
|
||||
- Vật sửa: `src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs`
|
||||
- Nền đo trước wave: `dotnet build` 0W/0E · `dotnet test` 549 PASS / 0 FAIL
|
||||
|
||||
---
|
||||
|
||||
## §0 — ĐỌC NGUYÊN VĂN F1 + F2 (reviewer-diff-dot1.md:33-80)
|
||||
|
||||
F1: `PurgeOrphanOpinionsAsync` (`:886-918`) dùng `ExecuteDeleteAsync` = auto-commit NGAY khi
|
||||
không có transaction ambient. Gọi tại `:681-683`, `SaveChangesAsync` mãi `:822`. 4 nguồn ném
|
||||
nằm GIỮA (`ResolveUserNamesAsync:686` round-trip · `Remove():771` severed-association ·
|
||||
`db.PurchaseEvaluations…ToListAsync:794-800` round-trip · ghi changelog N phiếu trong SaveChanges).
|
||||
Tiêu chí nghiệm thu: test giả lập SaveChanges ném SAU purge ⇒ số row opinion KHÔNG đổi.
|
||||
|
||||
F2: điều kiện purge `:889-891` (+6 khối lặp tới `:915`) rộng hơn nhãn "mồ côi" ở CẢ HAI vế —
|
||||
(a) `!db.PurchaseEvaluations.Any(p => p.Id == … && !p.IsDeleted)` coi phiếu XOÁ MỀM ngang phiếu
|
||||
không tồn tại; (b) `o.IsDeleted` — opinion xoá mềm trên phiếu CÒN SỐNG = bản ghi kiểm toán.
|
||||
Phương án (a) của reviewer: chỉ purge khi phiếu cha thật sự KHÔNG CÒN ROW
|
||||
(`IgnoreQueryFilters().Any(...)`), phiếu xoá mềm thì CHẶN admin như phiếu sống.
|
||||
|
||||
---
|
||||
|
||||
## §1 — ĐO NỀN TRƯỚC KHI SỬA (không tin doc, chạm đĩa)
|
||||
|
||||
| Sự thật | Bằng chứng |
|
||||
|---|---|
|
||||
| `IApplicationDbContext` KHÔNG expose `Database` | `grep -n "Database\|BeginTransaction" src/.../Common/Interfaces/IApplicationDbContext.cs` → **0 hit** |
|
||||
| Handler đã có tiền lệ ép kiểu `((DbContext)db)` | `ApprovalWorkflowV2AdminFeatures.cs:941` (trong `DetachTrackedOpinionsOn`) |
|
||||
| Tiền lệ transaction repo | `ContractCodeGenerator.cs:35-59` — `var context = (DbContext)db; await using var tx = await context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);` + try/Commit/catch/Rollback |
|
||||
| `Microsoft.EntityFrameworkCore.Relational` CÓ trong Application | `SolutionErp.Application.csproj` — `<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" />` |
|
||||
| 7 bảng `*LevelOpinion` KHÔNG có global query filter | `grep -rn "HasQueryFilter" Configurations/` → chỉ 1 hit `PurchaseEvaluationConfiguration.cs:84` (bảng CHA). ⇒ vế `o.IsDeleted` trong purge là THẬT (không phải dead code) |
|
||||
| FK opinion→cha = **Cascade**, opinion→Level = **Restrict** | `PurchaseEvaluationLevelOpinionConfiguration.cs:20-28` |
|
||||
| SQLite fixture BẬT FK enforcement thật | test cũ `UpdateAwDefinitionTests.cs:562-565` assert `DbUpdateException` khi `Remove(level)` có opinion ⇒ FK Restrict fire thật trong SQLite |
|
||||
| Fixture SQLite hỗ trợ transaction | `SqliteDbFixture.cs:36-40` comment + `UseSqlite(shared connection)` |
|
||||
|
||||
### Hệ quả SUY RA TỪ ĐO (quan trọng cho thiết kế)
|
||||
|
||||
FK cha→opinion là **Cascade** ⇒ xoá cứng phiếu cha thì opinion cũng đi theo ⇒ **"mồ côi thật"
|
||||
(cha KHÔNG CÒN ROW) gần như không sinh ra được qua đường ứng dụng**. Nghĩa là sau khi thu hẹp
|
||||
theo F2, `PurgeOrphanOpinionsAsync` gần như là **lớp phòng-vệ-chiều-sâu** cho dữ liệu lệch sinh
|
||||
ngoài luồng (sửa DB tay / import / FK tắt), KHÔNG còn là đường chạy thường ngày. Đúng ý F2:
|
||||
guard chặn HẾT những gì còn cha, purge chỉ dọn thứ đã không thể khôi phục.
|
||||
|
||||
---
|
||||
|
||||
## §2 — VÁ F1 (rào giao dịch) — ĐÃ LAND
|
||||
|
||||
Kỹ thuật chọn: `await using var tx` + `CommitAsync` cuối, **KHÔNG try/catch** — đúng khuôn nhà
|
||||
(`LeaveOtApprovalFeatures.cs:34-44`, `ProposalFeatures.cs:386`, `TravelVehicleApprovalFeatures.cs:822`).
|
||||
Dispose của `IDbContextTransaction` rollback nếu chưa Commit ⇒ **diff là CHÈN-THÊM, 0 dòng thân
|
||||
method bị thụt lề lại** (nếu bọc try/catch thì ~150 dòng phải re-indent = nhiễu review, khó soi).
|
||||
|
||||
- Mở rào: ngay TRƯỚC `PurgeOrphanOpinionsAsync` (thao tác không-hoàn-tác đầu tiên). Guard phía
|
||||
trên chỉ ĐỌC `AsNoTracking` rồi ném, không cần nằm trong rào; mở sớm hơn chỉ kéo dài giữ khoá.
|
||||
- KHÔNG dùng `IsolationLevel.Serializable` như 4 code-generator: ở đó Serializable chống ĐUA
|
||||
cấp-số; ở đây yêu cầu là NGUYÊN TỬ, siết isolation chỉ làm màn quản trị dễ deadlock.
|
||||
- `ExecuteDeleteAsync` tự enlist vào transaction đang mở của context ⇒ 7 câu DELETE nay cùng
|
||||
sống/chết với `SaveChangesAsync`.
|
||||
|
||||
## §3 — VÁ F2 (thu hẹp purge + chặn hồ sơ xoá mềm) — ĐÃ LAND
|
||||
|
||||
Phân hoạch KÍN, không còn kẽ (đây là điểm thiết kế chính):
|
||||
|
||||
| Trạng thái chữ ký | Xử lý sau vá | Ai lo |
|
||||
|---|---|---|
|
||||
| cha SỐNG + ý kiến SỐNG | chặn 409 (thông điệp CŨ, giữ nguyên) | `CountLiveOpinionsAsync` (tầng 1) |
|
||||
| cha XOÁ MỀM (row còn) | **chặn 409 + nêu TÊN hồ sơ** ← MỚI | `ListRetainedOpinionsAsync` (tầng 2) |
|
||||
| ý kiến XOÁ MỀM, cha sống | **chặn 409 + nêu TÊN hồ sơ** ← MỚI | `ListRetainedOpinionsAsync` (tầng 2) |
|
||||
| cha KHÔNG CÒN ROW | purge (xoá cứng) — đường duy nhất còn lại | `PurgeOrphanOpinionsAsync` |
|
||||
|
||||
- Áp CẢ 7 bảng chữ ký (PE · HĐ · Đề xuất · Nghỉ phép · Tăng ca · Công tác · Đơn xe), tầng 2
|
||||
JOIN sang cha để lấy mã hồ sơ: `MaPhieu` · `MaHopDong` · `MaDeXuat` · `MaDonTu`×4.
|
||||
- Guard tầng 2 áp cho **cả `toRemove` lẫn `approverChanged`** — reviewer viết "chặn admin **như
|
||||
phiếu sống**", mà phiếu sống thì cả bỏ-người lẫn đổi-người đều bị chặn. Đổi người trên dòng
|
||||
còn giữ chữ ký của hồ sơ xoá mềm = khi khôi phục, ý kiến người A hoá thành của người B.
|
||||
- `IgnoreQueryFilters()` BẮT BUỘC ở tầng 2 và ở purge: PE/Contract có global filter `!IsDeleted`
|
||||
(`PurchaseEvaluationConfiguration.cs:84`) — không tắt thì hồ sơ xoá mềm biến mất khỏi JOIN và
|
||||
ta lại kết luận "mồ côi" = đúng cái sai đang chữa.
|
||||
- Sửa luôn dòng changelog `"Dọn N chữ ký mồ côi (thuộc phiếu đã xoá)"` → `"(hồ sơ mang chữ ký đã
|
||||
KHÔNG CÒN trong hệ thống)"` — nhãn cũ nay MÔ TẢ SAI việc đang làm.
|
||||
|
||||
### Đo ngay sau khi vá (trước khi đụng test)
|
||||
|
||||
`dotnet build SolutionErp.slnx` → **0 Warning / 0 Error**.
|
||||
`dotnet test --filter UpdateAwDefinitionTests` → **14 PASS / 1 FAIL**. Đúng 1 fail, đúng cái phải
|
||||
fail: `F14_F5a_OpinionBelongsToDeletedPe_RemovalAllowed_AndOrphanPurged` — test này khẳng định
|
||||
CHÍNH hành vi mà F2 gọi là sai ("phiếu xoá mềm ⇒ cho gỡ + purge chữ ký"). Thông điệp thật nhận
|
||||
được: `… còn 1 chữ ký/ý kiến đang LƯU, thuộc: Phiếu duyệt NCC PE-F14b-001 — đã xoá …`
|
||||
⇒ chứng minh 3 việc cùng lúc: guard tầng 2 CHẠY · `IgnoreQueryFilters()` DỊCH ĐƯỢC trên SQLite ·
|
||||
thông điệp NÊU ĐÚNG TÊN hồ sơ. 0 test nào khác đỏ ⇒ vá không lan.
|
||||
@ -0,0 +1,33 @@
|
||||
# sub-implementer-frontend-6 — WAVE 5 · [F-7] nối dây FE cho `PUT /approval-workflows-v2/{id}` (vá F3 HIGH)
|
||||
|
||||
> run `2026-07-27-S155-pe-delete-approver` · WAVE 5 (vá 3 FLAG HIGH trước deploy) · vai `implementer-frontend`
|
||||
> Ghi TRONG LÚC LÀM, flush từng phần (#53 đã ăn 5/5 sub run này). File luôn hợp lệ giữa chừng.
|
||||
|
||||
## 0. F3 nói gì (đọc nguyên văn `reviewer-diff-dot1.md:84-113`, không tóm tắt lại theo trí nhớ)
|
||||
|
||||
- **Đo được, không phải cảm giác:** `grep -rn "api.put" fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` → **0 hit**.
|
||||
3 đường ghi duy nhất của Designer: `api.delete :286` · `api.patch …/user-selectable :297` · `api.post('/approval-workflows-v2') :589`.
|
||||
- **Hệ quả:** ~600 dòng BE mới (`UpdateAwDefinitionCommand` + Validator + Handler + 3 helper) là **mã chết** với người dùng UAT.
|
||||
Owner tick cờ F6 → Lưu → POST → sinh **Id MỚI** → phiếu đang treo pin **Id CŨ** (`PurchaseEvaluationWorkflowService.cs:674` `w.Id == awId`) ⇒ cờ **không bao giờ tới** phiếu cần cứu.
|
||||
- **Reviewer cho 3 đường (a)/(b)/(c); owner đã chốt (a) = nối dây FE trong đợt 1.**
|
||||
- **Tiêu chí nghiệm thu reviewer đặt (`:113`):** tick cờ F6 cho 1 NV trên version đang có phiếu treo → lưu bằng đường "Sửa tại chỗ" → `GET /api/approval-workflows-v2` trả `allowApproverDelete=true` trên **đúng `id` cũ**, `Version` **KHÔNG tăng**.
|
||||
|
||||
## 1. Hợp đồng BE — đọc THẲNG từ mã, không tin MEMORY-PACK
|
||||
|
||||
| Neo | File:line | Sự thật |
|
||||
|---|---|---|
|
||||
| Endpoint | `ApprovalWorkflowsV2Controller.cs:45-51` | `[HttpPut("{id:guid}")] [Authorize(Policy="Workflows.Create")]`, `mediator.Send(cmd with { Id = id })` → **204 NoContent** |
|
||||
| Command | `ApprovalWorkflowV2AdminFeatures.cs:449-464` | `{ Id, Name, Description, Steps, Reason?=null, Code?=null, ApplicableType?=null, CeoApprovalThreshold?=null }` |
|
||||
| Step input | `:442-447` | `Id` **BẮT BUỘC** (Guid, không nullable) · `Order` · `Name` · `DepartmentId` · `Levels` |
|
||||
| Level input | `:424-440` | `Id` **Guid?** (`null` = dòng MỚI) · `Order` · `Name` · `ApproverUserId` · **7 cờ an-toàn** (4×Return, EditDetails, EditBudget, **AllowApproverDelete**) · **2 cờ echo `bool?`** (SkipToFinal, Finalize) |
|
||||
|
||||
**3 bẫy payload (task nêu) — đối chiếu mã, cả 3 đều THẬT:**
|
||||
|
||||
1. **`CeoApprovalThreshold` ECHO NGHIÊM** — `:529` `if (request.CeoApprovalThreshold != def.CeoApprovalThreshold) throw ConflictException`.
|
||||
KHÔNG phải "null = bỏ qua" (khác hẳn `Code`/`ApplicableType` ở `:521`/`:525` dùng `is string`/`is int` nên null mới được bỏ qua).
|
||||
⇒ gửi thiếu ⇒ `null != 5_000_000` ⇒ **409**. Comment BE `:459-463` tự khai lý do: null LÀ giá trị có nghĩa (= bỏ ngưỡng).
|
||||
2. **Chỉ trường hạng AN TOÀN.** Cấm (409): ±Bước (`:539`) · đổi `Step.Order` (`:554`) · đổi `Level.Order` từng dòng (`:575`) · ±Cấp / bỏ trống Cấp (`:618-630`) · `Code` · `ApplicableType` · `CeoApprovalThreshold` · F5 `AllowApproverFinalize` (`:580`) · F2 `AllowApproverSkipToFinal` (`:583`) · bỏ/đổi người đang có **chữ ký sống** (`:658-673`).
|
||||
Cho sửa: thêm/bớt người trong Cấp CÓ SẴN · đổi `ApproverUserId` · 7 cờ quyền-tại-chỗ (gồm **F6**) · tên (workflow/step/level) · `Step.DepartmentId` (`:705-706`, BE **không** kiểm NV có thuộc Phòng hay không).
|
||||
3. **Payload liệt kê tay từng field** (`:605-621` khối POST) — thêm field mà quên dây = **silent drop**, TS xanh, BE default `false`, UI vẫn tick. Đúng bẫy đã xảy ra thật ở `sub-implementer-frontend-2.md §2.4`.
|
||||
|
||||
<!-- phần 2+ ghi tiếp sau khi áp thay đổi -->
|
||||
Reference in New Issue
Block a user