wal: flush 20260801T0855

This commit is contained in:
pqhuy1987
2026-08-01 08:55:22 +07:00
parent bcb33322b9
commit 4163f42fd2
2 changed files with 99 additions and 1 deletions

View File

@ -26,4 +26,72 @@ không có nhánh "sửa 1 app quên app kia". ✅
(cùng bảng, cùng row mà `ResolveActingLevel` sẽ chấm lúc duyệt) ⇒ không có nguồn thứ hai.
- 0 đổi logic BE: `git diff` trên `src` chỉ 2 hunk, cả 2 nằm trong DTO + projection. ✅
(các mục dưới điền dần)
## §2 (đề bài 1) `actingLevel` FE có mirror ĐÚNG `ResolveActingLevel` không
BE (`ContractSigningPlanWorkflowService.cs:463-468`):
`own = pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId)``if (own is not null) return own;`
`if (isAdmin) return pendingLevelGroup.First();` → còn lại **ném Forbidden**.
FE (`KhkkWorkflowPanel.tsx:135-137`):
`currentLevels.find(l => l.approverUserId === user?.id) ?? (isAdmin ? currentLevels[0] : undefined)`.
| Ca | BE chấm level nào | FE chấm level nào | Khớp? |
|---|---|---|---|
| Actor ∈ Cấp, cấp CÓ cờ | own (có cờ) | own (có cờ) → hiện ô-tích | ✅ |
| Actor ∈ Cấp OR-of-N, **cờ ở NGƯỜI KHÁC cùng Cấp** | own (KHÔNG cờ) ⇒ không finalize | own (KHÔNG cờ) ⇒ ẩn ô-tích, gửi `true` = no-op | ✅ (ca đề bài hỏi kỹ — không rò) |
| Admin **có** slot riêng trong Cấp | own | own (vì `find` chạy TRƯỚC nhánh isAdmin) | ✅ |
| Admin **không** có slot | `pendingLevelGroup.First()` | `currentLevels[0]` | ⚠️ xem §6 (thứ tự 2 truy vấn) |
| Không phải approver, không Admin | **Forbidden 403** | `undefined` ⇒ ẩn ô-tích | ✅ (và nút Duyệt đã `disabled` bởi `blockedByLevel` `:130`) |
| `currentLevels` **RỖNG** (phase ≠ ChoDuyet, hoặc con-trỏ null, hoặc idx ngoài biên) | không tới `ApproveV2Async` (409 `:250-251`) | `find`→undefined, `currentLevels[0]``undefined` ⇒ eligible=false | ✅ **không nổ** (index-out-of-range trên mảng rỗng trong JS = `undefined`, không throw) |
| `user` chưa nạp (`user?.id` undefined) | — | `find` không khớp (`approverUserId``string` non-null) ⇒ undefined; `isAdmin`=false ⇒ nút Duyệt disabled | ✅ |
Vế **so-khớp GUID dạng chuỗi**: `user.id` sinh từ `res.data.user` (`AuthContext.tsx:53-56`, DTO login
serialize `Guid` → chữ thường có gạch), `approverUserId` cũng là `Guid` serialize cùng kiểu ⇒ `===`
hợp lệ; và đây là **cùng phép so đã sống từ W3** (`:127` `actorIsCurrentApprover`), không phải phép mới.
**Mirror ĐÚNG 6/7 ca**; ca thứ 7 (Admin duyệt-thay trên Cấp **trộn cờ**) là finding §6/G-4.
## §3 (đề bài 2) Body — rò field ở action khác? khớp hợp-đồng `bool?`?
`KhkkWorkflowPanel.tsx:151-157`:
`{ action, comment, ...(a === Approve ? { applyLevelFinalize: eligible ? state : true } : {}) }`
| Action | Key có trong JSON? | Giá trị | BE nhận | Ảnh hưởng |
|---|---|---|---|---|
| `submit` | **KHÔNG** | — | `ApplyLevelFinalize = null``?? true` (`Controller:213`) | `SubmitAsync` **không nhận tham số** (`Service:122`) ⇒ vô hại |
| `return` / `reject` | **KHÔNG** | — | như trên | `ReturnOrRejectAsync` **không nhận tham số** (`:130-136`) ⇒ vô hại |
| `approve`, cấp thường | CÓ | `true` | `true` | `actingLevel.AllowApproverFinalize && true` = **false** ⇒ no-op (§4) |
| `approve`, cấp có cờ, giữ tick | CÓ | `true` | `true` | finalize (đúng ý người bấm) |
| `approve`, cấp có cờ, bỏ tick | CÓ | `false` | `false` | rơi xuống advance thường ⇒ trình tiếp (đúng mục tiêu F-1) |
**0 rò field** sang action khác; và ngay cả khi rò cũng không có chỗ đọc ở BE (3 nhánh kia không có tham số).
**absent vs null vs true** — hợp-đồng `ContractSigningPlanTransitionBody(string Action, string? Comment = null, bool? ApplyLevelFinalize = null)`:
key vắng ⇒ System.Text.Json để `bool?` = `null` (bằng ĐÚNG giá trị mặc định khai báo, nên **không phụ thuộc**
vào chuyện STJ có tôn trọng default-parameter-value của positional record hay không — cả hai đường đều ra `null`)
`?? true`. FE **không bao giờ** gửi `null` tường minh (spread bỏ hẳn key, không set `undefined`).
⇒ 3 trạng thái absent/null/true hội tụ về CÙNG một hành vi. ✅ Khớp hợp-đồng.
## §4 (đề bài 3) Regression cấp thường + rò ô-tích ở phase khác
**Cấp thường (không cờ), trước ⟂ sau vá:**
- trước: body `{action, comment}` ⇒ BE `?? true``applyLevelFinalize=true`
- sau: body `{action, comment, applyLevelFinalize: true}` ⇒ BE `true`
**cùng một giá trị đi vào cùng một biểu thức** `if (actingLevel.AllowApproverFinalize && applyLevelFinalize)`
(`Service:276`), mà vế trái = `false` ⇒ nhánh không vào ở CẢ HAI thế giới. Hành vi **y hệt**, không phải
"giống về mặt cảm tính". ✅
(Điểm yếu còn lại là của thiết kế BE, không phải của diff: no-op này chỉ đúng vì BE **AND** với cờ cấp —
FE gửi `true` cho người không có quyền finalize là dữ liệu thừa, không phải quyền thừa.)
**Rò ô-tích ở phase khác — 3 lớp chặn ĐỘC LẬP, hỏng 1 lớp vẫn không rò:**
1. `currentLevels` chỉ khác `[]` khi `isWaiting` (`:118-121`) ⇒ DaDuyet/TraLai/TuChoi/Nháp ⇒ `actingLevel=undefined``approverFinalizeEligible=false`.
2. Nút "Duyệt" (đường DUY NHẤT đặt `action=Approve`) chỉ render trong `{isWaiting && (…)}` (`:312-335`).
3. Chính JSX ô-tích đòi `action === Approve` (`:443`).
**0 rò**. Kiểm chéo: `KhkkTransitionAction.Approve` được gán ở đúng 1 site (`:323`), grep 2 app cho thấy
không có site thứ hai. ✅
**Reset trạng thái:** `setApplyLevelFinalize(true)` đặt ở `onClick` mở dialog (`:322`) ⇒ bỏ tick rồi **Huỷ**
rồi mở lại vẫn về mặc định. (PE chỉ reset trong `onSuccess` `:288` ⇒ PE **giữ** tick sau khi Huỷ — chỗ này
KHKK **chặt hơn** khuôn.) ✅

