Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4a.md
2026-08-01 09:37:26 +07:00

20 KiB
Raw Blame History

GATE-K4A: PASS-WITH-FLAGS 4

Gate K4a — adversarial review wave K4a (49 row menu KHKK 8-nhóm)

  • Spawn: S166 2026-08-01, HEAD 7a903cf, diff CHƯA commit.
  • Artifact ghi TỪNG MỤC (anti-#53). Dòng-1 = verdict.

Mốc đo (neo — sẽ re-đo trước verdict, bài S166 cây-phình)

  • HEAD THẬT lúc review = ce15c64 ("wal: flush 20260801T0914"), KHÔNG phải 7a903cf như đề bài. Đã kiểm git show --stat ce15c64: chỉ chạm .claude/** (WAL + agent-memory + run-folder) ⇒ KHÔNG có code src/fe lọt vào commit đó. Đề bài lệch 1 commit governance = vô hại, ghi để neo.
  • git diff --stat -- src fe-user fe-admin @T0 = 6 file, +213 / 5.
  • git status --porcelain -- src fe-user fe-admin tests @T0 = 6 M + 1 ?? (tests/SolutionErp.Infrastructure.Tests/Application/KhkkGroupMenuSeedTests.cs) — khớp kỳ vọng 7 file.
  • Ngoài scope code: 5 file .claude/** M (memory/run-folder/session-counter) — KHÔNG tính cây-phình code, nhưng lead PHẢI git add có chọn lọc (đừng git add -A kéo .session-counter.json vào commit code).

Mục 7 — thứ tự static initializer (ĐO)

MenuKeys.cs: KhkkGroupNumbers :72 → KhkkLeafActions :73 → KhkkGroup/KhkkLeaf :76-77 → KhkkGroupMenuKeys :81-84 → spread .. KhkkGroupMenuKeys trong All :244. Thứ tự văn bản ĐÚNG. Nếu ai dời khai báo xuống dưới All: spread mảng null → NRE trong static ctor → TypeInitializationException ở BẤT KỲ truy cập MenuKeys.* ⇒ T1+T2 (và ~mọi test chạm MenuKeys) ĐỎ. ⇒ có răng, tuy răng là "nổ" chứ không phải assert-đẹp. PASS.

Mục 6 — vá-10/11/13 (comment) — ĐO

  • Vá-13 1893: grep -n 1893 DbInitializer.cs = 0 hit; grep -rn 1893 src/ chỉ ra file .dll nhị phân (nhiễu, không phải mã nguồn). 2 comment đã trỏ :2162-2179 + mỏ neo existingItems.TryGetValue. Kiểm neo mới: existingItems.TryGetValue THẬT ở :2164, vòng foreach bắt đầu :2162, added++ kết :2178, đóng ngoặc :2179 ⇒ khoảng trỏ ĐÚNG (chứa trọn nhánh "key đã tồn tại → re-set Order → continue" :2164-2176). PASS.
  • Vá-11 revoker: comment :2335-2340 khai "filter chỉ bắt prefix Hrm*/Off* + key Personal ⇒ 0 match Khkk_*" — sẽ verify bằng đọc RevokeTemporarilyHiddenModulesAsync (mục dưới).
  • Vá-10 lý-do site-2: comment khai GetMyMenuTreeQuery.cs:96 HasAccess — sẽ verify.

Mục 4 — nhãn 8 nhóm vs transcribe (ĐO BẰNG SCRIPT, không mắt)

So khớp CHUỖI THÔ (==, không chỉ NFC) giữa khkkGroupLabels[2..8] + ("Khkk_G1", …) trong DbInitializer.cs và bảng danh-muc-sp002-transcribe.md:22-29: 8/8 rawEq=True, gồm cả nhóm dài nhất N2 "2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)" (CÓ vế "phá dỡ") và N8 có em-dash "8. HĐ chưa ủy quyền — CEO (5.2)". Ngoài ra tree.Add("Khkk_G1", …) == labelBackfill["Khkk_G1"] đúng từng ký tự ⇒ DB-trống và DB-đang-chạy hội tụ CÙNG một chuỗi. PASS.

Mục 1 — routes/params fe-user (ĐO TRÊN ĐĨA)

Khớp:

  • /khkk/workflow-matrix CÓ route fe-user (App.tsx:83), KHÔNG có fe-admin (:107-110 chỉ list/create/:id) ⇒ lời khai "fe-admin không có WorkflowMatrixViewPage" ĐÚNG.
  • filter=ChoDuyet / filter=DaDuyet: FILTER_TO_PHASE (KhkkListPage.tsx:40-46) có ĐÚNG 2 khoá ChoDuyet/DaDuyet — khớp hoa/thường từng ký tự (Record<string,…> ⇒ case-SENSITIVE, sai chữ là ?? null = list không lọc, câm).
  • view=deleted: :62 searchParams.get('view') === 'deleted' — khớp chính xác, đổi endpoint sang /contract-signing-plans/deleted.
  • Không leaf nào rơi if (!path) return null Ở FE-USER: regex phủ đúng 6 action, mọi nhánh có return.
  • Thứ tự query param: queryMatches dựng URLSearchParams 2 phía rồi so theo KHOÁ ⇒ thứ tự không ảnh hưởng. OK.

🔴 FLAG A (MAJOR) — ?group=n KHÔNG AI ĐỌC. Phép đo: grep -rn "get('group')" fe-user/src fe-admin/src = 0 hit; grep -n group WorkflowMatrixViewPage.tsx chỉ ra <colgroup> (:195/:200). Hệ quả đo được, không phải suy diễn:

  • /khkk/list?group=5&filter=ChoDuyet trả cùng dữ liệu với group=2 (query key ['khkk-list', {phase, search, page, deletedView}] — không có group; params gửi BE cũng không có).
  • /khkk/create?group=5 KHÔNG chọn sẵn nhóm 5.
  • /khkk/workflow-matrix?type=10&group=5 bỏ qua group hoàn toàn (:53 chỉ đọc type) ⇒ 7 leaf "Luồng duyệt" của 7 nhóm hiện y hệt nhau: toàn bộ workflow type-10. ⇒ Sau K4a, sidebar có 8 nhóm nhưng nội dung 8 nhóm giống hệt nhau (trừ nhóm 1 vốn cũng vậy). Comment Layout.tsx:196-199 CÓ khai đúng bản chất ("?group= là DANH TÍNH điều hướng, không phải bộ lọc") ⇒ đây là khai-báo-thật, không phải claim sai; nhưng nó là trạng-thái người dùng NHÌN THẤY và chưa thấy dòng nào trong artifact 3 lane nói "K4a giao 8 nhóm rỗng-lọc". Cần lead/owner dispose tường minh (giống FLAG-3) TRƯỚC khi commit, kèm chỉ đích wave nào nối (K4b? K5?).

🔴 FLAG B (MAJOR) — 2 mỏ-neo dòng MỚI trong fe-user/Layout.tsx SAI, lệch đúng bằng chính khối vừa chèn. Cùng lớp lỗi mà diff này vừa VÁ ở BE (vá-13 1893), tái phát ở FE trong CÙNG một lượt:

Comment mới nói Dòng THẬT (đo grep -n) Lệch
TRANSIENT_QUERY_KEYS (:311) — Layout.tsx:198 :345 +34
MenuLeaf :342 if (!path) return nullLayout.tsx:189 :376 (và :431) +34
Khối K4a chèn vào fe-user Layout.tsx đúng +34 dòng ⇒ tác giả tính mỏ neo trên bản TRƯỚC khi chèn
chính khối của mình. Đây là mỏ-neo tự-vô-hiệu ngay lúc land, không phải trôi theo thời gian.
Đối chứng: 6 mỏ neo còn lại trong cùng đợt vá ĐỀU ĐÚNG (MenuKeys.cs:77 ✔ · DbInitializer.cs:1960 ✔ ·
khkkGroupLabels :2127-2136 ✔ · labelBackfill:2235 ✔ · GetMyMenuTreeQuery.cs:96 ✔ ·
ContractSigningPlanFeatures.cs:420 regex ^KHKK-N([1-8])$ ✔ · KhkkListPage :61/:62 ✔) ⇒ lệch chỉ ở
đúng 2 mỏ neo TRỎ VÀO CHÍNH FILE ĐANG SỬA. Không chặn chạy, nhưng chính vá-13 vừa chứng loại lỗi này
tốn 1 lượt điều tra.

Mục 6 (tiếp) — vá-8/10/11/15 nội dung có tự-mâu-thuẫn không

  • Vá-11 (revoker) ĐÚNG, đã đo: RevokeTemporarilyHiddenModulesAsync :2554-2567 lọc StartsWith("Hrm") || StartsWith("Off") || MenuKey == PersonalKhkk_G* 0 match. Comment khớp.
  • Vá-10 (lý-do site-2) ĐÚNG: GetMyMenuTreeQuery.cs:96 static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess); ⇒ group KHÔNG cần row của chính nó; đính chính lý-do sai cũ là chính xác.
  • Vá-8 (fe-admin isAdminHidden) ĐÚNG: :207-209 thân hàm đúng là return key.startsWith('Ct_') ⇒ lời khai "hàm này CHỈ ẩn Ct_*" khớp đĩa; KeHoachKyKet KHÔNG bị ẩn. Và MenuGroup (fe-admin/Layout.tsx:223-262) render header + container vô điều kiện ⇒ 7 nhóm rỗng THẬT SỰ hiện trên sidebar admin (khớp FLAG-3 lead đã dispose, không phải suy đoán).
  • Vá-15 (2 khuôn key trong MenuKeys.cs) ĐÚNG: Ct_*/Pe_* thật sự KHÔNG có trong All (chỉ factory), còn AwV2_*/Khkk_G* có ⇒ mô tả 2 khuôn không tự mâu thuẫn.

