Single-user, local-first web app: ingests expenses from Gmail, categorizes with an LLM, dashboard kills the spending blindspot.
Read in order: BUILD_BRIEF.md (what & why — decisions are FINAL) → HANDOVER_OPUS.md
(agent context) → LEARNING_LOG.md (the point of the whole exercise).
Node 22+ (node:sqlite, node:test, plain fetch) · SQLite file DB · vanilla HTML +
Chart.js (CDN) dashboard on 127.0.0.1 only · no framework, no build step, no TypeScript.
The only dependency allowed is a PDF text extractor (decided in build session).
npm test # node --test — scaffold ships green; keep it green
npm run dev # dashboard at http://127.0.0.1:8321
npm run eval# golden-set categorization eval (needs LLM_URL/LLM_MODEL/LLM_KEY + eval/golden.jsonl)EXPENSE_KEY env var (32-byte hex) encrypts secrets at rest — see src/secrets.js.
schema.sql single source of schema truth (app + tests)
src/adapters/ ONLY place that knows about Gmail (swap-ready boundary — D-001)
src/parse/ PURE parsers: alerts / statement PDF / receipts
src/normalize.js RawEmail contract + merchant/amount normalization
src/reconcile.js provisional↔statement matching; statement is truth (D-002)
src/categorize.js override → LLM (taxonomy-constrained) → confidence gate (D-003)
src/secrets.js AES-256-GCM encrypted token/password storage
eval/ golden-set eval — build & run BEFORE trusting any model