Files
solution-erp/.claude/agent-memory/reviewer/MEMORY.md
pqhuy1987 1f8947e763
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m33s
[CLAUDE] Auth: golive Văn phòng số — public Read+Create module Office cho mọi role (allow-list 16 key) + 6 test
Anh chốt "public văn phòng số cho all user eoffice". Mở quyền Xem + Tạo (self-service) module Office cho mọi role trên cả 2 app.

- NEW SeedAllRolesOfficeModulePermissionsAsync (DbInitializer): grant CanRead+CanCreate=true cho allow-list 16 key Office, chạy SAU RevokeTemporarilyHiddenModulesAsync để THẮNG revoke (mirror đúng pattern S65 HRM public). Upgrade-only: nâng false→true trên row prod đã có, KHÔNG hạ, KHÔNG đụng CanUpdate/CanDelete. No migration (seed-logic, idempotent).
- ALLOW-LIST 16: Off + Off_Dashboard + Off_DanhBa + Off_PhongHop(View/Book) + Off_DeXuat(List/Create/Inbox) + Off_DonTu(Leave/Ot/Travel) + Off_DatXe + Off_ItTicket.
- GIỮ ẨN (ngoài allow-list → revoke vẫn che non-Admin): Off_PhongHop_Manage (admin CRUD phòng), Off_AttendanceReport (báo cáo chấm công — riêng tư), Off_ChamCong (Cá nhân — golive riêng). HRM (trừ Hồ sơ NS S65) + Personal VẪN ẩn (anh chỉ mở Office).
- reviewer PASS 0 blocker (security): cascade-safe (Off KHÔNG phải inherit-root trong GetMyMenuTree → excluded-3 giữ false, không lan); KHÔNG mở write-path thật (Office controller dùng [Authorize] self-service + [Authorize(Roles=Admin)] cho admin-write — CanCreate chỉ mở menu + nút tạo FE, API authz độc lập menu key; quản lý phòng double-protected).
- +6 test OfficeModulePermissionSeedTests (286→292) lock: allow-list read+create=true · excluded-3 stay hidden (load-bearing) · admin not demoted · no-leak HRM/Personal · upgrade-only preserves admin-raised Update/Delete.
- Build slnx 0/0 · dotnet test 292 PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:33:32 +07:00

42 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.

