27 KiB
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 -bfe-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..HEADchỉ chạm.claude/**(đogit diff --stat) ⇒ toàn bộ code K4a+K4b nằm ở working-tree. Mốc so sánh dùng7a903cflà ĐÚ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, ....PagedRequestlàabstract record(Common/Models/PagedResult.cs:15) với 4 propertyinit⇒ trộn positional-ctor + object-initializer HỢP LỆ (record kế thừa record; nếuPagedRequestlàclassthì đã 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ônList(: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ƯỚCSearch— đú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.ApprovalGrouplàintkhô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
groupcho cả 2 nhánh (KhkkListPage.tsx:124, nằm ngoài tam-nguyêndeletedViewnên áp cho cả/contract-signing-planslẫ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:
- Derive thuần —
urlFilter/urlPhase/deletedView/groupđọc thẳngsearchParamsmỗi render (:74-80).useSearchParamsbámlocationnên đổi query (cùng route) VẪN re-render ⇒ 3 trục danh tính luôn tươi. Trước K4bphaselàuseState(initialFilter…)— khởi tạo 1 lần, đây đúng là nguồn của lỗi "phải F5". - 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áchundefinedkhỏinulllà ĐÚNG và cần: gộp lại thì bấm "Tất cả" trên leaf?filter=ChoDuyetsẽ bị URL kéo ngược. - Reset trong render (
:95-101) —navKey = ${group}|${urlFilter}|${deletedView}, so vớilastNavKey, 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ảiuseEffect⇒ 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ớiuseQueryvớipage/chipPhaseCŨ. TanStack v5getOptimisticResultsẽ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. searchCỐ Ý không reset (lane FLAG-4, lead giữ). Nó nằm trongqueryKeynê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.
filterthô không bao giờ được gửi đi; thứ gửi đi làphase. Hai chuỗifilterkhác nhau mà cùng giải mã ra mộtphasesẽ 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áckhiế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
filterVẪN cần thì nó CÓ mặt:navKey(:95) dùngurlFilterTHÔ, nên hai leaf?filter=rác1/?filter=rác2vẫ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
filterthô (ví dụ in tên bộ lọc lấy từ URL). HiệnurlFilterchỉ chảy vàourlPhase+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 manggroup— 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 ĐÃ manggroup=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ạiisAdminHidden(: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êngWfViewtrỏ Designer nên cố ý không manggroup(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=ntrả đú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.