Files
solution-erp/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/sub-review-claims-4.md
pqhuy1987 d7eaeceaa7 [CLAUDE] Docs: spec "Ke hoach ky ket Hop dong" S157 (khuc con thieu PE -> HD) + review 4 lang kinh
Anh giao: dieu tra quy trinh CON THIEU giua Duyet NCC va HD NTP/NCC, ra spec day
du + chia wave. Chay /fable-real invest (Fable deep-pass) roi /fable-clone
reviewer 4 lang kinh.

PHAT HIEN NEN (lead do, invest + reviewer xac nhan tu nguon goc):
- 7 phase [LEGACY] cua ContractPhase CHINH LA quy trinh ISO 9 buoc ma-hoa cung
  (khop 1:1 dung thu tu). Khung V2 sinh ra de bieu dien chinh quy trinh do.
- Khoang trong nam DUNG giua ISO-1 va ISO-2, va ISO doc CUNG KHONG CO no
  => thieu o CA HAI tang (doc + code) => spec vua la spec phan mem vua la de
  xuat bo sung ISO.

OWNER CHOT trong cua: (O-1) cau truc trinh ky GIONG, chi khac NOI DUNG => tai
dung khung duyet V2, 0 bang approval moi => truc tranh luan Q2 hom qua DONG.
(O-2) SLA chi tham khao => khong SLA-engine, khong wave SLA.
(O-3) "cho hop dong cu tu tu" => lo hong an ninh HD NGOAI SCOPE.

REVIEW 4/4 lane sach 0 chet (dong goi C2 an), 112 diem. Claim nen DUNG VUNG.
Bat 7 loi NANG, da va het trong ban nay:
1. Ten menu key `Csp*` != tien to policy `ContractSigningPlans.*` => Program.cs
   khong sinh policy => HTTP 500 luc chay. Repo DA CO test bat
   (AuthorizePolicyRegressionTests.cs:138-139). Day la loi TU-VAP-BAY-MINH-VUA-
   NHAN-DIEN: §2.4 bat dung bay Pe_* roi §2.3 van vap bang co che khac.
2. W2 day rieng tao duoc 0 phieu (wiring Designer type-10 roi khoi moi wave)
   => keo vao W1, tai pham dung §F.0 S155.
3. Thieu bang Approvals ma service duoc lenh copy CO ghi vao.
4. Changelogs thieu 4 cot => copy service khong bien dich.
5. Thieu 3 site ApprovalWorkflowV2AdminFeatures => FK Restrict no 500.
6. Status enum bat dau tu 0 => bay falsy JS.
7. 4 anchor lech, nang nhat ApprovalWorkflowLevel.cs KHONG TON TAI.

Kem: bo tieu chi dark-launch sai co che (IsVisible chi pass-through, filter la
CanRead) - thay "sqlcmd dem policy" (khong do duoc, policy in-memory) bang test
regression - OR-of-N nang tu 1 ve len 4 ve co ca am - cat CodeSequences (tai
dung WorkflowAppCodeSequence dung chung 4 module Office).

Su co #53: invest luot 1 skeleton-ruot-rong (202K tok, dia 1.661B khung rong)
=> ghi-dia-trong-luc-lam la CAN KHONG DU, phai verify RUOT. Luot 2 resume cat
con 2 muc => CLEAN 23.085B. Luot 3 tien-trinh CLI thoat, §C/§D chua ghi => lead
tu viet theo H21 (engine propose-only, spec do lead ghi).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:30:03 +07:00

17 KiB

LĂNG KÍNH 4/4 — VERIFY ĐỘC LẬP CLAIM file:line (reviewer, S157)

Đích chấm: spec-ke-hoach-ky-ket-hd-28-07-2026.md §② (dòng 95-204). Phép đo: MỞ FILE THẬT trên đĩa (Read/Grep/Glob), KHÔNG dựa skill-doc, KHÔNG dựa sub-invest-kehoach-1.md.

Verdict

