Date: 2026-04-05
Reviewer: Codex
Plan reference: experiments/plans/plan-010.md
Scope: Phase 3 review pass after /deslop; focus on T1–T4 implementation and review-stage blockers before /peer-review
- Authenticated Phase 3 routes consistently gate on
getSessionUser()and preserve the app’s session-cookie auth model. - PostHog single-emission discipline holds for the new review-scope events:
transactions_view_opened,transactions_filter_applied,scope_changed,merchant_rollup_clicked,coaching_facts_expanded, andcoaching_narrative_*. - Facts-grounded coaching still preserves the safe fallback path: rule-based advisory
messageremains available when Gemini output is absent or invalid. - Transactions, merchant rollups, and scope parsing all retain server-side ownership checks for
statement_id/statement_ids.
[HIGH] apps/money-mirror/src/lib/dashboard.ts — dashboard math was capped to the first 1000 transactions in scope.
- Impact: Overview totals, advisory inputs, and coaching facts could diverge from Transactions and merchant rollups for larger accounts or broader unified ranges.
- Fix applied: replaced row-limited dashboard math with full-scope SQL aggregates for category totals, debit/credit totals, heuristic signals, and
transaction_count.
[HIGH] apps/money-mirror/src/app/api/transactions/backfill-merchant-keys/route.ts — merchant backfill could loop forever on rows whose normalizeMerchantKey() returns null.
- Impact: the route could repeatedly re-read unresolved
merchant_key IS NULLrows until timeout. - Fix applied: changed the batch walk to a forward cursor over a stable ordered snapshot and skip updates when the normalized key is
null.
[MEDIUM] apps/money-mirror/src/components/ScopeBar.tsx — scope editor state drifted from the active URL scope.
- Impact: opening “Edit scope” on an existing unified view showed defaults and could silently overwrite the active range on re-apply.
- Fix applied: hydrate local scope form state from the parsed URL scope whenever the active scope changes.
- Touched-file lint passes:
src/lib/dashboard.ts,src/app/api/transactions/backfill-merchant-keys/route.ts,src/components/ScopeBar.tsx npm testpasses.npm run buildstarts cleanly but was not observed to completion within this review session, so build status is not claimed as a verified pass from this artifact alone.
- Files reviewed deeply in this pass: dashboard aggregation, transactions backfill, unified scope UI, review-stage telemetry/auth surfaces
- CRITICAL issues: 0
- HIGH issues: 2 (both fixed)
- MEDIUM issues: 1 (fixed)
- LOW issues: 0
- Codex verdict: No remaining critical/high blockers from this pass
- Workflow note: review stage remains open until the PM finishes any additional model review passes and ingests those findings before
/peer-review
Date: 2026-04-05 Reviewer: Claude Code (claude-sonnet-4-6) Scope: 26 files across Phase 3 T1–T4. Full-pass review of all new API routes, lib modules, and components. The three Codex fixes (H1 dashboard math, H2 backfill loop, M1 scope URL drift) were confirmed resolved before this pass began.
- Auth guard on every new route —
getSessionUser()called before any data access on all 9 new API routes ✓ - Ownership enforcement in
/api/transactions—WHERE user_id = ${user.id}in both single and multi-statement ownership checks ✓ - Input validation — date format (YYYY-MM-DD regex), UUID regex, category/type allowlists, search length cap, merchant_key length cap all enforced before DB ✓
- Atomic write preserved —
persist-statement.tsfail-closed pattern unchanged ✓ - Cursor-based backfill pagination —
AND id > cursor ORDER BY id ASC LIMIT 500terminates correctly ✓ - PostHog fire-and-forget — all server-side telemetry uses
.catch(() => {})in hot paths ✓ - No PostHog dual-emission —
transactions_filter_applied,transactions_view_opened,scope_changed,coaching_facts_expanded,merchant_rollup_clicked,coaching_narrative_*are server-side only ✓ - AbortController in TransactionsPanel — search and filter requests cancel in-flight fetches correctly ✓
- SessionStorage guard on view-opened — fires once per session, wrapped in try/catch ✓
- Gemini timeout at 9s —
TIMEOUT_MS = 9_000with note explaining AbortSignal limitation ✓ - Zod schema validation on Layer A facts —
buildLayerAFactsvalidates every row, full output validated vialayerAFactsSchema.parse()✓ - Citation validation —
validateCitedFactIdsensures Gemini only cites fact IDs that exist in Layer A ✓ - No
console.login production code (onlyconsole.error) ✓ - No
anytypes or@ts-ignorein reviewed files ✓ - No hardcoded secrets or API keys ✓
- Gemini JSON sanitization — strips markdown codeblocks before
JSON.parse✓ - Fan-out worker contract preserved — worker returns HTTP 502 on failure ✓
[HIGH] advisory-engine.ts:100 — wrong per-year food delivery annualization for multi-month scopes
- Impact:
food_delivery_paisa * 12on a 3-month unified scope produced a 36× annual estimate (3× inflated); users saw incorrect ₹ figures in advisory cards. - Fix applied: Dropped "That's ₹X per year" sentence entirely. Message now reads "…this period. Review how much of this was genuine necessity vs convenience."
[HIGH] DashboardClient.tsx:60-95 — loadDashboard had no AbortController
- Impact: Rapid scope changes (e.g. Last 30d → This month → Last month) could produce a race where a slow Gemini-enriched response from an old scope resolved last and overwrote
result,advisories, andcoachingFactswith stale data. - Fix applied: Added
dashboardAbortRef = useRef<AbortController | null>(null). EachloadDashboardcall aborts the previous in-flight fetch, creates a new controller, passes{ signal: ac.signal }tofetch, and returns early onAbortError.
[MEDIUM] Missing Sentry.captureException in 4 API catch blocks
- Files:
api/dashboard/route.ts,api/dashboard/advisories/route.ts,api/transactions/route.ts,api/transactions/backfill-merchant-keys/route.ts - Fix applied: Added
import * as Sentry from '@sentry/nextjs'andSentry.captureException(err)beforeconsole.errorin each catch block.
[MEDIUM] Advisory copy wrong for multi-month unified scopes
advisory-engine.ts:84— Subscription headline said₹X/mo in subscriptions(/moimplied monthly when scope may span months).advisory-engine.ts:111— NO_INVESTMENT headline said "No investments detected this month" ("this month" wrong for multi-month or arbitrary date ranges).- Fix applied: Headline →
₹X in subscriptions this period; NO_INVESTMENT → "No investments detected in this period".
[MEDIUM] N+1 UPDATE queries in backfill-merchant-keys/route.ts
- Impact: 500 normalizable rows in a batch = 500 individual
UPDATEround-trips. On large accounts this could exhaust the Vercel serverless timeout. - Fix applied: Accumulates
(ids[], keys[])arrays for the batch, then issues a singleUPDATE … SET merchant_key = data.key FROM unnest($ids::uuid[], $keys::text[]) AS data(id, key) WHERE …per batch page.
[MEDIUM] DashboardClient.tsx — 457 lines (exceeded 300-line limit)
- Fix applied: Extracted all state, effects, memos, and handler callbacks to
useDashboardState.tshook.DashboardClient.tsxis now 159 lines (shell + JSX only). RepeatedScopeBar + StatementFiltersblock extracted to a localscopeAndFiltersvariable.
[MEDIUM] TransactionsPanel.tsx — 342 lines (exceeded 300-line limit)
- Fix applied: Extracted filter UI to
TxnFilterBar.tsx(111L) and row renderer toTxnRow.tsx(61L).TransactionsPanel.tsxis now 166 lines.
[LOW] Inline <style> tag for @keyframes spin in DashboardClient.tsx
- Fix applied: Moved
@keyframes spin { to { transform: rotate(360deg); } }toglobals.css. Inline<style>tag removed.
npm test: 76/76 passed ✓npm run lint: 0 errors, 0 warnings ✓npm run build: clean build, TypeScript clean, all 23 routes compiled ✓
- Files reviewed: 26
- CRITICAL issues: 0
- HIGH issues fixed: 2
- MEDIUM issues fixed: 5
- LOW issues fixed: 1
- Verdict: Review passes. Ready for
/peer-review.