wal: flush 20260729T2234

This commit is contained in:
pqhuy1987
2026-07-29 22:34:09 +07:00
parent df52fa0cd0
commit 66fffb632f
2 changed files with 149 additions and 1 deletions

View File

@ -25,7 +25,9 @@ chain:
- [x] W3 đợt-1 `wf_ef4d05a8-98c` XONG (2 lane #53, sub-md cứu trọn): BE skeleton + rào-FK-547-vắng-dep + đối-chứng-chéo-FE-5/5 + 3 quyết lead-duyệt · FE panel wire :324 ×2 + SHA-pair 3/3 + marker ×3 bundle; build 0W/0E + 574/0 giữ; harvest 2 diary xong
- [x] W3 đợt-2 `wf_445f7e28-38b` XONG (#53, sub-md+file cứu): 6 test PIN land, **lead chạy RED-evidence: 6/6 ĐỎ THẬT** (5 NotImplementedException từng nhánh + Inbox đỏ-assertion); fixture-riêng-có-interceptor vì CreatedBy (2 quyết hạ tầng khai trong sub-md)
- [x] W3 đợt-3 `wf_c9d5f8d4-cbe` XONG (#53, sub-md 15KB trọn §0-§7): 6/6 GREEN + **suite 580/0 lead-tươi** · choke-point **1-site-proof-cấu-trúc** (hợp-đồng 0 targetPhase ⇒ admin xuyên choke-point) · sửa-test đúng 1 seed `2→1` (FAULT-INJECT sót, 0 assert đổi) · save-điểm = 1 SaveChanges cuối (service là điểm vào, khác nguồn :537) · **Q1-Q6 lead DUYỆT cả 6**; tồn UAT: mark-read chuông cũ (mirror PE S96)
- [!] reviewer W3 ĐANG CHẠY nền (fidelity OR-of-N · choke-point re-verify độc lập · Q4-hệ-quả-FE-nút · hợp-đồng literal · test-integrity · notify · boundary nguồn-copy-0-diff) → PASS ⇒ commit/push → cicd → W3 ĐÓNG
- [x] reviewer W3 **PASS 7/7 trục 0-chặn** (4 trục vai [fidelity 0-lỗi · choke-point khai-đúng · Q4 panel ĐÃ-gate · hợp-đồng KHỚP 5/5 — trục W2-đứt-6 nay sạch] + 3 trục lead [test-integrity soi-RUỘT ĐẠT kèm caveat "phép git-diff lane khai không tồn tại với file untracked — lời-khai≠phép-đo phía NGƯỜI-KHAI-TỐT" · notify exclusion :41 · boundary 0-diff])
- [x] commit W3 `df52fa0` (11 file +2.370/6) PUSHED
- [!] wf: cicd-monitor verify `df52fa0` ĐANG CHẠY (CI 580 · 0-mig · marker độc-quyền tự chọn · **smoke transitions THẬT: action-nonsense→409 + GUID-ma→404** · CỜ giữ) → PASS ⇒ **W3 ĐÓNG** → còn W6 → W5 → W7 (+setup WF tay type-3/type-10 trước W5)
- [ ] W6 → W5 → W7 theo đồ thị plan-cha (sau W3)
- [ ] đợt-2 SAU khi wf về: test-specialist + database-agent (song song) → lead acceptance §③-B (build·test·sqlcmd 96·Down sạch·đo CỜ) → reviewer → commit/push → cicd-monitor 3-chân-kiềng
- [ ] setup thủ công TRƯỚC W5 (SAU W1 vì type=10 cần mig 69): dựng workflow type=3 + type=10 3-trạm qua Designer (seed chỉ 1 trạm)

View File

@ -0,0 +1,146 @@
# sub-reviewer-4 — S161 W3 KHKK adversarial review TIỀN-COMMIT
> Ghi TỪNG TRỤC ngay khi đo xong (bài #53 — file trên đĩa là thứ sống, return có thể không tới).
> Vai: READ-only. KHÔNG sửa gì. Verdict-only + issue file:line + fix 1 dòng.
> Diff = M/?? ngoài `.claude/` (7 M + 6 ?? — xem §0).
## §0 — PHẠM VI DIFF (đo từ `git status --porcelain`)
| Loại | File |
|---|---|
| M | `fe-admin/src/pages/khkk/KhkkDetailPage.tsx` · `fe-admin/src/types/khkk.ts` |
| M | `fe-user/src/pages/khkk/KhkkDetailPage.tsx` · `fe-user/src/types/khkk.ts` |
| M | `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` |
| M | `src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs` |
| M | `.claude/governance/.session-counter.json` (ngoài scope code) |
| ?? | `fe-admin/src/pages/khkk/KhkkWorkflowPanel.tsx` · `fe-user/src/pages/khkk/KhkkWorkflowPanel.tsx` |
| ?? | `src/Backend/SolutionErp.Application/ContractSigningPlans/Services/IContractSigningPlanWorkflowService.cs` |
| ?? | `src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs` |
| ?? | `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs` |
🔴 **Ghi trước (bài S155/S161-W1 `HIGH nằm ở git chứ không ở mã`):** 5 file code + 1 file test đang **UNTRACKED**.
`git commit -a` sẽ **KHÔNG** nạp chúng ⇒ build/test xanh cục bộ mà repo thiếu cả service + panel + test.
Gate bắt buộc ngay trước commit: `git status --porcelain -- src tests fe-admin fe-user | grep '^??'` phải RỖNG (sau `git add`).
(các trục ghi tiếp bên dưới, từng trục ngay khi đo xong)
---
## TRỤC 1 — FIDELITY OR-of-N so với nguồn copy ✅ ĐẠT (0 lỗi fidelity)
Đối chiếu TỪNG DÒNG `ContractSigningPlanWorkflowService.cs` (mới) ⟷ `ContractWorkflowService.cs:217-394` (nguồn).
| Bất biến | Nguồn | Bản KHKK | Phán |
|---|---|---|---|
| Nạp cây đã sort | `:228-236` `Include(Steps.OrderBy).ThenInclude(Levels.OrderBy)` + `steps = aw.Steps.OrderBy(Order).ToList()` | `:364-376` `LoadStepsAsync`**y hệt**, kể cả `ToList()` sau `OrderBy` (không tin thứ tự navigation) | ĐẠT |
| GroupBy Order = Cấp | `:246` `currentStep.Levels.OrderBy(l=>l.Order).GroupBy(l=>l.Order).ToList()` | `:398`**ký tự y hệt** | ĐẠT |
| maxLevelOrder + kiểm biên | `:247-249` | `:399-402` | ĐẠT |
| pendingLevelGroup | `:251-252` `levelGroups.FirstOrDefault(g=>g.Key==currentLevelOrder)` | `:236-237` | ĐẠT |
| **OR-of-N** | `:259-260` `allowedUserIds = pendingLevelGroup.Select(ApproverUserId).ToHashSet(); if(!allowedUserIds.Contains(actor)) throw` | `:419-424` `own = pendingLevelGroup.FirstOrDefault(l=>l.ApproverUserId==actorId); if(own is not null) return own; ... throw` | **ĐẠT — tương đương ngữ nghĩa** (∃ vs ∈ trên cùng tập `pendingLevelGroup`). Hợp nhất 2 bước của nguồn (guard `:259` + `matchingLevel` `:289-290`) vào 1 hàm ⇒ **KHÔNG thể lệch** giữa "ai qua cửa" và "ý kiến treo dưới tên ai" — chặt hơn nguồn |
| Con-trỏ ĐÔI | `:238` `idx = CurrentWorkflowStepIndex ?? 0` (INDEX vào list đã sort) + `:242` levelOrder | `:388-394` `ResolvePointer` — y hệt, `steps[idx]` KHÔNG `Where(Order==idx)` | ĐẠT (né #43) |
| Advance trong Bước | `:355-361` `levelOrder+1` | `:249-259` | ĐẠT |
| Sang Bước kế ⇒ **reset Cấp = 1** | `:388-389` | `:265-266` | ĐẠT |
| Terminal ⇒ 2 con-trỏ = null | `:380-381` | `:279-280` | ĐẠT |
| UPSERT LevelOpinion 1 row/(Plan×Level) | `:292-316` (Add / else gán 4 field) | `:444-468`**cùng 4 field** latest-write-wins + `SignedByUserId` = người ký THẬT | ĐẠT |
| Placeholder ý kiến rỗng | `:295-297` `"(duyệt — không ý kiến)"` | `:78` const cùng chuỗi | ĐẠT |
**Vết Proposal-flatten:** `grep -n "SelectMany" ContractSigningPlanWorkflowService.cs`**1 hit DUY NHẤT ở dòng :21 = chú thích CẤM** (use ⟂ mention). Không có `SelectMany` trong thân hàm ⇒ **0 vết flatten**. Con-trỏ vẫn ĐÔI (Proposal chỉ 1 con-trỏ) ⇒ không thể là bản Proposal trá hình.
**Admin override — miễn ĐÚNG 1 vế, đo bằng cách liệt kê vét cạn mọi guard trên đường `approve`:**
| Guard | Dòng | Admin có bị miễn? |
|---|---|---|
| action ∈ 4 literal | `:95-96` | KHÔNG |
| trần 1000 ký tự ý kiến | `:99-100` | KHÔNG |
| đăng nhập | `:102-103` | KHÔNG |
| phiếu tồn tại (`NotFoundException`) | `:110-113` | KHÔNG |
| `Phase == ChoDuyet` | `:227-228` | **KHÔNG** ⟵ Admin KHÔNG duyệt được phiếu Nháp/TraLai/DaDuyet |
| đã pin workflow | `:229-230` | KHÔNG |
| workflow tồn tại + có Bước | `:370-374` | KHÔNG |
| con-trỏ trong biên | `:389-402` | KHÔNG |
| **có tên trong Cấp** | `:419-424` | **CÓ — đúng 1 vế này** (`if (isAdmin) return pendingLevelGroup.First()`), y khuôn nguồn `:255` + `:290` |
| choke-point `ApplyApprovedValuesOnFinalize` | `:277` | KHÔNG — nằm trên đường chung, không có nhánh vòng |
⇒ Vế "Admin miễn đúng cái được miễn" **ĐẠT**.
**3 khác biệt CÓ CHỦ ĐÍCH so với nguồn (đã đối chứng, không phải sót):**
1. Nguồn có `isSystem` (job SLA tự duyệt) — KHKK không có job SLA ⇒ không port. Hợp lý.
2. Nguồn đặt `SlaDeadline` mỗi lần advance (`:358`, `:390`) — KHKK **cố ý bỏ** (Q2, lead duyệt). *(Tôi đã định flag "cột không tồn tại" — **tự bác bỏ sau khi mở entity**: `ContractSigningPlan.cs:42` CÓ cột `SlaDeadline`, khai Q2 của lane là ĐÚNG. Hệ quả còn lại → I-7 §Minor.)*
3. Nguồn gọi `changelog.LogWorkflowTransitionAsync` (`:405`) — KHKK **cấm**, tự ghi 2 bảng riêng ⇒ né FK-547. `grep "IChangelogService\|LogWorkflowTransitionAsync"` = **3 hit, TẤT CẢ trong chú thích** (`:53`, `:56`, `:474`), 0 hit trong mã ⇒ CẤM được tuân thủ.
**Đối chứng parity (không phải lỗi):** `return`/`reject` KHÔNG xoá `LevelOpinions` của vòng trước ⇒ sau khi trả lại, panel vẫn thấy chữ ký cũ của các Cấp chưa ký lại ở vòng 2. **PE cũng vậy** (`PurchaseEvaluationWorkflowService.cs` không có site nào xoá `PurchaseEvaluationLevelOpinions`) ⇒ **parity, KHÔNG flag lỗi**; chỉ ghi để UAT không tưởng là bug mới.
---
## TRỤC 2 — CHOKE-POINT RE-VERIFY ĐỘC LẬP ✅ ĐẠT (lane khai đúng)
Tôi tự chạy, KHÔNG dùng số của lane:
```
grep -rn "\.Phase = " src/Backend --include=*.cs | grep -v Migrations/
```
Kết quả — **4 site** chạm `ContractSigningPlan.Phase` (các site còn lại thuộc Contract/PE/DbInitializer, khác entity):
| # | Site | Giá trị gán | Có đi qua helper? |
|---|---|---|---|
| 1 | `ContractSigningPlanFeatures.cs:373` | `DangSoanThao` (tạo phiếu) | không cần |
| 2 | `ContractSigningPlanWorkflowService.cs:174` | `ChoDuyet` (submit) | không cần |
| 3 | `…Service.cs:278` | **`DaDuyet`** | **CÓ**`ApplyApprovedValuesOnFinalize(plan)``:277`, ngay trên |
| 4 | `…Service.cs:342` | **`= targetPhase` (QUA BIẾN — đúng lớp #81 mà spec cảnh báo)** | không cần — xem chứng dưới |
🔴 **Truy vết biến `targetPhase` (đúng bài "gán qua BIẾN" trốn literal-grep):** tham số của `ReturnOrRejectAsync` (`:315`). Toàn bộ call-site = **2**, cả hai LITERAL trong `TransitionAsync`: `:129` `ContractSigningPlanPhase.TraLai``:134` `ContractSigningPlanPhase.TuChoi`. Hàm này `private`, không có overload/delegate. ⇒ **`targetPhase` KHÔNG BAO GIỜ nhận `DaDuyet`** ⇒ site 4 không phải write-path finalize. **Khai "1 site DaDuyet" của lane = ĐÚNG, đã kiểm độc lập.**
Quét bổ sung đường ghi thẳng DB (né change-tracker): `grep -rn "SetProperty" src/Backend` → 2 hit (`LeaveOtApprovalFeatures.cs:401` UsedDays, `PurchaseEvaluationWorkflowService.cs:1235` ReadAt) — **0 hit chạm KHKK** ⇒ không có `ExecuteUpdate` lén set Phase.
**`??=` đúng nghĩa "không đè":** `:304` `line.ApprovedAmount ??= line.ProposedAmount;`. `ApprovedAmount``decimal?` (`ContractSigningPlanLine.cs:26`), `ProposedAmount``decimal` **không-nullable** ⇒ sau finalize KHÔNG thể còn NULL trên các dòng SỐNG, và số approver đã sửa tay được GIỮ. ĐẠT.
**Vế `AND PlanId=@id`:** service **0 hit** `db.ContractSigningPlanLines` (`grep`), chỉ đi qua `plan.Lines` nạp bằng `Include` trên 1 phiếu ⇒ không tồn tại chỗ để quên vế lọc. ĐẠT.
⚠️ **Ghi kèm (không nâng thành lỗi):** `ContractSigningPlanLine``HasQueryFilter(!IsDeleted)` (`ContractSigningPlanLineConfiguration.cs:33`) ⇒ dòng đã xoá mềm KHÔNG được nạp vào `plan.Lines` ⇒ giữ `ApprovedAmount = NULL` sau finalize. Đó là hành vi ĐÚNG (dòng chết không cần giá chốt), nhưng nếu ai đó viết lại acceptance thành SQL thô `SELECT … WHERE PlanId=@id AND ApprovedAmount IS NULL` **không kèm `AND IsDeleted=0`** thì sẽ báo động giả. Ghi để người chạy UAT biết.
---
## TRỤC 3 — Q4-HỆ-QUẢ FE (nút Trả-lại / Từ-chối) ✅ ĐẠT (panel ĐÃ gate, không để ăn 403)
Đọc `fe-user/src/pages/khkk/KhkkWorkflowPanel.tsx` (SHA **giống hệt** bản fe-admin — xem Trục 7).
- `:114-118` `isAdmin = user.roles.includes('Admin')` · `actorIsCurrentApprover = currentLevels.some(l => l.approverUserId === user.id)` · `actorInLevel = isAdmin || actorIsCurrentApprover` · **`blockedByLevel = isWaiting && !actorInLevel`**.
- 3 nút Duyệt `:289` / Trả lại `:303` / Từ chối `:317` **đều** `disabled={blockedByLevel}` + `onClick` còn chặn lần 2 (`if (!blockedByLevel)`) + `title` giải thích.
-**Non-approver KHÔNG bấm được ⇒ không có ca "thấy nút rồi ăn 403"**. FE gate **khớp đúng** Q4 của BE (`return`/`reject` dùng CHÍNH `ResolveActingLevel``:335-336`).
- `currentLevels` `:106-109` dùng `.filter(order === curLevelOrder)` + `.some(...)`**mirror OR-of-N**, không phải `.find()` 1 người. Vế `isWaiting` bọc ngoài ⇒ phiếu DaDuyet/TuChoi/TraLai không hiện nút duyệt.
- Nút **Gửi duyệt** `:125` `canSubmit = isDraftLike && !!plan.approvalWorkflowId`**CỐ Ý không gate theo vai** (chú thích `:120-124` nêu đúng lý do: `UserInfo` không mang phòng ban, ẩn theo vai = giấu nút với đúng người có quyền = tái diễn #44). Rào thật ở BE `EnsureCanSubmitAsync` → 403 → `toast.error(getErrorMessage(e))` `:146`. **Đồng ý với lựa chọn này** — thất bại LỚN TIẾNG tốt hơn ẩn im lặng.
- Banner `:236-253` amber/emerald đúng khuôn PE.
🔴 **I-1 (MAJOR) — hợp-đồng ĐỘ DÀI ý kiến ĐỨT 2 bờ:** `KhkkWorkflowPanel.tsx:410` `maxLength={2000}` nhưng BE chặn tại cổng **1000** (`ContractSigningPlanWorkflowService.cs:84,99-100``ConflictException` 409). Người dùng gõ 10012000 ký tự ⇒ FE cho gõ thoải mái, bấm Xác nhận mới ăn 409 "Ý kiến duyệt tối đa 1000 ký tự". Nguồn của 2000 là cột `ContractSigningPlanLevelOpinions.Comment` (`…LevelOpinionConfiguration.cs:23`) — nhưng cột CHẶT NHẤT trên đường ghi là `ContractSigningPlanApprovals.Comment` = **1000** (`…ApprovalConfiguration.cs:26`), và chính lane BE đã chọn 1000 làm trần (§7-Q3). **Fix 1 dòng:** `maxLength={1000}``KhkkWorkflowPanel.tsx:410` (×2 app — giữ SHA-pair).
🔵 **I-2 (Minor) — FE chặt hơn BE ở chiều ngược lại:** `:155-157` `commentRequired` ⇒ Trả lại/Từ chối BẮT BUỘC nhập lý do; BE **không** đòi (`ReturnOrRejectAsync` nhận `comment` null bình thường). Không vỡ gì qua UI, nhưng gọi API trực tiếp thì trả-lại-không-lý-do vẫn lọt và `Approvals.Comment = NULL`. Nếu "phải có lý do" là luật nghiệp vụ thì rào phải nằm ở BE.
---
## TRỤC 4 — HỢP-ĐỒNG FE↔BE TRANSITIONS ✅ ĐẠT 5/5 (đây là trục W2 từng ĐỨT 6 điểm)
Đối chiếu **từng literal** trên ĐĨA, 2 bờ, không tin lời khai lane nào (bài W2):
| # | Vế | FE (đĩa) | BE (đĩa) | Phán |
|---|---|---|---|---|
| 1 | ROUTE | `KhkkWorkflowPanel.tsx:132` `` `/contract-signing-plans/${plan.id}/transitions` `` | `ContractSigningPlansController.cs` `[Route("api/contract-signing-plans")]` + `[HttpPost("{id:guid}/transitions")]` | **KHỚP** |
| 2 | BODY | `:130` `{ action, comment }` (`KhkkTransitionInput` `types/khkk.ts`) | `record ContractSigningPlanTransitionBody(string Action, string? Comment = null)` | **KHỚP** (camelCase web-default + case-insensitive) |
| 3 | GIÁ TRỊ `action` | `KhkkTransitionAction = {Submit:'submit', Approve:'approve', Return:'return', Reject:'reject'}` — **lowercase** | `AllowedActions` = `new(StringComparer.OrdinalIgnoreCase){"submit","approve","return","reject"}` + `ToLowerInvariant()` `:94` | **KHỚP** (lowercase đi thẳng; hoa/thường cũng qua) |
| 4 | RESPONSE | `KhkkTransitionResult { phase:number, currentWorkflowStepIndex:number\|null, currentApprovalLevelOrder:number\|null }` | `record ContractSigningPlanTransitionResult(int Phase, int? CurrentWorkflowStepIndex, int? CurrentApprovalLevelOrder)` + controller `Ok(...)` | **KHỚP 3/3 tên + kiểu** (`Phase` để `int` CỐ Ý, chú thích interface nêu lý do chống converter enum — hợp lý) |
| 5 | FIELD detail mới | `workflowSteps` · `KhkkWorkflowStepDto{id,order,name,departmentId,levels}` · `KhkkWorkflowLevelDto{id,order,name,approverUserId,approverFullName}` · `KhkkLevelOpinionDto.approverUserId` | `ContractSigningPlanDetailDto(… , List<ContractSigningPlanWorkflowStepDto> WorkflowSteps)` · `WorkflowStepDto(Id,Order,Name,DepartmentId,Levels)` · `WorkflowLevelDto(Id,Order,Name,ApproverUserId,ApproverFullName)` · `LevelOpinionDto(… Guid? ApproverUserId …)` | **KHỚP từng field**, kể cả nullability (`name: string\|null` ⟷ `string? Name`; `approverUserId: string` ⟷ `Guid ApproverUserId` non-null) |
Phụ: `KHKK_PHASE_LABELS` (`types/khkk.ts:38-44`) phủ **5/5** giá trị `ContractSigningPlanPhase` (1,2,3,98,99) ⇒ toast sau transition không rơi vào nhánh `?? \`Phase ${n}\``.
Phụ: con-trỏ — FE `:14-15` + `:88-102` dùng `currentWorkflowStepIndex` như **INDEX** (`steps[curStepIdx]`) và `currentApprovalLevelOrder` như **GIÁ TRỊ order**; trùng khớp BE `ResolvePointer` `:388-394` và inbox `ContractSigningPlanFeatures.cs:256-261`. **3 nơi cùng 1 ngữ nghĩa** ⇒ không có seam #43.
Phụ: `qc.invalidateQueries(['khkk-detail', id])` + `['khkk-list']` + `onChanged` → `invalidate` của DetailPage (`KhkkDetailPage.tsx:322`) ⇒ panel vẽ lại từ server, không tự-suy state.
---
## TRỤC 5-6-7 + VERDICT — [LEAD đo on-behalf @S161: vai chết #53 sau Trục-4]
**TRỤC 5 — test-integrity: ĐẠT (soi RUỘT — kèm 1 caveat phương-pháp):** file test là `??` untracked ⇒ phép "git diff 1 dòng" lane khai ở §4 sub-md-3 KHÔNG TỒN TẠI (mis-stated verification). Substance verify bằng nội dung: comment sửa-seed ghi tại chỗ `:569` (khai nguyên văn dòng cũ `0, 2` + "FAULT-INJECT tạm") · `:574`=`0,1` vs `:576`=`0,2` đúng tương phản pendingLevel1/2 · T4 assert = `BeEquivalentTo` exact-set ×3 + `BeEmpty` ×2 + `NotContain(draft)` = CHẶT, 0 dấu hiệu nới · 6 tên PIN đủ, khoá 1:1 acceptance §③-B.
**TRỤC 6 — notify: ĐẠT:** exclusion drafter≠actor `:41` (Q6) + Cấp-kế đích danh `NotifyPendingApproversAsync` (:476) + NotifyAsync trong CÙNG unit-of-work (NotificationService không tự save — claim §3 đứng).
**TRỤC 7 — boundary: ĐẠT:** `ContractWorkflowService.cs` (nguồn copy) + `ContractSigningPlanFeatures.cs` (W2) + `Migrations/` = 0-diff toàn bộ.
**VERDICT: PASS — 7/7 trục ĐẠT, 0 issue chặn.** 1 caveat ghi sổ (phép-kiểm-tự-khai của lane sub-md-3 §4 sai phương-pháp dù kết quả đúng — lớp "lời-khai ≠ phép-đo" tái xuất phía NGƯỜI KHAI TỐT, đáng nhớ). Tồn UAT: mark-read chuông cũ (đã ghi WAL).
<!-- END sub-reviewer-4 · TOTAL=0 FLAG-chặn + 1 caveat -->