Mục 2 — DELTA Menu keys 113 · Policies 452 (ĐO ĐỘC LẬP, không tin test cũng không tin lane)

Phép đo tự dựng (parse MenuKeys.cs, KHÔNG gọi lại bộ sinh — tránh tautology): tách phần tử của All, phân biệt token thường vs token spread, resolve từng const string sang giá trị chuỗi, rồi tự bung 7×(1+6):

  • token thường = 64 · resolve được 64/64 (0 token không tra ra hằng)
  • spread duy nhất = .. KhkkGroupMenuKeys → bung 49
  • |All| = 113 · trùng lặp = 0 (duplicates: [])
  • Policies = 113 × 4 = 452, khớp đúng vòng sinh Program.cs:82-89 (foreach menu in MenuKeys.All / foreach action in MenuKeys.Actions / AddPolicy($"{menu}.{action}")), Actions = 4 phần tử (MenuKeys.cs:252). Đây là chỗ DUY NHẤT sinh policy theo menu-key (grep AddPolicy Program.cs = 2 hit, hit còn lại :113 là rate-limit auth-login, không tính). ⇒ 452 = 113×4 ĐÚNG LOGIC SINH, không phải số chép tay. PASS.

STATUS.md (lead vá SAU khi spawn — coi là phần của commit K4a):

  • :6 Menu keys 64→113 ✔ khớp máy · Policies 256→452 ✔ khớp máy.
  • bảng canonical :470-471 452 + 113 ✔ khớp máy, kèm câu "test T2 đỏ nếu drift" — câu này ĐÚNG (T2 assert HaveCount(113)), tức con số có phép-kiểm-làm-nó-trượt. Đạt cổng nhãn-đo-lường.
  • :6 test 614→620 (45D + 575I): file test mới có đúng 6 [Fact] (grep -c = 6) ⇒ số học khớp. Đang chạy dotnet test SolutionErp.slnx để đo tươi (kết quả ghi ở mục 8).
  • bảng canonical :465 Mig 71: đếm đĩa src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ (bỏ .Designer.cs + ModelSnapshot) = 71 ✔ khớp.

