Files
solution-erp/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/sub-reviewer-4.md
2026-07-29 16:54:56 +07:00

18 KiB

sub-reviewer-4 — LANE D: NHẤT QUÁN spec-cũ-ratified + QUYỀN OWNER (S160)

VERDICT: SUA-TRUOC-THI-CONG (4 HIGH · 3 MED · 4 LOW · 5 điểm củng cố). Chặn: W8 chạy bằng default O-B + Δ1 policy-prefix. W5 (trừ F-O7) và bảng default 5-câu-owner ĐỨNG.

Đích soi: plan-cha-dry-run-toan-trinh-29-07-2026.md + spec-wave-w5-cau-khkk-hd-29-07-2026.md + spec-wave-w8-authz-29-07-2026.md. Đối chiếu: runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md (RATIFIED, viết tắt SC = spec cũ) + run.md S160 + đĩa + curl prod. NOTE naming: task LƯỢT-1 gọi sub-reviewer-owner-consistency-s160.md, khối RUN-TRACE + ISOLATION chốt sub-reviewer-4.md — chọn theo ISOLATION (1 file duy nhất).

0. Trạng thái ghi-đĩa

  • khung rỗng (lượt 1) · [x] 3 file đích · [x] SC + run.md · [x] đo đĩa 9 lệnh · [x] curl prod · [x] chốt F-O1..F-O11

1. HIGH

F-O1 [HIGH] — O-B vá ĐÚNG NỬA SAU của thứ O-3 tuyên "NGOÀI SCOPE", nhưng plan xếp làm DEFAULT tự chạy

  • SC:49 (O-3), verbatim hệ quả: "Lỗ hổng an ninh ContractWorkflowService.cs:48-66 + controller HĐ class-trần: NGOÀI SCOPE, treo có chủ đích. Wave nào chạm module Contract phải hỏi lại anh." → nêu ĐÍCH DANH 2 vật.
  • SC:410-411 ("Ngoài mọi wave — KHÔNG làm trong run này") liệt lại đúng 2 vật, "anh chốt từ từ".
  • docs/STATUS.md:13 (canonical, S159 — 1 ngày trước): "❹-risk CÓ Ý THỨC (verbatim anh): «đang phát triển → cho hiển thị hết để mọi người góp ý, còn duyệt NCC thì cứ để như cũ» — menu HĐ public 13/13 role trong khi authz HĐ còn [Authorize] trần (lỗ hổng S156 treo chủ-đích) — đã cảnh báo, anh tái khẳng định." ⇒ owner ĐÃ được cảnh báo đúng lỗ này và TÁI KHẲNG ĐỊNH giữ nguyên.
  • plan:73 (§5 row 11): DEFAULT = O-B = gắn [Authorize(Policy)] 22 endpoint ghi + seeder grant ⇒ chính là vá vật thứ 2. Chỉ O-C (vật thứ 1) bị đánh dấu "cần anh gật riêng".
  • Mâu thuẫn nội bộ: run.md:17 ④ lead tự viết "riêng LỖ HỔNG authz S156 ... KHÔNG tự coi là được sửa hay được bỏ qua" ⟂ plan:73 biến nó thành default tự chạy.
  • Hệ quả nghiệp vụ (không chỉ hình thức): siết authz = có thể 403 đúng những người owner muốn "bấm thử để góp ý" (xem F-O3). Tức O-B đi ngược MỤC ĐÍCH owner phát biểu, không chỉ ngược thủ tục.
  • Sửa: chuyển O-B về cùng nhóm "chờ anh gật" với O-C, hoặc giữ default nhưng plan phải có 1 dòng Δ tường minh + 1 câu hỏi owner: "siết quyền ghi HĐ trước dry-run — đồng ý không, biết rằng siết sai grant = 403 người bấm thử?".

F-O2 [HIGH] — quyền quyết O-B bị chuyển từ OWNER sang REVIEW-ENSEMBLE (uỷ quyền sai cửa)

  • w8:3: "Option = REVIEW CHỐT quyết"; plan:73 "(O-A fallback nếu review bác)". Vật này do owner đóng ở O-3/STATUS.md:13, reviewer không có thẩm quyền mở lại.
  • Phụ: plan:27 ghi "option O-A/O-B mặc định" ⟂ plan:73 chọn hẳn O-B ⇒ 2 option cùng làm "mặc định" = 0 bit (không biết đang ở đâu thì không có gì để đảo).
  • Sửa: plan §5 row 11 ghi 1 giá trị default duy nhất + ghi rõ "quyết định thuộc owner, review chỉ khuyến nghị".

