127 lines
11 KiB
Markdown
127 lines
11 KiB
Markdown
# implement-synthesis — S155 · PE nút XÓA phiếu ở màn DUYỆT (approver-side)
|
||
|
||
> **run-id:** `2026-07-27-S155-pe-delete-approver` · phiên-LOGIC L7, window 2
|
||
> **Viết:** 2026-07-29 (retro-harvest @S159) — 🔴 **KHAI THẲNG: file này viết SAU, không phải ghi trong lúc làm.**
|
||
> Nguồn = artifact trên đĩa của chính run này + prod-state đã đo. Mọi con số dưới đây **derive từ file cụ thể**, không từ trí nhớ.
|
||
> Tiền lệ retro-harvest: `22ea8c5` (đã làm đúng việc này cho 6 folder S112–S114).
|
||
|
||
---
|
||
|
||
## 0. Vì sao file này tồn tại (và vì sao nó ra muộn 2 ngày)
|
||
|
||
Run này **đã hoàn tất và đã ship prod**, nhưng thiếu file `*-synthesis.md` phẳng ⇒ Sàn-3 tín-hiệu ① xếp nó vào **orphan run-folder** ở mọi lần bootstrap sau đó. Đây đúng **kẽ dạng-4** mà [`tiep.md §0`](../../commands/tiep.md) khai sẵn: *"run.md kể kết-quả mà không có heading synthesis nào"* — máy đo **tên file**, không đo **đã harvest chưa**.
|
||
|
||
Anh chốt @S159: *"nối mạch tiếp cho xong hẳn rồi bootstrap"* ⇒ đóng nợ bằng cách viết thật, không dán nhãn.
|
||
|
||
---
|
||
|
||
## 1. Đề bài (nguyên văn UAT, không diễn giải)
|
||
|
||
Phiếu `PE/2026/A/046` · dự án FLOCK03 · hạng mục MAT-16 · trạng thái **Đã gửi duyệt** · cờ GẤP(PRO). Khối HÀNH ĐỘNG chỉ có `✓ Duyệt` · `← Trả lại` — **thiếu Xóa**.
|
||
|
||
**Tra Sol (user):** *"Chỗ duyệt - tạo giúp em nút xóa với"* · *"mấy bạn bắt sai cái gói thầu"* · *"phải xóa thì nó mới ko có lũy kế lên"*
|
||
**Anh (owner):** *"Hic hôm trước có cái chỗ cho huy / mọi người nói là ko cần cái đó / giờ lại cần"*
|
||
|
||
🔴 **Đây là lỗi SỐ LIỆU, không phải UX.** `ComputePendingAsync` (S134) tính phiếu `ChoDuyet` có winner vào **lũy kế TẠM TÍNH** ⇒ phiếu bấm sai gói thầu đang treo ở bước duyệt **đang ăn ngân sách hạng mục sai**. `Trả lại` đẩy về `TraLai=98` chứ **không triệt tiêu** ⇒ số vẫn lệch. Đúng thứ user mô tả bằng chữ *"lũy kế lên"*.
|
||
|
||
---
|
||
|
||
## 2. Owner chốt — 3 lượt, 13 quyết định
|
||
|
||
| Lượt | # | Câu | Anh chốt |
|
||
|---|---|---|---|
|
||
| 2 | (5) | Quyền xóa gắn vào đâu | Cờ **`AllowApproverDelete` per-Cấp-duyệt** — **bác** `Department.ManagerUserId` |
|
||
| 2 | (6) | Màn "Đã xóa" | **CHỈ XEM**, chưa làm khôi phục |
|
||
| 2 | (7) | Sửa quy trình duyệt | *"thêm người/điều chỉnh quyền → **KHÔNG** cần tạo mới; đổi **quy trình** mới bắt buộc tạo mới"* |
|
||
| 3 | (8) | Xóa nháp vs xóa-khi-duyệt | **TÁCH 2 quyền riêng** |
|
||
| 3 | (9) | Cờ F6 per-người hay per-cấp | **PER-NGƯỜI** (`matchingLevel`) |
|
||
| 3 | (10) | `AllowApproverFinalize` | **PHÁ VỠ** |
|
||
| 3 | (11) | `CeoApprovalThreshold` | **PHÁ VỠ** |
|
||
| 3 | (12) | `Step.DepartmentId` | **AN TOÀN** |
|
||
| 3 | (13) | Cách chạy hmw | **CHIA 2 ĐỢT — F trước, A–E sau** |
|
||
|
||
🔴 **Khai trung thực về (5):** quyết này **đổi nghĩa** so với chữ đầu *"người đứng đầu phòng"* → thành *"cấp nào admin tick"*. Owner chọn **với trade-off đặt rõ trên bàn** — KHÔNG phải lead hiểu nhầm rồi hợp thức hoá sau.
|
||
|
||
🎁 **Lời giải gọn cho (8)** (giữ vì nó là lý do `Menu keys=54`/`Policies=216` **không đổi**): đường xóa **nháp GIỮ NGUYÊN không policy** (Drafter không mất gì) · đường xóa-khi-duyệt = **endpoint RIÊNG** mang policy **root có sẵn** `PurchaseEvaluations.Delete` ⇒ **0 key mới**.
|
||
|
||
---
|
||
|
||
## 3. Kết quả từng stage (tick theo ĐĨA, không theo ledger cũ)
|
||
|
||
| Stage | Verdict | Artifact |
|
||
|---|---|---|
|
||
| S1 — 2 Invest BE/FE | ⚠️ **cả 2 dính `#53`**, cứu từ đĩa + SendMessage-resume làm nốt | `sub-invest-be-1.md` 31.371 B · `sub-invest-fe-2.md` 22.074 B |
|
||
| S1-bis — Invest #3 (menu · "Đã xóa" · "người đứng đầu phòng") | ✅ | `sub-invest-menu-3.md` 21.748 B |
|
||
| S1-ter — Invest #4 (workflow versioning) | ✅ **25 phát hiện** | `sub-invest-wfver-4.md` 29.479 B |
|
||
| S2 — spec + checklist | ✅ **37.490 B**, 2 phần, ~50 mục | `spec-pe-delete-approver.md` |
|
||
| S3 — review spec | **PASS-WITH-FLAGS — 17 FLAG (4H/8M/5L)** · F.0 **không bác được** sau 6 đường tấn công · lead vá 4H+8M vào spec | `reviewer-spec-review.md` 28.259 B |
|
||
| S4-đợt1 — hmw 5 wave (schema F6 + Mig) | ✅ **DEPLOY `b1bae77` · CICD PASS 6/6** | `cicd-verify-dot1.md` · `reviewer-diff-dot1.md` 47.113 B |
|
||
| Đ2-W1 — BE | ✅ xóa-by-approver + list deleted + menu seed + changelog | `sub-d2-be-1.md` |
|
||
| Đ2-W2 — FE | ✅ nút xóa + 3 mục menu + màn "Đã xóa" | `sub-d2-fe-2.md` · `sub-d2-fe-3.md` |
|
||
| Đ2-W3 — test + review + deploy | reviewer **FAIL 12 FLAG** (H1 = DTO thiếu field) → lead vá → **DEPLOY `4464a46`** | `reviewer-diff-dot2.md` 51.970 B · `sub-d2-test-4.md` |
|
||
| Fix-nhanh UAT | ✅ **`f4494cf`** — click "Danh sách" lại highlight "Đã duyệt" | xem §4 |
|
||
|
||
**Phân đợt vì sao:** §F.0 chứng minh A–E **không chạy được** nếu thiếu F ⇒ đợt 1 xong là UAT được ngay, biết chắc nền đúng trước khi xây tiếp; blast-radius ≥6 module nên tách để truy lỗi dễ.
|
||
|
||
---
|
||
|
||
## 4. 🔴 ĐÍNH CHÍNH ledger — `f4494cf` ĐÃ được verify
|
||
|
||
`run.md:173` ghi: *"**cicd verify cho commit này CHƯA chạy**"*. **Dòng đó STALE.** Đối chứng 2 nguồn độc lập:
|
||
|
||
| Nguồn | Bằng chứng |
|
||
|---|---|
|
||
| `cicd-verify-s157-diadiem.md:38` | bảng pre-state: `id` **531** · Run **#418** · `f4494cfe` · **success** · 07-27 14:36:54→14:42:35 |
|
||
| `cicd-verify-s157-diadiem.md:58` | bundle `BK8w3QxU` / `DYnxSI0T` **khớp chính xác baseline STATUS S156**, `Last-Modified` trỏ đúng run #418 |
|
||
| `changelog/sessions/2026-07-28-0930-*.md:17` | **cicd PASS 6/6** — lead tự đo @S156: endpoint 2/2 → 401 (control bịa 404 chứng phép đo có phân biệt) · smoke 8/8 · **D3 giao-tập-ID = 0** (37 sống ⟂ 9 xóa) · bundle rotate ×2 + 4/4 byte-marker |
|
||
|
||
⚠️ **Giới hạn khai thẳng — đừng đọc quá:** KHÔNG có artifact `cicd-verify` **riêng** cho `f4494cf`. Cái tồn tại là (a) Gitea Run #418 **success** ghi trong bảng pre-state của lượt sau, và (b) phép đo 6/6 của **lead** ghi trong session log. Hai vết độc lập, đủ để lật dòng ledger — **không đủ** để nói "cicd-monitor đã chấm riêng commit này".
|
||
|
||
🔸 **Errata phụ (nhỏ, không load-bearing):** session log `:17` viết *"Run #531 success 5m41s"* — số Run thật là **#418**; `531` là **`id` (task-id nội bộ Gitea)**, cột đầu của bảng `:35-38`.
|
||
|
||
> 🔴 **ERRATA-CỦA-ERRATA @S159 (H2 `harvest-curator` F-02 bắt — lead ghi on-behalf):** bản đầu của chính dòng này gọi `531` là **"test-count"** — **SAI**. Header bảng `cicd-verify-s157-diadiem.md:35` = `| id | run# | head_sha | status | created | updated |`; dãy `id` 529/530/531 ↔ run# 416/417/418 **liên tục, khoá chặt** (đối chứng `cicd-verify-dot1.md:40` *"id=529 · run_number=#416"* + `cicd-verify-dot2.md:41` *"Run #417 (task id=530)"*). Test-count thời điểm đó = **532** (§7 file này), **không phải 531** — lệch đúng 1 nên suy-diễn sai nghe rất lọt tai.
|
||
> ⭐ **Mỉa mai đáng giữ:** errata này kết bằng *"đọc bảng đúng cột"* trong khi **chính nó đọc sai cột**. Phần SỬA (Run thật = #418) vẫn ĐÚNG và load-bearing; hỏng ở **lời giải thích**. Đây là mislabel, không phải bịa — và là lý do vòng kiểm độc lập tồn tại.
|
||
|
||
---
|
||
|
||
## 5. Ràng buộc CỨNG — thứ đáng mang sang việc sau
|
||
|
||
Đây là phần **tái sử dụng được**, không phải sử ký:
|
||
|
||
- **7a** — đường xóa **nháp** GIỮ NGUYÊN không policy; gắn vào là `Drafter` mất quyền xóa nháp của chính mình (**đo thật: 11/13 role `CanDelete=0`**)
|
||
- **7c** — seeder `:2120-2126` **skip-if-exists** và **không bao giờ chạm `CanDelete`** ⇒ sửa seeder **KHÔNG vá được prod**, phải tick tay hoặc viết upgrade-path
|
||
- **B2** — guard xóa-khi-duyệt = đúng lượt **∧** `matchingLevel.AllowApproverDelete` (**PER-ROW**, khuôn `:859`, **KHÔNG** `g.Any(...)` `:1182`)
|
||
- **8a** — `IgnoreQueryFilters()` **GỠ** filter chứ không ĐẢO ⇒ **BẮT BUỘC** kèm `.Where(x => x.IsDeleted)`
|
||
- **8b** — handler mới **mặc định 0 rào** ⇒ phải sao khối IDOR `PurchaseEvaluationFeatures.cs:596-617`
|
||
- **D3** — `IgnoreQueryFilters` chỉ ở **đúng 1 handler mới**; CẤM helper dùng chung trả `IQueryable`
|
||
- **12** — thêm key vào **CẢ HAI** danh sách permission seed `:2092-2096` **và** `:2492-2496`
|
||
- **11** — đổi nhãn phải qua `labelBackfill`; sửa `tree.Add` là **vô tác dụng** trên DB đang chạy
|
||
- **19** — `PeWorkflowPanel.tsx` phải giữ **md5 giống nhau ×2 app**
|
||
|
||
---
|
||
|
||
## 6. ⭐ Bài học đắt nhất — lớp lỗi lặp ĐÚNG 4 LẦN trong riêng cửa này
|
||
|
||
**"Cơ-chế đúng, thứ đi qua nó không có"** — cả 4 ca **build sạch + test xanh**:
|
||
|
||
1. policy **tồn tại** nhưng 11/13 vai `CanDelete=0`
|
||
2. endpoint PUT **sống** nhưng **0 dây FE** gọi
|
||
3. policy **chặn đúng** approver cần dùng
|
||
4. DTO **thiếu field** FE đang đọc
|
||
|
||
⇒ Chốt chặn đã dựng: **T27** (fault-inject xác nhận **có răng**). Đây là cùng họ với memory `feedback_mechanism_right_data_absent` — và là lần thứ 4 nó tái diễn trong **một cửa sổ**.
|
||
|
||
🔸 **Bài phụ (meta, lộ ra khi viết file này):** ledger `run.md` **tụt hậu so với đĩa** và tự khai điều đó ở dòng bắt đầu bằng *"🔧 Ledger này tụt hậu so WAL tới `/tiep` @S156"* — nhưng dòng tự khai ấy vẫn để nguyên claim sai ở dòng ngay trên nó (*"cicd verify cho commit này CHƯA chạy"*).
|
||
|
||
> 🔧 **Cite đổi sang NEO VĂN BẢN @S159** (H2 F-03 + method m#24): bản đầu dẫn `run.md:174`, và số đó **chết sau 2 phút** vì chính lead thêm 2 dòng vào `run.md` ngay sau khi viết file này (nay là `:176`). Luật rút ra: **file được dẫn nằm trong CÙNG đợt sửa ⇒ cite bằng neo văn bản, không bằng `:NNN`** — hoặc tick ledger TRƯỚC rồi mới viết synthesis. **Doc tự khai về mình là nguồn YẾU NHẤT.** Cách chữa rẻ: tick ledger **ngay tại thời điểm deploy**, không đợi cửa sau đối chứng.
|
||
|
||
---
|
||
|
||
## 7. State sau run (đo tươi, canonical = `docs/STATUS.md`)
|
||
|
||
Mig **68** `AddPeAllowApproverDelete` (AddColumn trên `ApprovalWorkflowLevel`, **no new table**, 89 bảng giữ, reversible Down) · test **532 → 562 (+30)** · commit chain `b1bae77` → `4464a46` → `f4494cf`.
|
||
|
||
---
|
||
|
||
<!-- END implement-synthesis · S155 · TOTAL=7 mục · retro-harvest @S159 2026-07-29 -->
|