Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k5.md
2026-08-01 10:45:31 +07:00

17 KiB
Raw Blame History

GATE-K5: PASS — 0 blocker · 6/6 SOI đạt (mất-phiếu + regression GĐ3/4 chứng được là KHÔNG xảy ra) · 1 CLARIFY + 5 MINOR treo · rào commit: menus.json untracked ở gốc, đừng git add -A

Re-đo TRƯỚC verdict (bài S166 F-1 ②): HEAD trôi ff4d4dc → 13f7f82 → 80b8cf5 trong lượt review, toàn wal: flushgit diff --name-only 13f7f82..HEAD -- src tests fe-admin fe-user = 0. Cây code vẫn đúng 4 file, +466/108, 2 cặp sha mirror y nguyên, 0 untracked trong vùng code ⇒ verdict đo trên đúng thứ sắp commit.

Gate K5 — adversarial review cây pipeline GĐ2 mở 8 sub-folder nhóm (S166, 2026-08-01)

Vai: reviewer (READ-only, verdict-only). Ghi TỪNG MỤC, Edit sau mỗi mục (chống #53).

§0 Mốc đo (neo trước khi soi)

  • Lệnh: git status --porcelain + git rev-parse HEAD lúc 1st-measure.
  • 🔴 LỆCH TIỀN-ĐỀ #1: spec lead nói "diff chưa commit trên HEAD 88e7ecf". Đĩa đo được HEAD = ff4d4dc9d0b745867f83b5349f3c239d6b2ee798. Phải truy git log xem 88e7ecf nằm đâu trước khi tin base.
  • 🔴 RÀO GIT #1: git status?? menus.json ở ROOT repo (untracked, ngoài spec 4 file). Cần đo: file gì, có bị commit ké không.
  • Cây làm việc lúc mốc-0 (8 file M + 3 ??): 4 file code đúng spec + 4 file .claude/** + 3 untracked.

§1 Phân lập diff + rào git (đo, không tin lời khai)

  • git log: 88e7ecf LÀ tổ tiên của HEAD ⇒ tiền-đề lead đúng về nội dung, sai về tên mốc: giữa 88e7ecf và HEAD có 3 commit wal: flush. git diff --name-only 88e7ecf..HEAD = 6 file, TOÀN BỘ .claude/**, 0 file code ⇒ base code vẫn là 88e7ecf. Verdict K5 đo trên git diff (working-tree vs HEAD).
  • 🔴 Cây PHÌNH GIỮA LƯỢT (lần thứ 3 trong 3 gate liên tiếp): HEAD ff4d4dc lúc đo-0 → 13f7f82 sau ~2 tool-call. Phân lập: git diff --stat -- fe-admin fe-user src tests = đúng 4 file, +466/108 khớp spec TỪNG SỐ.
  • git status --porcelain -- src tests fe-admin fe-user = 0 dòng ?? ⇒ không có file code chưa track (bài "HIGH nằm ở git" KHÔNG tái phát trong vùng code).
  • 🔴 RÀO GIT — menus.json 44.656 B nằm ở ROOT repo, untracked (mtime 10:25, nội dung = payload /api/menus/me, sản phẩm phụ của lane cicd verify prod). Không phải file của K5, nhưng nếu lead commit bằng git add -A/git add . thì nó lên repo. Đây là finding về THAO TÁC COMMIT sắp tới, không phải về diff K5.
  • Cùng loại: .claude/agent-memory/implementer-frontend/archive/2026-H2.md untracked (hợp lệ, nên add có chủ đích).

§2 Build gate — TÔI TỰ CHẠY, không nhận lời khai

lệnh nơi chạy exit
npx tsc -b fe-user 0
npx tsc -b fe-admin 0
Lời khai lane (tsc -b ×2 exit 0 + npm run build ×2) khớp phần tôi đo lại. Bundle hash lane khai (index-CZUcTUUg.js / index-BZWvx0Y8.js) tôi KHÔNG kiểm lại — chưa deploy nên không có ô canonical để đối chứng; ghi rõ là lời khai chưa đo, đừng đọc thành đã-verify (bài K4a ③).

§3 SOI-1 — Node nhóm đúng nguồn + route khớp K4b

  • 1 nguồn, 0 map thứ hai. grep KHKK_APPROVAL_GROUPS|khkkGroupMenuLabel toàn fe-user/src+fe-admin/src: consumer = hook K5 (:217/:228/:230) + KhkkCreatePage + KhkkListPage, tất cả import từ @/types/khkk. Không có mảng 8 phần tử nào khác, không có bảng nhãn thứ hai.
  • 8 node dựng bằng KHKK_APPROVAL_GROUPS.map(...) (hằng [1..8] as const), nhãn khkkGroupMenuLabel(n) = "{n}. " + KHKK_APPROVAL_GROUP_LABELS[n]derive, không chép.
  • 🟢 ĐỐI CHIẾU LIVE (tôi tự đo, không nhận lời khai): so nhãn cây suy ra ⟂ nhãn thật trong payload /api/menus/me (menus.json prod) → 8/8 KHỚP TỪNG KÝ TỰ, cả 8 node Khkk_G1..G8 đều displayLabel = null. Tức hôm nay cây và sidebar gọi cùng một nhóm bằng cùng một tên.
  • Route: onOpenAll → /khkk/list?group=${n}. Đối chiếu K4b: Layout.tsx:217 khkkLeafPath(group,'List') = /khkk/list?group=${group}, và leaf tĩnh Khkk_List: '/khkk/list?group=1' (:63) ⇒ chuỗi TRÙNG KHÍT.
  • Nối end-to-end (3 chặng, đo cả 3): KhkkListPage.tsx:79-80 đọc searchParams.get('group') + rào ^[1-8]$:124 gửi group lên server → BE ContractSigningPlanFeatures.cs:757-758 q.Where(p => p.ApprovalGroup == request.ApprovalGroup). Không phải tham-số-trang-trí (khác hẳn bài K4a ①).
  • Sáng đúng mục sidebar: Layout.tsx:388-390 isActive = pathname khớp && queryMatches(...) (so khớp TẬP query) ⇒ ?group=n từ cây ăn khớp leaf nhóm n. Lời khai lane "trang tự sáng đúng mục" đúng, đã kiểm.

§4 SOI-2 — Phân phiếu, fallback, bất-biến Σ (1 MINOR)

  • KHÔNG có ca phiếu rơi khỏi cả 9 ngăn. Vòng :215-224 là if/else toàn phần: khớp typeof g === 'number' && GROUPS.includes(g) → ngăn n; mọi thứ còn lại (null · undefined · thiếu field · 0 · 9 · NaN · chuỗi) → khkkUngrouped. Không nhánh nào continue/bỏ qua.
  • Σ enforce ở đâu — trả lời thẳng: ở CẤU TRÚC, không phải ở kỷ luật. khkkLeaves và các ngăn cùng sinh từ một mảng khkkItems bằng một phép phân hoạch; stage.leaves giữ vai danh sách phẳng canonical và stageCount() (PipelineStageFolders.tsx:96) vẫn đếm trên nó ⇒ chip số của folder cha không thể lệch tổng các ngăn. console.warn :252 chỉ là chuông, và là DEV-only.
  • Thứ tự đúng: ngăn "(chưa phân nhóm)" push:238-245 TRƯỚC khi reduce tính Σ ở :251 ⇒ chuông không tự báo động giả.
  • Ngăn lạc gần như không thể xảy ra (đo BE): ApprovalGroupint non-null, validator InclusiveBetween(1,8) (:353-356), default 1 (:399), backfill Mig 71 = 1, và 0 đường update nào ghi lại field này. ⇒ nhánh fallback là phòng thủ, không phải đường sống.
  • 🟡 MINOR-1 — nhãn ngăn lạc hứa một đằng, đi một nẻo: onOpenAll: () => navigate('/khkk/list') (:243). Không kèm ?group= ⇒ BE không lọc gì cả, trang mở ra là toàn bộ phiếu của cả 8 nhóm, trong khi nhãn và tooltip nói "Mở danh sách: (chưa phân nhóm)". Sidebar cũng không sáng mục nào (query rỗng không khớp leaf nào). BE hiện không có bộ lọc "không thuộc nhóm nào" để trỏ tới, nên đây là hạn chế thật chứ không phải quên gõ. Đề nghị: bỏ onOpenAll cho riêng ngăn này (component đã có nhánh <span> khi thiếu onOpenAll, :151-155) — mất một cú bấm gây hiểu nhầm, không mất gì khác.

§5 SOI-3 — Ngăn rỗng render THẬT

Đo đường đi, không đọc lời khai — 3 chốt, không chốt nào lọc theo length:

  1. Dựng: KHKK_APPROVAL_GROUPS.map(...).map trên hằng 8 phần tử, 0 .filter trong cả khối.
  2. Vẽ: PipelineStageFolders.tsx:287-295 stage.groups !== undefined ? stage.groups.map(g => <GroupFolder…>) — vẽ thẳng mảng nhận được, không điều kiện phụ.
  3. Trong ngăn: GroupFolder count === 0 → vẫn render <details> đủ (chevron + tên bấm được + SoonChip), thân in "Chưa có phiếu ở nhóm này."
  • Nhánh "Chưa có dữ liệu ở giai đoạn này." (:296) nay bị nhánh groups chặn trước ⇒ GĐ2 rỗng hiện 8 ngăn thay vì 1 dòng. Đúng spec "HIỆN ĐỦ 8 kể cả rỗng".
  • Không đụng hideEmpty: vẫn lọc ở tầng stage bằng stageCount() = leaves.length ⇒ đang tìm-kiếm mà gói không có KHKK thì cả folder GĐ2 ẩn, không đổ ra 8 ngăn rỗng.

§6 SOI-4 — GĐ3/GĐ4 regression 0 ĐỔI HÀNH VI (chứng bằng tương đương từng vế)

Nghi-can số-1 là đúng chỗ phải nghi: builder GĐ3/4 bị viết lại vòng lặp. Tôi chứng tương đương 4 vế thay vì tin "cùng tập, cùng thứ tự":

vế CŨ (seen + lặp peItems) MỚI (Set + lặp id) kết
tập id mọi p.contractId truthy if (p.contractId) add bằng
thứ tự lần-xuất-hiện-ĐẦU trong peItems Set giữ thứ tự chèn, chèn khi duyệt peItems bằng
khử trùng seen.has → continue Set tự khử bằng
dòng phụ p.tenGoiThau (p = phiếu ĐẦU mang id đó) peItems.find(p => p.contractId === contractId) = cũng là phiếu ĐẦU bằng
  • c.type KHÔNG bị đụng: vẫn chỉ xuất hiện ở onOpen GĐ4 → /hard-copies?type=${c.type} (:322), khớp HardCopiesPage.tsx:64-67. Hành vi 7-loại-HĐ nguyên vẹn.
  • stage4 an toàn theo cấu trúc: thân if (linkage.canHardCopy && c?.hasSealedCopy) chỉ đọc c, 0 tham chiếu p — nên việc biến p biến mất khỏi scope vòng lặp không thể làm rơi HĐ. (Nếu có tham chiếu thì tsc đã đỏ; tôi kiểm cả hai đường.)
  • Union hôm nay đúng 1 nguồn pe.contractId ⇒ tập HĐ y hệt trước K50 ca HĐ đang hiện bị biến mất. Vế "HĐ 2..N liên-danh vô hình" là giới hạn dữ liệu có từ trước, K5 không làm xấu đi.
  • Ghi chú (không phải lỗi): peItems.find nằm trong vòng ⇒ O(n·m); n = số phiếu 1 gói thầu nên vô hại.
  • 🟡 CLARIFY-2 — một dòng checklist đọc được 2 nghĩa: "GĐ2 by approvalGroup · GĐ3/4 by c.type". Lane đọc là "kiểm trục, đừng đụng"; câu hỏi SOI-4 của lead cũng đọc y vậy ("hành vi 7-loại-HĐ … có biến mất ca nào không"). Nghĩa còn lại — GĐ3/4 cũng mở sub-folder theo 7 loại HĐ — thì K5 mới làm nửa checklist. Hai người cùng chọn một nghĩa không chứng minh nghĩa đó đúng ý owner. Đề nghị ghim cách đọc vào commit message để nó không quay lại thành "sót".

§7 SOI-5 — Chỗ cắm FLAG-1: 0 field bịa

  • grep mọi dòng + của diff chứa lines hoặc contractIds: 9 hit, TOÀN BỘ nằm trong // comment. Dòng chạy được duy nhất đọc dữ liệu là if (p.contractId) — field có thật (PeListItem:126).
  • Control âm: grep "lines\|contractIds" fe-user/src/types/purchaseEvaluation.ts = 0 hit ⇒ đúng, lines[]/contractIds không tồn tại trên DTO list; nếu ai đó lỡ gõ thành mã chạy thì tsc đỏ ngay. Lời khai FLAG-1 của lane đo lại thấy đúng.
  • Hình dạng "gom TẬP id" (Set<string>) là vế wire được thật, không phải hứa suông: thêm nguồn 2 = thêm 1 dòng tại :290-291, không phải sửa vòng lặp.

§8 SOI-6 — SHA-pair ×2, kế thừa, và phép đo prod

  • Mirror: sha256 2 file đổi ×2 app: usePipelineStages.ts = edff99314e8b75de39b0123bcdad5d98d8081a5bd294e2e05b1ef784094a19ac (khớp hash lane khai) PipelineStageFolders.tsx = bed52f39d595c86a24afd27180146871d52e29943731ffb52f7caf23e66de936 Thêm phép đối chứng độc lập: diff từng cặp file = rỗng cả hai ⇒ identical theo byte, không chỉ theo hash.
  • INDEX_PAGE_SIZE = 200 KHÔNG đổi, indexTruncated KHÔNG đổi, dòng cảnh báo "Chỉ mục tải 200 bản ghi mới nhất…" vẫn ở chân folder cha (:301-305). FLAG-3 (cảnh báo không lặp ở từng ngăn) đúng như lane khai — kế thừa, không phải mới.
  • 🔵 Đo prod (tôi tự curl, admin token, http=200): GET /api/contract-signing-plans?pageSize=200total = 0. Bảng KHKK trên prod rỗng hoàn toàn.
    • ⇒ Phân bố approvalGroupchân-lý-rỗng: {}. Tôi không dùng nó để khẳng định hay bác bỏ điều gì về phân bố nhóm — khai thẳng là chưa đo được, không chờ.
    • ⇒ Nhưng nó cho một sự thật dùng được ngay cho UAT: hôm nay mở cây trên prod, GĐ2 của mọi gói thầu sẽ hiện đúng 8 dòng rỗng chip "SẮP", 0 phiếu. Đó là ĐÚNG theo spec — cần nói trước để người UAT không đọc thành "hỏng".
  • 🟡 MINOR-3 — chữ "SẮP" trên ngăn rỗng: SoonChip vốn nghĩa "tính năng sắp có"; đặt lên nhóm rỗng nó thành "nhóm này sắp có", trong khi sự thật là "nhóm này chưa có phiếu". Lane bám tiền lệ folder-GĐ nên nhất quán, nhưng K5 nhân nó lên 8 lần mỗi gói và (theo phép đo prod ở trên) 100% ngăn đều rỗng ngày đầu. Chữ trong thân ngăn đã nói đúng ("Chưa có phiếu ở nhóm này.") nhưng chỉ thấy sau khi bung. Quyết định thuộc owner, không phải lỗi mã.
  • 🟡 MINOR-4 — a11y: <button> lồng trong <summary> (PipelineStageFolders.tsx:141-160). <summary> mang role=button ngầm ⇒ đây là nội dung tương tác lồng nhau, trình đọc màn hình có thể không đọc nút bên trong. Đã kiểm tiền lệ tại base 88e7ecf: cả 3 <summary> của PipelineTreePanel và cả 3 của PurchaseEvaluationsListPage chỉ chứa span/svg ⇒ K5 là khuôn MỚI, không có tiền lệ để bám. Bàn phím/chuột vẫn chạy đúng (preventDefault + stopPropagation chặn nhịp đóng/mở).
  • 🟡 MINOR-5 — bằng chứng mạnh nhất chết theo phiên: "15/15 OK" của lane chạy bằng script new Function dựng tại chỗ, không commit. FE không có hạ tầng test nào (fe-user/package.json chỉ có dev|build|lint|preview, không vitest/jest). Vật canh bất-biến Σ còn lại trong repo là console.warn DEV-only — bản prod bị loại bỏ khi build. Tức nếu mai ai sửa vòng phân hoạch thành lọc, không gì đỏ. Không đòi lane dựng hạ tầng test trong wave này; ghi để đừng ai đọc "15/15" thành "đã có lưới an toàn".
  • MINOR-6: với GĐ2, toKhkkLeaf chạy 2 lần/phiếu (1 cho khkkLeaves đếm, 1 cho ngăn) ⇒ stage.leaves thành mảng chỉ để đếm, leaf trong đó không bao giờ được vẽ. Vô hại, nhưng là mỡ thừa nếu sau này gói có nhiều phiếu.

§9 Các cổng còn lại

Cổng Kết Ghi chú
Cat 1 Wire/claim PASS 0 // Mock, 0 alert(, 0 TODO/placeholder trong dòng +. Không phải wave BE nên không có verb để curl; đã curl prod để đo dữ liệu (§8).
Cat 2 Schema N-A git diff --name-only -- src tests = 0 file. 0 migration, 0 entity, 0 DTO.
Cat 3 Security PASS Gate GĐ2 vẫn KHKK_MENU_KEYS = ['KeHoachKyKet'] — đúng key policy của endpoint, KHÔNG OR Khkk_* (gotcha #85/#82 giữ nguyên). Ngăn nhóm nằm bên trong thân GĐ2, mà locked chặn trước cả groups ⇒ không mở thêm mặt tấn công, không đẻ lệnh gọi API mới.
Cat 4 Code quality PASS tsc -b ×2 = 0 (tôi chạy). Mirror byte-identical. Anti-fiddle: đúng 4 file trong phạm vi spec, 0 file lạc. LeafList tách ra là bê nguyên văn khối JSX cũ (đối chiếu từng dòng khối - với thân hàm mới: ul/li key/className/badge/secondary trùng khít) ⇒ tách hàm KHÔNG kèm sửa lén giao diện.
Cat 5 Test N-A (khai rõ) FE không có hạ tầng test — xem MINOR-5. Không có test BE để tăng vì 0 file BE.
Cat 6 Writing N-A Diff là mã + chú thích nội bộ, không phải văn bản hướng ra ngoài. Chuỗi hiển thị mới đều tiếng Việt đủ dấu: "Chưa có phiếu ở nhóm này.", "(chưa phân nhóm)", "Mở danh sách: …".

§10 VERDICT

PASS — 0 blocker. 6/6 mục SOI của lead đều đo được và đều đạt; 2 chỗ tôi nghi nhất (mất phiếu im lặng · regression GĐ3/4) đều chứng được là không xảy ra, bằng tương đương từng vế chứ không bằng lời khai. Phần lõi K5 là loại hiếm: bất-biến Σ được bảo đảm bởi cấu trúc (một mảng nguồn, một phép phân hoạch, một chỗ đếm), nên nó không phụ thuộc vào ai đó nhớ cộng cho khớp. Còn treo (không chặn commit): MINOR-1 nhãn ngăn lạc đi sai chỗ · CLARIFY-2 cách đọc dòng checklist GĐ3/4 · MINOR-3 chữ "SẮP" · MINOR-4 nút lồng trong summary · MINOR-5 không có lưới test · MINOR-6 mỡ thừa. Trước khi commit — 1 việc THAO TÁC: menus.json (44.656 B) đang nằm untracked ở gốc repo; đừng git add -A, hãy add đúng 4 file code (+ artifact lane/gate nếu muốn).

Câu nặng nhất: cây nay hiện đủ 8 nhóm, nhưng prod đang có 0 phiếu KHKK (total=0, tôi tự curl) — nên thứ người UAT nhìn thấy ngày đầu là 8 dòng "SẮP" rỗng ở mọi gói thầu, đúng spec mà rất dễ bị đọc thành hỏng; cần nói trước.

GATE-K5-END