Files
solution-erp/.claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-reviewer-3.md
2026-07-30 00:19:52 +07:00

18 KiB
Raw Blame History

sub-reviewer-3 — W7 bản cứng Hdc_* (1-MỐC) — ADVERSARIAL REVIEW TIỀN-COMMIT

Vai: reviewer · Run: 2026-07-29-S161-khkk-w7-bancung · Ngày 2026-07-29/30 Luật: READ-only. KHÔNG sửa code-tree. Ghi TỪNG TRỤC ngay khi đo xong (chống #53).


§0 — Trạng thái sống (chỉ tick khi ĐÃ ĐO)

  • §A Đo tiền-đề: git status + 3 sub-md
  • §1 Trục 1 — FE HardCopiesPage ruột
  • §2 Trục 2 — Hợp-đồng FE↔BE
  • §3 Trục 3 — Boundary O-A
  • §4 Trục 4 — 2 quan sát test-lane
  • §5 VERDICT (lead finalize on-behalf — return #53 stale hơn đĩa; tick §1-§4 do lead sau verify nội-dung từng §)

§A — Tiền-đề ĐO TƯƠI (2026-07-30, trước khi soi 1 dòng nào)

A.1 🔴 FLAG-0 (MAJOR — quy trình): sub-md của lane FE KHÔNG TỒN TẠI

Đề bài nói "đã có ... + sub FE + ...". Đo ls:

.claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/
  run.md                       1.688 B
  sub-implementer-backend-0.md 11.514 B
  sub-test-specialist-0.md     17.563 B

⇒ CHỈ 2 sub-md. find .claude/workflows/runs -name "*frontend*" cho ra W1/W2/W3 nhưng KHÔNG có W7. Hệ quả review: lane FE không có lời tự-khai nào — mọi kết luận về FE ở dưới đều đo THẲNG TỪ ĐĨA, không có đối-chứng-chéo của chính lane đó. Đây là ca #53 (garble/skeleton-rỗng) hoặc lane chưa từng ghi đĩa; lead cần biết vì 1 trong 5 nguồn bằng chứng của wave này bị khuyết.

A.2 git status (đo tươi)

 M .claude/governance/.session-counter.json      ← hook, không phải lane nào
 M fe-admin/src/App.tsx · Layout.tsx · types/contracts.ts
 M fe-user/src/App.tsx · Layout.tsx · types/contracts.ts
 M src/Backend/.../ContractsController.cs · ContractFeatures.cs · Dtos/ContractDtos.cs
?? fe-admin/src/pages/contracts/HardCopiesPage.tsx
?? fe-user/src/pages/contracts/HardCopiesPage.tsx
?? tests/SolutionErp.Infrastructure.Tests/Application/ContractHardCopyTests.cs

diffstat: src/Backend = 3 files, +31/4 · fe-* = 6 files, +36/14 · 3 file MỚI untracked.

A.3 🔴 FLAG-1 (HIGH — nằm ở git, KHÔNG ở mã) — 3 file MỚI đang UNTRACKED

Lớp S155/S161 tái diễn: file tracked-M và file untracked trộn nhau trong cùng một wave.

File untracked Ai tham chiếu tới nó Nếu commit thiếu
fe-admin/src/pages/contracts/HardCopiesPage.tsx fe-admin/src/App.tsx:23 import { HardCopiesPage } (đã tracked-M) CI FE build ĐỎ — import trỏ file không tồn tại
fe-user/src/pages/contracts/HardCopiesPage.tsx fe-user/src/App.tsx:17 (đã tracked-M) CI FE build ĐỎ
tests/.../Application/ContractHardCopyTests.cs không ai — nhưng là TOÀN BỘ acceptance §③-B suite tụt 590 → 587, mất 3 test PIN

git commit -a KHÔNG nạp file untracked ⇒ commit sẽ chứa import mồ côi ở CẢ 2 app. Gate BẮT BUỘC ngay trước commit: git status --porcelain -- src tests fe-admin fe-user | grep '^??' phải RỖNG (hoặc git add đích danh 3 file trên). Đây là HIGH quy trình, không phải lỗi mã.


§1 — TRỤC 1: FE HardCopiesPage ruột (ĐO XONG)

Nguồn: fe-user/src/pages/contracts/HardCopiesPage.tsx (445 dòng) — mirror fe-admin byte-identical (sha256 = 09c6e681…a71a9 cả 2, diff = rỗng). §3.9 mirror ĐẠT.

1.1 Upload multipart — ĐÚNG field-name endpoint thật

Bờ FE (HardCopiesPage.tsx:264-272) Bờ BE (ContractsController.cs:88-104) Khớp
form.append('file', file) IFormFile file (:92) — bind theo TÊN
form.append('purpose', String(3)) [FromForm] AttachmentPurpose purpose = Other (:93) (3 = SealedCopy, ContractAttachment.cs:5-11)
form.append('note', …) chỉ khi non-empty [FromForm] string? note = null (:94)
POST /contracts/{id}/attachments [HttpPost("{id:guid}/attachments")]
download …/attachments/{aid}/**download** [HttpGet("{id:guid}/attachments/{attId:guid}/download")] (:106)

Validator bờ BE khớp UI: 20 MB (ContractAttachmentFeatures.cs:28 MaxBytes) = chữ "tối đa 20 MB" (:343); RuleFor(x => x.Purpose).IsInEnum()3 hợp lệ. MIME whitelist 8 loại ⊇ accept= của input (:351). (Ghi chú: SEALED_COPY = 3 là hằng FE tự khai lại — KHÔNG có enum chung; đây là điểm đứt-hợp-đồng tiềm năng nếu BE đổi số, nhưng số 3 đã ghim bằng test T3 nên có lưới.)

1.2 Badge chỉ vẽ trên list phase=9 (caveat "cờ không xét phase" ĐƯỢC CHẶN Ở PHÍA GỌI)

:79 query hardcode phase: ContractPhase.DaPhatHanh (=9, types/contracts.ts:10) — người dùng KHÔNG có cách nào đổi phase trên trang này (không ô lọc phase). ⇒ mọi hàng render đều là phase 9. Sweep consumer: grep -rn "hasSealedCopy" fe-*/src = đúng 4 hit (2 khai type + 2 render), CẢ 2 đều trong HardCopiesPage. ⇒ cờ "không xét phase" của BE không rò ra màn nào khác. Caveat T-ghim của lane test là ĐÚNG ở tầng BE và ĐÃ ĐƯỢC TRUNG HOÀ ở tầng FE. Không phải lỗi.

1.3 UI ghi chú 1-mốc

:106-110 hộp amber: "Ký giám đốc · đóng dấu · thủ tục lưu bộ gốc được thực hiện NGOÀI hệ thống (trên giấy). … Có file scan là xem như xong." — khớp literal chốt @S161 + đóng rủi ro §③-C.

1.4 Map 7 loại HĐ 1..7 (đối chiếu 3 nguồn, không tin 1 nguồn)

ContractType.cs:5-11 (BE enum) ≡ MenuKeys.cs:34-35 ContractTypeCodesTYPE_CODE_TO_INT (Layout.tsx:19-25) ≡ nhãn seed DbInitializer.cs:1981-1987 "01."…"07." ≡ ContractTypeLabel (types/forms.ts:13-21):

int enum BE code staticMap Hdc_* nhãn seed :1981-1987
1 HopDongThauPhu ThauPhu /hard-copies?type=1 01. HĐ Thầu phụ - Bảng cứng
2 HopDongGiaoKhoan GiaoKhoan ?type=2 02. HĐ Giao khoán - Bảng cứng
3 HopDongNhaCungCap NhaCungCap ?type=3 03. HĐ Nhà cung cấp - Bảng cứng
4 HopDongDichVu DichVu ?type=4 04. HĐ Dịch vụ - Bảng cứng
5 HopDongMuaBan MuaBan ?type=5 05. HĐ Mua bán - Bảng cứng
6 HopDongNguyenTacNCC NguyenTacNcc ?type=6 06. HĐ Nguyên tắc NCC - Bảng cứng
7 HopDongNguyenTacDichVu NguyenTacDv ?type=7 07. HĐ Nguyên tắc Dịch vụ - Bảng cứng

7/7 khớp, 0 lệch. grep "Hdc_" Layout.tsx | grep -c "coming-soon" = 0 ở CẢ 2 app (acceptance :29 ĐẠT).

1.5 FLAG-2 (MINOR) — chú thích đầu file SAI về route của root HopDongCung

HardCopiesPage.tsx:13 (cả 2 app, byte-identical) khai:

//   HopDongCung (root)            → /hard-copies (không query = tất cả loại)

ĐO trên đĩa: fe-user/src/components/Layout.tsx:80 HopDongCung: '/dashboard' · fe-admin KHÔNG có entry HopDongCung nào (grep = 0 hit ngoài chính dòng chú thích này). Vô hại về hành vi (root có 7 con ⇒ MenuNodeRenderer (Layout.tsx:192-196) render nó thành MenuGroup, path không bao giờ được dùng), nhưng chú thích đang mô tả một lối vào KHÔNG TỒN TẠI. Nhánh typeFilter === null ("tất cả loại", :66) hiện không có lối vào từ UI — chỉ tới được bằng gõ URL tay.

1.6 FLAG-3 (MINOR) — chú thích "ẩn hẳn khối này" trái với mã ngay dưới nó

:406 khai "levelOpinions null ⇒ ẩn hẳn khối này". Mã :407-416 render khối LUÔN LUÔN, chỉ đổi ruột thành câu "Hợp đồng này không có chữ ký duyệt điện tử." Hành vi thực tế (hiện placeholder) hợp lý hơn với HĐ V1 legacy, nên đề nghị sửa CHÚ THÍCH cho khớp mã, không đổi mã.

1.7 FLAG-4 (MINOR) — note dùng chung khi thả NHIỀU file một lượt

:283-286 for (const f of Array.from(files)) upload.mutate(f) — input có multiple (:348) và vùng kéo-thả nhận N file. Cả N mutation cùng đọc một biến note qua closure ⇒ N file nhận CÙNG ghi chú; isPending (:353) chỉ phản ánh mutation cuối nên chữ "Đang tải lên…" có thể tắt sớm khi file khác còn bay. Không mất dữ liệu (mọi file vẫn POST), chỉ là UX. Ca 1-file — đường đi thực tế của "1 bộ scan" — hoàn toàn đúng.

1.8 Ghi nhận cho prod-verify (không phải lỗi)

Bề mặt upload purpose=3 đã tồn tại từ trước W7: ContractAttachmentsSection.tsx:19 PURPOSES = [1,2,3,99], nhãn 3: 'Bản scan đã đóng dấu'. ⇒ HĐ nào đã từng được đính kèm purpose=3 qua màn chi tiết sẽ bật badge ngay khi deploy, chưa cần thao tác gì trên trang mới. Khi chạy acceptance "upload → badge lật", phải chọn HĐ có 0 attachment purpose=3 để phép thử có chiều âm thật.


§2 — TRỤC 2: hợp-đồng FE↔BE (ĐO XONG — 4/4 KHỚP)

Đi NGƯỢC lời khai: đọc từng bờ trên ĐĨA, không tin bảng đối-chứng của lane BE.

# FE gửi/đọc (file:line) BE nhận/trả (file:line) Verdict
1 params.type = typeFilter ?? undefined (HardCopiesPage.tsx:80) → query-string type= [FromQuery] ContractType? type = null (ContractsController.cs:25) → new ListContractsQuery(phase, supplierId, projectId, type) (:27), vị-trí 4 khớp record ContractFeatures.cs:285-289
2 params.phase = 9 (:79) [FromQuery] ContractPhase? phase (:20) — 0 dòng đụng
3 c.hasSealedCopy (:194), type hasSealedCopy: boolean (types/contracts.ts:96/:100) bool HasSealedCopy (ContractDtos.cs:25) — camelCase do Program.cs không override PropertyNamingPolicy (tự kiểm lại, không mượn kết luận lane BE)
4 params.search (:81) [FromQuery] string? search (:19) + handler CÓ áp :329-336
5 Paged<T>{items,total,totalPages} (types/master.ts:1-7) PagedResult<T> Items/Total + computed TotalPages (PagedResult.cs:3-9) — property tính toán VẪN serialize

Phép đo mạnh nhất — tsc --noEmit từng app (mạnh hơn grep vì bắt cả field thiếu lẫn thừa):

fe-admin$ npx tsc --noEmit -p tsconfig.app.json   → EXIT=0, 0 dòng lỗi
fe-user $ npx tsc --noEmit -p tsconfig.app.json   → EXIT=0, 0 dòng lỗi

⇒ KHÔNG tái diễn lớp hợp-đồng-đứt-2-bờ của S161-W1/W2. Lý do cấu trúc: wave này chỉ thêm 1 field và 1 query-param, và cả hai đều được lane BE đo trước rồi lane FE dùng lại đúng tên.


§3 — TRỤC 3: boundary O-A (ĐO XONG — 4/4 GIỮ NGUYÊN)

Phép đo = liệt kê vét cạn mọi file thay đổi (tracked + untracked), rồi hỏi từng ranh giới.

Ranh giới spec §②-4 / §⑤ Lệnh đo Kết quả
attachment upload/download Contract 0-diff git status --porcelain | grep -i Attachment NONEContractAttachmentFeatures.cs không nằm trong tập thay đổi
0 policy mới git diff | grep '^+.*\[Authorize' NONE
0 migration git status --porcelain | grep -i migration NONE (và 0 cột/bảng mới — HasSealedCopy là field DTO, không phải cột)
menu 142, chỉ staticMap git status --porcelain | grep -iE 'MenuKeys|DbInitializer|menuKeys.ts' NONE |MenuKeys.All| không đổi ⇒ số policy (derived) không đổi ⇒ MenuItems prod 142 giữ (canonical docs/STATUS.md:6)

Bổ sung — ContractsController.cs CÓ bị sửa nhưng chỉ 1 hunk @@ -20,8 +20,11 @@ nằm trọn trong method List; 3 endpoint attachment (:88/:106/:113) 0 dòng đụng. [Authorize] trần cấp class (:13) giữ nguyên; policy Contracts.Read của /deleted (:32, có từ S159) không bị nới.

3.1 Ghi nhận an-ninh — vùng O-A anh ĐÃ CHỦ ĐỘNG để mở (KHÔNG phải finding mới)

Ghi lại để lead có mặt-bằng khi quyết W8, không tính vào verdict vì spec §②-4 chốt O-A và diff = 0:

  • DownloadContractAttachmentQueryHandler (ContractAttachmentFeatures.cs:115-123) không có rào quyền nào — chỉ khớp (AttachmentId, ContractId). Ai đăng nhập + biết cặp ID là tải được bản scan HĐ đã đóng dấu. Giảm nhẹ: muốn biết attachmentId phải qua GET /contracts/{id} mà endpoint đó rào (ContractFeatures.cs:614-638, đã gồm vế isHardCopyActor của W6) ⇒ W7 không mở rộng bề mặt lộ so với trước.
  • UploadContractAttachmentCommandHandler (:59-80) cũng không có rào quyền/phase — spec §③-C khai thẳng "hành vi HIỆN TRẠNG". W7 làm tăng khả-năng-xảy-ra trên thực tế vì nay có thêm một lối vào upload nổi bật (panel trang bản cứng) bên cạnh lối cũ ở màn chi tiết HĐ. Đề nghị đưa vào W8-authz cùng lỗ [Authorize] trần đã ghi ở STATUS.md:17 mục ③.

§4 — TRỤC 4: 2 quan sát của lane test (ĐO XONG)

4.1 (a) +35/4 (BE tự khai) vs +31/4 (test lane đo) → +31/4 ĐÚNG

Đo lại bằng hai phương pháp độc lập, cùng cho một kết quả:

git diff --numstat -- src/Backend
  4  1  ContractsController.cs
 18  2  ContractFeatures.cs
  9  1  Dtos/ContractDtos.cs
                        → cộng dọc = 31 thêm / 4 xoá

git diff -- src/Backend | grep -c '^+[^+]'  → 31
git diff -- src/Backend | grep -c '^-[^-]'  → 4

lane test ĐÚNG, sub-implementer-backend-0.md:152 SAI +4 dòng. Không ảnh hưởng hành vi (mã trên đĩa là mã đã build + đã test), nhưng đúng lớp số-tự-khai thay cho số-đo: con số +35 không đi kèm lệnh nào sinh ra nó, nên không có phép kiểm nào làm nó TRƯỢT. Khuyến nghị lead khi tổng hợp: lấy +31/4, và ghi errata 1 dòng trong sub-md của lane BE.

4.2 (b) Caveat "cờ chết 2 màn" (Đã xóa · Hộp thư luôn false) → CHẤP NHẬN cho 1-mốc, KHÔNG cần +2 dòng derive

Ba căn cứ ĐO ĐƯỢC (không phải cảm tính):

  1. Không có người tiêu thụ ⇒ không có lời nói dối hiển thị. grep -rn "hasSealedCopy" fe-admin/src fe-user/src = 4 hit, toàn bộ nằm ở types/contracts.ts (khai) + HardCopiesPage.tsx:194 (vẽ). Màn "Đã xóa" và "Hộp thư" không hề đọc field này ⇒ hôm nay false không hiện ra đâu cả.
  2. Cái giá của việc derive không nhỏ như "2 dòng". Muốn cờ sống ở 2 màn kia phải sửa 2 projection (ContractFeatures.cs:466 + :545) và mở thêm 2 phép EXISTS cho GetMyInbox — vốn là truy vấn chạy mỗi lần đổ chuông. Đổi lấy 0 lợi ích hiện tại, trong một wave có chỉ-thị "1-MỐC, scope tối thiểu".
  3. Rủi ro duy nhất đã có lưới. Nguy cơ thật là: sau này ai đó thêm cột "Bản cứng" vào lưới Hộp thư mà không đụng BE ⇒ hiện "Chưa có bản cứng" cho HĐ đang CÓ. Nhưng ca đó đã bị ghim bởi ContractHardCopyTests.cs:387-395, kèm thông điệp assert nói thẳng "Đây là ranh giới scope W7 đang được ghim, KHÔNG phải phép đo nói rằng HĐ đó không có bản cứng" — người sửa sau đọc đúng câu họ cần để biết mình đang đổi hợp đồng chứ không phải sửa test cho vừa. Ranh giới này còn được ghi lần thứ hai ngay tại chỗ khai field (ContractDtos.cs:18-25).

Khuyến nghị: GIỮ NGUYÊN. Nếu lead vẫn muốn derive, đó là quyết định mở-scope có chủ đích, và khi đó phải sửa cả 3 chỗ cùng lượt: 2 projection + 2 assert đang ghim BeFalse (đổi assert lúc đó là hợp lệ vì hợp đồng đã đổi — khác hẳn ca "nới assert cho test xanh").


§5 — VERDICT (lead finalize ON-BEHALF @S162 — return sub #53 stale hơn đĩa: khai "đang viết Trục 3+4" trong khi đĩa đã có ĐỦ §1-§4 kèm evidence; đúng đường (3) feedback_return_terminates_agent)

PASS-có-điều-kiện — 0 lỗi hành-vi. Điều kiện commit = xử FLAG-1 (git add đích danh 3 file untracked).

Flag Mức Xử lý
FLAG-0 sub-md FE vắng MAJOR quy-trình GHI NHẬN — lane FE #53 từ đợt-1 (lead đã khai trong WAL); mọi kết luận FE của review này đo THẲNG TỪ ĐĨA nên không phụ thuộc lời khai lane
FLAG-1 3 file MỚI untracked HIGH git GATE commit: git add đích danh + porcelain-??-rỗng (đúng khuôn F-0 S155 đã áp W1-W6)
FLAG-2 comment root HopDongCung MINOR ĐÃ VÁ @S162 (comment ghi đúng: root = MenuGroup, path không dùng) — SHA-pair giữ C7DCC20B…
FLAG-3 comment "ẩn hẳn khối" MINOR ĐÃ VÁ @S162 (comment ghi đúng: khối vẫn hiện, ruột placeholder) — cùng SHA-pair
FLAG-4 note chung khi multi-file MINOR UX GIỮ — ca thật là 1 bộ scan/HĐ; ghi nhận cho backlog UX
§4.1 BE tự-khai +35 vs đo +31 errata ĐÃ GHI errata vào sub-implementer-backend-0.md @S162; số tổng hợp wave = +31/4
§4.2 cờ-chết-2-màn khuyến nghị GIỮ NGUYÊN (3 căn cứ đo được; đã ghim 2 lớp test+DTO-comment)
§3.1 upload/download 0-rào an-ninh W8 KHÔNG tính verdict (O-A anh chốt để mở) — chuyển hồ sơ W8-authz

Điểm neo chất lượng: hợp-đồng FE↔BE 4/4 + tsc --noEmit ×2 EXIT=0 · boundary 4/4 (attachment 0-diff, 0 policy, 0 mig, menu 142 giữ) · map 7 loại khớp 3 nguồn độc lập · badge phase-9 chặn ở phía gọi (hardcode :79, sweep 4-hit đúng chỗ).