🔴 FLAG C (MAJOR) — ô canonical bundle STATUS.md:479 SAI NGAY LÚC VIẾT, đo bằng 2 lệnh curl. Ô mới ghi admin mySTlx42 (css Cf3iCOOv) · user/eoffice CZAYiWWa (css DX1FRk5n). Đo LIVE 2026-08-01 trong lượt review:

Site STATUS vừa ghi Đo live (curl | grep assets/index-) Khớp?
admin.solutions.com.vn (js) mySTlx42 1yiNV4VH
admin (css) Cf3iCOOv Cf3iCOOv
eoffice.solutions.com.vn (js) CZAYiWWa DoULfmdT
eoffice (css) DX1FRk5n DX1FRk5n

2 css khớp ⇒ phép đo của tôi đang trỏ ĐÚNG site (không phải đo nhầm chỗ). Chỉ 2 hash JS lệch — đúng như chính ô đó tiên đoán ("F-1 7a903cf + K4a push sau đó SẼ rotate tiếp"). Vấn đề: ô này là hàng canonical, và nó đang ghi giá-trị đã chết trong khi giá-trị sống lấy được bằng 1 lệnh curl 25 giây. Sửa rẻ: hoặc thay bằng 2 hash vừa đo, hoặc ghi thẳng "chưa đo lại sau 7a903cf" thay vì để hash cũ đứng ở ô canonical. (Đây là ca claim mạnh hơn việc đã làm — nhãn "FROZEN-hợp-lệ K3" đọc như đã-xác- nhận, nhưng thứ được xác nhận là mốc K3, không phải trạng thái prod lúc commit.)

