Files
solution-erp/.claude/agent-memory/reviewer/MEMORY.md
pqhuy1987 e7e99d10f2 [CLAUDE] Docs: S73 closeout — Mig 54 PE gia de xuat + CCM duyet-done (STATUS/HANDOFF/session-log + review run-trace + agent-memory harvest)
- STATUS/HANDOFF S73: Mig 54 · test 334 · bundle Bv3jUCNo/BWlMBQz6 (Run #313 feature + #314 fix)
- session log 2026-06-18-S73-pe-gia-de-xuat-ccm-done.md
- run-trace runs/2026-06-18-mig54-pe-review (custom-inline review, bu post-hoc) + _ledger 2 row (R1 schema 1/4 + R2 free-text 2/3)
- agent-memory flush 5 sub + reconcile implementer-frontend cwd-misland stray -> canonical

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:32:41 +07:00

38 KiB
Raw Blame History

Reviewer Agent — Persistent Memory

Persistent diary cross-session. Auto-injected first ~200 lines at spawn (L1 HOT). Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 archive/ on-demand · L3 RAG search_memory just-in-time. Keep entry ≤ 1.5K chars (gotcha #53). Full verbatim history pre-S40 → git d2f52ba + archive/2026-05-q1..q2.md; S51→S57 detail → archive/2026-06.md (S69 curate). Archive map: archive/_INDEX.md + per-period .gist.md.

📁 Area memory (L2 on-demand — Read khi review vùng tương ứng)

  • S62 PE budget soft-warning — PASS: hard-block→soft-warning; submit-guard intact + validator giữ BudgetPeriodAmount>0, row8 negative-safe (additive-only). Validator class PurchaseEvaluationFeatures.cs:317 (reconcile S63: stray cwd-misland → canonical, anchor verified).
  • Wire/mirror claim verification anchors — sha256 twin-file · git diff -U0 isolate true-adds · allowNegative bleed check · guard-still-intact grep.

🎯 Role baseline

Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod UAT (*.solutions.com.vn). Tools: Read, Grep, Glob, Bash (curl + git diff + sqlcmd read) + 5 RAG MCP. Skills: dependency-audit-erp + contract-workflow + permission-matrix. Output: PASS/FAIL + concrete issues file:line. NEVER write code.


🚨 Recurring bug patterns (catch priority)

  • #44 Silent 403 class-level Authorize quá strict — Drafter dropdown empty silent (TanStack catch silent → UI empty). Grep \[Authorize\(Policy=.*\)\] class-level + curl non-admin expect 200. Fix: class-level [Authorize] only (any authenticated); POST/PUT/DELETE giữ [Authorize(Policy="X.Create")].
  • #43 Step.Order ≠ index 0-basedWhere(s=>s.Order==i) wrong row. Fix: EF query → in-memory OrderBy(Order).ToList() → index.
  • #42 Dual schema V1/V2 — Service phải branchif (entity.ApprovalWorkflowId is Guid awId) ApproveV2Async else V1Legacy.
  • Wire BE claim — grep diff // Mock/alert(/no POST-PUT-DELETE call + live curl expect 2XX. Severity CRITICAL block.
  • Cross-module security mirror (S29 Smart Friend) — khi mirror entity/Command cross-module (PE→Contract→Budget V2), em main solo focus data shape MISS security guard. Pattern: aw.ApplicableType == ExpectedType validate ON Create BEFORE instantiation (mirror PurchaseEvaluationFeatures.cs:62-77). Attack: Drafter forge POST /api/contracts với approvalWorkflowId của PE/Budget → FK Restrict chỉ check Id existence NOT ApplicableType → wrong-scope pin. Also re-verify IsActive+IsUserSelectable server-side. Password ≥12 chars (Identity reject 11-char legacy). Severity MAJOR block push.
  • #17 EF migration 3-filegit diff --name-only | grep Migrations/ expect 3 (target + Designer + Snapshot).
  • #47 .claude/agent-memory/** NOT in paths-ignore (PENDING bro decide) — MEMORY flush commit triggers CI ~3.5min waste. paths-ignore hiện ['docs/**','**/*.md','.claude/skills/**'] missing agent-memory. Severity minor (CI waste). ⚠️ S40 note: agent-memory commits đang trigger — recommend bro add.

📋 5-category checklist (EVERY review)

  • Cat 1 Wire BE/feature claim: grep mock markers diff + await api\.(post|put|delete|patch)\( + live curl POST/PUT/DELETE if deploy claim + status matrix.
  • Cat 2 Schema integrity: 3-file rule Mig + column types vs entity def. Reference docs/gotchas.md (55 active).
  • Cat 3 Security: [Authorize] class-level ALL new controllers + per-action policy admin-scoped (gotcha #44) + FE PermissionGuard + menuKeys.ts mirror BE MenuKeys.cs + FluentValidation + EF parameterized.
  • Cat 4 Code quality: dotnet build SolutionErp.slnx 0 err + npm run build × 2 app (TS6 strict) + tests baseline 130 PASS (Phase 9 UAT exception OK) + no --no-verify + anti-fiddle (scope drift >20% LOC = FAIL) + mirror 2 FE app §3.9.
  • Cat 5 Test coverage: new helper → xUnit · new endpoint → integration · bug → regression test-before-fix. Phase 9 UAT test-after default OK (feedback_uat_skip_verify). Baseline 130.
  • Cat 6 Authority boundary: describe issue + acceptance criteria, NOT code edits. Escalate disagreement explicit.

⚠️ Anti-patterns + 🛡️ Smart Friend guard

  1. Recommend code edits (only describe issue+criteria) · 2. Skip live curl if deploy claim · 3. Accept "wire" without grep proof · 4. Defer to em main authority (escalate explicit) · 5. Skip MEMORY · 6. Lower bar match em main (Smart Friend Cognition anti-pattern).

Smart Friend (Cognition): NEVER lower bar. Em main code fine → PASS. Em main issues → FAIL with specifics regardless social pressure. "Quality ceiling set by primary, not escalation." Value = raise quality through catch.


🧠 SOLUTION_ERP review essentials (S40 verified)

  • Tests baseline: 130 PASS (58 Domain + 72 Infra). Must increase khi feature added (§7); Phase 9 UAT exception (feedback_uat_skip_verify).
  • Gotchas: 55 active (docs/gotchas.md, format ### N. highest #55). Latest #53 truncation · #54 529-fallback · #55 truncation-mid-exploration.
  • Migrations: 40 latest AddAttendances (path src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/). Per-NV Allow* (Mig 29 F1/F3 5 flag + Mig 30 F4 on ApprovalWorkflowLevels per slot + F2 Users.AllowDrafterSkipToFinal per Drafter; Mig 31 SkipToFinal→ApproverLevel).
  • Endpoints: ~211 · 84 SQL tables.
  • Identity password ≥12 chars (reject 11-char). Test creds: admin admin@solutions.com.vn/Admin@123456 (full) · UAT nv.test@solutions.com.vn/TestUser@123456 (Drafter CCM).
  • Prod: api/admin/eoffice.solutions.com.vn. Pin: MediatR 12.4.1 (flag Version="14) · Swashbuckle 6.9.0 · Node CI 20.x.
  • Conventions: docs/rules.md (§3.9 mirror 2 FE, §5.2 commit, §6.5 docs narrative, §7 test timing, §2.8 pin).

📅 Recent activity (FIFO — older → archive/git)

  • 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) PASSPeSuggestedPriceFeatures.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].

  • 2026-06-18 (S72 Q2 phản-biện CONFIRM finding isReal=false / not-an-issue — Mig 54 isSystem-exempt dead-branch): anh giao bác-bỏ finding "isSystem miễn-chọn-giá AN TOÀN/dead-code". Cố refute ×3 angle, KHÔNG bác được → finding ĐÚNG mọi điểm. (1) IPurchaseEvaluationWorkflowService.TransitionAsync = DUY NHẤT 1 caller backend-wide (PurchaseEvaluationFeatures.cs:505 human handler, throws Unauthorized nếu UserId null :499, pass currentUser.UserId non-null :508) → isSystem (:54 cần actorUserId null) LUÔN false trên PE-path. (2) SlaExpiryJob:63 inject IContractWorkflowService (KHÔNG PE) + query db.Contracts only → caller AutoApprove duy nhất KHÔNG chạm PE. (3) ApplyApprovedPriceOnFinalize (:908) chỉ gọi từ ApproveV2Async (:853,885) reachable chỉ qua approve-block :243 gate decision==Approve, còn isSystem cần AutoApprove → mutually-exclusive (lý do độc lập #2). (4) KHÔNG PE SLA hosted-service (chỉ SlaExpiryJob/Contract + ItTicketSlaJob). (5) non-admin AutoApprove tới ChoDuyet → throw Conflict :275 (no alt-finalize bypass price). Test header PeApprovedPriceFinalizeTests.cs:27-31 tự-ghi OBSERVATION report-em-main KHÔNG-fix + reflection-invoke isolation. Finding line# lệch (cite 911-916/SlaExpiryJob 76,100, actual 908-923) nhưng substance khớp. Dead-branch harmless defensive-return. LEARNED: "dead-code an-toàn?" load-bearing proof = đếm CALLER của method chứa branch (grep \.TransitionAsync\( + verify mỗi caller's service-TYPE qua DI) — single-human-caller + actorUserId-non-null-invariant kills isSystem độc lập với decision-gate; 2 lý do trực-giao mạnh hơn 1. Tag [s72, q2-phan-bien, isSystem-dead-branch, single-caller-proof, not-an-issue, confirm-finding].

  • 2026-06-18 (S72bis Mig 54 RE-REVIEW commit 1d86abc — anh 4 regression-Q a/b/c/d focus, PASS, 0 finding): Independent 2nd pass on SAME commit as S72 entry below, anh asked 4 targeted Qs. (a) AUTO→OPT-IN regression — in-flight + V1 SAFE: S69 auto-finalize block REMOVED; in-flight V2 phiếu mid-ChoDuyet carry NO new flags (come from NEW request not stored state) → intermediate levels advance normal (no ApplyApprovedPrice call line 870/894), terminal calls ApplyApprovedPrice → final approver picks price (candidate guaranteed exist, see d). CCM below-threshold WITHOUT tick now advances to CEO (intended safer, no strand). V1 legacy ApproveV1LegacyAsync signature does NOT receive new params + terminal line~990 does NOT call ApplyApprovedPrice → V1 finalize 100% unchanged, ApprovedPrice* stays null per entity comment. Tests cover: NoFlag→advance-CEO, AtLastSlot-no-double, all 3 fail-closed guards throw-before-mutate. (b) Mig 54 safe: 5-col additive-nullable, 0 backfill, 0 lock (AddColumn nullable=metadata-only SQL Server no rebuild); Down() drops all 5 reversible; 3-file rule OK (.cs+Designer+snapshot all 5 cols); EF config HasPrecision(18,2)×4 + HasMaxLength(20) match migration types. (c) DTO positional OK: 7 fields inserted between CeoApprovalThreshold↔ApprovalWorkflowId in BOTH record def + construction, same order (ProMin/ProMax/Ccm/ApprovedAmount/ApprovedSource/canEditPro/canEditCcm), types match (5 nullable + 2 bool); build-PASS confirms compiler-checked positional. (d) NO deadlock — decisive: submit-guard PurchaseEvaluationWorkflowService.cs:174-216 enforces (ALL paths incl Admin/system) winnerQuoteTotal>0 (line194 "chưa có giá chào thầu" if<=0) → Ncc candidate {amount:winnerQuoteTotal} ALWAYS present+positive at final approval → priceCandidates.length>=1 always → amber "Chưa có giá nào" (length===0) is DEAD UI unreachable → human always can pick ≥Ncc → priceMissing disables btn til pick → BE never gets null human-path → no Conflict-loop. winnerQuoteTotal BE Sum() over empty=0m (never null), FE type number non-null. OR-of-N currentIsFinalApprover = lastFlowLevel.status==='Current' true for EVERY viewer at last level (position-based BE ComputeLevelStatus:987), BUT approve buttons disabled=blockedByV2Level + onClick !isDisabled&&setTarget (line310-321) → non-approver can't open dialog → price-selector-for-all harmless. FE mirror: PeWorkflowPanel+PeDetailTabs byte-identical 2 apps (hash df2975a/ab08dad); type files differ pre-existing BUT Mig54 fields diff-identical. Setter handlers fail-closed Forbidden-before-side-effect, PRO Min<=Max validator, NO phase-guard (documented intentional mirror-budget S61). LEARNED: for "deadlock?" Q the load-bearing proof is tracing the SUBMIT-guard invariant (winnerQuoteTotal>0) forward to the finalize candidate-set — the FE dead-UI branch (length===0) is provably unreachable BECAUSE submit already rejected zero-price phiếu; never assess FE button-enable in isolation. SURPRISE: isSystem-exempt in ApplyApprovedPrice = dead via public ApproveV2Async (needs decision==AutoApprove + PE has no SLA-job) — test-specialist self-flagged OBSERVATION header, honest. Tag [s72bis, mig54-reReview, regression-Q-abcd, submit-guard-invariant-forward-trace, no-deadlock-proof, v1-untouched, or-of-n-safe].

  • 2026-06-18 (S72 Mig 54 PE giá-đề-xuất + CCM-finalize OPT-IN — financial go-live review, PASS, 0 blocker): Pre-commit uncommitted diff 17-file (+922/-102), DUYỆT TÀI CHÍNH go-live thứ Hai. 3 nhóm: ① giá đề xuất PRO(Min/Max)+CCM(1 giá) setter role-gate + người-duyệt-cuối chọn giá CHỐT (ApplyApprovedPriceOnFinalize); ③ CCM-finalize ĐỔI AUTO(S69)→OPT-IN ô-tích-tay finalizeByCcmDelegation. Threading 7-lớp KHỚP (body→Send→command→handler→interface→service→ApproveV2; controller :129 + TransitionPeBody :337-341 + command :462-465 + handler :515-517 + iface :30-34 + svc sig :47-49) — 0 lớp drop param (bẫy "F1+F2 wire fail 2 ngày" né). ③ fail-closed order verified (PurchaseEvaluationWorkflowService.cs:830-867): flag=false→skip finalize advance-CEO (test 1a, đổi-chính); flag=true check THEO THỨ TỰ threshold-null→Conflict(:832) / role≠CostControl→Forbidden(:835) / winnerQuoteTotal>=ceoThreshold strict-<→Conflict(:849) TRƯỚC set DaDuyet — 0 lỗ CCM/khác bỏ CEO. ① ApplyApprovedPriceOnFinalize gọi CẢ 2 nhánh DaDuyet (terminal :885 + CCM-deleg :853); human null-giá→Conflict, isSystem miễn, source∈{Ncc,ProMin,ProMax,Ccm} whitelist. Setter authz (PeSuggestedPriceFeatures.cs) Forbidden fail-closed TRƯỚC side-effect, đúng role (Pro=Procurement :53, Ccm=CostControl :109, Admin cả 2). Cross-stack FE/BE field-name khớp camelCase (finalizeByCcmDelegation/approvedPriceAmount/approvedPriceSource). FE currentIsFinalApprover = lastFlowLevel.status==='Current' (BE ComputeLevelStatus :978-991 = pointer==last) — OR-of-N: group cấp cuối 1 entry → "Current" cho mọi viewer NHƯNG nút mở-dialog disable bởi blockedByV2Level (:310,321) khi actor∉approvers → người-không-phải-cuối KHÔNG thấy bộ chọn. priceMissing disable Xác nhận đúng. Migration 3-file OK (Mig 54 additive-nullable, Designer 5 col, snapshot). Tests 334 PASS (45 Dom+289 Infra, +28: PeCcm 6→11 + PeApprovedPrice 10 + PeSuggestedSetter 13). 3 MINOR non-block: (a) ApplyApprovedPriceOnFinalize TRUST client amount — KHÔNG cross-check amount==stored-value-của-source (snapshot-semantic CHỦ ĐÍCH per comment; field display/audit-only, grep xác nhận KHÔNG drive Contract-from-PE value → low-sev); (b) edge winnerQuoteTotal==0 candidate amount=0 hợp lệ (submit-guard ép >0 nên unreachable thực tế); (c) stray fe-user/.claude/agent-memory/implementer-frontend/MEMORY.md NOT-gitignored (sub-agent cwd-misland gotcha) — em main ĐỪNG git add -A (chỉ add file cụ thể) + reconcile→canonical. LEARNED: combined-flag probe (skipToFinal+finalizeByCcmDelegation) SAFE — skipToFinal return (:818) trước finalize khi không-last-slot, last-slot no-op fall-through finalize-once (no double, guard vẫn full). For financial-approve review the 2 load-bearing proofs: (1) fail-closed guard order = throw TRƯỚC mọi set Phase=DaDuyet (reload-assert ChoDuyet trong test) + (2) trusted-client-amount chỉ MINOR khi field không feed downstream money (grep consumer = DTO-only). SURPRISE: isSystem-exempt branch trong ApplyApprovedPriceOnFinalize = defensive/dead qua public ApproveV2Async (approve-branch gate decision==Approve, isSystem cần AutoApprove; PE no SLA-job) — test-specialist tự ghi OBSERVATION header, honesty tốt. Tag [s72, mig54-pe-price, ccm-finalize-opt-in, fail-closed-order, trusted-client-amount-minor, currentIsFinalApprover-or-of-n, cwd-misland-stray, financial-golive-pass].

  • 2026-06-18 (S71 FINALIZE double-check H9+H10+checklist — lens R3 cross-cutting+residuals, GAPS-FOUND, 3 completion-gap): anh giao "hoàn chỉnh lại TOÀN BỘ" (not just Part C). Verdict GAPS-FOUND (no defect/no-bug — all gaps are deferred-incompleteness anh now wants closed). PASS items: (1) Containment model đồng-bộ MỌI file — 4 owning (_ledger.md:4/hmw.js:89,113/workflows/README:38/runs/README:78) + agents/README:162 + harvest-curator:52 + tooling-auditor + session-end/start ALL repoint Harness-10 "tracked-change NGOÀI run-folder+code-disjoint=vi-phạm". 0 file giữ old B6 operative. (agents/README:8 wave-mode = frozen 06-07 chronology, OK; harness_123 user-mem:13 = stale FE-ref noted below.) (2) Frozen-evidence INTACTgit diff --name-status f36aab8^..HEAD: broadcasts/_index.md additive-2-rows + 2 outbox NEW (A), 0 modify; harness-2 adap-report/error-ledger/pre-S70 sessions NOT in changed-set. (3) 3 h10 run-folder = run.md+harvest/ complete, sub-md/ only .gitkeep (EXPECTED — read-only subs scribed to harvest). ledger 2-beat all CLOSED, 0 orphan. (4) gitignore runs/=NOT-IGNORED, wave-*/agent-teams=IGNORED ✓. (5) email content_sha256 e5f09d57c22e MATCH body-lstrip, outward-VN full-grammar Cat-6 PASS. 🔴 3 COMPLETION-GAP (em-main fix to "hoàn chỉnh"): (G1 HIGH) over-cap curate-debt — reviewer/MEMORY.md 33782B (>30720 soft + >25600 auto-inject; spawn already truncated ~8KB HOT) + investigator-codebase 29819B (>25600). memory-budget.json measured STALE (reviewer 24795/inv 24052 = S70 snapshot) → re-run scripts/measure-agent-memory.ps1 + curate L1→L2 (additive, archive/ + _INDEX exist). (G2 MED) stale memory claims — Harness-9 user-mem line14 "cả 4 <25KB (đóng P1 curate-debt)" now FALSE post-S71; harness_123 user-mem:13 describes wave-mode as operative (superseded). (G3 MED) NO Harness-10 user-memory — biggest structural change (wave→tracked-runs + containment flip) has 0 feedback/project memory; 3 lessons uncaptured (engine-no-fs→em-main-scaffold-fragile · custom-workflow-needs-delta-guard-race · check-ignore-exit-trap). 2 MINOR-info: check-ignore exit-trap EXPLANATION imprecise in gitignore:96-98 + email#3 ("exit 0 for BOTH") — plain check-ignore actually exits 1 for negation (only -v --no-index gives 0-for-both); the recommended COMMAND still works correct → low-sev, email frozen. Learned: "complete the whole thing" audit must check budget.json measured_bytes vs DISK (snapshot drift re-accumulates after each over-cap session); honest-self-disclosure (STATUS+email both flag the over-cap) ≠ done — disclosure is what anh asks to CLOSE. surprise: I am ADDING to the very curate-debt I'm flagging (this entry pushes reviewer further over-cap) — G1 curate must run NOW. Tag [s71, finalize-r3, over-cap-curate-debt, stale-memory-claim, missing-h10-usermem, gaps-found].

  • 2026-06-18 (S71 Harness-10 adap run-trace convention — Stage-3 REVIEW lens R1 frozen-evidence+containment, PASS, 0 blocker): Governance/infra-only (wave-folder→run-trace .claude/workflows/runs/<run-id>/ TRACKED). 10 modified (8 H10 + investigator MEMORY residual + CLAUDE.md pre-existing) + 1 untracked runs/. NO product/test/csproj/package.json/migration → test baseline 306 untouched, deps N/A. Spec path trap: spec said runs/... but actual .claude/workflows/runs/... (verify disk, không tin claim path). R1 verify ALL PASS: (1) Frozen-evidence 0-touchgit status --porcelain on broadcasts/** · adap-reports/2026-06-07-harness-2 · error-ledger · sessions/* · STATUS · HANDOFF · */archive/* ALL empty = none touched. (2) Containment wording đồng-bộ 4 chỗ_ledger.md:4hmw.js:89/113workflows/README:38runs/README:78 ALL = "tracked-change NGOÀI run-folder + code-disjoint = vi-phạm" (model thay Harness-2 B6 "mọi tracked = vi-phạm"). (3) gitignore exit-code-trapcheck-ignore runs/.../run.md && echo IGNORED || echo NOT=NOT (re-included via :83 !.claude/**); wave-x/wave.md=IGNORED (legacy :93 kept); trap-note PRESENT gitignore :96-98. No new ignore rule shadows runs/. residuals verified as-claimed: investigator MEMORY +6 (3 S71 diary, 29819B≈29.8KB over-cap, race artifact closeout); CLAUDE.md pure test-count 263→306 flush. hmw.js node --check=PARSE-OK, args.run w/ legacy args.wave fallback :91, sub-md/ subdir :103. harvest-curator DEDUP axis (sha/substring before APPEND); session-end idempotent VERIFY-not-re-APPEND; session-start orphan-scan. 6× .gitkeep present. 1 MINOR (non-block, actionable): runs/ currently UNTRACKED (git ls-files empty, ?? runs/) = tracked-ELIGIBLE not-yet-committed; docs say "TRACKED" = post-commit steady-state — em main MUST git add runs/ in SAME commit else run-trace invisible to git-diff audit model depends on. Learned: "TRACKED" containment = 2-level — check-ignore NOT-IGNORED (eligible) vs git ls-files (committed); model only works after git add. surprise: internal var const wave = (A.run&&A.run.dir)?A.run:... keeps name wave but reads A.run first — cosmetic-only, downstream identical (not bug). Verdict PASS — safe commit (git-add-runs/ caveat). Tag [s71, harness-10-runtrace, frozen-evidence-clean, containment-wording-4file-sync, gitignore-exit-trap, tracked-eligible-vs-committed].

  • 2026-06-17 (S69 GOLIVE Văn phòng số public-all-roles authz — PASS, 0 blocker, gotcha #44-family CLEAN): 1-file BE-only DbInitializer.cs (+81, new SeedAllRolesOfficeModulePermissionsAsync :2261 + call :2055 AFTER S65 HRM grant → AFTER revoke :2042). NOT deployed (static + Dev-DB review, build PASS). Near-exact mirror of S65 HRM method, ONLY delta = +CanCreate=true (HRM was read-only). 8 verify ALL PASS: (1) Ordering — grant call sits after RevokeTemporarilyHiddenModulesAsync (:2042) + after S65 (:2048) → grant wins revoke. (2) Allow-list EXACTLY 16 Off keys — Off/Dashboard/DanhBa/PhongHop(+View+Book)/DeXuat(+List+Create+Inbox)/DonTu(+Leave+Ot+Travel)/DatXe/ItTicket; const names map correct values per MenuKeys.cs:99-120; NO PhongHopManage/AttendanceReport/ChamCong; array contains ZERO Hrm*/Personal/Pe*/Master key → no leak. (3) Upgrade-only correct — row exists→only flips CanRead/CanCreate false→true (if(!row.CanRead)+if(!row.CanCreate)), NEVER touches CanUpdate/CanDelete, never lowers; new row→read+create=true, update/delete=false (Permission.cs defaults false anyway). (4) 3 excluded keys STAY HIDDEN — decisive cascade check: Off is NOT one of the 4 inherit-roots in GetMyMenuTreeQuery (:56-59,:70-73,:80-83 = Contracts/Workflows/PE/PeWorkflows ONLY) → granting Off does NOT cascade to children; each Off child reads its OWN resolved flags (:65, falls to false-tuple if no row); PhongHop_Manage(parent=Off_PhongHop:1830)/AttendanceReport(parent=Off:1845) not-in-list→revoke-false→filtered by HasAccess(:96); ChamCong re-parented to Personal(:1850/:1962) under hidden Personal root, not under Off, not granted→hidden. (5) Admin unharmed — MenuPermissionHandler:27 Admin bypass; Dev DB: all 18 Off rows belong to Admin already read+create=true → upgrade branch no-op. (6) No real write-path opened — KEY for golive: grep Controllers for Off menu keys = 0 matches; Office controllers gate writes by class-level [Authorize] (any-auth, self-service create) + per-action [Authorize(Roles="Admin")] for true admin writes (MeetingRoomsController Create/Update/Delete=Manage-rooms :26/34/43, Attendances :37/42, LeaveBalances :23/28) — NOT by Off_*.Create policy. So broad CanCreate grant only drives FE menu+button (usePermission/PermissionGuard); API write-auth untouched, admin CRUD stays Admin-only regardless. (7) No migration — seed-logic only; all 16 keys in MenuKeys.All:157-161 (seeded). (8) Idempotent — 2nd run: rows already true→0 change; SaveChanges gated if(added>0||upgraded>0). Dev DB baseline (307 perms,13 roles): 0 non-admin Off rows exist→method takes add-branch for 12 non-admin roles (creates 16 read+create rows each, 3 excluded never added). build Infrastructure 0err/0warn. 0 rogue write (only cicd-monitor/MEMORY.md noise, read-only respected). Learned: for a public-grant golive the load-bearing security proof is TWO-fold — (a) cascade-safety = confirm the granted root is NOT an inherit-root (else siblings leak, gotcha #44-family) AND trace excluded keys' ParentKey to a non-granted/hidden parent; (b) write-path-safety = grep that the broadly-granted menu key is NOT used as a controller [Authorize(Policy=)] (here Office uses class [Authorize]+per-action Roles=Admin, so CanCreate is FE-only — granting it cannot escalate API writes). surprise: the "Manage rooms" admin function is double-protected — excluded from allow-list (menu hidden) AND its API is [Authorize(Roles=Admin)]; menu-hide alone would've been insufficient but the controller gate makes the broad grant safe even if a key had slipped. Verdict PASS — safe commit+deploy. Tag [s69, office-golive-authz, public-all-roles, inherit-root-no-cascade, off-not-policy-key-fe-only-grant, gotcha44-family-clean, admin-write-double-protected].

  • 2026-06-17 (S69 Văn phòng số RE-SKIN static logic-preservation — PASS, 0 blocker): 10 pages presentation-only re-skin → PURO PageHeader/KpiCard + Hồ sơ-NS idiom (9 fe-user office + 1 fe-admin AttendanceReport). NOT built yet, fe-admin not mirrored (em main next). Strongest proof = exact API/queryKey diff OLD-vs-NEW byte-identical ALL 8 fe-user pages (grep api\.(get|post|put|delete) + queryKey:[...] sorted -u, zero delta): proposals POST /submit + /{kind} · workflow-apps POST /{k}+/submit+PUT /workflow · meeting-bookings POST/DELETE+invalidate · it-tickets PUT /{id}/assign · directory/departments/attendance-report/excel-blob all UNCHANGED. Mutation side-effects (onSuccess/onError/invalidateQueries/setActionDialog/setComment/navigate) 1:1 (line-shift only). ProposalCreate validation !title.trim() throw + required + submit-disabled intact. AttendanceReport exportExcel blob (createObjectURL→a.download→click→revoke) intact. Cat2 orphans CLEAN: 0 unused import — flagged Users(=UsersIcon alias) + FormEvent/ReactNode (React.* namespace not named-import) + Accent(comment word) all FALSE-alarm verified. Cat3 shared-comp contract: PageHeader{eyebrow,title,subtitle,icon,accent,actions} + KpiCard{label,value,icon,accent,active,onClick} props all match real sig; KpiCard onClick wired to REAL filter state (ItTickets setFilter/WorkflowAppsList setStatusFilter/ProposalsList — driving actual client .filter()), InternalDirectory 2 KpiCards INTENTIONALLY inert (no onClick=presentational counts, matches comp design — NOT dummy). Shared comps + index.css NOT modified (git status -- ui/ + *.css EMPTY; sha256 identical fe-user==fe-admin per ls). Cat4 color-trap CLEAN: grep added lines for (teal|violet|amberx|greenx)-(200|300|400|800|900) = ZERO; index.css confirms accents ship only 50/100/500/600/700 (brand has full 50-900 so brand-800 valid); gotcha #66 — 0 gradient/dark-bg headings added (all headers on light surface use accent-ink text-brand-800/{accent}-700 via PageHeader). Cat1 mock-markers: 0 //Mock/alert/TODO-wire. Client-side filter additions (ItTickets filter/breached, WorkflowAppsList statusFilter useMemo) = presentation views over fetched items, NO new query/endpoint. 2 MINOR (non-block): (a) ProposalDetail status badge now renders TWICE — PageHeader actions slot + existing status-row (cosmetic dup, both presentation); (b) it-tickets/workflow-apps client-filter is view-only over a pageSize:100/50 first-page fetch (pre-existing pagination limit, re-skin doesn't worsen). Learned: for pure re-skin, the decisive logic-preservation proof is grep api-call + queryKey sorted -u OLD-vs-NEW byte-equality across every page — faster + more rigorous than reading each hunk; orphan-import heuristic (body-occ<=1) flags X as Y aliases + React.X namespace + comment-words as false-positives, always grep the actual usage line before flagging build-break. surprise: custom accent palettes (amberx/greenx/teal/violet) deliberately ship NO -800 stop so headings MUST use -700 (brand is the only -800-bearing accent) — a -800 on a non-brand accent = silent no-class Tailwind v4, the re-skin respected this everywhere. Verdict PASS — safe for em main to build+mirror. Tag [s69, office-reskin, presentation-only, api-querykey-byte-equal, color-trap-clean, kpicard-inert-vs-filter, gotcha66-clean].

  • 2026-06-16 (S65 PE mục E HoSoLink review — em-main PROXY, PE-Workflow reviewer-stage died-empty): Review mục-E hyperlink render + HoSoLink BE wiring (5a0aaa4). Reviewer-stage trong Workflow pe-hoso-link-rename-pro return RỖNG → em main self-gate evidence: Detail DTO hoSoLink present + null backward-compat phiếu thật (Run #293 GET 200); Create/Update +trailing-optional HoSoLink=null KHÔNG vỡ call-site (grep 0 manual ctor — KHÁC CreateDepartmentCommand #291 CS7036 vì positional-required vs trailing-optional); mirror fe-user==fe-admin SHA256 IDENTICAL (PeDetailTabs+PeWorkspaceCreateView); hyperlink <a target=_blank rel=noopener noreferrer> no reverse-tabnabbing; rename "Dự trù PRO"→"Ngân sách PRO" CHỈ display (giữ "Ghi chú từ PRO" + field-code). LEARNED: hyperlink free-text = no server-side XSS (render-as-href client-only); absolute-set Update (null=clear) chủ đích. SURPRISE: reviewer-stage chết-rỗng trong fan-out = lý do verify-heavy task vẫn cần em-main self-gate dù có Workflow (verdict feedback_workflow_fanout_reliability). Tag [s65, pe-section-e-review, em-main-proxy-self-gate, hosolink-backward-compat, workflow-fanout].

  • 2026-06-16 (S65 public Hồ sơ NS read for all roles — static pre-commit, PASS, 0 blocker, gotcha #44 family CLEAN): 1-file change DbInitializer.cs (+66, call-site :2046 SAU revoke :2040 + new SeedAllRolesHrmProfileReadPermissionsAsync :2203). Prod NOT deployed (static review, build PASS đã claim). 7 verify ALL PASS: (1) Ordering — grant gọi SAU RevokeTemporarilyHiddenModulesAsync trong SeedAsync → grant thắng (git diff confirms call sits immediately after revoke). (2) Upgrade path prod-critical — method MUTATES existing row if(!row.CanRead){row.CanRead=true;upgraded++} (EF change-tracked → SaveChanges persists); NOT skip-existing-noop. Correctly fixes S58-class bug (revoke set CanRead=false on prod rows → upgrade flips true). (3) Scope precisehrmKeys = new[]{MenuKeys.Hrm, MenuKeys.HrmHoSo} EXACTLY 2; NO Hrm_Dashboard/Hrm_Config*/Off*/Personal. Hrm is NOT one of 4 inherit-roots (Contracts/Workflows/PE/PeWorkflows in GetMyMenuTree:56-59) so granting Hrm root does NOT cascade to Dashboard/Config children → they keep own false flags → filtered out by HasAccess(n)=n.CanRead||Children.Any(HasAccess). Menu shows Hrm root → Hồ sơ NS leaf ONLY (HrmHoSo ParentKey=Hrm:1806, Dashboard sibling ParentKey=Hrm:1850 stays hidden). (4) Read-only — add-path CanCreate/Update/Delete=false; upgrade-path touches ONLY CanRead. (5) No regression — Admin bypass at MenuPermissionHandler:27 untouched; revoke unchanged; Off/Personal/Dashboard/Config stay hidden after full seed. (6) Idempotent — 2nd run: row.CanRead already true → if(!row.CanRead) false → 0 change. (7) No non-Admin write pathMenuPermissionHandler Read→AnyAsync(CanRead) is what GET checks; all 19 EmployeesController write actions (main+5 satellite) require Hrm_HoSo.Create/Update/Delete which grant leaves false → 403. surprise/monitor-note (NOT a defect, NOT introduced by this change): HrDashboardController/HrmConfigsController/Attendances/LeaveBalances carry ONLY class-level [Authorize] (any-auth, NO per-action Hrm_.Read policy) — so their data was already reachable by direct URL pre+post S65 (menu-hide ≠ API-lock; S58 revoke comment DbInit:2153-2155 explicitly acknowledged this). S65 does NOT widen it (only touches perm matrix rows Hrm+Hrm_HoSo + menu filter). cicd-monitor must NOT assume "Dashboard hidden in menu"=="dashboard data unreachable". Spec comment said "6 catalog Hrm_Config" but there are 6 config leaves + Hrm_Config subgroup = 7 keys — cosmetic count, all stay hidden, not a code bug. Learned: for menu-key read-grant, verify the granted root is NOT an inherit-root (else cascade leaks siblings) + trace HasAccess filter + confirm leaf ParentKey chains to the visible root; upgrade-path correctness = grep that method MUTATES row (not skip-existing) when a prior revoke pre-set the flag false on prod. Verdict PASS — safe commit. Tag [s65, public-hrm-hoso, upgrade-path-correct, inherit-root-no-cascade, gotcha44-family-clean, menu-only-not-api-lock-monitor-note].

🔄 Curate trigger

  • ~30KB → archive recent → L2 archive/<period>.md. Stale >3mo → remove.

  • Last curate: 2026-06-18 S71 (Harness-9 L1→L2, same-role race append over auto-inject cap) (36.7→24.2KB): moved 10 entries (byte-exact) → archive/2026-06.md — oldest FIFO tail S33/S35/S43/S49 + Smart-Friend-cumulative + archive-pointer + die-meta S57bis/S60 + redundant bottom Harness-10 R2/R3 (dup of S71 H10 entries kept). KEPT foundation + newest cluster (S71×2/S69×3/S65×2). Verified 10/10 moved lines grep -Fxf present-once in archive; numstat archive +N -0. _INDEX.md +10 pointer lines (substring sha-keyed). gist NOT updated (skip — em-main distill later). Prev: S70 (42.5→24.8KB) · S40 (28.4→18KB).
  • Prev curate: 2026-06-17 S70 (Harness-9, em-main + Stage-B workflow) (42.5→24.8KB): moved 9 entries S51→S57 (byte-exact) → archive/2026-06.md; KEPT foundation + 6 newest (S69×2/S65×2/S60/S57bis) + S49/S43/S35/S33 tail + Smart-Friend-cumulative + archive-pointers. Built archive/_INDEX.md (substring sha-keyed) + .gist.md (4-field distill-gen:1). Also Stage-C audit actor (wf_9520d8cd-4fe — verify 0-byte-loss/pointer/coverage). No re-ground (additive-only). Prev: S40 (28.4→18KB) · S34 q2 · S22 q1.
  • Prev curate: 2026-05-29 S40 em main proxy (28.4→~18KB): archived S33 Plan C + S33 startup + S32×2 + S29 wrap detail → q2 + git d2f52ba; refreshed stale (81/111→130 test, 47→55 gotcha, 31→40 mig, ~146→211 endpoints). Foundation (bug patterns + 5-category + Smart Friend guard + cross-module security) preserved. Prev: S34 q2 · S22 q1.