F-O3 [HIGH] — W8 bước 3 (seeder grant) KHÔNG THỂ ĂN trên prod ⇒ O-B deploy = 403 toàn bộ, giết đúng dry-run

  • w8:19 bước 3: "Seeder grant (gotcha #84 — CÙNG COMMIT): cấp Contracts Create/Update cho role tham gia dry-run".
  • Đĩa bác: DbInitializer.cs:2233-2243 — với key non-PE, nếu row đã tồn tại thì continue (skip-existing, comment nguyên văn :2242 "Key non-Pe: skip-existing (giữ nguyên như cũ)"). Nhánh insert :2247-2253 đặt CanRead=true, CanCreate=isPe (⇒ false cho Contracts), CanUpdate=false, CanDelete=false.
  • Row Contracts/Ct_* đã tồn tại prod (grant "hiển thị hết" S159 — DbInitializer.cs:2209-2213 comment: "nhánh grant key-thường là skip-existing ⇒ trên prod row false KHÔNG được nâng ở đây — re-grant prod đã chạy SQL trực tiếp cùng ngày").
  • ⇒ O-B thêm [Authorize(Policy="Contracts.Create/Update/Delete")] có răng ngay, còn grant no-op403 cho 13/13 role non-Admin trên 22 endpoint ghi (MenuPermissionHandler.cs:41-50 khớp p.MenuKey==req.MenuKey + cờ tương ứng; chỉ Admin bypass :27-31).
  • Bài học ĐÃ CÓ trong chính SC mà w8 không mang theo: SC:271 (NOTE @S160) "ĐỔI nhãn/parent/CanRead của key ĐÃ TỒN TẠI phải đi đường update-backfill kiểu labelBackfill đợt-5, KHÔNG phải insert; và CẤM SQL tay". Thêm vết đắt: DbInitializer.cs:2287-2296 ghi Run #423 — grant SQL tay bị revoker lật, "447/494 row rơi, 11/13 role về 0".
  • Acceptance w8:32 (đếm Permission rows per-role + restart-proof) CÓ răng — nhưng chỉ bắt SAU deploy (w8:21 "chạy NGAY phép 403/201 sau deploy").
  • Sửa: bước 3 phải ghi rõ nhánh upgrade riêng cho key non-PE (đọc row tồn tại → set CanCreate/CanUpdate → SaveChanges), idempotent, và nghiệm thu bằng restart THẬT; nếu không có nhánh này thì O-B cấm deploy.

F-O4 [HIGH] — Δ1 (plan §5 row 12) All += KeHoachKyKet: sinh policy nhưng 0 grant ⇒ #44 silent-403; đồng thời mất hạt per-leaf và nới Read thành default-OPEN

Đo đĩa (5 mệnh đề, tất cả verify được bằng lệnh trong file này):

  1. MenuKeys.cs:41 public const string KeHoachKyKet = "KeHoachKyKet";NGOÀI All (mảng All :163-183, không chứa) ⇒ plan:48 nói đúng, 0 policy hiện tại.
  2. Menu row root có thật: DbInitializer.cs:1774 (MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, ...); 6 leaf + G1 dùng key Khkk_* (:1779-1786), G1 có parent = MenuKeys.KeHoachKyKet.
  3. Grant: :2206 .Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung }) với comment :2204-2205 "2 root placeholder GĐ2/GĐ4 — CanRead-only mọi role (IsPeKey=false ⇒ nhánh read-only)"CanCreate/Update/Delete = false, và skip-existing chặn nâng (F-O3).
  4. MenuPermissionHandler.cs:41 p.MenuKey == req.MenuKeykhớp ĐÚNG-KHOÁ, KHÔNG kế thừa root ⇒ CanRead trên Khkk_List/Khkk_Create (13/13 role, :2177-2180) KHÔNG cứu policy KeHoachKyKet.Create.
  5. ⇒ W2/W3 lên prod: POST /api/contract-signing-plans 403 im lặng cho 13/13 role, chỉ Admin qua (:27-31) — đúng gotcha #44, đúng thứ dry-run cần nhất ("mọi người bấm thử"). Hai hệ quả nữa chưa được khai trong row 12:
  • Mất hạt: SC §2.4 :210-212 ratified "+4 const ContractSigningPlans/_List/_Create/_Inbox VÀ đưa CẢ 4 vào All". Plan chỉ đưa 1 root ⇒ mọi endpoint chung 1 tiền tố; 6 leaf Khkk_* (đã seed + granted) nằm ngoài All ⇒ 0 policy, quyền leaf thành trang trí.
  • Nới quyền đọc: root KeHoachKyKet đang CanRead mọi roleKeHoachKyKet.Read = default-OPEN cho toàn module (đọc ProposedAmount/ApprovedAmount per-NCC), trong khi SC :224 seed admin-permission = default-CLOSED. Owner-verbatim "hiển thị hết" (STATUS.md:13) nói về MENU, không nói về quyền đọc số tiền qua API.
  • Trả lời câu hỏi của đề bài ("Δ1 cần anh gật lại không?"): đổi TÊN KEY = KHÔNG cần (hệ quả kỹ thuật của skeleton Khkk_* mà chính owner chốt @S159; SC:269 còn CẤM đẻ bộ key mới). Đổi mức mặc định đọc (closed→OPEN) + bỏ hạt per-leaf = CẦN 1 dòng cho anh — nó đổi ai đọc được số tiền, không phải chi tiết đặt tên.