View File

@ -42,3 +42,33 @@ Wave K1→K3 (`50e6d8c`/`0779f2d`/`15349e8`, S164-S165) cập-nhật **dòng :6*
**resolve:** re-ground 5 ô `:465/:466/:470/:471/:472` về 71 / 97 / 256 / 64 / 614 (bao gồm sub-số `45D+569I`), rồi chạy lại detector — kỳ vọng FLAG `SKILL.md:93` biến mất và TOTAL đổi.
---
## FLAG-2 — `view-stale-count` — HIGH
**view:** `docs/STATUS.md:479` — chuỗi THẬT: `**Bundle hash live (prod):** admin **\`B43R6Y17\`** · user **\`NLI5umBg\`** … **Đo LIVE bằng curl 2026-07-31 @S164**`
**source:** `.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k3.md:86,88` — POST-deploy đo LIVE 31/07 18:26-18:27 +07: admin `index-mySTlx42.js` **ROTATE** · eoffice `index-CZAYiWWa.js` **FROZEN**
Lệch: view nói `B43R6Y17`/`NLI5umBg`, source nói `mySTlx42`/`CZAYiWWa`. **0/2 khớp.**
🔴 Nặng gấp đôi vì hai lẽ:
1. `docs/STATUS.md:6` cố ý **KHÔNG chép** số bundle mà trỏ *"canonical ở `:479`"* (đúng B1) ⇒ ai tuân B1 sẽ đáp thẳng vào ô sai. Đây là **con trỏ tốt tới nội-dung hỏng** — nguy hơn số rời rạc vì nó được cả hệ tin.
2. Chính vai cicd **đã nói ra** rồi mà chưa ai lật sổ: `sub-cicd-verify-k3.md:93-94` viết nguyên văn *"Đính chính mốc PRE trong đề bài: spec giao việc ghi baseline `admin B43R6Y17 · user NLI5umBg (@S164)` — KHÔNG khớp gì đo được"*. Phát-hiện đã tồn tại **trên đĩa**; khoảng cách nằm ở chỗ nó chưa đi từ run-folder về sổ owner-facing (đúng lớp bài S162: *nơi lead làm việc ≠ nơi anh đọc*).
Ghi chú danh-pháp (đừng lẫn khi vá): `:479` gọi app thứ hai là **user**, cicd gọi **eoffice** — cùng site, khác tên. Nếu vá thì thống nhất một tên, kẻo sinh vocab-fork mới.
**resolve:** `:479` = admin `mySTlx42` · user/eoffice `CZAYiWWa` (FROZEN, không phải ship-fail — cicd §5 dự-đoán TRƯỚC khi đo), kèm mốc 2026-07-31 18:26 +07 sau run #436. Hết flag khi hash ở `:479` khớp lần curl mới nhất.
---
## FLAG-3 — `view-stale-count` — LOW
**view:** `docs/STATUS.md:6` — chuỗi THẬT: `counter **39**`
**source:** `.claude/governance/.session-counter.json` → `counter = 40` (`last_ticked_head` = `24ea71e2…` = HEAD hiện tại, session S166; detector `H24-5` xác nhận `[OK] OK-reachable`)
Lệch 39 vs 40 (1). SEV LOW **có điều kiện**: đây là stale **biết-trước** giữa phiên (S166 vừa tick, STATUS được re-stamp ở closeout). Ghi FLAG để tally by-class không mất mẫu, KHÔNG phải để đòi vá ngay.
🔴 Nhưng đừng đọc thành vô hại: `counter` là **đầu vào của chính nhịp H24** (`h24_cadence.deep_every`). Một sổ owner-facing trình sai counter làm owner không thấy được món nợ `deep` 15/15 mà chính lượt này đang trả. Nếu closeout quên bump, LOW này lặng lẽ sống tiếp.
**resolve:** re-stamp `counter 40` ở closeout S166; hoặc bỏ hẳn con số khỏi `:6` và trỏ khoá `.session-counter.json` (kiểu B1 mà `:6` đã dùng đúng cho bundle).
---