267 lines
27 KiB
Markdown
267 lines
27 KiB
Markdown
GATE-K4B: PASS-WITH-FLAGS (4 MAJOR, 5 MINOR — 0 CHẶN; vá MAJOR-B + chạy cổng MAJOR-D trước khi gõ commit)
|
||
|
||
# Gate K4b — adversarial review wave K4b (nối `?group=` end-to-end), S166
|
||
|
||
**Mốc đo (neo lúc mở gate):** HEAD = `14ea2ea` (wal: flush 20260801T0945); commit code cuối = `7a903cf`.
|
||
Spec nói HEAD `7a903cf` — thực tế trên đĩa có thêm 6 commit `wal:` phía trên (chưa xác minh chỉ-WAL, sẽ đo).
|
||
`git status --porcelain` lúc mở gate: 18 M + 1 ?? (`tests/.../KhkkGroupMenuSeedTests.cs`).
|
||
`git diff --stat`: 541 insertions / 82 deletions / 18 files.
|
||
|
||
Scope chấm = K4b only: KhkkListPage ×2 · KhkkCreatePage ×2 · fe-user/Layout.tsx (staticMap-G1) · BE ContractSigningPlansController.cs (/deleted +group) · ContractSigningPlanFeatures.cs (ListDeleted).
|
||
K4a (MenuKeys/DbInitializer/Layout-regex/types/STATUS/test) ĐÃ qua gate K4a — không chấm lại.
|
||
|
||
---
|
||
|
||
## Ghi TỪNG MỤC (append khi đo xong, không soạn-gộp)
|
||
|
||
### M1 — Đo nền (tự chạy, không tin claim)
|
||
- `dotnet build SolutionErp.slnx` — **Build succeeded, 0 Warning, 0 Error** (56.4s, tôi tự chạy).
|
||
- `npx tsc -b` fe-admin — **EXIT=0**; fe-user — **EXIT=0** (tôi tự chạy, không đọc log nền của lane).
|
||
- WAL 6 commit `7a903cf..HEAD` chỉ chạm `.claude/**` (đo `git diff --stat`) ⇒ toàn bộ code K4a+K4b nằm ở working-tree. Mốc so sánh dùng `7a903cf` là ĐÚNG về nội dung code dù HEAD ghi khác.
|
||
|
||
### M2 — Mirror 2 app (sha256, 16 ký tự đầu)
|
||
| file | fe-admin | fe-user | |
|
||
|---|---|---|---|
|
||
| `pages/khkk/KhkkListPage.tsx` | `fa4c7aa268f35eeb` | `fa4c7aa268f35eeb` | MATCH (khớp con số `fa4c7aa2` spec nêu) |
|
||
| `pages/khkk/KhkkCreatePage.tsx` | `545b4c2fe0ebae88` | `545b4c2fe0ebae88` | MATCH |
|
||
| `types/khkk.ts` | `7a99b86518948093` | `7a99b86518948093` | MATCH |
|
||
|
||
`Layout.tsx` lệch by-design (đã khai trong cả 2 file: admin `Khkk_WfView` → Designer, không mang `group`; admin không nới regex `Khkk_G*`).
|
||
|
||
### M3 — FLAG-1 (code LEAD tự viết) — BE `/deleted` + group
|
||
- Ký-pháp: `public record ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null) : PagedRequest, ...`. `PagedRequest` là `abstract record` (`Common/Models/PagedResult.cs:15`) với 4 property `init` ⇒ trộn positional-ctor + object-initializer HỢP LỆ (record kế thừa record; nếu `PagedRequest` là `class` thì đã không biên dịch được). Build 0E xác nhận.
|
||
- Controller mirror List **ĐÚNG TỪNG NÉT**: `[FromQuery(Name = "group")] int? group = null` đặt ở vị trí tham số đầu, `new Query(group) { Page = …, PageSize = …, Search = …, SortDesc = … }` — cùng khuôn `List` (`:42`, anchor này ĐÚNG, tôi đếm lại).
|
||
- Handler: vế lọc `if (request.ApprovalGroup is not null) q = q.Where(p => p.ApprovalGroup == request.ApprovalGroup)` nằm **SAU** khối IDOR non-admin và **TRƯỚC** `Search` — đúng thứ tự list sống (`:755-758`). `IgnoreQueryFilters().Where(x => x.IsDeleted)` giữ nguyên phía trên ⇒ lọc nhóm không đụng vế soft-delete.
|
||
- `ContractSigningPlan.ApprovalGroup` là `int` **không nullable**, default `= 1` (`ContractSigningPlan.cs:41`) ⇒ so sánh `== request.ApprovalGroup` (int? vs int) dịch SQL thành so sánh giá trị, không sinh ca NULL-semantics.
|
||
- **Không có validator biên 1..8** cho `/deleted` — nhưng list SỐNG cũng không có ⇒ **consistent**, ghi nhận chứ không tính lỗi. `?group=999` = lọc rỗng (200 + `items: []`, `total: 0`), không 500. `?group=abc` = model-binding fail ⇒ 400 do `[ApiController]`. Cả hai hành vi giống hệt list sống.
|
||
- FE gửi `group` cho **cả 2 nhánh** (`KhkkListPage.tsx:124`, nằm ngoài tam-nguyên `deletedView` nên áp cho cả `/contract-signing-plans` lẫn `/deleted`) ⇒ khớp BE mới. Title/subtitle màn "Đã xóa" in tên nhóm — hết là lời nói dối vì server đã lọc thật.
|
||
|
||
---
|
||
|
||
## MỤC 1 — Vá-5: derive `phase`/`group` từ URL + `navKey` reset (`KhkkListPage.tsx:70-102`)
|
||
|
||
**Cơ chế lane dùng:** KHÔNG phải `useEffect`. Là hai lớp:
|
||
1. **Derive thuần** — `urlFilter`/`urlPhase`/`deletedView`/`group` đọc thẳng `searchParams` mỗi render (`:74-80`). `useSearchParams` bám `location` nên đổi query (cùng route) VẪN re-render ⇒ 3 trục danh tính luôn tươi. Trước K4b `phase` là `useState(initialFilter…)` — khởi tạo 1 lần, đây đúng là nguồn của lỗi "phải F5".
|
||
2. **Lớp override 3-trạng-thái** — `chipPhase: KhkkPhaseValue | null | undefined`, `undefined` = chưa bấm chip (nghe URL), `null` = user chọn "Tất cả", số = user chọn trạng thái. Tách `undefined` khỏi `null` là ĐÚNG và cần: gộp lại thì bấm "Tất cả" trên leaf `?filter=ChoDuyet` sẽ bị URL kéo ngược.
|
||
3. **Reset trong render** (`:95-101`) — `navKey = ${group}|${urlFilter}|${deletedView}`, so với `lastNavKey`, khác thì `setLastNavKey` + `setChipPhase(undefined)` + `setPage(1)`. Đây là khuôn React chính thống "điều chỉnh state khi prop/input đổi", KHÔNG phải `useEffect` ⇒ không có lượt vẽ bằng dữ liệu cũ.
|
||
|
||
**Bảng ca chuyển tôi soi từng ca:**
|
||
|
||
| Ca | navKey trước → sau | reset `chipPhase` | reset `page` | `phase` cuối |
|
||
|---|---|---|---|---|
|
||
| leaf→leaf cùng nhóm khác filter (`?group=2` → `?group=2&filter=DaDuyet`) | `2\|\|` → `2\|DaDuyet\|` | CÓ | CÓ | `DaDuyet` |
|
||
| leaf→leaf khác nhóm cùng filter (`?group=2&filter=ChoDuyet` → `?group=5&filter=ChoDuyet`) | `2\|ChoDuyet\|` → `5\|ChoDuyet\|` | CÓ | CÓ | `ChoDuyet` (đúng, chỉ đổi `group`) |
|
||
| vào deleted (`?group=3&filter=DaDuyet` → `?group=3&view=deleted`) | `3\|DaDuyet\|` → `3\|\|del` | CÓ | CÓ | `null` |
|
||
| ra khỏi deleted | `3\|\|del` → `3\|\|` | CÓ | CÓ | `null` |
|
||
| root trần `/khkk/list` ↔ leaf nhóm 1 `?group=1` | `\|\|` ↔ `1\|\|` | CÓ | CÓ | `null` |
|
||
| mount lần đầu | `lastNavKey` khởi tạo = `navKey` | KHÔNG (đúng) | KHÔNG | theo URL |
|
||
|
||
⇒ **Vá-5 ĐẠT.** Cả 6 ca đều re-render đúng, không cần F5.
|
||
|
||
**2 ghi chú kỹ thuật (không phải lỗi):**
|
||
- Lượt render bị React huỷ (lượt phát hiện `navKey` đổi) vẫn chạy tới `useQuery` với `page`/`chipPhase` CŨ. TanStack v5 `getOptimisticResult` sẽ `build()` một ô cache rỗng cho khoá cũ đó; effect không chạy trên render bị huỷ nên **không có fetch thừa**, ô rỗng bị gc. Vô hại.
|
||
- `search` CỐ Ý không reset (lane FLAG-4, lead giữ). Nó nằm trong `queryKey` nên không đẻ sai dữ liệu; chỉ là lựa chọn UX.
|
||
|
||
**MINOR-1 (không phải hồi quy):** bấm LẠI đúng leaf đang đứng ⇒ `navKey` không đổi ⇒ chip override KHÔNG nhả. Ví dụ: đứng ở "Đang duyệt" nhóm 1, bấm chip "Tất cả", rồi bấm lại mục "Đang duyệt" trong sidebar → vẫn hiện tất cả. Hình dạng này ĐÃ có trước K4b (khi đó `phase` là `useState` khởi tạo 1 lần) nên K4b không làm xấu đi. Muốn chữa thì so bằng `location.key` thay vì `navKey`.
|
||
|
||
---
|
||
|
||
## MỤC 2 — Vá-3 cache-key + phán FLAG-2 (phase-decoded thay filter-thô)
|
||
|
||
Khoá hiện tại: `['khkk-list', { group, phase, search, page, deletedView }]` (`KhkkListPage.tsx:112`).
|
||
|
||
**Đủ trục chưa — đối chiếu với ĐÚNG tập tham số gửi lên server** (`:117-128`): `phase` (bỏ khi deleted), `group`, `search`, `page`, `pageSize` (hằng), + chính `url` (`/contract-signing-plans` vs `/deleted`).
|
||
|
||
| Thứ quyết định response | Có trong khoá? |
|
||
|---|---|
|
||
| endpoint (`deletedView`) | CÓ |
|
||
| `group` | CÓ (vá-3, trục mới) |
|
||
| `phase` | CÓ |
|
||
| `search` | CÓ |
|
||
| `page` | CÓ |
|
||
| `pageSize` | hằng `PAGE_SIZE` — không cần |
|
||
|
||
⇒ **Không thiếu trục nào.**
|
||
|
||
**PHÁN FLAG-2 (lane nêu, lead nhận, tôi phán lại — ĐỘC LẬP): dùng `phase` đã giải mã là ĐÚNG, không phải nhân nhượng.**
|
||
- Lý do quyết định: khoá cache phải định danh **REQUEST**, không định danh **URL**. `filter` thô không bao giờ được gửi đi; thứ gửi đi là `phase`. Hai chuỗi `filter` khác nhau mà cùng giải mã ra một `phase` sẽ sinh **hai request byte-identical** ⇒ gộp một ô cache là đúng, tách ra mới là lỗi (đôi cache, đôi lượt mạng cho cùng một câu hỏi).
|
||
- Ca lệch mà spec hỏi (`?filter=rác` → `phase=null` → trùng khoá với "tất cả"): **hành vi ĐÚNG**. `?filter=rác` khiến trang hiển thị mọi trạng thái, y hệt "tất cả" — cùng màn hình, cùng dữ liệu, nên cùng ô cache. Không có gì để phân biệt.
|
||
- Chỗ raw `filter` VẪN cần thì nó CÓ mặt: `navKey` (`:95`) dùng `urlFilter` THÔ, nên hai leaf `?filter=rác1` / `?filter=rác2` vẫn được coi là hai lượt điều hướng khác nhau và vẫn reset chip/page. Hai vai trò tách bạch đúng chỗ.
|
||
- Điều kiện để phán này SAI (nêu ra để sau này ai đổi thì biết): nếu trang bắt đầu render khác nhau theo chuỗi `filter` thô (ví dụ in tên bộ lọc lấy từ URL). Hiện `urlFilter` chỉ chảy vào `urlPhase` + `navKey`, không chảy vào JSX — tôi đã grep hết đường đi của nó trong file.
|
||
|
||
**MINOR-5:** trong `deletedView`, `phase` vẫn nằm trong khoá nhưng KHÔNG được gửi (`:119`). Chip bị ẩn ở màn xoá (`:201 {!deletedView && …}`) và không leaf nào sinh URL vừa `filter` vừa `view=deleted`, nên thực tế `phase` luôn `null` ở đó. Chỉ URL gõ tay mới đẻ được 2 ô cache cho cùng một response. Vô hại, có sẵn từ trước K4b.
|
||
|
||
---
|
||
|
||
## MỤC 3 — FLAG-1: bản vá LEAD TỰ VIẾT (BE 3 chỗ + FE 2 nhánh) — soi kỹ nhất
|
||
|
||
*(số đo chi tiết đã ghi ở M3 phía trên; đây là phần phán)*
|
||
|
||
**3 chỗ BE — đối chiếu từng nét với list SỐNG:**
|
||
|
||
| | List sống (K2, `:756-758` + Controller `:42,49`) | `/deleted` (K4b, lead viết) | Khớp? |
|
||
|---|---|---|---|
|
||
| record | `ListContractSigningPlansQuery(…, int? ApprovalGroup = null) : PagedRequest` | `ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null) : PagedRequest` | KHỚP |
|
||
| controller param | `[FromQuery(Name = "group")] int? group = null` | y hệt | KHỚP |
|
||
| truyền | `new Query(phase, …, group) { Page = … }` | `new Query(group) { Page = … }` | KHỚP khuôn |
|
||
| vị trí lọc | SAU khối IDOR, TRƯỚC `Search` | SAU khối IDOR, TRƯỚC `Search` | KHỚP |
|
||
| vị ngữ | `p.ApprovalGroup == request.ApprovalGroup` | y hệt | KHỚP |
|
||
|
||
**Positional-record trộn object-initializer có compile đúng thứ tự không** (spec hỏi thẳng): CÓ, và không có bẫy thứ tự. C# chạy constructor positional TRƯỚC, rồi mới gán object-initializer; hai tập không giao nhau (`ApprovalGroup` chỉ ở ctor; `Page/PageSize/Search/SortDesc` chỉ ở initializer, đều là `init` accessor của `PagedRequest`). Điều kiện ngầm khiến nó hợp lệ: `PagedRequest` phải là **record** (record không kế thừa được class) — đã kiểm: `public abstract record PagedRequest` (`Common/Models/PagedResult.cs:15`). `dotnet build` 0E do tôi tự chạy xác nhận.
|
||
|
||
**Validator biên 1..8:** KHÔNG có, ở CẢ HAI endpoint ⇒ **consistent, ghi nhận chứ không trừ điểm** (đúng như spec dự liệu). Hành vi đo được bằng suy luận kiểu:
|
||
- `?group=999` → `int?` bind được → `Where(ApprovalGroup == 999)` → **200 + `items: []`, `total: 0`** (không 500, không lộ dữ liệu).
|
||
- `?group=abc` → bind fail → **400** tự động do `[ApiController]`.
|
||
- `?group=` (rỗng) → `null` → xem tất cả.
|
||
- Không có ca nào NỚI phạm vi thấy được, vì vế lọc nằm SAU IDOR và chỉ thu hẹp.
|
||
|
||
**FE 2 nhánh:** `group: group ?? undefined` (`:124`) đặt NGOÀI biểu thức tam nguyên `deletedView`, nên áp cho cả `/contract-signing-plans` lẫn `/deleted` — đúng "cả 2 nhánh". FE chỉ gửi khi `group` đã qua `/^[1-8]$/` (`:80`) nên rác không tới server. Title/subtitle màn xoá in tên nhóm (`:166,175`) — hợp lệ VÌ server đã lọc thật; nếu thiếu vá BE thì đúng là "tên nhóm nói dối" như chú thích lead viết.
|
||
|
||
🔴 **MAJOR-A — `sợi-dây-1-argument`, 0 test.** Toàn bộ tính năng này treo trên 2 mẩu: `(group)` ở Controller và 3 dòng `if` ở handler. Tôi grep `tests/` cho `ListDeletedContractSigningPlansQuery` → **0 hit**; không test nào chạm handler này. Phép thử phản chứng: xoá `(group)` khỏi `new ListDeletedContractSigningPlansQuery(group)` ⇒ tính năng chết câm, mà `dotnet build` vẫn 0E, bộ test lead báo 41/41 vẫn xanh, `tsc` ×2 vẫn EXIT=0 — **không phép kiểm nào TRƯỢT**. §7 cho phép test-after với feature mới nên đây không phải điều kiện chặn commit, nhưng phải vào sổ nợ K4c: 1 test "hai phiếu xoá khác nhóm, `?group=n` chỉ trả 1" là đủ. (Khuôn có sẵn: `ContractSigningPlanApprovalTests`.)
|
||
|
||
---
|
||
|
||
## MỤC 4 — staticMap G1 vs regex nhóm 2..8: có key nào trùng-match 2 đường không?
|
||
|
||
**Đo:** `staticMap` (fe-user `:63-87`) dùng 6 key **KHÔNG-infix**: `Khkk_List`, `Khkk_Create`, `Khkk_Pending`, `Khkk_Approved`, `Khkk_WfView`, `Khkk_Deleted`. Regex (`:212`) là `^Khkk_G([1-8])_(WfView|List|Create|Pending|Approved|Deleted)$` — **bắt buộc có khúc `G{n}_`**. Hai tập **rời nhau theo cấu trúc**: không chuỗi nào vừa có vừa không có infix. Thêm nữa `if (staticMap[key]) return staticMap[key]` (`:139`) chặn TRƯỚC regex, nên kể cả có trùng thì staticMap thắng — thứ tự an toàn.
|
||
|
||
**Đo phía dữ liệu (không suy từ code FE):** `MenuKeys.KhkkGroupNumbers = [2,3,4,5,6,7,8]` (`MenuKeys.cs:72`) ⇒ BE **không hề sinh** key `Khkk_G1_*`; `grep -rn "Khkk_G1_" src/Backend fe-admin/src fe-user/src` = **0 hit**. Nhánh `1` trong dải `[1-8]` của regex là nhánh CHẾT — lane đã khai rõ là cố ý, tôi xác nhận vô hại hôm nay.
|
||
|
||
⚠️ **Bẫy ngủ (ghi để người sau biết, KHÔNG tính lỗi hôm nay):** nếu sau này ai seed `Khkk_G1_List`, sẽ có **hai leaf khác key nhưng cùng URL** `/khkk/list?group=1` (một từ staticMap, một từ regex). `queryMatches` (`:353-374`) so khớp active bằng **URL chứ không bằng key** ⇒ tái hiện đúng lớp bug UAT S155 "mục render sau thắng". Ai chuyển nhóm 1 sang khuôn infix thì phải **bỏ 6 dòng staticMap cùng lượt**.
|
||
|
||
**6 leaf nhóm 1 — kiểm từng URL (fe-user):**
|
||
|
||
| key | URL | đúng? |
|
||
|---|---|---|
|
||
| `Khkk_List` | `/khkk/list?group=1` | ĐÚNG |
|
||
| `Khkk_Create` | `/khkk/create?group=1` | ĐÚNG |
|
||
| `Khkk_Pending` | `/khkk/list?group=1&filter=ChoDuyet` | ĐÚNG (`FILTER_TO_PHASE.ChoDuyet` tồn tại) |
|
||
| `Khkk_Approved` | `/khkk/list?group=1&filter=DaDuyet` | ĐÚNG |
|
||
| `Khkk_Deleted` | `/khkk/list?group=1&view=deleted` | ĐÚNG (`get('view') === 'deleted'`) |
|
||
| `Khkk_WfView` | `/khkk/workflow-matrix?type=10&group=1` | route sống, **`group` KHÔNG có người đọc** — xem MAJOR-C |
|
||
|
||
`group` không nằm trong `TRANSIENT_QUERY_KEYS` (`:349` = `id, q, editHeader, page, phase, awId`) ⇒ so khớp active NGHIÊM theo `group`; 8 leaf cùng hành động khác nhóm phân biệt được, và root trần `/khkk/list` (0 key query) không khớp leaf `?group=1` (1 key) vì `queryMatches` so cả SỐ LƯỢNG khoá. Đúng ý đồ.
|
||
|
||
🔴 **MAJOR-C — tham-số-trang-trí lan sang staticMap.** `grep "searchParams.get('group')"` trên cả 2 app = **4 hit, TẤT CẢ nằm ở `KhkkListPage` + `KhkkCreatePage`**; `WorkflowMatrixViewPage` **0 hit**. Tức `Khkk_WfView` (mới thêm `&group=1` trong ĐÚNG diff K4b này) và 7 leaf `Khkk_G{2..8}_WfView` (K4a) đều mở ra **cùng một ma trận workflow type-10**, không lọc theo nhóm. Đây là đúng finding ① tôi đã nêu ở gate K4a; K4b **không sửa mà thêm cái thứ 8**. Lead đã disposition FLAG-3 = K4c/K5 theo build-order ⇒ tôi KHÔNG chặn commit vì nó, nhưng ghi rõ: hôm nay `&group=1` ở dòng `Khkk_WfView` chỉ có tác dụng **phân biệt active-state**, KHÔNG có tác dụng lọc nội dung. Ai đọc URL mà tưởng nó lọc thì hiểu sai.
|
||
|
||
---
|
||
|
||
## MỤC 5 — Mirror 2 app
|
||
|
||
Bảng sha256 đã ghi ở **M2**: `KhkkListPage.tsx` = `fa4c7aa268f35eeb` ở CẢ HAI app (khớp đúng `fa4c7aa2` spec yêu cầu xác minh sau khi lead cp lại), `KhkkCreatePage.tsx` = `545b4c2fe0ebae88` ×2, `types/khkk.ts` = `7a99b86518948093` ×2. Không file nào lệch.
|
||
|
||
`Layout.tsx` lệch by-design và **lệch có khai ở cả hai phía**:
|
||
- admin `Khkk_WfView` → `/system/approval-workflows-v2/ContractSigningPlan` (Designer), **cố ý KHÔNG mang `group`** — chú thích ngay tại chỗ (`fe-admin/.../Layout.tsx:37-39`) nói rõ Designer không đọc tham số này. Tôi đồng ý: mirror mù ở đây mới là lỗi.
|
||
- 5 dòng `/khkk/*` còn lại của admin ĐÃ mang `group=1` (`:41-45`) ⇒ không có ca "một app nói dối, một app nói thật".
|
||
- admin không nới regex `Khkk_G*` — hệ quả 42 leaf nhóm 2..8 drop im lặng bên admin; **đã khai bằng khối ràng-buộc-ngược tại `isAdminHidden`** (`:199-210`) và là trạng thái K4a chấp nhận, không phải phát sinh mới ở K4b.
|
||
|
||
---
|
||
|
||
## MỤC 6 — Hồi quy đường cũ không-param
|
||
|
||
| Đường | Kỳ vọng | Đo được | |
|
||
|---|---|---|---|
|
||
| `/khkk/list` trần (root `KeHoachKyKet`, fe-user staticMap `:83`) | xem TẤT CẢ nhóm | `groupParam === null` ⇒ `group = null` ⇒ `group ?? undefined` ⇒ axios bỏ tham số ⇒ BE `ApprovalGroup is null` ⇒ không thêm vế `Where` | ĐÚNG |
|
||
| tiêu đề trang khi trần | không in tên nhóm | `group !== null ? … : 'Kế hoạch ký kết HĐ'` | ĐÚNG |
|
||
| `/khkk/create` không param | giữ default cũ | `presetGroup = null` ⇒ `useState(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)`, hằng `= 1` (`types/khkk.ts:122`) | ĐÚNG |
|
||
| `/khkk/list?group=rác` | không tự đoán nhóm 1 | regex `/^[1-8]$/` trượt ⇒ `null` ⇒ xem tất cả | ĐÚNG |
|
||
|
||
**MINOR-2:** trên `KhkkCreatePage`, hai nút "Huỷ" (`:165`, `:373`) đều `navigate('/khkk/list')` **không mang `group`** — người dùng đi từ leaf nhóm 5 vào lập phiếu, bấm Huỷ thì rơi về danh sách MỌI nhóm. Bất đối xứng với chiều đi (nút "Lập kế hoạch" ở `KhkkListPage:186` có mang `group`). Không sai dữ liệu, chỉ mất ngữ cảnh.
|
||
|
||
**MINOR-3:** đi từ `/khkk/create?group=3` sang `/khkk/create` (trần) thì ô chọn GIỮ 3, trong khi chú thích `:64` viết "không có tham số thì giữ nguyên default cũ" — chữ và việc lệch nhau một nhịp (nhánh `if (presetGroup !== null)` `:82` cố ý không đụng khi về null). Hành vi thực tế hợp lý hơn chữ; sửa CHỮ là đủ.
|
||
|
||
**Đã kiểm giao thoa preset × rào nhóm-đã-dùng:** vào `?group=3` rồi chọn PE đã có kế hoạch nhóm 3 ⇒ `selectPe` (`:147-154`) nhảy sang nhóm trống đầu tiên; nếu người dùng đổi leaf sang nhóm đã dùng thì `groupTaken` bật, nút Lưu **disabled** (`:380`) + dòng đỏ (`:313`). Preset KHÔNG phá được rào 409. ĐẠT.
|
||
|
||
---
|
||
|
||
## MỤC 7 — Mỏ neo số dòng (tái phát bài K4a ②)
|
||
|
||
🔴 **MAJOR-B — mỏ neo bị CHÍNH diff này làm sai, và sai ngay lúc land.**
|
||
`fe-user/src/components/Layout.tsx:204-205` viết:
|
||
> ``Tên tham số `filter` / `view` lấy ĐÚNG từ `KhkkListPage` (`:61` `get('filter')` → `FILTER_TO_PHASE`, `:62` `get('view') === 'deleted'`)``
|
||
|
||
Đo hai đầu:
|
||
- `git show 7a903cf:fe-admin/src/pages/khkk/KhkkListPage.tsx | grep -n` → `61: get('filter')`, `62: get('view')` — **đúng ở thời điểm K4a viết**.
|
||
- Trên đĩa hôm nay (sau K4b) → **`74:` `get('filter')`, `76:` `get('view')`** — lệch +13 / +14, đúng bằng khối chú thích + derive mà K4b chèn vào đầu component.
|
||
|
||
Vì K4a và K4b **vào CHUNG một commit** (lead đã khai), mỏ neo này sẽ sai **từ byte đầu tiên** của commit — không phải trôi dần theo thời gian. Đây đúng lớp lỗi tôi đã nêu ở gate K4a mục ②, tái phát sau đúng một wave, lần này **xuyên file** (K4b sửa file A làm hỏng chú thích trong file B). Mỉa mai đo được: chính khối chú thích đó, cách 2-3 dòng, dặn hai lần *"grep tên hằng — đừng neo số dòng"* và *"số dòng trượt theo edit"*.
|
||
|
||
**Tiêu chí nghiệm thu để gỡ:** bỏ `:61`/`:62`, thay bằng mỏ neo grep-được (ví dụ trích chính chuỗi `searchParams.get('filter')` / `FILTER_TO_PHASE`), hoặc cập nhật thành `:74`/`:76` **và** đo lại sau khi mọi edit của cả 2 wave đã nằm yên. Sửa 1 dòng, không đụng logic, không cần build lại.
|
||
|
||
**Quan sát kèm (KHÔNG tính cho K4b — nợ có sẵn từ K2):** `ContractSigningPlanFeatures.cs:332` viết ``client hiện tại (`KhkkCreatePage.tsx:73-75` ×2 app) CHƯA gửi trường này``. Hai vế đều đã sai TRƯỚC K4b: `:73-75` pre-K4b là chú thích của query `workflows`, không phải chỗ dựng body; và client **CÓ** gửi `approvalGroup` (`KhkkCreatePage.tsx:119`, do chính K2 thêm). K4b chỉ đẩy nó lệch thêm 21 dòng. Ghi vào sổ nợ, đừng tính vào cổng này.
|
||
|
||
---
|
||
|
||
## MỤC 8 — Bảng 6 hạng mục + nghiệm thu
|
||
|
||
| # | Hạng mục | Kết quả | Ghi chú |
|
||
|---|---|---|---|
|
||
| 1 | Wire BE / feature claim | **PASS** | `group` đi trọn FE→BE ở CẢ 2 nhánh; BE lọc thật, `total`/số-trang nói thật |
|
||
| 2 | Toàn vẹn schema | **PASS** | 0 migration, 0 đổi cột; `ApprovalGroup` là `int` non-null default 1 nên vế lọc không sinh ca NULL |
|
||
| 3 | Bảo mật | **PASS** | vế lọc đặt SAU IDOR (chỉ thu hẹp) ×2 handler; `[Authorize(Policy="KeHoachKyKet.Read")]` giữ nguyên cả class lẫn action; không endpoint mới |
|
||
| 4 | Chất lượng mã | **PASS-WITH-FLAGS** | build 0E + tsc ×2 EXIT=0 (tôi tự chạy); mirror sha ×3 khớp; **MAJOR-B** mỏ neo sai |
|
||
| 5 | Độ phủ test | **PASS-WITH-FLAGS** | §7 test-after cho feature mới ⇒ không chặn; **MAJOR-A** vế lọc `/deleted` 0 test, xoá đi vẫn xanh toàn bộ |
|
||
| 6 | Chất lượng văn xuôi | **N-A** | không có nội dung hướng ra ngoài trong diff K4b |
|
||
|
||
**Nghiệm thu spec-cụm2 (đo, không suy):**
|
||
- *"bấm leaf→leaf ĐỔI kết quả không cần F5"* → **ĐẠT**, 6/6 ca chuyển ở bảng MỤC 1.
|
||
- *"URL G1 6 leaf mang `group=1`"* → **ĐẠT**: fe-user 6/6 (kể cả `WfView`); fe-admin 5/5 mục trỏ `/khkk/*`, riêng `WfView` trỏ Designer nên cố ý không mang `group` (lệch đã khai ở cả hai file).
|
||
|
||
**Xử lý 4 FLAG của lane:** FLAG-1 → lead vá đủ 3 chỗ BE + 2 nhánh FE, tôi xác nhận ĐÚNG (còn nợ test). FLAG-2 → tôi phán **KHÔNG phải lỗi**, phase-decoded là lựa chọn đúng (lập luận + điều kiện phản chứng ở MỤC 2). FLAG-3 → carry sang K4c, nhưng K4b vừa đẻ thêm instance thứ 8 (MAJOR-C). FLAG-4 → giữ, `search` nằm trong `queryKey` nên không sai dữ liệu.
|
||
|
||
---
|
||
|
||
## VERDICT
|
||
|
||
**GATE-K4B: PASS-WITH-FLAGS (3 MAJOR, 5 MINOR) — không có lỗi CHẶN.**
|
||
|
||
Đủ điều kiện vào commit gộp K4a+K4b. Đề nghị làm 1 việc rẻ TRƯỚC khi commit:
|
||
- **MAJOR-B** (sửa 1 dòng chú thích `fe-user/.../Layout.tsx:204-205`) — vì nó sai ngay tại commit này chứ không trôi dần, sửa sau sẽ phải đẻ commit riêng cho một dòng chữ.
|
||
|
||
Hai cái còn lại vào sổ nợ K4c, KHÔNG chặn hôm nay:
|
||
- **MAJOR-A** — 1 test cho vế lọc nhóm ở `/deleted` (2 phiếu xoá khác nhóm, `?group=n` trả đúng 1).
|
||
- **MAJOR-C** — `WorkflowMatrixViewPage` đọc `?group=` (đang là tham số trang trí ở 8 URL).
|
||
|
||
MINOR 1-5 tuỳ lead, không cái nào đụng dữ liệu.
|
||
|
||
END — gate K4b đóng.
|
||
|
||
---
|
||
|
||
## MỤC 9 — Re-đo NGAY TRƯỚC khi phát verdict (bài S166 "cây phình giữa lượt review")
|
||
|
||
Giữa lượt review cây CÓ đổi: HEAD `14ea2ea` → `bc1e7c9` (2 commit `wal:` mới), `git diff --stat` tổng 541→664 dòng thêm. Tôi đo lại để verdict không neo vào ảnh chụp cũ:
|
||
|
||
- `git diff --stat 14ea2ea..HEAD` = **3 file, TẤT CẢ trong `.claude/`** (memory implementer-frontend, `sub-implfe-k4b.md`, chính artifact này). **0 file code.**
|
||
- `git diff --stat -- src fe-admin fe-user tests docs` = **13 file, 444+/59−**, từng dòng số **trùng khít** lần đo đầu (Layout admin 28, ListPage 90 ×2, CreatePage 24 ×2, types 25 ×2, Layout user 50, Controller 5, Features 9, MenuKeys 42, DbInitializer 79, STATUS 12).
|
||
- sha256 3 file mirror đo lại: `fa4c7aa2` / `545b4c2f` / `7a99b865` ×2 app — **không đổi**.
|
||
|
||
⇒ Verdict phát trên đúng thứ tôi đã soi. Không có code lọt vào sau lưng cổng.
|
||
|
||
🔴 **MAJOR-D — nguy cơ lúc commit, KHÔNG phải lỗi mã.** `git status --porcelain -- src tests` cho ra **`?? tests/SolutionErp.Infrastructure.Tests/Application/KhkkGroupMenuSeedTests.cs`** — file test của K4a vẫn **CHƯA ĐƯỢC TRACK**. Commit gộp mà lead dùng `git commit -am` hoặc liệt file bằng tay thiếu dòng này thì bộ test 49 dòng menu đi kèm K4a sẽ **rơi lại ngoài commit**: mã seed lên repo, test chứng minh nó thì không. Cổng rẻ, chạy trước khi gõ commit:
|
||
|
||
```
|
||
git status --porcelain -- src tests | grep '^??' # phải RỖNG
|
||
```
|
||
|
||
Nếu chưa rỗng thì `git add` đúng đường dẫn đó rồi mới commit.
|
||
|
||
---
|
||
|
||
## TOTAL — chốt cuối (đính chính con số ở MỤC 8: là **4 MAJOR**, không phải 3; MAJOR-D sinh ra sau lượt re-đo)
|
||
|
||
**GATE-K4B: PASS-WITH-FLAGS — 4 MAJOR + 5 MINOR, 0 lỗi CHẶN.**
|
||
|
||
| mã | mức | nội dung | làm khi nào |
|
||
|---|---|---|---|
|
||
| MAJOR-A | major | vế lọc nhóm ở `/deleted` **0 test** — xoá đi vẫn xanh toàn bộ (`sợi-dây-1-argument`) | sổ nợ K4c |
|
||
| MAJOR-B | major | mỏ neo `fe-user/.../Layout.tsx:204-205` trỏ `KhkkListPage :61/:62`, đĩa là `:74/:76` — sai từ byte-0 của commit | **trước commit** (sửa 1 dòng chữ) |
|
||
| MAJOR-C | major | `?group=` trên `WfView` là tham số trang trí, 0 người đọc; K4b thêm instance thứ 8 | sổ nợ K4c/K5 (lead đã disposition) |
|
||
| MAJOR-D | major | `tests/…/KhkkGroupMenuSeedTests.cs` còn `??` chưa track — commit gộp dễ bỏ rơi test của K4a | **trước commit** (`git add` rồi commit) |
|
||
| MINOR-1..5 | minor | bấm-lại-leaf không nhả chip · "Huỷ" mất `group` · chữ-vs-việc ở `:64` CreatePage · `phase` thừa trong khoá khi deleted · search `/deleted` chỉ khớp `MaKeHoach` | tuỳ lead |
|
||
|
||
Nghiệm thu spec-cụm2: **2/2 ĐẠT** (leaf→leaf không cần F5; 6 leaf G1 mang `group=1`).
|
||
Cả 4 FLAG của lane đã xử lý: FLAG-1 xác nhận vá đúng · FLAG-2 phán **không phải lỗi** (có điều-kiện-phản-chứng) · FLAG-3 carry K4c · FLAG-4 giữ.
|
||
|
||
END — gate K4b đóng, artifact hoàn chỉnh tới dòng này.
|