2. MED

F-O5 [MED] — nhãn "12 default ĐẢO ĐƯỢC, anh veto lúc nào cũng kịp" mâu thuẫn với chính §4 của plan

  • plan:59 (tiêu đề §5): "dry-run chạy bằng default ĐẢO ĐƯỢC — anh veto lúc nào cũng kịp".
  • plan:55 (§4.4) tự khai: HĐ ≥ phase 5 KHÔNG xoá được qua API (ContractFeatures.cs:632-633); phiếu KHKK/PE DaDuyet không xoá; sequence không rollback. plan:54 (§4.3): KHÔNG cờ IsDryRun.
  • ⇒ "đảo được" đúng cho , sai cho DỮ LIỆU đã sinh trên prod. Nặng nhất: default #2 (Q6 — giá nào vào HĐ, sai thì HĐ đã phát hành mang số sai) và #6 (format mã KHKK đóng băng vào Mig 69 — chính SC:306-308 đã cảnh báo "'không chặn' chỉ đúng nghĩa không-chờ-trả-lời, KHÔNG đúng nghĩa không có quyết định nào bị khoá").
  • Sửa: hạ câu tiêu đề §5 xuống "đảo được ở mã; dữ liệu/HĐ đã sinh thì không" + đánh dấu #2/#6 là default một-chiều.

F-O6 [MED] — O-3 bị THU HẸP mà không khai Δ tường minh (W6/W7 chạm Contract nhưng không có cửa hỏi)

  • SC:49: "Wave nào chạm module Contract phải hỏi lại anh" (phạm vi = MỌI wave).
  • plan:90 rút còn "O-3 còn hiệu lực cho phần SỬA service"; w8:3 lặp lại y vậy ⇒ cắt mất vế controller và vế "mọi wave".
  • Hệ quả trên bảng wave: W6 (plan:25 "sẵn chạy") nằm TRỌN trong module Contract (view-guard + inbox + vai-trình); W7 (plan:26 "sẵn chạy"); W8-O-B (plan:73 default). 3 wave chạm Contract, 0 cửa hỏi. (Điểm sáng: w6:32 có tự rào "đụng nhánh Reject-trước-guard = KHÔNG PHẢI wave này (O-C, cần anh gật riêng — đừng «tiện tay sửa»)".)
  • Suy luận công bằng: lệnh anh @S160 (run.md:8-15) "dry-run luôn cho đến hợp đồng cứng" phủ W5/W6/W7 (đó là ĐƯỜNG ĐI của dry-run). Nhưng W8 không nằm trên đường đi — nó là đổi hành vi bảo mật ⇒ không suy ra được từ câu đó.
  • Sửa: plan §1 (hoặc §5 row 11) thêm 1 dòng Δ: "O-3 :49 được owner mở @S160 cho phần chạm-để-chạy (W5/W6/W7); phần chạm-để-đổi-hành-vi-bảo-mật (W8 O-B/O-C) VẪN đóng, chờ gật."

