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.mdS160 + đĩa + curl prod. NOTE naming: task LƯỢT-1 gọisub-reviewer-owner-consistency-s160.md, khối RUN-TRACE + ISOLATION chốtsub-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:19bước 3: "Seeder grant (gotcha #84 — CÙNG COMMIT): cấpContractsCreate/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đặtCanRead=true, CanCreate=isPe(⇒ false choContracts),CanUpdate=false,CanDelete=false. - Row
Contracts/Ct_*đã tồn tại prod (grant "hiển thị hết" S159 —DbInitializer.cs:2209-2213comment: "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-op ⇒ 403 cho 13/13 role non-Admin trên 22 endpoint ghi (MenuPermissionHandler.cs:41-50khớpp.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-2296ghi 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):
MenuKeys.cs:41public const string KeHoachKyKet = "KeHoachKyKet";— NGOÀIAll(mảngAll:163-183, không chứa) ⇒ plan:48 nói đúng, 0 policy hiện tại.- 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 keyKhkk_*(:1779-1786), G1 có parent =MenuKeys.KeHoachKyKet. - 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). MenuPermissionHandler.cs:41p.MenuKey == req.MenuKey— khớp ĐÚNG-KHOÁ, KHÔNG kế thừa root ⇒ CanRead trênKhkk_List/Khkk_Create(13/13 role,:2177-2180) KHÔNG cứu policyKeHoachKyKet.Create.- ⇒ W2/W3 lên prod:
POST /api/contract-signing-plans403 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-212ratified "+4 constContractSigningPlans/_List/_Create/_InboxVÀ đưa CẢ 4 vàoAll". Plan chỉ đưa 1 root ⇒ mọi endpoint chung 1 tiền tố; 6 leafKhkk_*(đã seed + granted) nằm ngoàiAll⇒ 0 policy, quyền leaf thành trang trí. - Nới quyền đọc: root
KeHoachKyKetđang CanRead mọi role ⇒KeHoachKyKet.Read= default-OPEN cho toàn module (đọcProposedAmount/ApprovedAmountper-NCC), trong khi SC:224seed 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/PEDaDuyetkhông xoá; sequence không rollback. plan:54 (§4.3): KHÔNG cờ IsDryRun. - ⇒ "đảo được" đúng cho MÃ, 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:3lặ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:32có 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" có 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:17ghi 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—:161nguyê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/contractsbị 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:7chỉ khoanh "(2) Create ăn sequence thật (ContractFeatures.cs:113-118)" = nửa cửa. Acceptancew8:28MoiEndpointGhi_CoAuthorizePolicy(reflection trênContractsController) 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:11khai Δ tường minh; đĩa chứngDbInitializer.cs:1774"Kế hoạch ký kết HĐ" +:1779"1. Kế hoạch ký kết HĐ (NCC-TP)"; owner-verbatimdocs/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:9cù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:134bọcSeedDemoContractsAsynctrongif (!demoSeedDisabled); path tạo-mới:892chạy khi không còn HĐ[DEMO]⇒ env không bật cờ (Dev/staging) sẽ tái sinh 7 HĐ demo ⇒ quy ước lọcZZTESTcủ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)
- 5/5 anchor §③-A của W5 HIT trên đĩa:
CreateContractFromEvaluationFeatures.cs:53-60(guardPhase!=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...")). - W5 (c) khớp SC:143-144 —
ContractIdnằm trênLines(per-NCC), không phải header;w5:16gọi đúng "cột C6 sẵn từ W1". Không có drift cardinality. - W8 §③-A anchor đúng:
ContractsController.cs:13[Authorize]class-trần ·:28-29[HttpGet("deleted")]+[Authorize(Policy="Contracts.Read")]·MenuKeys.cs:168Contracts, Forms, Reports,(⇒ 4 policyContracts.*sống thật). - plan:48 đúng —
KeHoachKyKetcó const, ngoàiAll. - 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:13và ghi rõ quyết định thuộc owner. Bỏ cụm "O-A/O-B mặc định" (0 bit). - A2 (F-O3):
w8:19phả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/Deletecho 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-leafKhkk_*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:7liệt cửaPOST /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).