Mục 3 — vá-4/12: diff có CHẶN phép đo prod không?

Không chặn. Đo từng vế:

  • A4 (SELECT DISTINCT MenuKey FROM Permissions WHERE MenuKey LIKE 'Khkk%'MenuItems.Key): mọi key được grant đều có menu-row — site-1 cấp theo MenuKeys.All (113 key, invariant T6 K1 phủ), site-2 cấp KhkkKeys() = 7 key G1 (có row cứng ở tree initializer) + 49 key mới (có row từ vòng seed mới) ⇒ tập grant ⊆ tập row by-construction. Không có đường sinh Permission cho key không-row.
  • A5 (restart ×2 → Khkk% = 56 row): 7 + 49 = 56; không key nào khác bắt đầu bằng Khkk (AwV2_KhkkN* bắt đầu bằng AwV2) ⇒ phép đếm LIKE 'Khkk%' sạch, không nhiễu.
  • ⚠️ Bẫy CHO LANE CICD, không phải lỗi của diff: MenuItems có 2 cột override (MenuItem.cs: IsVisible mặc định true, DisplayLabel nullable — Mig 27). Seeder không bao giờ ghi 2 cột này (grep DisplayLabel DbInitializer.cs = 0 hit) ⇒ 49 row mới sinh ra IsVisible=true, DisplayLabel=null (đúng, hiện được). NHƯNG với Khkk_G1 trên prod: nếu admin từng bấm đổi tên qua PATCH /api/menus/{key} thì DisplayLabel khác null và fe-user render DisplayLabel, không phải Label (GetMyMenuTreeQuery.cs:86-88) ⇒ acceptance đo NHÃN TRÊN UI có thể thấy chữ cũ dù labelBackfill đã đổi Label thành công. ⇒ khi nghiệm thu A5 phải đo Label bằng SQL, và nếu đo trên UI thì kiểm DisplayLabel IS NULL trước. Ghi ra đây để cicd không kết luận nhầm "backfill hỏng".

Mục 8 — idempotent + revoker-proof: test đủ răng chưa?