F-O7 [MED] — O-B không phủ CỬA TẠO HĐ THẬT mà dry-run dùng (bridge endpoint)

  • HĐ dry-run sinh qua POST /api/purchase-evaluations/{peId}/create-contract (w5:17 ghi rõ FE gọi CÙNG endpoint này).
  • Đĩa: endpoint ở PurchaseEvaluationsController.cs:339; class chỉ [Authorize] trần (:15). grep -c "Authorize(Policy" = 3 nhưng 2/3 là COMMENT (:147, :161:161 nguyên văn "🔴 CỐ Ý KHÔNG có [Authorize(Policy=...)] — owner chốt 2026-07-27..."), attribute thật chỉ :188 (PurchaseEvaluations.Read). ⇒ bridge endpoint = 0 policy.
  • ⇒ Sau O-B: POST /api/contracts bị siết, nhưng cửa tạo HĐ per-winner + ăn sequence thật vẫn mở cho mọi user đăng nhập. w8:7 chỉ khoanh "(2) Create ăn sequence thật (ContractFeatures.cs:113-118)" = nửa cửa. Acceptance w8:28 MoiEndpointGhi_CoAuthorizePolicy (reflection trên ContractsController) cũng không bắt cửa này.
  • ⚠️ Cộng hưởng: W5 (w5:14) THÊM param vào chính body endpoint chưa có khoá ⇒ mở rộng cửa trước khi khoá.
  • Sửa: w8 §① liệt cửa thứ 3 = bridge endpoint; hoặc khai tường minh "cố ý để mở, lý do X" (kiểu comment :161) để không tưởng đã phủ hết.

3. LOW

F-O8 [LOW-MED] — nhãn 2 đời: dùng ĐÚNG, nhưng file RATIFIED chưa được stamp errata

  • ĐÚNG: plan:13 + w1:11 khai Δ tường minh; đĩa chứng DbInitializer.cs:1774 "Kế hoạch ký kết HĐ" + :1779 "1. Kế hoạch ký kết HĐ (NCC-TP)"; owner-verbatim docs/STATUS.md:13 "❷ Kế hoạch ký kết HĐ (đời trước «Đề xuất ký kết hợp đồng» — superseded)" + docs/HANDOFF.md:9 cùng nội dung. W5/W8 không dùng nhãn ⇒ 0 rủi ro.
  • Tồn dư: SC :27 vẫn ghi "OWNER CHỐT NHÃN @S159: tên phiếu/menu = «Đề xuất ký kết hợp đồng» — mọi label VN trong spec đọc theo tên này" và SC :223 vẫn dạy seed nhãn cũ, dù CÙNG FILE đã được chèn NOTE @S160 tại :268-271 ⇒ pass bảo trì bất đối xứng. Ai đọc §2.4 trước w1 sẽ seed sai nhãn.
  • Sửa: stamp errata 1 dòng tại SC:27 + SC:223 (không viết lại spec — đúng luật "chỉ BỔ SUNG").

F-O9 [LOW · CỦNG CỐ] — 5 câu owner cũ: default KHỚP trạng thái treo, KHÔNG lấn

SC:488 khai "5 câu owner (O-Q1 · O-Q2 · Q3 · Q6 · Q11) vẫn nguyên"; plan không đâu tuyên "đã trả lời". Từng dòng:

plan §5 Câu Đối chiếu SC Phán
#1 Q3 SC:364 "W3 mặc định chỉ làm đường THƯỜNG; 2 đường kia chờ anh" + phán "PHÁ VỠ" S155 KHỚP — plan chọn bảo thủ; w3:7 nhất quán (0 bật AllowApproverFinalize/CeoApprovalThreshold)
#2 Q6 SC:400 "mặc định thiết kế: KH thắng + audit lệch" KHỚP (nhưng xem F-O5: hệ quả DỮ LIỆU không đảo được)
#3 Q11 SC:401 hỏi "chặn hay cảnh báo", không nêu default plan chọn mềm = hướng đảo-được (nâng 409 sau) — hợp lý, không lấn
#4 O-Q1 SC:62 "cảnh báo mềm (tiền lệ S62) — O-Q1 owner quyết mềm/cứng" KHỚP nghiêng sẵn của SC
#5 O-Q2 SC:391-393 plan DEFER W4, 0 dev = giữ hiện trạng ⇒ không tiêu quyền nào
⇒ 5/5 nằm TRONG biên SC đã ghi. Đây là điểm củng cố (cân bằng với 4 HIGH ở trên).