SPEC §② VỮNG — 39 ĐÚNG / 4 SAI (toàn bộ là lệch ANCHOR, 0 sai NỘI DUNG) / 5 KHÔNG KIỂM ĐƯỢC. Hai claim quyết-định-kiến-trúc (B.1 "Proposal phá OR-of-N" và "Pe_* ngoài MenuKeys.All") ĐÚNG, khớp từng dòng. Trước khi giao implementer phải sửa 4 anchor ở mục "Claim SAI" — 1 trong 4 là đường dẫn file KHÔNG TỒN TẠI (implementer mở sẽ trượt).

Claim ĐÚNG

A. B.1 — ProposalFeatures.cs phá OR-of-N (claim quyết định "copy Contract, không copy Proposal")

# Claim gốc (spec dòng 99-104) Đo được trên đĩa Verdict
1 SelectMany flatten mọi Level xuyên Steps :427-429 ProposalFeatures.cs:427-429 = wf.Steps.OrderBy(s => s.Order).SelectMany(s => s.Levels.OrderBy(l => l.Order).Select(l => new { Step = s, Level = l })).ToList() ĐÚNG — khớp DÒNG chính xác
2 ElementAtOrDefault(order-1) lấy 1 row :433 :433 = allLevels.ElementAtOrDefault(p.CurrentApprovalLevelOrder.Value - 1) ĐÚNG — chính xác
3 So đúng 1 ApproverUserId :439 :439 = if (!isAdmin && currentSlot.Level.ApproverUserId != currentUser.UserId.Value) — so 1 Guid, KHÔNG có tập allowedUserIds ĐÚNG — chính xác
4 Comment tự thú "Lite version: assume 1 step per workflow" :424 = // Multi-step workflow: traverse step-by-step. Lite version: assume 1 step per workflow ĐÚNG — nguyên văn
5 Entity Proposal chỉ có CurrentApprovalLevelOrder, KHÔNG có CurrentWorkflowStepIndex Domain/Office/Proposal.cs:27CurrentApprovalLevelOrder; grep CurrentWorkflowStepIndex trong **/Proposal.cs = 0 hit ĐÚNG
6 Hệ quả: 2 người cùng Cấp bị biến thành 2 nấc TUẦN TỰ (AND) CỦNG CỐ THÊM (tôi tự đo): :473-481 advance = if (CurrentApprovalLevelOrder < allLevels.Count) CurrentApprovalLevelOrder + 1 — so Order (1-based trong Bước) với Count của danh sách ĐÃ FLATTEN. Không GroupBy(Order) ở bất kỳ đâu ⇒ N row cùng Order = N slot tuần tự. ĐÚNG
7 OR-of-N hợp lệ theo ApprovalWorkflow.cs:81-94 Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:81 mở "Cấp = tập các row CÙNG Order", :82 "1 Cấp có thể có N row ⇒ N người duyệt OR-of-N", kết :94 "Cấp 2 Phòng Cung ứng có 3 người"; :95public class ApprovalWorkflowLevel ĐÚNG — khung 81-94 khít

Kết luận B.1: claim ĐÚNG 7/7. Khuyến nghị "PHẢI copy ContractWorkflowService.ApproveV2Async" có cơ sở đo được, không phải suy diễn.

B. MenuKeys.All (claim quyết định "theo khuôn Off_DeXuat, không theo Pe_*")

