Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k6.md
2026-08-01 11:58:34 +07:00

19 KiB
Raw Blame History

GATE-K6: PASS-WITH-FLAGS 5 — luật ẩn ĐÚNG và N=15 tái-lập được trên DỮ LIỆU PROD SỐNG (không chỉ trên seed), nhưng comment mới nhồi 3 con-số và 1 trong 3 ("28 leaf") đã SAI ngay lúc land.

Gate K6 — reviewer adversarial (S167, 2026-08-01)

Đối tượng: diff UNCOMMITTED fe-admin/src/components/Layout.tsx (+52/19). Spec pin: sub-invest-fable-b2-cum2.md:67-100 (K6 mục 1-4 + acceptance C1-C4). Lane artifact: sub-implfe-k6.md. Phép đo mạnh nhất của lượt này: cây menu LIVE PROD (GET https://api.solutions.com.vn/api/menus/me, token admin, HTTP 200, 44.656 byte) đem áp CHÍNH tập key cắt ra từ file đã ship. Lane chỉ đo được từ SEED; tôi đo được thực tế đang chạy.


R0 — Rào residual-write (đo TRƯỚC khi đọc diff)

git status --porcelain cho 8 file M + 3 file ??. Trừ nhóm .claude/* (governance — ngoài phạm vi gate; trong đó .claude/WAL.md.claude/sessions/session-9/_tiep-4.md là nghi-thức /tiep của lead, KHÔNG phải dư-lượng của lane), phần cây mã chỉ có đúng 1 file: fe-admin/src/components/Layout.tsx.

  • git diff --stat -- fe-admin/src1 file changed, 52 insertions(+), 19 deletions(-) — khớp con số lane khai.
  • 0 residual-write. Không có file BE, không có file fe-user, không có file test nào bị chạm.
  • Đo lại --stat một lần nữa ngay trước khi chốt verdict (chống "cây phình giữa review"): vẫn đúng 1 file mã, 52/19.

R1 — 12-key: so TỪNG CHỮ vs spec + chống gõ-nhầm

Tôi không gõ tay lại tập key. Tôi cắt nó ra khỏi file đã ship bằng script riêng (k6-verify.cjs, regex bắt khối const ADMIN_HIDDEN_KEYS = new Set<string>([...]), xoá comment trước rồi mới lấy chuỗi trong nháy đơn — vì comment trong khối có chứa chữ Ct_* dễ lọt vào phép trích).

Kết quả trích (n = 12, đúng thứ tự trong file):

["PurchaseEvaluations","KeHoachKyKet","Contracts","HopDongCung","Forms","Reports",
 "Master","Hrm","Off","Personal","Workflows","PeWorkflows"]

So với 12 key spec liệt ở sub-invest-fable-b2-cum2.md:73-86: khớp 12/12, đúng từng ký tự, đúng thứ tự, 0 thừa, 0 thiếu, 0 sai chính tả.

Luật ghép cũng khớp nguyên văn spec — Layout.tsx:246:

return ADMIN_HIDDEN_KEYS.has(key) || key.startsWith('Ct_')

R1b — Chống "key gõ đúng chính tả nhưng không tồn tại" (typo-noop)

Một key viết sai sẽ không báo lỗi gì cả: Set.has() trả false, module vẫn hiện, tsc vẫn xanh. Nên tôi kiểm sự TỒN TẠI của từng key trong cây prod sống:

Key Có trong cây prod? Parent thật
PurchaseEvaluations · KeHoachKyKet · Contracts · HopDongCung · Forms · Reports · Master · Hrm · Off · Personal OK (10/10) null (root)
Workflows · PeWorkflows OK (2/2) System

Control-âm (3 key bịa gần giống, để chứng phép kiểm này phân biệt được chứ không phải luôn trả OK): PurchaseEvaluation → MISS · Offi → MISS · Personnel → MISS.

⇒ Cả 12 key đều trỏ vào node có thật; 2 key Workflows/PeWorkflows đúng là CON của System nên bắt buộc phải liệt riêng như spec nói.

R1c — Tập ẩn có bỏ sót root nghiệp vụ nào không?

Cây prod có đúng 12 root: Dashboard · PurchaseEvaluations · KeHoachKyKet · Hrm · Off · Personal · Contracts · HopDongCung · Forms · Reports · Master · System. Tập ẩn phủ 10/12; 2 root còn lại là DashboardSystem — đúng bằng phần spec CHỪA (:91). Không có root nghiệp vụ nào lọt lưới.

Đối chiếu nguồn mã: DbInitializer.cs:1926-2049 (danh sách tree) cũng cho đúng 12 tuple có Parent = null, và DbInitializer.cs:2177điểm INSERT menu-row DUY NHẤT trong toàn bộ backend (grep new MenuItem|MenuItems.Add chỉ 1 hit ghi). Nghĩa là mọi menu-row hợp lệ đều sinh từ đúng danh sách này.


R2 — C1: N = 15, đo trên PROD chứ không chỉ trên seed

Cây prod: 200 node · 166 lá trước lọc (trùng khít con số lane dựng từ seed — hai đường đo độc lập ra cùng một cây).

Áp bộ lọc đệ quy (tôi dựng lại filterForAdmin độc lập, dùng tập key cắt từ file):

15 lá sau lọc, đúng tên từng mục:

Dashboard · Users · Roles · Permissions · MenuVisibility
AwV2_DuyetNcc · AwV2_DuyetNccPhuongAn
AwV2_KhkkN1 · AwV2_KhkkN2 · AwV2_KhkkN3 · AwV2_KhkkN4
AwV2_KhkkN5 · AwV2_KhkkN6 · AwV2_KhkkN7 · AwV2_KhkkN8

Trùng 15/15 với danh sách lane khai ở sub-implfe-k6.md:41-47.

R2b — Hai phép kiểm lane KHÔNG làm, mà C1 thật sự cần

C1 hỏi "còn THẤY bao nhiêu lá", mà filterForAdmin mới là bước một. Còn hai bước nữa mới ra được cái mắt nhìn thấy:

  1. resolvePath trả nullMenuLeaf return null ⇒ lá biến mất im lặng (Layout.tsx:331-333, gotcha #50). Tôi kiểm từng lá trong 15 lá: 5 lá đầu nằm trong staticMap (Dashboard:56, Users:63, Roles:64, Permissions:65, MenuVisibility:66); AwV2_DuyetNcc/AwV2_DuyetNccPhuongAn đi nhánh :178; 8 lá AwV2_KhkkN{1..8} đi nhánh :188. 0 lá bị drop. Vậy 15 lá lọc được cũng đúng là 15 lá hiện ra.
  2. Nhóm bị rỗng-hoá: MenuNodeRenderer:256 phân loại theo children.length > 0, nên một GROUP sống sót mà mất sạch con sẽ tụt xuống render như LEAF. Tôi so cây trước/sau lọc theo từng node: 0 group bị rỗng-hoá. (System giữ 5 con: 4 leaf quản trị + ApprovalWorkflowsV2; ApprovalWorkflowsV2 giữ đủ 10 con.)

R2c — Control-âm (chứng phép đếm phân biệt được, không phải luôn ra 15)

Bỏ key khỏi tập Lá sau lọc
KeHoachKyKet 63 (+48)
Master 29 (+14)
Off 29 (+14)
Personal 16 (+1)
tập RỖNG (giữ mỗi luật Ct_) 125

Con số +48 trùng khít control-âm lane khai (15 → 63). Phép đo có răng.


R3 — C2 (fe-user 0-diff) và C4 (không chạm route/quyền)

C2 — git diff --stat -- fe-user/srcrỗng, 0 file. Control-dương chạy cùng lệnh: git diff --stat -- fe-admin/src → 1 file. Vậy "rỗng" là rỗng thật, không phải lệnh hỏng.

C4 — ba tầng chứng, tầng sau mạnh hơn tầng trước:

  1. Trong diff: grep các dòng + tìm usePermission|PermissionGuard|Route|navigate|path:|element= → đúng 1 hit, và là dòng COMMENT ("Route fe-admin còn nguyên…"), 0 dòng mã. Control-dương: ADMIN_HIDDEN_KEYS cho 2 hit.
  2. Trong kiến trúc: toàn fe-admin chỉ có 2 nơi đọc menu từ AuthContextLayout.tsx:362 (lọc, chỉ để render) và usePermission.ts:15 (đọc cây THÔ, không lọc). Nghĩa là can()PermissionGuard hoàn toàn không biết K6 tồn tại. Đây mới là chứng cấu trúc cho "display-only", mạnh hơn việc diff không chạm route.
  3. Trên prod, bằng curl thật (token admin):
Verb Endpoint Kỳ vọng Thực tế
GET /api/menus/me 200 200
GET /api/purchase-evaluations?page=1&pageSize=1 200 200
GET /api/contract-signing-plans?page=1&pageSize=1 200 200

Hai module vừa bị ẩn khỏi menu vẫn trả 200 cho admin ⇒ đúng là bớt lối đi, không thu hồi quyền. Route đích cũng có thật trong fe-admin/src/App.tsx: /purchase-evaluations (:88) và /khkk/list (:108) — hai ví dụ mà comment mới nêu đích danh đều là route sống, không rơi vào catch-all :124.


R4 — Chất lượng comment (mục 4 của gate)

Ba thứ gate yêu cầu thì comment ĐỦ CẢ BA:

  • Lineage 2 đảo, đủ 3 mốc:203 S29 ẩn → :204-205 S57 bỏ ẩn (đảo lần 1) → :206-207 S164 QĐ7 ẩn lại (đảo lần 2).
  • Câu chốt:209-210: "cả hai đều từng đúng, mốc sau thắng mốc trước. Đừng 'khôi phục' S57 vì tưởng gặp bug."
  • Display-only #82 khai rõ:212-216, có đủ vế "muốn CHẶN thật thì phải làm ở BE authz [Authorize(Policy=…)]". Ràng buộc ngược với resolvePath cũng được giữ lại (:224-226), nên khớp nối K4a không mất.

Nhưng vế thứ tư thì hỏng:

MAJOR-1 — comment nhồi 3 con-số, 1 số đã sai ngay lúc land. Gotcha #84 mục (4) viết thẳng: "Comment quanh seeder nêu LUẬT + lệnh grep tự-kiểm, CẤM ghi số-đếm (số thành nợ tự-lan — 2 lần lệch trong 1 ngày @S159)". Diff đưa vào 3 số:

Vị trí Câu Đo trên prod Phán
:229 "Khkk_G1..G8 + 48 leaf chết theo" 48 lá dưới KeHoachKyKet ĐÚNG
:221 "42 leaf Khkk_G{2..8}_*" 42 ĐÚNG
:245 "mở lại root thì 28 leaf không ùa về theo" 42 lá (49 key Ct_* = 7 group + 42 leaf) SAI

Số 28 là con số của thời Ct_<Type>_<Group|List|Create|Pending>; S155 đợt-2 đã thêm WfView/Approved/Deleted nên mỗi loại HĐ nay 6 lá, 7 loại = 42. Số này còn nằm nguyên trong skill permission-matrix (dòng × 7 type = 28 leaf) nên nhiều khả năng được chép từ doc stale chứ không phải tự đếm — đúng bệnh "số thành nợ tự-lan" mà #84 cảnh báo. Tự nó không đổi hành vi chạy, nhưng nó là một câu SAI mới toanh được thêm vào file, ngay dưới đoạn đang dặn người sau đừng đọc nhầm.

Acceptance để đóng MAJOR-1: hoặc bỏ cả ba số và thay bằng luật + lệnh tự-kiểm (ví dụ nêu "mọi key Ct_*" kèm lệnh grep đếm tại chỗ), hoặc giữ hai số đúng và sửa 28 → 42 kèm ghi rõ mốc S155. Không nhận phương án "để nguyên vì chỉ là comment".

MINOR-2 — câu mở đầu xếp sai tầng. :197-198 liệt "Hệ thống (Người dùng / Vai trò / Phân quyền / Menu eOffice) · Quy trình duyệt (Mới)" bằng cùng một dấu chấm giữa dòng, đọc thành hai mục ngang hàng; thực tế ApprovalWorkflowsV2 là CON của System (DbInitializer.cs, Order 96). Lồng nó vào trong ngoặc của "Hệ thống" là hết mơ hồ.

MINOR-3 — || key.startsWith('Ct_') nay là nhánh chết. Contracts đã bị ẩn ở tầng root nên không node Ct_* nào còn đi tới được luật prefix. Comment :243-245 đã tự khai điều này và nêu lý do giữ (phòng khi mở lại root). Tôi ĐỒNG Ý giữ — chỉ ghi lại để lượt sau không tưởng là vừa phát hiện ra thứ mới.


R5 — Hệ quả không ai khai: 3 module mất lối vào ở CẢ HAI app

CLARIFY-1 (cần owner biết, không phải lỗi mã). Cây prod cho thấy 4 node đang mang isVisible = false: Hrm, Off, Personal, Pe_DuyetNccPhuongAn (và Pe_DuyetNccdisplayLabel admin đặt lại).

  • fe-user/src/components/Layout.tsx:243 lọc n.isVisible !== falseHrm/Off/Personal hiện đã tắt bên eOffice.
  • fe-admin KHÔNG đọc isVisible (grep 0 hit) ⇒ trước K6, fe-admin là lối vào menu cuối cùng của ba module đó.
  • Sau K6, ba key này nằm trong tập ẩn ⇒ không còn mục menu nào cho Hrm/Off/Personal ở bất kỳ app nào.

Vẫn còn đường thoát: quyền không đổi (deep-link chạy), và trang Menu eOffice (MenuVisibility) vẫn nằm trong 15 lá sống nên admin bật lại được. Nhưng spec K6 chỉ nói "module nghiệp vụ làm việc ở fe-user" — với ba module này thì fe-user đang tắt sẵn, nên hệ quả thật là "biến mất khỏi menu", khác với điều câu spec gợi ra. Cần owner xác nhận đây là ý muốn, hoặc bật isVisible lại bên eOffice trước khi K6 lên prod.


R6 — Build tươi + kiểm chứng con số lane khai

  • npx tsc -b trong fe-admin: exit 0, không phát ra chẩn đoán nào. Chạy SAU diff nên không dính bẫy snapshot cũ (gotcha #68).
  • npm run build: PASS, và tái lập trùng từng chữ số với artifact lane — dist/assets/index-B0OrpamE.js 1,726.07 kB (gzip 429.28) · index-AgG72YRZ.css 86.93 kB (gzip 14.51). Tên file băm trùng nghĩa là nội dung bundle giống hệt ⇒ claim đo-lường của lane là thật, không phải chép lại.
  • Hai cảnh báo (chunk > 500 kB, INEFFECTIVE_DYNAMIC_IMPORT) đúng là có sẵn từ trước, không do diff.

MINOR-4 — câu khai của lane hơi rộng. sub-implfe-k6.md:56 viết "git diff --stat = đúng 1 file"; chạy trần thì git diff --stat cho 8 file (7 file .claude/* đang dirty). Câu đúng phải là "đúng 1 file MÃ". Không sai bản chất, nhưng người đọc sau có thể tưởng cây sạch tuyệt đối.


R7 — Disposition 3 FLAG của lane

FLAG-1 (N = 15) — CHẤP NHẬN, và tôi xác nhận bằng đường đo độc lập. Lane nói spec quên cộng 8 leaf Designer K3 vào công thức của chính nó: đúng. Spec :97 viết N = Dashboard + Users + Roles + Permissions + MenuVisibility + |leaf AwV2 trong DB| rồi chốt "7 (nếu 2 AwV2) / 8 (nếu 3)", trong khi cùng dòng lại nói "SAU K3 = N+8". K3 đã land và đã lên prod (10 row AwV2* đang sống), nên đáp số hiện tại là 15, không phải 7/8. Số 15 của tôi đo từ payload prod, không đo từ seed.

FLAG-2 (dư-lượng prod: có row AwV2_Contract chèn tay không?) — ĐÓNG.

  • Đường sqlcmd: không chạm được, tôi đã thử chứ không đoán. Binary có trên máy (/c/Program Files/Microsoft SQL Server/Client SDK/ODBC/170/Tools/Binn/sqlcmd), nhưng chuỗi kết nối prod là Server=.\SQLEXPRESS + Password=__SET_VIA_SECRETS__ (appsettings.Production.json.example:3) — tức DB nằm local trên VPS, không có endpoint lẫn mật khẩu nào từ máy này ra tới. Khai đúng: không đo được bằng SQL.
  • Đường thay thế mạnh hơn: /api/menus/me trên prod với token admin. Kết quả: đúng 10 key AwV2*AwV2_DuyetNcc, AwV2_DuyetNccPhuongAn, AwV2_KhkkN1..N8. Không có AwV2_Contract. Đồng thời 8 group Khkk_G1..G8 đều đã có mặt trên prod.
  • Phụ chứng phía mã: grep AwV2_Contract toàn repo = 0 hit, với control-dương AwV2_KhkkN1 = 2 hit (nên biết chắc lệnh grep còn sống). Nhánh resolvePath:178 có nhận code Contract nhưng đó là bộ giải route chờ sẵn, không phải menu-row — lane phân biệt đúng chỗ này.
  • Giới hạn của phép đo, khai thẳng: /api/menus/me đã lọc theo quyền (GetMyMenuTreeQuery chỉ trả node CanRead hoặc có con CanRead). Một row chèn tay mà không kèm row Permissions sẽ vô hình trong phép đo này — nhưng nó cũng vô hình trên sidebar admin, tức không ảnh hưởng con số C1. Vậy: C1 đóng trọn vẹn; còn mệnh đề "bảng MenuItems không tồn tại row nào khác" thì đây chỉ là cận dưới. Bối cảnh liên quan: gotcha #84 nói mọi sửa tay trên prod đều tạm thời nếu có code-path ngược chạy lúc khởi động, nên kể cả có row chèn tay thì nó cũng không bền.

FLAG-3 (nhánh chết trong resolvePath) — GIỮ, đừng dọn lượt này. Ba lý do: (1) file này đã đảo hai lần, dọn đi là xoá đúng thứ phải dựng lại nếu có lần đảo thứ ba; (2) nhánh chết ở đây là dữ liệu tĩnh trong staticMap, không tốn gì lúc chạy và tsc không coi là lỗi; (3) dọn sẽ phình diff ra ngoài phạm vi spec K6, tức tự chuốc lấy lỗi trôi-phạm-vi. Điều kiện kèm theo: giữ nguyên cảnh báo ngược ở :224-226 (gỡ KeHoachKyKet khỏi tập thì phải nới regex Khkk_G* trước) — tôi đã kiểm cảnh báo này còn ĐÚNG: staticMap hiện chỉ có 6 key Khkk_* không-infix của nhóm 1, chưa hề có nhánh Khkk_G{n}_*. Nếu sau này muốn dọn thì tách một lượt riêng.


R8 — Bảng 6 hạng mục

Hạng mục Kết luận Ghi chú
1. Wire BE / feature claim PASS Diff thuần FE hiển thị; 3 endpoint prod trả 200 chứng quyền không đổi
2. Schema integrity N-A 0 migration, 0 entity, 0 menu-key mới
3. Security PASS usePermission đọc cây thô ⇒ guard không bị nới/siết; không đụng [Authorize]
4. Code quality PASS-WITH-FLAGS tsc -b exit 0, build tái lập trùng băm; trừ MAJOR-1 comment sai số
5. Test coverage N-A có điều kiện fe-admin không có hạ tầng test (package.json chỉ dev/build/lint/preview); 15 lá này chỉ được canh bởi phép đo thủ công của gate, không có lưới tự động
6. Writing quality PASS Comment tiếng Việt câu trọn, đủ dấu; nội bộ nên không soi theo chuẩn outward

R9 — Tổng hợp finding

# Mức Vị trí Nội dung
1 MAJOR fe-admin/src/components/Layout.tsx:245 "28 leaf" sai (thật 42 lá / 49 key Ct_*); 3 số-đếm trong comment vi phạm gotcha #84 mục (4)
2 CLARIFY fe-user/src/components/Layout.tsx:243 + dữ liệu prod Hrm/Off/Personal đang isVisible=false bên eOffice ⇒ sau K6 mất lối vào menu ở cả hai app; cần owner xác nhận
3 MINOR Layout.tsx:197-198 Câu mở đầu xếp "Quy trình duyệt (Mới)" ngang hàng "Hệ thống", thực tế là con của System
4 MINOR Layout.tsx:243-246 Luật prefix Ct_ nay là nhánh chết — cố ý, đã khai, đồng ý giữ
5 MINOR sub-implfe-k6.md:56 "git diff --stat = đúng 1 file" nên viết là "1 file MÃ" (thực tế 8 file dirty)

Không có blocker. Cả 5 mục đều không chặn commit về mặt hành vi; MAJOR-1 nên sửa trước khi commit vì nó là một câu sai mới được thêm vào, mà sửa chỉ tốn một dòng.

R10 — Những chỗ diff này chịu được soi (ghi để không chỉ kể lỗi)

  • Chọn liệt ROOT thay vì liệt từng leaf là đúng cấu trúc: filterForAdmin:249-253 cắt đệ quy cả cây con, nên tập 12 key tự động theo kịp mọi leaf BE thêm sau này mà không phải bảo trì.
  • Ràng buộc ngược resolvePath ↔ tập ẩn (di sản K4a) không bị mất khi viết lại trọn khối comment — đây là chỗ dễ rơi nhất khi thay comment cũ bằng comment mới.
  • Con số lane khai đều tái lập được: 166 lá trước lọc, 15 lá sau lọc, control-âm +48, tên bundle sau build. Không có con số nào thuộc loại "tự khai không kiểm được".

END gate-k6 — VERDICT=PASS-WITH-FLAGS 5