F-O10 [LOW] — baseline "0 HĐ" ĐÚNG (tôi đo lại độc lập) nhưng thiếu 1 mệnh đề nhân-quả + 1 điều kiện env

  • Curl độc lập (2026-07-29, token admin 468B): GET /api/contracts?page=1&pageSize=1{"items":[],"total":0,...} · GET /api/contracts/deleted?...{"items":[],"total":0,...}plan:43 ĐỨNG (measured-label gate PASS: claim có phép phản chứng và phép đó đã chạy).
  • Nguồn "7 HĐ V1" của sổ S156 = demo seeder, không phải HĐ nghiệp vụ: docs/changelog/recently-done-archive-2026-04.md:22 "SeedDemoContractsAsync — 7 demo HĐ varied phases ... 7 HĐ covering 7 ContractType". Plan gọi "STALE" đúng KẾT LUẬN nhưng thiếu vế nguyên nhân (0 ≠ "ai đó xoá 7 HĐ thật").
  • Điều kiện env: DbInitializer.cs:134 bọc SeedDemoContractsAsync trong if (!demoSeedDisabled); path tạo-mới :892 chạy khi không còn[DEMO]env không bật cờ (Dev/staging) sẽ tái sinh 7 HĐ demo ⇒ quy ước lọc ZZTEST của plan §4.1 phải cộng bộ lọc [DEMO].

F-O11 [LOW] — literal SC:331 đã tự-lão-hoá (không lây sang bộ mới)

SC:331 (acceptance W2) dạy đối chứng "grep -c "Authorize(Policy" ContractsController.cs = 0 hit toàn file". Đĩa hôm nay = 1 (/deleted thêm @S159 đợt-5: ContractsController.cs:28-29). Bộ spec MỚI không chép literal này (grep "0 hit toàn file" trên plan-cha+spec-wave-* = 0 hit) và w8:11 dẫn đúng tiền lệ /deletedđã tránh được; chỉ cần errata ở SC.


4. ĐIỂM CỦNG CỐ (đo được, chống Smart-Friend một chiều)

  1. 5/5 anchor §③-A của W5 HIT trên đĩa: CreateContractFromEvaluationFeatures.cs:53-60 (guard Phase!=DaDuyet + winners + pe.ContractId) · :68-71 (pin V1: WorkflowDefinitions ... IsActive) · :88-90 (SUM(q.ThanhTien) đúng 3 dòng) · :145 (pe.ContractId = contracts[0].Id) · ContractWorkflowService.cs:115-116 (ConflictException("HĐ chưa pin workflow definition...")).
  2. W5 (c) khớp SC:143-144ContractId nằm trên Lines (per-NCC), không phải header; w5:16 gọi đúng "cột C6 sẵn từ W1". Không có drift cardinality.
  3. W8 §③-A anchor đúng: ContractsController.cs:13 [Authorize] class-trần · :28-29 [HttpGet("deleted")]+[Authorize(Policy="Contracts.Read")] · MenuKeys.cs:168 Contracts, Forms, Reports, (⇒ 4 policy Contracts.* sống thật).
  4. plan:48 đúngKeHoachKyKet có const, ngoài All.
  5. Q3 default ⟂ w3:7 không mâu thuẫn (SC:362-365 tự khai xung đột và đã tự giải theo hướng plan chọn).

5. YÊU CẦU SỬA TRƯỚC THI CÔNG (acceptance cho lượt vá)

  • A1 (F-O1/F-O2/F-O6): plan §5 row 11 + w8:3 — O-B chuyển thành "CHỜ ANH GẬT" ngang O-C, hoặc thêm dòng Δ tường minh vs SC:49 + STATUS.md:13 và ghi rõ quyết định thuộc owner. Bỏ cụm "O-A/O-B mặc định" (0 bit).
  • A2 (F-O3): w8:19 phải mô tả nhánh upgrade grant cho key non-PE; acceptance thêm 1 dòng: "sau restart THẬT, SELECT COUNT(*) FROM Permissions WHERE MenuKey='Contracts' AND CanCreate=1 ≥ số role dry-run".
  • A3 (F-O4): plan §5 row 12 bổ sung: (i) bước cấp Create/Update/Delete cho key mới qua nhánh upgrade; (ii) khai hệ quả Read default-OPEN + hỏi owner 1 dòng; (iii) chốt còn giữ hạt per-leaf Khkk_* hay không.
  • A4 (F-O5): sửa tiêu đề §5 + đánh dấu #2/#6 là default một chiều.
  • A5 (F-O7): w8:7 liệt cửa POST /api/purchase-evaluations/{id}/create-contract (hoặc khai cố-ý-để-mở).
  • A6 (F-O8/F-O11): stamp errata SC:27 · SC:223 · SC:331 (1 dòng/chỗ, không viết lại).