# sub-reviewer-0 — adversarial spec review PE lũy kế tạm-tính (S134) > Ghi-đĩa-trong-lúc-làm (chống #53). Append từng issue khi tìm thấy. Return = digest. ## Trạng thái - [x] Đọc spec + investigator - [x] Đọc code BE (accumulator :29-71 / features :880-1009 / workflow service :280-321, :840-949, :998-1041, :1112, :1128-1155 / seeder :1290-1352 / entity changelog / DTO :331-353) - [x] Đọc code FE (PeDetailTabs :1290-1600 / PeWorkflowPanel :95-134, :400-539 + SHA tự đo 6 file) - [x] 8 trục verdict — DONE, xem cuối file ## VERDICT: GO_WITH_FIXES (0 Critical / 2 Major spec-fix / 9 nits — Major đều sửa bằng chữ trong spec trước khi hmw chạy) ## Issues (append dần) ### Trục 3 (D4) — kết quả soi từng nghi vấn - **(a) CLEAR:** seeder `DbInitializer.cs:1328-1341` set snapshot **TRỰC TIẾP** (mirror thủ công, KHÔNG gọi helper) → D4 changelog-in-helper KHÔNG gây changelog rác lúc seed/restart. #75-class không xảy ra. - **(b) CLEAR:** 5 call-site đều là finalize thật (Phase → DaDuyet): `:311` admin-override chỉ gọi helper khi `targetPhase == DaDuyet`; `:865` level-finalize; `:904` CCM-miễn-CEO; `:937` all-steps-done; `:1112` V1 legacy. Note "Duyệt khi N phiếu trước chưa duyệt" đúng ngữ cảnh cả 5. - **(c) SaveChanges:** mọi path qua helper đều save — V2/V1 qua `TransitionAsync:301`, admin-override `:316`. OK. - **[M1] D4 thiếu shape đầy đủ cho changelog row:** helper `ApplyBudgetSnapshotOnFinalizeAsync(evaluation, ct)` KHÔNG có `actorUserId`. `PurchaseEvaluationChangelog` (mirror `LogTransitionAsync:1145-1155`) cần `EntityType/Action/PhaseAtChange/UserId/UserName/Summary/ContextNote`. Spec chỉ nói ContextNote. Nếu hmw tự thêm param actor vào helper → ĐỔI 5 call-site → vi phạm chính acceptance A6 "0 đổi 5 call-site". Spec PHẢI chốt: UserId=null + UserName="Hệ thống" (audit actor tái dựng từ row Transition cùng batch) + EntityType/Action/PhaseAtChange value cụ thể — nếu không hmw sẽ tự quyết 2 hướng ngược nhau. - **[N1] `AddAsync` sai pattern codebase:** `LogTransitionAsync:1145` dùng `db.PurchaseEvaluationChangelogs.Add(...)` (sync). Spec ghi "AddAsync" — hoạt động nhưng lệch pattern; nên ghi `.Add(...)`. - **(d) Chi phí:** ComputePendingAsync ~3 query (list + delta1 + delta2), spec ghi "+1 query" là under-count — vẫn chấp nhận được (finalize tần suất thấp). Note. ### Trục 2 — construction site - Grep `new PeBudgetSummaryDto` toàn repo = ĐÚNG 2 site `Features.cs:946/:975` (khớp spec A2). Session log S74 xác nhận pattern grep này (gotcha #65). Tests KHÔNG construct DTO (chỉ 1 comment `PeBudgetFreezeTests.cs:21`) → append-cuối không gãy compile chỗ nào. - **[M2] Banner PeWorkflowPanel KHÔNG tự ẩn trên phiếu terminal — mâu thuẫn nội tại spec:** predicate banner spec item 7 = `pendingPriorPes.length > 0 && !budgetFrozen` — KHÔNG có gate `readOnly`/phase. Phiếu TraLai/TuChoi/Nháp đi nhánh live (`Features.cs:957`) → pending fields FILL → banner "cần duyệt phiếu trước trước phiếu này" hiện trên phiếu ĐÃ từ chối/trả lại = sai ngữ cảnh. Edge-6 spec claim "banner duyệt tự ẩn (không còn action duyệt)" — SAI với predicate đã cho (banner không phụ thuộc action; V2-banner :418 có `!readOnly`, action buttons :439 có `next.length > 0` — banner spec không kế thừa gate nào). Fix: thêm `!readOnly && evaluation.phase === ChoDuyet` (hoặc tối thiểu `!readOnly && next.length > 0`) vào predicate banner + sửa câu edge-6 cho khớp. Dialog-block đã tự gate qua `isApproveAction` — không dính. ### Verify PASS (positive — chống trượt kép) - **SHA tự đo (git hash-object):** PeDetailTabs 2-app IDENTICAL `5534addb9a78...` ✅ khớp spec; PeWorkflowPanel IDENTICAL `a83a58e1d8e1...` ✅; types KHÁC `2bf7736`≠`ab4a2ec` ✅ (spec errata lead đúng — số investigator cũ `47c97df1`/`9d88e671` stale). Measured-label gate PASS. - **Trục 1 (chính-xác bất động):** ComputeAsync untouched ⇒ mọi consumer bất động — live `:962`, snapshot `:1018`, backfill Mig 67 one-time, seeder direct-set `:1334-1340`. ComputePendingAsync chỉ ĐỌC (AsNoTracking window y hệt) + fill field MỚI + AddAsync changelog — không mutate field cũ nào. Không có đường lệch số cũ. - **Soft-delete peer:** `PurchaseEvaluationConfiguration.cs:84` HasQueryFilter !IsDeleted ✅ tự áp cho ComputePendingAsync (cùng DbSet). - **FE công thức khớp:** `row6 = row2 + row4` (:1318), `r5 = row1 + r3` per-cột (:1325) → row5_tạm = r5+delta1, row6_tạm = row6+delta2 đúng đại số. Render sites lũy kế toàn FE = CHỈ PeDetailTabs (:1315-1316/:1498/:1503 + r5 qua colCalc) — grep 2 app không còn site nào khác (không print/export/list) → spec liệt đủ, S89 miss-2-sites không tái diễn. - **Trục 4 predicate:** delta1 NOT IN (1,7,10,99) = {98, legacy 2-6} đúng (10/7 đã trong row1 chính-xác — không double-count); delta2 NOT IN (1,7,99) ⊃ {10 có winner} = gap chính đề bài ✓ + {98, legacy} có winner hợp lý; pendingPriorPes NOT IN (7,99) ⊇ cả 2 delta-set + nháp list-only (D1) — 3 tập nhất quán. EF translate: `!array.Contains(p.Phase)` → NOT IN, enum non-null → không bẫy null-semantics. - **Trục 5 dialog:** block chèn TRÊN bộ chọn giá chỉ RENDER thêm — `priceMissing` (:514) + footer disabled (:522) không đổi → acceptance B3 "diff vùng disabled = 0" đo được. - **D4 mọi path SaveChanges:** V2/V1 → `TransitionAsync:301`; admin-override → `:316`. Changelog entity + DbSet có sẵn (`IApplicationDbContext.cs:63`). - **Baseline test 520 khớp** (S133 suite 520/0). A5/A6/B1/B2/B3/B4/C1-C9 đều có lệnh đo được. ### Trục 6 — acceptance đo được: PASS có 1 gap - **[N5] A3 live-JSON verify cần DATA chưa tồn tại:** Dev DB multi-pair (cùng ProjectId+WorkItemId ≥2 phiếu) = 0 (invest PHẦN 5). A3/A4 curl cần dựng 2 phiếu cùng cặp trước — spec nên thêm 1 dòng "tạo 2 phiếu cùng Dự án × Hạng mục qua UI/API trước khi curl" để hmw không bịa acceptance đã-pass. - B2 "phiếu frozen KHÔNG render (gate đầu nhánh)" hơi code-read-feel — chấp nhận được vì C7 (test DTO defaults) + gate biến grep được; có thể thêm "grep điều kiện `budgetFrozen` xuất hiện trong block tạm-tính". ### Trục 7 — edges THÊM ngoài 7 edge spec - **Tie CreatedAt bằng nhau:** strict `<` → 2 phiếu cùng timestamp không thấy nhau — pre-existing y hệt ComputeAsync (nhất quán, không mới). Không cần fix. - **[N2] MaPhieu nullable:** entity `string? MaPhieu` (:10, gen at create `Features.cs:158` nhưng data cổ/nullable schema). DTO spec `PePendingPriorPeDto(Guid Id, string MaPhieu, int Phase)` non-null → nên `string?` + FE fallback "(chưa có mã)". - **[N9] Danh sách pending KHÔNG render link:** approver click link phiếu Nháp → 403 S89 guard (`Features.cs:893-898`) → toast lỗi khó hiểu. Spec nên chốt "plain text mã + badge, KHÔNG link". - **Peer có winner nhưng 0 quote IsSelected:** delta2 = 0 nhưng count đếm peer → sub-dòng "+0đ từ 1 phiếu" — mirror y semantics PrevSelectedCount hiện tại (chấp nhận, nhất quán). - **DaDuyet-no-snapshot fallback [N7]:** `Features.cs:926-928` phiếu DaDuyet mà SnapshotAt null → nhánh live → BudgetFrozen=false → tạm-tính HIỆN trên phiếu DaDuyet dị dạng này. Pre-existing class (nhánh live đã hiện số live cho row1/2 từ S133) — sau backfill Mig 67 + 5 site + seeder về lý thuyết không còn row nào — ghi nhận để không ngạc nhiên UAT, không fix. - **Performance:** ComputePendingAsync ~3 query (list + delta1 + delta2) mỗi GET detail non-frozen + mỗi finalize — bảng PE nhỏ, peers per cặp ít, chấp nhận. Spec ghi "+1 query" là under-count [N4]. - **List dài không cap:** thực tế peers/cặp < 10 — không cần cap; nếu muốn phòng thì `.Take(20)` — optional. ### Trục 8 — mâu thuẫn nội tại / thiếu cho hmw - M1 (D4 shape) + M2 (banner gate vs edge-6) = 2 mâu thuẫn/thiếu chính — đã ghi trên. - **[N1]** Spec ghi `AddAsync` — pattern codebase = `db.PurchaseEvaluationChangelogs.Add(...)` (`:1145`). Dùng Add cho khớp. - **[N3]** Spec "map inline nhỏ nếu chưa có map dùng chung" — map ĐÃ CÓ: `PurchaseEvaluationPhaseLabel` (types cả 2 app, PeWorkflowPanel đang dùng :465) → chỉ thẳng dùng nó, khỏi inline. - **[N6]** Cite "mirror pattern LogTransition `:1073`-vùng" — thực tế `LogTransitionAsync` ở `:1128-1155`. Số dòng lệch (hmw grep ra được nhưng sửa cho sạch). - **[N8]** Sub-dòng "{n} phiếu chưa duyệt": chốt n = count TƯƠNG ỨNG delta (row1/5 → pendingSubmittedCount; row2/6 → pendingSelectedCount), KHÔNG dùng `pendingPriorPes.length` (list ⊇ delta-set, dùng nhầm → "từ 3 phiếu" khi delta chỉ từ 1). - **INV-2 [A‖B] song song:** DTO field names + JSON shape đã pin cứng trong spec → B không chờ A, race DTO-shape KHÔNG thật. OK. ## CHECKLIST-8-TRỤC (verdict per trục) 1. Chính-xác bất động: **PASS** — ComputeAsync untouched + 4 consumer enumerated + ComputePendingAsync read-only. 2. R1-S133 frozen: **PASS** (2 site construction verified duy nhất; DTO append-cuối an toàn; fallback DaDuyet-no-snapshot = pre-existing note N7). 3. D4-trong-helper: **PASS-WITH-M1** — seeder direct-set (không gọi helper) nên KHÔNG changelog rác; 5 site đều finalize thật + SaveChanges đủ; NHƯNG spec thiếu shape row (actor/EntityType/Action/vị trí trong nhánh wiKey) → M1. 4. Predicate mới: **PASS** — 3 tập nhất quán, không double-count, EF translate OK. 5. FE: **PASS-WITH-M2** — công thức + vị trí + priceMissing không đụng; banner thiếu gate readOnly/phase → M2. 6. Acceptance: **PASS** — đo được trừ A3 cần seed data (N5). 7. Edge sót: 5 edge mới soi — không cái nào block; 3 thành nits (N2/N7/N9). 8. Mâu thuẫn/thiếu: **2 Major + 5 nits** — đều sửa được bằng chữ trong spec, không cần đổi thiết kế.