📁 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-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].

  • 2026-06-12 (S60 đợt1 PE submit-guard + drafter-bypass gate — KHÔNG DELIVER, die mid-run, on-behalf em main ghi hộ, H2-proposed): Task: review 37122f0 cross-stack (BE TransitionAsync submit-guard đủ-4-thông-tin mục 3 + bypass người-soạn-trong-chuỗi V2 BƯỚC-ĐẦU-only + FE PeDetailTabs ×2 + 14 PeSubmitGuardAndBypassTests 240→254). Die mid-run #53-class (commit body tự khai "Reviewer die mid-run → em main self-gate evidence-checklist PASS 0 blocker") → ship Run #283 PASS prod-verified, bundle rotate both. LEARNED: self-gate em main đứng vững lần 2 (sau S57bis) — checklist deterministic (test gate + diff scope + prod smoke 401/404-control) đủ cho PE refinement cross-stack. SURPRISE: die lần 3 trong 2 ngày (S57bis die-0-byte ×2 + S60 mid-run) DÙ promote-tier inherit Fable 5 → model-tier KHÔNG phải nguyên nhân die (nghi resume-kill/harness class) — trend data cho Harness-4. Tag [s60, die-mid-run-3rd, self-gate, on-behalf].

  • 2026-06-11 (S57bis product gate — KHÔNG DELIVER, die-0-byte ×2, on-behalf em main ghi hộ, H2-proposed): Cả 2 spawn (email-gate đầu + final gate) chết 0-byte output 0 return (resume-kill class #3, ref feedback_agent_kill_recovery) → em main SELF-GATE evidence-checklist: grep authz key-set + role-string vs AppRoles + Mig 49 Up/Down reversible + 240 test + Run #381 + prod smoke 401/404-control. LEARNED: output-file size=0 + im >5 phút = chết, KHÔNG đợi thêm; KHÔNG re-spawn >2 lần trong session có --resume. SURPRISE: khác S52 killed-with-partial — lần này 0-byte tuyệt đối (không gì recover được từ return). Tag [s57bis, die-0-byte-x2, self-gate, on-behalf].

  • 2026-06-10 (S57-resume Harness-4 two-tier adopt gate — PASS-with-fixes, 0 blocker): Gate trước send-email + commit (governance, không product code). Self-report spawn: claude-fable-5[1m] (reviewer = promote-list inherit → direct promote-tier evidence, em main cite được). Independent re-verify ALL GREEN: grep frontmatter = đúng 7 pin claude-opus-4-8 + 4 inherit + 0 [1m]-in-frontmatter (2 body-text hits hợp lệ: database-agent.md:46 + README.md:9 MỚI — adap-report "match duy nhất" stale-by-own-edit) + 0 project-pin settings. Evidence track-record 8/8 REAL vs HANDOFF/STATUS/own-memory (S51 MAJOR · S54 QTV-decoy · S53 Mig46 · S56 H2-4.5/5 + dept-IT-0-user · S57 ×3 controller +5/+5/+5 [Authorize(Roles="Admin,CatalogManager")] working-tree). Nấc G-011 đúng mọi chỗ load-bearing (demote = executed-file·pending-restart, 0 overclaim runtime). Fixes: hash PLACEHOLDER trước send (nac: sent + "SENT ✓" premature = đúng status-verb class broadcast cảnh báo) · STATUS "(runtime resolve 1M)" thiếu attribution AI_INFRA-s20 · hmw.js:91 log "same-model inherit" stale + :9 "8-agent" vs 9 roles · adap-report "(13)" vs "11" count · invalid-role typo → rơi 'opus' (fail-direction xuống vs H4.5 nghiêng-quality). Learned: gate adopt-governance = re-run MỌI grep claim + cross-check evidence vs HANDOFF nguyên văn; n=2 demoted spawn-test double-duty làm inherit-chain proof là HỢP LỆ (registry cached = chạy config cũ) nhưng cần phrase rõ kẻo đọc nhầm thành promote-list spawn-test. Tag [s57, harness-4, two-tier-gate, pre-send-gate, g011].

  • 2026-06-09 (S56 pre-golive authz live-curl — PASS, 0 blocker): Live prod curl 8 new endpoints. 8/8 return 401 unauth; admin-authed: hrm-configs/vehicles(2)+drivers(2), leave-balances/my(5 lazy), attendances/report+excel(200, 6797B xlsx) all 200; non-admin Drafter correctly 403 on the 2 Admin-only attendance endpoints. gotcha #44 silent-403 sweep CLEAN: capability GET /it-tickets/assignable-staff returns HTTP 200 {canReassign:false,staff:[]} for non-IT Drafter (NOT swallowed 403) + {true,[]} admin — handler returns flag, doesn't throw (WorkflowAppsFeatures.cs:466). assign-mutation guard fail-closed (:504). E2E: GET /projects payload has all +4 fields (70/70), CAL01 Investor live. Off_AttendanceReport menu key in admin /menus/me. 1 MINOR (non-block, defense-in-depth): PUT /it-tickets/{id}/assign checks NotFound BEFORE Admin-OR-IT Forbidden (WorkflowAppsFeatures.cs:496-508) → existence-oracle leak; mutation itself fail-closed → post-golive hardening only. Tag [s56, pre-golive-verify, authz-clean, gotcha44-clean, notfound-before-forbidden-minor].

  • 2026-06-09 (S55 Phase-1 FE visual redesign pre-commit — PASS, 0 blocker, verdict-first survived): 14 fe-admin files VISUAL/CSS-only (NAMGROUP density + SOLUTION brand). Independent re-verify GREEN: npm run build fe-admin = ✓ 607ms, 1945 modules, 0 TS err (only PRE-EXISTING warns: CSS @import-order + >500KB chunk + INEFFECTIVE_DYNAMIC_IMPORT realtime.ts — git-confirmed none introduced, @import lines untouched in diff). Regression Cat1 ALL preserved: Button cva variant keys (primary/secondary/outline/ghost/danger) + size (sm/md/lg) STABLE — only Tailwind class VALUES swapped, defaultVariants intact (51 call-sites safe); Input/Select/Textarea/Label = forwardRef+...props+className passthrough unchanged, only cn() literal; Dialog {open,onClose,title,children,footer,size} destructure + sm/md/lg→max-w map intact (+aria-label="Đóng" = a11y GAIN); DataTable Column<T> type UNCHANGED (diff starts after type def) — render/sortable/align/width + sort + Pagination props intact, RowActions/RowActionButton purely ADDITIVE; Layout MenuLeaf className-only (brand left-rail via before:), nav/resolver/permission-filter/routing untouched; PhaseBadge phase→ContractPhaseColor/Label map intact; PageHeader/EmptyState/TopBar pure class. DashboardPage data-flow (useQuery/navigate/fmtMoney/BarChart/PhaseBadge) preserved, STAT_TONE+SectionLabel additive, +cn import only. Brand Cat3: Be Vietnam Pro KEPT (grep: @import:3 + --font-sans:22 + font-family:34 all unchanged — initial blocker RETRACTED after grep); only brand-/slate/semantic colors, 0 off-brand hex/indigo. a11y: focus-visible rings present everywhere (brand-500); Label self-documents slate-500 (~4.6:1 AA-pass) chosen over NAMGROUP zinc-400. Tailwind v4 (^4.2.3)ring-current/15, shadow-xs, slash-opacity all valid v4. noScopeCreep: exactly 14 fe-admin, 0 fe-user, 0 BE/src (only noise = frontend-designer/MEMORY.md agent file). 2 MINOR (non-block, a11y-floor): text-slate-400 on white for small hint/empty text (DashboardPage hints ~line 50/64, DataTable empty-cell, EmptyState was-400-stays-400) ≈3.5-4:1 — borderline-fail WCAG-AA for <18px, but these are de-emphasized hints not primary content + PRE-EXISTING tone (redesign mostly UPGRADED slate-400→500 on EmptyState desc + Pagination); accept for hint role, revisit if audit. Learned: font-drop scare = grep the 3 load-bearing lines (@import/--font-sans token/font-family) BEFORE flagging — diff hunk lower in file ≠ font removed; emit PASS/FAIL line-1 FIRST (gotcha #53 truncation survival, mirror S51/S55). surprise: Tailwind v4 shadow-xs is real (v3's shadow-sm renamed) — don't flag as typo; v4 slash-opacity on currentColor (ring-current/15) is valid. Verdict PASS — safe commit+deploy. Tag [s55-fe, visual-redesign, namgroup-density, verdict-first, regression-clean, slate400-minor].

  • 2026-06-09 (S55 master-data import pre-commit — PASS [em main proxy — reviewer return truncated gotcha #53 before verdict, mirror S51]): Reviewed Mig 48 AddProjectMasterFields (Project +4 nullable col Year/Investor/Location/Package) + SeedRealMasterDataAsync (62 Project+71 WorkItem+3 Supplier per-code idempotent ungated) + FE ProjectsPage form +4 ×2 app. Reviewer ran 293s/31-tools nhưng truncated mid-thought (nghi cached-binary 2.76s build → muốn forced clean rebuild + Project tests). Em main COMPLETED đúng việc nó định làm: dotnet test SolutionErp.slnx = clean rebuild + 216 PASS (58+158, 0 fail/skip) → giải tỏa cached-binary concern (test = fresh build). 10 dims GREEN: Mig Up=4 AddColumn/Down=4 DropColumn reversible + 3-file; seed 62/71/3 0-dup; per-code idempotent ungated line 118 (reaches prod); FLOCK01 collision skip-demo-wins; FE↔BE 4 nullable both sides (tránh S51 mismatch); test-file compile-fix +4 null legit; gotcha #57 index untouched; runtime Dev proof (data landed, Investor col populates). 0 rogue write (read-only respected, git clean of code). Learned: long adversarial review return truncates (gotcha #53) → reviewer nên emit PASS/FAIL verdict SỚM (trước deep re-verify) để sống sót truncation; em main complete được đúng pending-check (clean dotnet test) deterministic. Verdict PASS — safe commit. Tag [s55, master-import, em-main-proxy-truncate, runtime-dev-proof].

  • 2026-06-08 (S54 ItTicket reassign authz Admin-OR-dept-IT cross-stack pre-commit — PASS, 0 blocker, gotcha #44 disarmed correctly): Controller /assign hạ [Authorize(Roles="Admin")][Authorize] any-auth; authz moved INTO AssignItTicketHandler (Admin-OR-IT Forbidden + assignee-must-IT Conflict) + new GetAssignableItStaffQuery capability endpoint. Independent re-verify GREEN: dotnet test SolutionErp.slnx = 216 PASS (58 Dom + 158 Infra, Failed:0 Skipped:0, +13 matches 203→216 claim exactly) · fe-admin + fe-user tsc -p tsconfig.app.json --noEmit BOTH exit 0 clean. #2 CHÍ MẠNG role-string "Admin" CONFIRMED REAL (full chain traced): AppRoles.Admin="Admin" literal (AppRoles.cs:5) → SeedRolesAsync Name=roleName (DbInit:1485) so DB Role.Name=="Admin" → Identity GetRolesAsync returns NAMES → JwtTokenService:32 new Claim(ClaimTypes.Role, r) → CurrentUserService:30-31 FindAll(ClaimTypes.Role)cu.Roles.Contains("Admin") CORRECT. "QTV" (DbInit:1458 RoleLabels) = ShortName DISPLAY label only = decoy. Program.cs JWT sets NO RoleClaimType override (ClaimTypes.Role symmetric write/read). Same proven pattern as every existing Roles="Admin" endpoint. Bypass airtight: controller any-auth → handler sole gate; guard if(!isAdmin && !(itDeptId is Guid mine && myDeptId==mine)) Forbidden fail-CLOSED when itDeptId null (non-admin blocked; admin still passes role-branch). Department.Code=="IT" IS seeded (DbInit:2082 "Phòng CNTT") so live non-null. Capability 0-leak: non-auth → {canReassign:false, staff:[]} (no name leak), [Authorize] any-auth → 0 silent-403. Defense-in-depth intact: FE nút {canReassign&&} but PUT still hits handler guard → 403/409 → onError: toast.error(getErrorMessage(err)) surfaces (NOT swallowed). FE SHA256 page 4bcaf2f IDENTICAL both apps (types.ts correctly NOT identical — admin AttendanceReportDto vs user HrDashboardDto diverge below; added AssignableStaff block byte-identical); old fe-user page was read-only kanban (NO app-specific logic lost — diff purely additive); fe-user has all imports (apiError.getErrorMessage, Dialog size/footer/onClose props match, Select passthrough, sonner). Test 13-fact NOT happy-path: Case5 Forbidden side-effect assert AssignedToUserId.Should().BeNull() (red-able by contrast vs Case6/7 same-handler success), Case3/3b empty-staff 0-leak, Case8 Conflict msg-exact. prove-by-contrast ĐỦ CHẶT (partition non-IT-throws vs IT/admin-succeed identical handler). 1 MINOR defer: assignee-must-IT NEW vs old handler (git HEAD: old allowed admin→ANY active user); itDeptId-null → even admin Conflict — fail-closed acceptable+spec-requested, cosmetic (prod IT-dept seeded). Learned: authz role-string review = trace FULL chain const→seed Name→GetRolesAsync(names not codes)→Claim(ClaimTypes.Role)→reader AND grep JWT cfg for RoleClaimType override (none=symmetric) — display-code (QTV) in RoleLabels/ShortName dict = classic decoy. surprise: moving authz controller→handler is the CORRECT gotcha #44 fix (not a smell) when paired with BE-computed capability flag for FE gating + handler as sole gate. Verdict PASS — safe commit. Tag [s54, it-ticket-reassign-authz, gotcha44-disarmed, role-string-chain-verified, cross-stack-clean].

  • 2026-06-08 (S52-late Task C ItTicket admin reassign + Task D AttendanceReport menu-key pre-commit — PASS, 0 blocker): Migration-FREE (menu = idempotent DbInitializer seed). 5 prod files: MenuKeys.cs (const+All[]), DbInitializer.cs (1 seed tuple), fe-admin {menuKeys.ts, Layout.tsx staticMap, ItTicketsPage.tsx}. Independent re-verify GREEN: dotnet build SolutionErp.slnx 0-warn/0-err · npm run build fe-admin tsc-b+vite OK (1945 modules, only pre-existing CSS @import + >500KB + ineffective-dynamic-import warns). menuKeyMatchOk: "Off_AttendanceReport" byte-identical 4 places (MenuKeys.cs:125 const == menuKeys.ts:68 == seed parent key == Layout staticMap:87); seed leaf parent=MenuKeys.Off order=8 icon=FileBarChart (verified valid lucide alias FileChartColumn as FileBarChart — getIcon resolves via Icons[name]); App.tsx route /attendance/report → AttendanceReportPage PRE-EXISTING committed S52 (6a66429, no diff, 170-LOC real page not stub) — Layout maps to REAL route. types/menu.ts correctly NOT mirrored (key:string not typed union). admin auto-perm via SeedAdminPermissionsAsync iterating MenuKeys.All (DbInit:1917, idempotent Contains:1919); new-leaf-on-existing-DB confirmed (upsert loop:1845-1862 TryGetValue-miss→Add). reassignCorrect: FE PUT /it-tickets/{id}/assign body {assignedToUserId} MATCHES BE record AssignItTicketBody(Guid AssignedToUserId) (ItTicketsController:42); endpoint [Authorize(Roles="Admin")] (:34) under class [Authorize] — admin-app FE calling = correct; user-list reuses EXISTING /users GET (PagedResult<UserDto> items{id,fullName,email}, no new BE endpoint, lazy enabled:target!==null); 204 NoContent handled (no body-parse); invalidateQueries(['it-tickets']) on success; handler sets BOTH AssignedToUserId+AssignedToFullName (WorkflowAppsFeatures:467-468) + validates assignee IsActive→NotFound, no try-catch (GlobalExceptionMiddleware). feUserUnchanged: git diff -- fe-user/ EMPTY (Task C = fe-admin-only divergence, documented top-comment ItTicketsPage:3-5). noScopeCreep: git status prod = EXACTLY the 5 expected files, agent-memory noise ignored, no new migration, no BE beyond MenuKeys+DbInitializer, ItTicketsPage diff 98+/5- all Task-C-scoped (imports+state+mutation+Pencil-btn+Dialog), 0 mock/alert markers. Learned: menu-key wiring = verify byte-identity across the FULL mirror set (BE const + BE All[] + seed parent + FE menuKeys + FE staticMap) + confirm the target route actually EXISTS (grep App.tsx) — a staticMap entry pointing to a non-existent route silently drops the leaf (gotcha #50). surprise: lucide FileBarChart is a deprecated-alias (re-exported from FileChartColumn) but still valid — d.ts grep confirmed before flagging. Verdict PASS — safe to commit. Tag [s52-late, it-ticket-reassign, attendance-report-menukey, menukey-mirror-5way, gotcha44-disarmed, gotcha50-disarmed].

  • 2026-06-08 (S53 gotcha #57 EXT Mig 47 — Master catalog filtered-unique pre-commit — PASS, 0 blocker, Smart Friend clean): 4th/5th/6th cumulative gotcha #57 (after Holiday Mig 43 S45 + HRM ×3 Mig 45 S51). 3 Master configs (Department:18/Project:19/Supplier:24) Code unique index .IsUnique()+.HasFilter("[IsDeleted] = 0") + Mig 47 (3-file) + 3 new tests. Independent re-verify ALL GREEN: build SolutionErp.slnx 0-warn/0-err · full suite 203 PASS (58 Dom + 145 Infra, Failed:0 Skipped:0, +3) · 3 new tests run isolated 3-passed-0-skipped. Cat correctness: filter string byte-identical to HolidayConfiguration:18 (xxd 5b 49 73 44 65 6c 65 74 65 64 5d 20 3d 20 30 — spaces around =, not guessed); index STAYS unique:true (2 active same-Code still violate — active uniqueness preserved); Supplier Type index (:25) UNTOUCHED non-unique unfiltered (snapshot:3590 bare). Mig Up=3×Drop+3×Create-filtered, Down=3×reverse-unfiltered (reversible). Snapshot+Designer both show filter on all 3 Master Code idx. Test NOT tautology: seeds IsDeleted=true row → real Create*CommandHandler (app-check AnyAsync(Code==req.Code) thru HasQueryFilter !IsDeleted PASSES) → asserts NotThrow + active-count==1 + IgnoreQueryFilters all==2; RED-before confirmed (3 failed SqliteException UNIQUE on unfiltered). Cmd signatures match test calls (Project 7-arg/Supplier 9-arg/Dept 4-arg). noScopeCreep: git status = exactly 3 configs + snapshot + Mig47 (2 untracked) + 1 test + 2 MEMORY; no FE, no extra mig, no stray. Mig 47 latest in seq (after Mig46 ItTicket SLA). Learned: cookie-cutter EXT of proven pattern → discriminator = byte-compare filter string (xxd) vs canonical sibling + verify index still unique (filter must NARROW scope not DROP uniqueness). app-level dup-check existence = the test premise; verify handler actually has AnyAsync(Code) else test premise false. surprise: implementer claimed "2 pre-existing DocxRenderer warnings" but clean incremental rebuild = 0 warn (unrelated, non-issue). Verdict PASS — safe commit. Tag [s53, gotcha57-ext, mig47, master-catalog, smart-friend-clean].

  • 2026-06-08 (S52 P11-E AttendanceReport + P11-F MaTicket codegen pre-commit — PASS, 0 blocker): Migration-free (no schema). Independent re-verify: build 0-err · 191 PASS (58 Dom + 133 Infra, +5: 3 ItTicketCodeGen + 2 AttendanceReport) · fe-admin tsc --noEmit exit 0. Cat3 gotcha #44 attack DISARMED: [Authorize(Roles="Admin")] ×2 report endpoints — verified AppRoles.Admin = "Admin" literal (AppRoles.cs:5) == attribute string == FE user?.roles.includes('Admin'); "QTV" (DbInit:1454) = display-code NOT role-name; pattern proven (Catalogs/HrmConfigs identical). Cat3 camelCase contract MATCH field-for-field BE record PascalCase→FE interface (year/month/rows/grandTotal*/userId/fullName/ot*) — ASP.NET default camelCase, no Program.cs override. BE handler correct: .Year/.Month in IQueryable (EF-translatable DateTime), .DayOfWeek+holidaySet only AFTER .ToListAsync() (in-memory) — IQueryable-translation attack handled; holiday-check BEFORE weekend BEFORE weekday (test 2026-06-01 Mon-but-holiday proves override); DateOnly.FromDateTime correct (Holiday.Date=DateOnly); OtPolicy fallback 1.5/2.0/3.0; IsDeleted via AuditableEntity all 3 entities. Exporter mirrors ContractExcelExporter, ClosedXML 0.105.0, RenderResult(Content,FileName,ContentType) ctor order correct, DI registered. MaTicket codegen: e untracked at codegen time → inner SaveChanges persists ONLY sequence row, no double-insert; gen-on-Create (kanban no-workflow) vs Leave/OT gen-on-Submit — semantically correct; git-show confirms MaTicket was ALWAYS null pre-P11-F (closes gap). 1 MINOR (informational, defer): sequence-gap-on-failure — codegen commits seq in own Serializable tx BEFORE Add(e)+SaveChanges; ticket-insert fail → burned IT/2026/NNN gap. NOT new defect = identical to existing Leave/OT pattern (project-wide accepted trade-off, cosmetic). MyAttendancePage MIRROR divergence intentional+documented (fe-user untouched, §3.9 OK). 0 mock markers. Learned: when spec NAMES an attack vector (gotcha #44 role-string), verify the LITERAL const value not just attribute presence — "QTV" display-code was the decoy; role-name match is the real check. surprise: Bash tool = bash not PowerShell (Select-String fails exit 127 → use grep). Verdict PASS — safe to commit. Tag [s52, p11ef, attendance-report, mat-codegen, gotcha44-disarmed].

  • 2026-06-08 (S51 P11-C Vehicle+Driver + gotcha #57 pre-commit — PASS, 1 MAJOR caught) [em main proxy — reviewer return truncated gotcha #53]: Reviewed Mig 44 (Vehicle/Driver catalog) + Mig 45 (filter 3 HRM unique) + FE KIND_CONFIG +2 + 5 tests (186 PASS). Independent build+test re-verify GREEN. CAUGHT 1 MAJOR (Cat 3 cross-stack contract): Driver FE↔BE required-field mismatch — FE render phoneNumber/licenseNumber/licenseClass OPTIONAL nhưng BE validator NotEmpty() + EF .IsRequired() NOT NULL → empty submit = 400/500. Root = inconsistent em-main brief (BE "mirror Vehicle"=required vs FE spec quên required). Fix: FE +required:true (align BE all-required như Vehicle). Cats khác clean (Mig diff clean, Authorize Roles=Admin writes, gotcha #57 grep-complete 3 HRM, DbInitializer idempotent + #51 infra-gated, SHA256 mirror, no copy-paste Driver↔Vehicle). Learned: parallel fan-out (BE∥FE file-disjoint) → bất kỳ inconsistency trong SHARED em-main contract chỉ lộ lúc integration; green tests ≠ correct contract (no test chạm empty-optional path). reviewer = the net. surprise: transient mid-deploy bundle hash (cicd lesson) + reviewer self-truncate trước khi ghi MEMORY → em main proxy. Verdict PASS post-fix. Tag [s51, p11-c, gotcha57, contract-mismatch-catch].

  • 2026-06-07 (S49 Harness 1/2/3 adopt pre-commit — PASS all 3, no blocker): Governance/infra adopt (no product code, no test impact). VERIFIED: H1/H2 = 2 sub scope-DISJOINT + tools [Read,Grep,Glob,Bash+4RAG] NO store_memory/Write (INFORM-only); genuinely TAILORED not copy-paste (SE 4-RAG vs AI_INFRA 2-RAG · dropped effort:max + agent-ops-monitor/sister · Fidelity→SE reviewer). H2 5-trục in harvest-curator.md + session-end §L.b(f). H2 wave-mode hmw.js mirror AI_INFRA + B6 git check-ignore VERIFIED (wave-*/+agent-teams/ ignored · hmw.js/README tracked). H3 self=se complete substitution · SHA256 canonical formula byte-identical send==check · 13 .gitkeep exact · adap-apply base-path outbox\all\. honest nấc executed-file/verified-runtime-PENDING. G-015 scan = 6 hits ALL negating ("KHÔNG enforced") = correct honesty. 1 MINOR (non-block): README:11/18 "7-agent" ASCII diagram = PRE-EXISTING drift (git diff proved work này chỉ touch load-bearing title/decision-tree/tool-grant/matrix; diagram predates S47 frontend-designer) → tooling-auditor H1 designed-to-catch = self-validating adoption. learned: git diff base..head = discriminator introduced-defect vs pre-existing-drift (đừng đổ lỗi work mới cho drift cũ); name-collision tailor-verify = diff frontmatter AI_INFRA-canonical vs SE-instance. surprise: mojibake scan false-pos trên "ĐÃ" (U+00C3 = valid VN uppercase, KHÔNG double-encode → verify codepoint in-context trước flag); broadcast floor "12 .gitkeep" UNDERCOUNT (correct=13 incl all/ adap-channel — em main đúng). Verdict PASS, safe commit + restart. Tag [s49, harness-adopt, governance, max-clean].

  • 2026-05-30 (S43 P11-B LeaveBalance pre-commit — PASS, Max no-truncate): 14 file (LeaveBalance entity+config+Mig42 + Features + Controller + deduction hook + Create/Update LeaveType guard + embed balance + FE×4 + tests). 154 PASS (130→154). Deduction exactly-once VERIFIED (terminal else only, guard Status!=DaGuiDuyet chặn re-approve; advance/reject/return no-deduct). FK invariant fully closed — grep 2 write site LeaveTypeId (Create + UpdateDraft) cả 2 guard AnyAsync→Conflict, bogus type không thể tới terminal FK insert. Embed balance = RequesterUserId (approver thấy đúng người tạo). admin [Authorize(Roles=Admin)]. 2 MINOR defer: concurrency lost-update UsedDays (no RowVersion — human-sequential accept) · stale line-num comment. Verdict PASS. Tag [s43, p11b-leavebalance, max-clean].

  • 2026-05-28 (S35 G-H2 BE CRUD 16 endpoint pre-commit — PASS, Smart Friend 8× CLEAN): 2 NEW file HrmConfigFeatures.cs 439 + Controller 137. build clean, 130/130 PASS. Cat1: 0 mock, 8 ConflictException (Holiday Update composite (Year,Date) BOTH fields). Cat3: class [Authorize] + 12 per-action [Authorize(Roles="Admin")]. Cat5: 8 Validator MaxLength MATCH EF source (Code=50 not spec 20). 2 MINOR defer: ListHolidays no IsActive filter (inconsistent sibling) · OtPolicy "1 active unique" NOT enforced handler (G-P1 ambiguous nếu 2+ active). Verdict PASS. Tag [s35, smart-friend-8x-clean].

  • 2026-05-26 (S33 Plan B G-H1 Phase 2 pre-commit — PASS, Smart Friend 6× CLEAN): 17 file (3 BE + 6 FE new + 6 mod + 2). SHA256 mirror 3 file IDENTICAL admin==user. 5 endpoint real mediator.Send 0 mock. Mig 34 AddEmployeeProfiles 7 table UNIQUE indexes + FK Cascade. SeedDemoEmployeeProfiles NOT gated DemoSeed (gotcha #51 ✓). gotcha #50 Layout staticMap mirror ✓. 3 MINOR defer: EmployeeCode race SERIALIZABLE low-risk · Update 3 bool not nullable (partial reset) · Delete DateTime.UtcNow direct. Verdict PASS. Tag [s33, hrm-mig34, smart-friend-6x].

  • Smart Friend cumulative 8× CLEAN: (1) S22 #44 silent-403 · (2) S25 #48 SQLite tie-break · (3) S29 password ≥12 · (4) S29 ApplicableType cross-module · (5) S33 BW test · (6) S33 Plan B Phase 2 · (7) S35 FE forms · (8) S35 G-H2. Plus 9× G-O2 (S36, em không track ở đây). 2 MAJOR catches total (S29 password + S29 ApplicableType); rest clean với MINOR defer.

  • Archived S29-S33 detail + S32 startup → archive/2026-05-q2.md + git d2f52ba (S40 curate): S33 Plan C B-Wrap 9/9 [Fact] verify · S33 startup drift audit (CLAUDE.md SEVERE → patched S40) · S32 wrap/startup standby · S29 wrap 2 MAJOR catch detail. KEY absorbed in bug patterns + Smart Friend cumulative above.


🔄 Curate trigger

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

  • Last 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.