# Claim gốc (spec dòng 181-183) Đo được Verdict
8 OffDeXuat + 3 con đều trong All MenuKeys.cs:168 = OffDeXuat, OffDeXuatList, OffDeXuatCreate, OffDeXuatInbox, nằm trong mảng All (:156-174) ĐÚNG nội dung — nhưng anchor spec ghi :171 là SAI (xem S1)
9 Pe_* sinh bằng factory, NẰM NGOÀI All Factory :134-137 (Pe_{t} / _List / _Create / _Pending), :145-146 (_Approved / _Deleted), :150 (_WfView); mảng All chỉ chứa root PurchaseEvaluations (:162) — không một literal Pe_ nào trong All. Comment :142-144 tự khai đúng chính sách này. ĐÚNG
10 ⇒ per-action policy Pe_* không tồn tại Program.cs:82-89 = foreach (var menu in MenuKeys.All) foreach (var action in MenuKeys.Actions) opts.AddPolicy($"{menu}.{action}", ...) — policy sinh duy nhất từ All ⇒ key ngoài All không có policy ĐÚNG (suy ra bằng cấu trúc, không phải phỏng đoán)
11 Policy TỰ SINH khi key vào MenuKeys.All (Program.cs:82-89) Khớp từng dòng: :82 foreach All · :84 foreach Actions · :86-87 AddPolicy · :89 đóng ĐÚNG — chính xác

C. Khuôn Contract phải copy (§2.1 + §2.3)

