[CLAUDE] Docs: S76 closeout — PE ngan sach ma tran 3 cot + bang luoi + badge quyen-NS
STATUS/HANDOFF (Mig 55->56, test 339->344, gotcha 69->70, bundle jOqxW4-p/DbsznVvR Run #319, Phase +S76, In Progress->Recently Done) + gotcha #70 (FE absolute-set echo stale-echo data-loss -> useIsFetching gate) + ef-core skill Mig 56 row + session log 2026-06-19-S76 + agent-memory harvest (impl-FE stray->canonical + 4 sub diary). Curate-debt carry: reviewer 45KB + inv-codebase 35KB keep-floor-hit manual-condense. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -61,6 +61,8 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
||||
|
||||
## 📅 Recent activity (FIFO — older → archive/git)
|
||||
|
||||
- **2026-06-19 (S76 Part2+3 PE budget-edit BADGE display-only — Lens-2 FE badges+render-safety, PASS, 0 blocker):** 2 cờ bool CanEditProBudget/CanEditCcmBudget vào 2 DTO approver (AwLevelDto designer + PurchaseEvaluationApprovalLevelApproverDto PE-flow), suy từ ROLE (KHÔNG đổi authz). Badge "✎ NS PRO" (amber) / "✎ NS CCM" (sky). **PASS Lens-2:** (1) **role-set KHỚP gate** — `proBudgetEditors=GetUsersInRoleAsync(Procurement)∪Admin` / `ccmBudgetEditors=CostControl∪Admin` (ApprovalWorkflowV2AdminFeatures.cs:155-157 + PurchaseEvaluationFeatures.cs:976-978) đảo-chiều set-lookup 3-query/req no-N+1; khớp 1:1 gate thật canEditPro/canEditCcm `:800-801`=isAdmin‖Procurement / isAdmin‖CostControl; AppRoles consts verified (`AppRoles.cs:5,9,10`). (2) **role-set DEFINE trước cả 2 site** — PE-flow define `:978` < approvalFlow `:1045` < currentApproval `:1064` (cùng V2-branch scope); designer define `:155` < ToDto `:184`. (3) **DTO flag flows CẢ flow+current** — `PurchaseEvaluationApprovalFlowLevelDto.Approvers` (`:152`) + `PurchaseEvaluationCurrentApprovalDto`-path (`:145`) đều dùng `PurchaseEvaluationApprovalLevelApproverDto` → badge hiện ở Panel-flow lẫn current. (4) **PeWorkflowPanel `.join('/')→map`** giữ separator "/" (`{i>0 && <span>/</span>}`), giữ case rỗng `length===0?'(chưa cấu hình)'`, key=`a.userId` SAFE (validator `HaveNoDuplicateApproverInSameLevel:289` chặn dup ApproverUserId trong 1 level → no React key-collision). (5) **render-safety** — designer wrapper `flex`→`flex flex-wrap` + panel `flex flex-wrap gap-x-1 gap-y-0.5` → badge KHÔNG vỡ layout khi nhiều approver/badge. (6) **mirror 2-app PERFECT** — PeWorkflowPanel fe-admin==fe-user byte-IDENTICAL cả base lẫn now (diff empty); types +2 cờ ở CẢ 2 app (admin:235-236/user:238-239), block diff-identical (chỉ pre-existing inline-comment `//0-based` lệch = noise KHÔNG do change này). (7) **FE typecheck CLEAN** cả 2 app (`tsc --noEmit` exit 0). no mock/alert/TODO. **⚠️ SPEC-vs-DIFF MISMATCH (em-main framing wrong, NOT a code bug):** spec nói "KHÔNG migration" nhưng diff BUNDLES S76 Part1 (migration `AddProBudgetSplitToPeWorkItemBudget` + PeWorkItemBudget domain + PeBudgetSummaryDto + PeDetailTabs 306-LOC matrix rewrite WIRED PUT /budget/pro). Part1 spot-check sound (mig 3-file OK pure-ASCII gotcha#30-respect, PUT wired real not-mock). **🟡 MAJOR race STILL PRESENT (carry-over Part1, line 64 entry):** PeDetailTabs 2 PRO cell (`:1283` proInitial + `:1305` proAdjust) cùng `proMut.mutate` echo sibling từ `bs` server-snapshot, `invalidate()` fire-and-forget (`:1161` không await) → double-Save<refetch wipes sibling. Sev MAJOR data-loss tiềm ẩn, prob thấp. **LEARNED:** display-only capability-flag review = (a) confirm flag-compute KHỚP gate-thật bit-for-bit (đảo-chiều set-lookup must mirror the forward Roles.Contains check) + (b) confirm DEFINE-before-all-consumer-site trong cùng scope + (c) which DTO carries flag determines which UI surface shows badge (flow vs current — both here). For `.join→map` refactor verify separator+empty-case preserved + key-uniqueness backed by a BE validator. SURPRISE: adversarial value here = catching the spec's "KHÔNG migration" claim is FALSE (diff is combined Part1+2+3) — don't trust em-main's scope framing, read the actual changed-set. Tag [s76-part23, pe-budget-badge, display-only-capability-flag, role-set-mirrors-gate, join-to-map-separator-preserved, key-uniqueness-validator-backed, mirror-2app-byte-identical, spec-vs-diff-mismatch, major-race-carryover].
|
||||
- **2026-06-19 (S76 Part1 PE budget MA-TRẬN 3 cột Mig 56 — uncommitted, PASS w/ 1 MAJOR race + 2 MINOR):** Form ngân sách 1-cột→ma-trận [Dự án|PRO|CCM]. Entity +ProInitialAmount/+ProAdjustmentAmount (cột PRO mirror CCM Initial/Adjustment); ProEstimateAmount→LEGACY, Mig 56 Sql() UPDATE migrate idempotent (`WHERE ProEstimate NOT NULL AND ProInitial NULL` — chạy-1-lần-safe). **PASS:** authz fail-closed Forbidden TRƯỚC side-effect 2 handler (PRO=Admin‖Procurement `:90`, CCM=Admin‖CostControl `:160`; Admin nhập cả 2 đúng ý; neither-role blocked); compute `fullAmount`=CCM nếu hasCcm else proFull(ProInit+ProAdj) `:852`, migrate-value flow đúng (legacy ProEstimate→ProInitial→hasPro=true→fullAmount); `fullIsEstimate` `!hasCcm`→`!hasCcm&&hasPro` (improve, no badge khi empty); DTO 17-arg positional khớp def (2 new appended last, build-PASS compiler-checked); Block B 9-row dùng `full` authoritative INTACT; Mig 3-file OK (.cs+Designer+snapshot 18,2); FE 2-app SHA-twin `a93c8aa0`; 32 PeWorkItemBudget tests PASS (+5 S76: set-both-neg, validator neg-initial-fail/neg-adjust-pass, full-proFull-150, neg-proAdjust-70); no mock; anti-fiddle clean. **🟡 MAJOR race (pre-existing pattern, S76 WORSENS):** BudgetCell cross-field echo từ `bs` (server snapshot) KHÔNG local-state — PRO "Ban hành" save gửi `proAdjustmentAmount: bs.proAdjustmentAmount`, "V0" save gửi `proInitialAmount: bs.proInitialAmount`. `invalidate()` fire-and-forget (`:1170` không await refetch). 2 PRO cell nay đồng-cột → click Save cell-2 trong window [onSuccess fires (isPending→false, btn re-enabled) → refetch lands] đè cell-1 về STALE (vd: lưu Ban-hành=100 → ngay lưu V0=50 trước refetch → Ban-hành WIPED null). Trước S76 PRO chỉ 1 số nên window này vô hại; ma-trận 2-cột-PRO làm reachable. Sev MAJOR (data-loss tài-chính tiềm ẩn) nhưng prob thấp (cần double-click <refetch-latency); fix gợi-ý: disable sibling-cell Save khi mut.isPending HOẶC onMutate optimistic-merge HOẶC await invalidate. **🔵 MINOR:** (a) `parseVnd` strip `.` → "1.5"→15 (input cho `.` nhưng VND whole-number nên harmless); (b) stray `fe-user/.claude/agent-memory/implementer-frontend/MEMORY.md` NOT-gitignored (cwd-misland gotcha) → em main ĐỪNG `git add -A`. **LEARNED:** cross-field echo-from-server an-toàn khi 1-field/cột; thành race khi N-field cùng-cột share 1 mutation + fire-and-forget invalidate — window mở SAU isPending=false (btn enable) chứ không phải lúc in-flight; load-bearing = đếm field-cùng-cột share mutation + check invalidate awaited. Tag [s76, pe-budget-matrix, mig56-migrate-idempotent, cross-field-echo-race-worsened, fullIsEstimate-improve, cwd-misland-stray, parseVnd-dot-minor].
|
||||
- **2026-06-18 (S72ter-WIRE Mig 54 cross-stack-wire + verify-fix lane — uncommitted priceMissing, PASS no-new-deadlock):** Complement to S72ter-AUTHZ below (same fix, deadlock-lens). Fix = `priceMissing` old `length>0 && !source` → new `(length===0 || !source)`, 2-app SHA-twin `4d6c89d9`. **No new deadlock — 4-fact:** (1) fix CHỈ THÊM disable-cond `length===0` lên branch đã unreachable-by-invariant (submit-guard `:194` hard-block winnerQuoteTotal<=0 ALL-paths → Ncc candidate luôn ≥1 ở ChoDuyet) ⇒ không sinh lockout mới; (2) có giá→chọn→source set→`priceMissing=false`→nút "Xác nhận" mở→duyệt OK; (3) empty (giả định)→nút khoá + amber `:537` "nhập PRO/CCM hoặc chọn NCC" = lối-thoát RÕ, setter-path KHÔNG phase-gated (mirror-budget) cho nhập giá bất kỳ lúc→candidate xuất hiện→mở lại (no hard-lock); (4) intermediate-approve `shouldPickPrice=false` (chỉ `currentIsFinalApprover||finalizeByCcm`)→nút mở bình thường, khớp BE ApplyApprovedPrice chỉ terminal `:885`+CCM-deleg `:853` (intermediate advance `:870/:893` không gọi). 7-layer threading 0-drop re-confirm (ctrl `:129/:337`→cmd `:462`→handler `:515`→iface `:30`→svc `:47`→ApproveV2 `:822`). OR-of-N `currentIsFinalApprover` true mọi viewer cấp cuối (ComputeLevelStatus `:987` position-based) nhưng nút dialog `disabled=blockedByV2Level`+`!isDisabled&&setTarget` `:310/:320`→non-approver không mở→price-selector vô hại. **LEARNED:** "fix tạo deadlock?" = THÊM-disable lên branch-unreachable-by-invariant không thể sinh lockout; verify lối-thoát = setter KHÔNG phase-gated (giá nhập-được bất kỳ lúc) + amber-message ⇒ user luôn thoát empty-state. Tag [s72ter-wire, verify-fix, no-new-deadlock, escape-hatch-amber, 7layer-0drop].
|
||||
|
||||
- **2026-06-18 (S72ter Mig 54 AUTHZ+SECURITY lane double-check — uncommitted priceMissing FE-fix + committed 1d86abc re-verify, PASS, 0 issue):** anh giao 3 lane laser (a setters / b CCM-finalize bypass / c controller authz) on commit 1d86abc (deployed Run #313) + 1 uncommitted FE-fix. **Uncommitted diff = 2 LOC product only** (`priceMissing` both apps, SHA-identical `4d6c89d9`) + memory/ledger noise — em main đúng kỷ luật chỉ-touch-2-file. **(a) PASS** — `PeSuggestedPriceFeatures.cs` cả 2 setter ForbiddenException TRƯỚC mọi mutate+SaveChanges (load+NotFound→role-gate `:40-41`/`:109-110`→mutate); role đúng PRO=Admin‖Procurement, CCM=Admin‖CostControl; AppRoles consts tồn tại (`:5,9,10`). Phase-guard cố-tình-thiếu, documented mirror-budget S61 (non-regression). **(b) PASS no-bypass — 3 gate trực-giao chặn non-CostControl finalize-bỏ-CEO, TẤT CẢ throw TRƯỚC `Phase=DaDuyet`(:854):** (1) approver-match `:702-713` non-admin phải ∈ pendingLevel.ApproverUserId else Forbidden → forged-caller-not-at-level KHÔNG tới được finalize block; (2) `finalizeByCcmDelegation:830-851` threshold-null→Conflict / role≠CostControl→Forbidden / `winnerQuoteTotal>=ceoThreshold` strict-`<`→Conflict — 3 throw trước set; (3) block `return` no-fallthrough. `winnerQuoteTotal` recompute server-side từ Suppliers+Quotes.ThanhTien của SelectedSupplier (`:839-847`) KHÔNG trust client; threshold từ DB `aw.CeoApprovalThreshold`. skipToFinal+finalizeByCcm combo safe (skipToFinal `:818` return non-last-slot HOẶC `:797` no-op fall-through last-slot → finalize once, 3 guard vẫn áp). **(c) PASS** — class `[Authorize]:14` → 2 endpoint mới inherit any-auth, fine-grained ở handler Forbidden (gotcha#44-safe KHÔNG class-Policy-overstrict). **FE-fix sound strict-tightening:** old `length>0 && !source` để nút ENABLED khi candidates-empty → click → BE Conflict "Chọn 1 giá chốt"; new `(length===0 || !source)` disable nút khớp amber empty-state `:537` (trước fix message-hiện-cùng-nút-enabled = UX mâu thuẫn). `winnerQuoteTotal:number` non-null → candidates-non-empty thực tế (submit-guard >0), fix thuần defensive nhưng đúng. **LEARNED:** "finalize-bypass?" load-bearing proof = đếm guard giữa caller-entry và state-mutation + xác nhận MỖI guard throw TRƯỚC mutation đầu tiên (đây Phase=DaDuyet) + recompute-vs-trust-client của giá-trị-so-ngưỡng (winnerQuoteTotal server-Sum, không nhận body) → 3 gate độc lập (approver-match ∩ role ∩ amount<threshold) mạnh hơn 1; client chỉ chọn-source-label, BE tự tính amount-vs-threshold. **SURPRISE:** uncommitted-fix chỉ là edge defensive (candidates thực tế luôn ≥1 do submit-guard) nhưng vẫn đáng — nó xoá UX-mâu-thuẫn enabled-button-cùng-amber-empty + chống regression nếu submit-guard nới sau này. Tag [s72ter, mig54-authz-lane, finalize-bypass-3gate-proof, server-recompute-not-trust-client, fe-fix-strict-tighten, phase9-uat-pass].
|
||||
|
||||
Reference in New Issue
Block a user