Đo tươi suite (chính tôi chạy, không tin lane): dotnet test SolutionErp.slnx exit-0 — Domain 45/45, Infrastructure 575/575, tổng 620 PASS / 0 FAIL. Khớp đúng số STATUS :6 ghi (620 = 45D+575I). Con số này có phép-kiểm-làm-nó-trượt và tôi đã chạy phép kiểm đó.

  • Site-2 .Concat chạy 2 lần — KHÔNG double-grant. 3 lớp chặn, đọc trên đĩa: ① reviewKeys kết thúc bằng .Distinct() (:2480) ⇒ trong 1 lượt không có key lặp; ② nhánh key đã tồn tại: existingRows.TryGetValue((role.Id, key), …) → non-Pe continue (:2497-2508) ⇒ lượt 2 là no-op; ③ backstop DB: PermissionConfiguration.cs:26 HasIndex(x => new { x.RoleId, x.MenuKey }).IsUnique() ⇒ nếu 2 lớp trên hỏng thì NỔ chứ không âm thầm nhân đôi. PASS.
  • T3/T4 có răng cho phần chúng đo: T3 seed ×2 rồi so khkkAfter2.Equal(khkkAfter1) + totalAfter2 == totalAfter1 (đếm TOÀN bảng, không riêng họ Khkk) ⇒ bắt cả nhân-row chỗ khác. T4 đo DB↔DB lấy nhóm 1 làm mốc + chốt trước "6 nhãn nhóm 1 phân biệt" (chống vacuous). T5 dùng ma-trận cùng-ruột-khác-1-biến (G1 có entry backfill tự lành / G2 không có entry giữ chữ hỏng) — đúng cách chứng CƠ CHẾ chứ không chứng kết quả. Đây là bộ test viết tốt hơn mức trung bình của repo.

🔴 FLAG D (MAJOR) — bất-biến DUY NHẤT từng vỡ trên prod lại là bất-biến KHÔNG có test. RevokeTemporarilyHiddenModulesAsync không đụng Khkk_* — tôi đã verify bằng mắt trên LINQ (:2554-2567, chỉ Hrm/Off/Personal) và comment vá-11 khai đúng. Nhưng:

  • Không test nào khoá điều đó. Thêm 1 chữ || p.MenuKey.StartsWith("Khkk") vào bộ lọc ⇒ 620/620 vẫn XANH, còn thực tế 49 row CanRead vừa cấp bị lật sạch ở lần restart kế.
  • Đúng kịch bản đã xảy ra THẬT và được chính comment :2558-2565 dẫn lại: Run #423, "447/494 row rơi, 11/13 role về 0".
  • Khuôn có sẵn, rẻ: AdminOnlyModulesRevokeTests.cs:188 Revoke_DoesNotTouch_PeModule — mirror thành Revoke_DoesNotTouch_KhkkGroups là xong.
  • T3/T4/T5/T6 đều gọi từng seeder lẻ qua reflection, không chạy chuỗi SeedAsync đầy đủ ⇒ thứ tự grant → revoke → site-3 hoàn toàn ngoài vùng test. Vì vậy acceptance A5 (restart API ×2 THẬT) chưa thể thay bằng test — giữ nguyên là việc bắt buộc của lane cicd, đừng tick sớm.