# Claim Đo được (Infrastructure/Services/ContractWorkflowService.cs) Verdict
12 ApproveV2Async:217-394 :217 = private async Task ApproveV2Async(; hàm kế LogTransitionAsync bắt đầu :396 ⇒ thân kết ~:394 ĐÚNG (đầu chính xác, đuôi khớp mốc kề)
13 GroupBy Order :246 :246 = currentStep.Levels.OrderBy(l => l.Order).GroupBy(l => l.Order).ToList() ĐÚNG — chính xác
14 OR-of-N :259-260 :259 = allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet(), :260 = if (!allowedUserIds.Contains(actorUserId.Value)) ĐÚNG — chính xác
15 LevelOpinions UPSERT :292-316 :292 = db.ContractLevelOpinions (đọc bản cũ), :300 = .Add(new ContractLevelOpinion, :318 đã là comment skipToFinal ⇒ khối UPSERT nằm trọn 292-316 ĐÚNG
16 skipToFinal :320-352 :320 = if (skipToFinal), :332 = lastStep.Levels...GroupBy ĐÚNG (đầu chính xác)
17 Guard trình role-based :70-79 đòi Drafter|DeptManager :70-71 điều kiện DangSoanThao || TraLai, :73-75 !actorRoles.Contains(AppRoles.Drafter) && !...DeptManager, :77-79 throw new ForbiddenException ĐÚNG — khung 70-79 khít
18 ConflictException :115-116 khi HĐ V2-less trình :116 = throw new ConflictException("HĐ chưa pin workflow definition hoặc workflow không có step.") ĐÚNG về anchor (xem K5 về mệnh đề nhân-quả)
19 LogTransitionAsync báo Drafter :407-427 :396 = private async Task LogTransitionAsync(, :407 = if (contract.DrafterUserId is Guid drafterId && drafterId != actorUserId) ĐÚNG — notify quả thật CHỈ nhánh Drafter

D. Lỗ đang treo ở Contract (lý do "authz 2 tầng" + "inbox V2 từ ngày 1")

# Claim Đo được Verdict
20 ContractsController.cs:12-13 class-trần :11 [ApiController] · :12 [Route("api/contracts")] · :13 [Authorize] trần; grep Authorize\(Policy trong file = 0 hit ⇒ 0 per-action policy ĐÚNG (bằng chứng mạnh hơn claim: 0 hit toàn file)
21 ContractFeatures.cs:363-372 legacy-only :363 = PhaseActorRoles = new(), :364-372 chỉ map DangSoanThao … DangDongDau rồi }; ĐÚNG — khung khít
22 grep ChoDuyet = 0 hit Grep ChoDuyet trong **/ContractFeatures.cs = 0 occurrence / 0 file ĐÚNG — chính xác
23 PE có ResolveV2InboxIdsAsync để mirror Grep toàn src = 2 hit, chỉ trong PurchaseEvaluationFeatures.cs (Contract không có) ĐÚNG

E. Khuôn PE phải port (§2.3 mục 1-3)

# Claim Đo được (PurchaseEvaluationWorkflowService.cs) Verdict
24 choke-point + RULE grep-4-site, khuôn :1008 :1008 = // RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay TRƯỚC assignment (reviewer grep-check 4 site)., :1009 = private async Task ApplyBudgetSnapshotOnFinalizeAsync( ĐÚNG — anchor trúng ĐÚNG dòng RULE
25 AllowApproverFinalize :870-890 :870 = if (matchingLevel.AllowApproverFinalize && applyLevelFinalize) ĐÚNG (đầu chính xác)
26 finalizeByCcmDelegation + CeoApprovalThreshold :892-929 :892 = if (finalizeByCcmDelegation), :894 = if (aw.CeoApprovalThreshold is not decimal ceoThreshold) ĐÚNG (đầu chính xác)
27 ApplyReturnModeAsync:368-547 :368 = private async Task<string> ApplyReturnModeAsync( ĐÚNG (đầu chính xác)
28 HasQueryFilter(!IsDeleted) khuôn PurchaseEvaluationConfiguration.cs:84 :84 = b.HasQueryFilter(x => !x.IsDeleted); ĐÚNG — chính xác

F. Menu / seed / FE (§2.4)

# Claim Đo được Verdict
29 Seed 4 dòng menu khuôn DbInitializer.cs:1817-1820 :1817 OffDeXuat "Đề xuất" · :1818 OffDeXuatList "Danh sách" · :1819 OffDeXuatCreate "Tạo mới" · :1820 OffDeXuatInbox "Inbox duyệt"cả 3 nhãn VN spec đề xuất trùng khớp nhãn khuôn ĐÚNG — chính xác
30 admin-permission :2383 :2383 = MenuKeys.OffDeXuat, MenuKeys.OffDeXuatList, MenuKeys.OffDeXuatCreate, MenuKeys.OffDeXuatInbox, ĐÚNG — chính xác
31 ContractLevelOpinions FK Cascade + Restrict (Mig 33) ContractLevelOpinionConfiguration.cs:27 OnDelete(DeleteBehavior.Cascade) · :32 OnDelete(DeleteBehavior.Restrict) ĐÚNG
32 ApprovalWorkflowsV2Page.tsx:143 map typeCode→int fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:143 = // FE typeCode → BE int (giống MenuKeys ApplicableType) (đầu bảng TYPE_CODE_TO_INT, dùng ở :257) ĐÚNG
33 Bộ Proposal FE ~978 LOC/app: List 252 + Create 245 + Detail 386 + types 95 Đếm dòng thật fe-admin: ProposalsListPage.tsx 252 · ProposalCreatePage.tsx 245 · ProposalDetailPage.tsx 386 · types/proposal.ts 95 ⇒ tổng 978 ĐÚNG — 4/4 con số khớp tuyệt đối

G. Enum + cầu b.12→13 (§2.2 + §2.5)

# Claim Đo được Verdict
34 ApprovalWorkflow.cs:53-67 dừng ở TravelRequest = 9, slot 10 trống :53 = public enum ApprovalWorkflowApplicableType, :66 = TravelRequest = 9,, :67 = } — giá trị lớn nhất 9 ĐÚNG — khung 53-67 khít, slot 10 trống thật
35 cầu :53-54 = PE phải DaDuyet CreateContractFromEvaluationFeatures.cs:53-54 = if (pe.Phase != PurchaseEvaluationPhase.DaDuyet) throw new ConflictException(...) ĐÚNG — chính xác
36 :56-62 phép lấy winners :56 winners = pe.Suppliers.Where(s => s.IsWinner).ToList():62 supplierIds = winners.Select(w => w.SupplierId) ĐÚNG — khung khít
37 :88-90 SUM Quote.IsSelected per winner :88-90 = pe.Details.SelectMany(d => d.Quotes).Where(q => q.PurchaseEvaluationSupplierId == w.Id && q.IsSelected).Sum(q => q.ThanhTien) ĐÚNG — chính xác
38 changelog :121-140 (chỗ ghi ContextNote lệch) :121-130 ContractChangelogs.Add(...) (ContextNote :129) + :131-140 PurchaseEvaluationChangelogs.Add(...) (ContextNote :139) ĐÚNG — khung khít, 2 changelog chứ không phải 1 (implementer nhớ ghi note vào đúng bảng)
39 pin V1-only :68-71, :108 :68-71 = activeWfId lấy từ db.WorkflowDefinitions … IsActive; :108 = WorkflowDefinitionId = activeWfId; không có dòng nào set ApprovalWorkflowId trong cả khối new Contract{} :92-111 ĐÚNG

Bonus — measured-label gate: con số "849 LOC BE" (spec dòng 99) tái-lập được: đếm dòng 10 file Proposal*.cs trong src/ = đúng 849 (ProposalFeatures.cs 556 + Controller 86 + 4 Configuration 108 + 4 Domain 99). Nhãn "BE" cứu con số. ⚠️ Nhưng câu đặt số ngay sau tên 1 file ⇒ dễ đọc thành "file này 849 dòng" (thật: 556). Đề nghị đổi thành "twin nhỏ nhất — 849 LOC BE trải 10 file, riêng ProposalFeatures.cs 556".

Claim SAI

Cả 4 đều là lệch anchor / sai đường dẫn, KHÔNG có claim nào sai về nội dung kỹ thuật. Nhưng đây đúng lớp lỗi đã cắn repo hôm qua, và 1 cái làm implementer mở-file-trượt.

# Claim gốc Đo được Mức
S1 "Lead verify MenuKeys.cs:171: OffDeXuat+3 con đều trong All" (spec dòng 182) 4 key nằm ở MenuKeys.cs:168. Dòng :171Personal, — key khác hẳn, chẳng liên quan Off_DeXuat MINOR (nội dung đúng, anchor lệch 3 dòng)
S2 "cờ sẵn ApprovalWorkflowLevel.cs:116-125" (spec dòng 161) KHÔNG CÓ FILE NÀO TÊN ApprovalWorkflowLevel.cs (Glob **/ApprovalWorkflowLevel.cs = 0 hit). Class ApprovalWorkflowLevel nằm trong Domain/ApprovalWorkflowsV2/**ApprovalWorkflow.cs**:95. Khoảng dòng thì ĐÚNG: :116 AllowReturnOneLevel · :119 AllowReturnOneStep · :122 AllowReturnToAssignee · :125 AllowReturnToDrafter = true MAJOR — implementer mở theo spec sẽ file-not-found (tôi vừa dính đúng lỗi này khi đoán thư mục ở S5)
S3 "DELETE allow-list DangSoanThao|TuChoi, mirror PE :1404" (spec dòng 177) PurchaseEvaluationFeatures.cs:1404dòng TRỐNG. Allow-list thật ở :1405-1407. Drift này kế thừa từ comment trong mã: ApprovalWorkflow.cs:161 cũng ghi PurchaseEvaluationFeatures.cs:1404-1406 (lệch 1) MINOR
S4 "workflow ApplicableType==ContractSigningPlan (mirror Proposal :258-265)" (spec dòng 144) Khoảng :258-265 chỉ chứa phần nạp wfType + throw NotFound. Hai dòng quyết định nằm NGOÀI khoảng trích: :266-267 if (wfType.Value != (int)ApprovalWorkflowApplicableType.ProposalGeneral) throw new ConflictException(...). Ai copy đúng 258-265 sẽ copy phần đọc mà bỏ mất chính cái guard MINOR-nhưng-nguy (trích hụt đúng dòng load-bearing)

Claim KHÔNG KIỂM ĐƯỢC

# Claim Vì sao chưa kết luận
K1 Ranh CUỐI của 6 khoảng: ApproveV2Async:…394, skipToFinal:…352, ApplyReturnModeAsync:…547, AllowApproverFinalize:…890, CcmDelegation:…929, UPSERT:…316 Tôi chỉ verify dòng đầu + mốc liền kề (hàm/khối kế tiếp). Chưa đọc trọn ruột từng khoảng. Đầu đúng + mốc kề hợp lý ⇒ khả năng đúng cao, nhưng chưa đo ⇒ không tuyên ĐÚNG
K2 fe-user WorkflowMatrixViewPage.tsx (spec dòng 193-194 tự khai invest KHÔNG verify được) Tôi cũng chưa kiểm. Spec đã tự gắn cờ "PHẢI grep lại lúc làm" — giữ nguyên cờ đó, đừng gỡ
K3 ProposalFeatures.cs:161-228 (khuôn Get/List detail Include) Chưa đọc — hết ưu tiên
K4 Mig 69 · 7 CreateTable · 2 row docs/STATUS.md bump Claim về việc sẽ làm, không có vật trên đĩa để chấm
K5 Mệnh đề nhân-quả "pin V1-only ⇒ HĐ V2-less trình xong KẸT CỨNG :115-116" Anchor ĐÚNG, nhưng :116 ném khi WorkflowDefinitionId null HOẶC def không có step. Ở cầu này :108 set activeWfId (V1) ⇒ có kẹt hay không phụ thuộc DB prod: có tồn tại WorkflowDefinitions active + có step cho ContractType đó không. Chưa đo DB ⇒ mệnh đề đúng CÓ ĐIỀU KIỆN

Điểm CỦNG CỐ

  1. B.1 mạnh hơn spec viết. Ngoài 3 dòng spec nêu, ProposalFeatures.cs:473 còn so CurrentApprovalLevelOrder (Order 1-based trong Bước) với allLevels.Count (đếm row đã flatten toàn workflow) — trộn hai hệ đơn vị. Cùng lớp #43 Step.Order ≠ index 0-based. Thêm 1 lý do nữa để KHÔNG copy Proposal cho phần duyệt.
  2. Cat-3 authz: bằng chứng mạnh hơn claim. Spec chỉ trỏ ContractsController.cs:12-13; đo thật grep Authorize\(Policy = 0 hit toàn file ⇒ mọi endpoint HĐ chỉ cần "đăng nhập bất kỳ". Đúng bài học #82/S118 (menu-ẩn ≠ API-đóng). Yêu cầu "authz 2 TẦNG" của §2.3 giữ nguyên, nên nhấn thêm câu "gate acceptance = grep Authorize(Policy trong controller mới phải ≥ 4 hit".
  3. Thêm đường dẫn vào spec, đừng để tên trần. CreateContractFromEvaluationFeatures.cs thật nằm ở src/Backend/SolutionErp.Application/PurchaseEvaluations/ (KHÔNG phải .../Contracts/ như tiêu đề §2.5 "chạm module Contract" gợi ý). Spec để tên trần ⇒ tôi đoán sai thư mục và trượt 1 lượt đọc. Cùng cách chữa cho S2.
  4. §2.5 sửa 2 bảng changelog, không phải 1. :121-130 ContractChangelogs:131-140 PurchaseEvaluationChangelogs. Câu spec "ghi ContextNote lệch vào changelog :121-140" nên nói rõ ghi vào bảng nào (đề nghị: cả hai, để tra ngược từ 2 phía).
  5. Anchor drift lan từ MÃ sang SPEC. S3 chứng minh: comment trong ApprovalWorkflow.cs:161 ghi lệch 1 dòng, spec chép lại y nguyên. ⇒ quy tắc rẻ: mọi anchor file:line trong spec phải được mở lại lúc bắt đầu wave, vì mã đã trôi giữa lúc viết comment và lúc viết spec.
  6. Đề nghị gate rẻ cho implementer (không phải sửa mã, chỉ là phép chấm): trước mỗi wave, chạy 1 lượt "mở 5 anchor bất kỳ trong wave đó" — nếu ≥1 anchor trượt file/dòng, chấm lại toàn bộ anchor của wave.