Mục 9 — đụng key cũ? cây phình?

  • Va key: 0. Đo bằng chính phép parse All: duplicates: []; 7 key nhóm 1 (Khkk_G1, Khkk_WfViewKhkk_Deleted) không nằm trong All (đúng chủ ý, T2 khoá bằng NotContain); giao của 49 key sinh với 7 key nhóm 1 = ; họ AwV2_KhkkN1..N8 (K3) tách namespace hoàn toàn (AwV2_Khkk_), 8/8 vẫn nguyên trong All.
  • Kế thừa quyền: không lây. GetMyMenuTreeQuery chỉ inherit từ 4 root (Contracts, Workflows, PurchaseEvaluations, PeWorkflows); KeHoachKyKet không có case ⇒ 49 key mới không thừa cờ từ root, đúng thiết kế "grant từng dòng" (khớp gotcha #85 — endpoint vẫn gate KeHoachKyKet.*).
  • Cây phình (mốc CUỐI, re-đo sau khi lead vá STATUS):
    • HEAD trôi 2 lần trong lượt review: ce15c6421908b40cae243; cả 2 commit mới là wal: flush, đo git diff --stat ce15c64..HEAD -- src fe-user fe-admin tests docs = RỖNG ⇒ không có mã nào lọt vào commit governance. OK.
    • git diff --stat -- src fe-user fe-admin docs = 7 file, +219 / 11 (6 code + docs/STATUS.md).
    • git status --porcelain = 10 M + 1 ??. Trong đó 8 file thuộc commit K4a (6 code + STATUS + file test untracked); 3 file KHÔNG thuộc: .claude/auto-memory/MEMORY.md, .claude/auto-memory/feedback_return_terminates_agent.md, .claude/governance/.session-counter.json.
    • ⚠️ Note E: git add -A sẽ kéo 3 file governance vào commit mã. Add đích danh 8 file.
    • So mốc đầu (T0: 6 file code) → mốc cuối (7 file + test): phần thêm đúng bằng docs/STATUS.md mà coordinator đã báo trước ⇒ không có phình lén.

VERDICT — PASS-WITH-FLAGS 4 (0 blocker)

Phần lõi K4a đúng và đo được: 49 key by-construction 3 phía (All · seed menu-row · grant site-2), |All| = 113 / Policies = 452 khớp logic sinh, 8/8 nhãn khớp TỪNG KÝ TỰ với bảng owner đã soát, 0 va key, idempotent 3 lớp, revoker không đụng, suite 620/620 PASS đo tươi. Không có gì trong diff chặn phép đo prod A4/A5. Không kiến nghị sửa mã — dưới đây là vấn đề + tiêu chí nghiệm thu.

# Mức Vấn đề Tiêu chí đóng
A MAJOR ?group=n không code nào đọc (get('group') 0 hit ×2 app) ⇒ 7 nhóm mới × 6 leaf mở ra cùng một nội dung; WfView hiện TOÀN BỘ workflow type-10 chứ không riêng KHKK-N{n}. Chính spec đã tiên đoán (sub-invest-fable-b2-cum2.md:9 "silent-wrong, không lỗi build") Lead/owner dispose tường minh như FLAG-3, ghi rõ wave nào nối (K4b đọc group → params API; K4c lọc WfView) và không deploy ra người dùng trước khi nối, hoặc chấp nhận có văn bản
B MAJOR 2 mỏ neo MỚI trong fe-user/Layout.tsx sai đúng +34 dòng = kích thước khối vừa chèn: TRANSIENT_QUERY_KEYS (:311) → thật :345; MenuLeaf :342 → thật :376. Cùng lớp lỗi mà vá-13 vừa dọn ở BE Sửa 2 số, hoặc bỏ số dùng mỏ neo grep (như BE đã làm: grep existingItems.TryGetValue)
C MAJOR docs/STATUS.md:479 ghi bundle admin mySTlx42 / user CZAYiWWa, đo live cùng lượt = 1yiNV4VH / DoULfmdT (2 css khớp ⇒ đo đúng site). Ô canonical sai ngay lúc commit Thay bằng 2 hash vừa đo, hoặc ghi thẳng "chưa đo lại sau 7a903cf" thay vì để hash chết ở ô canonical
D MAJOR Bất-biến "revoker không đụng Khkk_*" — thứ đã vỡ thật trên prod (Run #423, 447/494 row rơi, chính comment :2558-2565 dẫn lại) — không có test. Thêm 1 chữ vào bộ lọc revoke thì 620/620 vẫn xanh Thêm Revoke_DoesNotTouch_KhkkGroups mirror khuôn sẵn có AdminOnlyModulesRevokeTests.cs:188
E NOTE 3 file .claude/** (auto-memory ×2 + .session-counter.json) đang dirty cùng cây Add đích danh 8 file (6 code + docs/STATUS.md + file test), KHÔNG git add -A

Ghi chú độc lập: A5 (restart API ×2 THẬT) không được coi là đã phủ bởi T3/T4 — 4 test đều gọi từng seeder lẻ qua reflection, chuỗi SeedAsync (grant → revoke → site-3) chưa từng chạy trong test.

END sub-reviewer-gate-k4a — VERDICT=PASS-WITH-FLAGS 4 (A ?group= chết-lâm-sàng · B 2 mỏ-neo tự-vô-hiệu · C ô bundle canonical sai đo-được · D bất-biến-từng-vỡ không test) · 0 blocker · suite 620/620 đo tươi · All=113/Policies=452 đo độc lập · nhãn 8/8 khớp từng ký tự