From 36bae023d65c6d6f5a80ec7d4532dc4d4a8a9aa5 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Tue, 14 Jul 2026 10:00:04 -0700 Subject: [PATCH] propose dashboard-overview, profile-avatars, rules-workbench changes --- .../changes/dashboard-overview/.openspec.yaml | 2 + openspec/changes/dashboard-overview/design.md | 29 +++++++ .../changes/dashboard-overview/proposal.md | 30 +++++++ .../specs/reporting/spec.md | 22 +++++ openspec/changes/dashboard-overview/tasks.md | 22 +++++ .../changes/profile-avatars/.openspec.yaml | 2 + openspec/changes/profile-avatars/design.md | 32 +++++++ openspec/changes/profile-avatars/proposal.md | 31 +++++++ .../profile-avatars/specs/auth/spec.md | 36 ++++++++ openspec/changes/profile-avatars/tasks.md | 17 ++++ .../changes/rules-workbench/.openspec.yaml | 2 + openspec/changes/rules-workbench/design.md | 46 ++++++++++ openspec/changes/rules-workbench/proposal.md | 36 ++++++++ .../specs/categorization/spec.md | 84 +++++++++++++++++++ .../rules-workbench/specs/reporting/spec.md | 22 +++++ openspec/changes/rules-workbench/tasks.md | 37 ++++++++ 16 files changed, 450 insertions(+) create mode 100644 openspec/changes/dashboard-overview/.openspec.yaml create mode 100644 openspec/changes/dashboard-overview/design.md create mode 100644 openspec/changes/dashboard-overview/proposal.md create mode 100644 openspec/changes/dashboard-overview/specs/reporting/spec.md create mode 100644 openspec/changes/dashboard-overview/tasks.md create mode 100644 openspec/changes/profile-avatars/.openspec.yaml create mode 100644 openspec/changes/profile-avatars/design.md create mode 100644 openspec/changes/profile-avatars/proposal.md create mode 100644 openspec/changes/profile-avatars/specs/auth/spec.md create mode 100644 openspec/changes/profile-avatars/tasks.md create mode 100644 openspec/changes/rules-workbench/.openspec.yaml create mode 100644 openspec/changes/rules-workbench/design.md create mode 100644 openspec/changes/rules-workbench/proposal.md create mode 100644 openspec/changes/rules-workbench/specs/categorization/spec.md create mode 100644 openspec/changes/rules-workbench/specs/reporting/spec.md create mode 100644 openspec/changes/rules-workbench/tasks.md diff --git a/openspec/changes/dashboard-overview/.openspec.yaml b/openspec/changes/dashboard-overview/.openspec.yaml new file mode 100644 index 0000000..64105fc --- /dev/null +++ b/openspec/changes/dashboard-overview/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-14 diff --git a/openspec/changes/dashboard-overview/design.md b/openspec/changes/dashboard-overview/design.md new file mode 100644 index 0000000..0a46e40 --- /dev/null +++ b/openspec/changes/dashboard-overview/design.md @@ -0,0 +1,29 @@ +# Dashboard Overview — Design + +## Context + +The dashboard (`src/routes/(app)/+page.svelte`) shows advisory banners, account balances, and last-sync time. Everything the new overview needs already exists in services: `reports.ts` computes monthly income/expense by category, `ledger.ts` lists transactions with filters and a limit, and `SpendingPie.svelte` renders the category pie on the Reports page. Amounts are integer cents; pending transactions have `posted = NULL` and an effective timestamp fallback (`posted → transacted_at → created_at`) already defined in `ledger.ts`. + +## Goals / Non-Goals + +**Goals:** +- Month-to-date glance on the dashboard: spend pie, income/expense totals, pending stats, recent transactions. +- Maximum reuse of existing aggregation and components; no schema changes. + +**Non-Goals:** +- Month selection on the dashboard (Reports already does that). +- Dashboard configurability/widgets. +- Any change to report-page behavior. + +## Decisions + +- **D1 — Reuse the monthly report aggregation for the current month.** The dashboard loader calls the same reports service used by the Reports page with the current `YYYY-MM`. This inherits the established semantics for free: posted-only, non-hidden accounts, transfer-kind excluded, uncategorized surfaced as its own line. No second aggregation path to keep consistent. +- **D2 — Pending stats are current-month, cross-account.** Count and sum of `pending = 1`, non-removed transactions of non-hidden accounts whose effective timestamp falls in the current month. Small addition to the reports (or ledger) service; uses the existing `monthRange` + effective-timestamp expression. +- **D3 — Recent transactions via `listLedger` with a small limit.** One call, `limit: 8` (design constant, not user-configurable), no filters — the existing sort (pending first, then effective date desc) is exactly the "what just happened" ordering wanted here. Each row links to the ledger. This also means any future display-name overlay in the ledger service is inherited automatically. +- **D4 — Pie shows expenses only; income and expenses get stat tiles.** Mixing income into a spend pie misreads; the pie reuses `SpendingPie` fed with the expense side of the month aggregation, uncategorized included as a slice. + +## Risks / Trade-offs + +- [Sparse early-month data makes the pie trivial] → Acceptable; sections render empty states ("No spending yet this month") rather than hiding, so the layout is stable. +- [Dashboard loader gains 2–3 queries] → All are indexed single-month scans on a personal-scale SQLite database; negligible. +- [`SpendingPie` may need light parameterization (size/legend) for dashboard density] → Prefer props over a forked component. diff --git a/openspec/changes/dashboard-overview/proposal.md b/openspec/changes/dashboard-overview/proposal.md new file mode 100644 index 0000000..6605509 --- /dev/null +++ b/openspec/changes/dashboard-overview/proposal.md @@ -0,0 +1,30 @@ +# Dashboard Overview + +## Why + +The dashboard currently shows account balances, sync status, and advisory banners — it answers "what do I have?" but not "what's happening this month?". The month-to-date picture (spend by category, money in/out, pending activity, latest transactions) already exists in the data model and reporting services but requires visiting Reports and Ledger separately. + +## What Changes + +- Add a current-month spending pie chart to the dashboard, reusing the existing `SpendingPie` component and monthly report aggregation. +- Add month-to-date stat tiles: total income, total expenses, and pending transaction count + amount (all scoped to the current month, posted-vs-pending per existing reporting semantics). +- Add a "recent transactions" list showing a handful of the latest transactions with links into the ledger. +- No schema changes; no new services beyond a pending-stats aggregate. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `reporting`: New requirement for a dashboard month-to-date overview (spend breakdown, income/expense totals, pending stats, recent transactions). Existing report-page requirements are unchanged. + +## Impact + +- `src/routes/(app)/+page.server.ts` / `+page.svelte` — dashboard loader and layout gain the new sections. +- `src/lib/server/services/reports.ts` — reuse of monthly aggregation; small addition for pending count/amount. +- `src/lib/server/services/ledger.ts` — reuse of `listLedger` with a small limit for recent transactions. +- `src/lib/components/SpendingPie.svelte` — reused as-is (or lightly parameterized). +- No migrations, no API changes, no new dependencies. diff --git a/openspec/changes/dashboard-overview/specs/reporting/spec.md b/openspec/changes/dashboard-overview/specs/reporting/spec.md new file mode 100644 index 0000000..3f766cd --- /dev/null +++ b/openspec/changes/dashboard-overview/specs/reporting/spec.md @@ -0,0 +1,22 @@ +# reporting — Delta for dashboard-overview + +## ADDED Requirements + +### Requirement: Dashboard month-to-date overview +The system SHALL display on the dashboard, scoped to the current calendar month and to non-hidden accounts: (1) a spending pie chart of expense totals by category computed from posted transactions with the same semantics as the monthly report (transfer-kind categories excluded, uncategorized shown as its own slice); (2) total income and total expenses from posted transactions; (3) the count and total amount of pending transactions whose effective date falls in the current month; and (4) a small fixed number of the most recent transactions, each linking to the transaction ledger. Each section SHALL render a clear empty state when the month has no qualifying data. + +#### Scenario: Current month at a glance +- **WHEN** a user views the dashboard during a month with posted transactions +- **THEN** the spend pie reflects that month's expense totals by category, and stat tiles show the month's total income and total expenses in integer-cent arithmetic + +#### Scenario: Pending stats +- **WHEN** the current month contains pending transactions +- **THEN** the dashboard shows their count and summed amount, distinct from posted income/expense totals + +#### Scenario: Recent transactions +- **WHEN** a user views the dashboard +- **THEN** the most recent transactions (pending first, then by effective date descending) are listed with date, account, displayed name, amount, and a link to the full ledger + +#### Scenario: Empty month +- **WHEN** the current month has no transactions +- **THEN** the overview sections show empty states rather than being hidden or erroring, and account balances remain visible diff --git a/openspec/changes/dashboard-overview/tasks.md b/openspec/changes/dashboard-overview/tasks.md new file mode 100644 index 0000000..e08623e --- /dev/null +++ b/openspec/changes/dashboard-overview/tasks.md @@ -0,0 +1,22 @@ +# Tasks — dashboard-overview + +## 1. Services + +- [ ] 1.1 Add a pending-stats aggregate (count + summed cents for pending, non-removed transactions of non-hidden accounts within a month) to the reports service, reusing `monthRange` and the effective-timestamp expression; unit tests alongside existing reports tests +- [ ] 1.2 Confirm the monthly report aggregation and `listLedger` cover the dashboard's needs (current-month expense-by-category incl. uncategorized; recent-8 listing) — extend only if a gap emerges, with tests + +## 2. Dashboard loader + +- [ ] 2.1 Extend `src/routes/(app)/+page.server.ts` to load current-month report totals, pending stats, and recent transactions (`listLedger` with `limit: 8`) alongside the existing data + +## 3. Dashboard UI + +- [ ] 3.1 Add stat tiles (month income, month expenses, pending count + amount) with `tnum` formatting and empty-state handling +- [ ] 3.2 Add the current-month spending pie reusing `SpendingPie.svelte` (parameterize size/legend via props if the dashboard needs a denser variant); expense categories plus an uncategorized slice; empty state when no spending +- [ ] 3.3 Add the recent-transactions list (date, account, displayed name, amount; pending indicated) linking to `/ledger` +- [ ] 3.4 Integrate the new sections into the existing dashboard layout without disturbing banners, balances, or last-sync display + +## 4. Verification + +- [ ] 4.1 Service tests pass (`deno` test suite) including new pending-stats tests +- [ ] 4.2 Verify in the browser: seeded current-month data renders pie/tiles/recents correctly; an empty month renders stable empty states diff --git a/openspec/changes/profile-avatars/.openspec.yaml b/openspec/changes/profile-avatars/.openspec.yaml new file mode 100644 index 0000000..64105fc --- /dev/null +++ b/openspec/changes/profile-avatars/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-14 diff --git a/openspec/changes/profile-avatars/design.md b/openspec/changes/profile-avatars/design.md new file mode 100644 index 0000000..8bf86ad --- /dev/null +++ b/openspec/changes/profile-avatars/design.md @@ -0,0 +1,32 @@ +# Profile Avatars — Design + +## Context + +Users are stored as `{did, handle}`; the handle is re-upserted on every login, so it never goes stale for active users. Handles appear in two places: the nav "who" chip (`(app)/+layout.svelte`) and categorization provenance (`actorHandle` in ledger history). The auth spec deliberately forbids the app from touching the user's PDS after login — the OAuth session exists solely to authenticate. Every avatar the app could ever need belongs to an allowlisted app user; there is no arbitrary-profile display. + +## Goals / Non-Goals + +**Goals:** +- Avatars in the nav chip and categorization provenance, with the handle/DID still discoverable. +- Zero expansion of the application server's data access; zero schema changes. + +**Non-Goals:** +- Server-side avatar caching or blob storage. +- Profile data sync (display names, bios). +- Avatars for non-user identities. + +## Decisions + +- **D1 — Resolve avatars in the browser via a public redirect service (atp.pics), keyed by handle.** `` returns a 302 to a cached, transformed image. Alternative considered: server-side `getProfile` against the public AppView at login, caching the CDN blob URL on the user row. Rejected because it adds resolution/refresh code and a stored URL that breaks when the user changes their avatar (CID-based), whereas handle-keyed resolution stays fresh via the existing login upsert. The browser fetching a public image also keeps the auth spec's story cleanest: the application server fetches nothing. atp.pics is operated by this project's own user, so the third-party-dependency concern is minimal; the spec stays mechanism-agnostic so the URL scheme can be swapped in one component. +- **D2 — One shared `Avatar` component with text fallback.** Renders the image; on load error or missing handle it falls back to the current textual presentation (handle text in provenance, handle chip in nav). Provenance must never become an empty circle. +- **D3 — Provenance popover works for hover, focus, and touch.** The avatar in categorization history is a focusable element; hover or keyboard focus reveals the handle, and the DID is exposed via accessible label/title (matching today's `title={did}` behavior on the nav chip). + +## Risks / Trade-offs + +- [atp.pics outage → broken avatars] → D2 fallback restores today's text UI; no functionality is lost. +- [Browser requests reveal handle lookups to the avatar service] → Public data, low sensitivity, and the operator is the app's own user; acceptable. +- [Popovers on touch devices are awkward] → Tap toggles the popover (focus-based), and the DID/handle remain in accessible attributes regardless. + +## Migration Plan + +Pure UI addition; deploy normally. Rollback = revert. No data or config changes. diff --git a/openspec/changes/profile-avatars/proposal.md b/openspec/changes/profile-avatars/proposal.md new file mode 100644 index 0000000..6cff6de --- /dev/null +++ b/openspec/changes/profile-avatars/proposal.md @@ -0,0 +1,31 @@ +# Profile Avatars + +## Why + +Users are currently represented by bare handle text (nav chip, categorization provenance). ATProto identities come with public profile pictures; showing them makes provenance glanceable ("who categorized this?") and the app feel personal — without expanding the app's access to user data. + +## What Changes + +- Display the logged-in user's avatar in the nav "who" chip alongside their handle. +- Replace the handle text in categorization history / provenance displays with the actor's avatar; the handle (and DID) appear in a popover on hover/focus, so provenance detail is preserved and keyboard/touch accessible. +- Avatars are resolved from **public** ATProto data by the browser (via an avatar service such as atp.pics, keyed by handle); the application server never fetches profile data and never uses its OAuth session to read the user's PDS. +- Graceful degradation: when an avatar fails to load or a user has none, fall back to the current text presentation. +- Amend the `auth` spec's post-login data-access requirement to explicitly permit public, unauthenticated profile-picture resolution while continuing to forbid authenticated PDS access. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `auth`: The "no PDS access after authentication" requirement is clarified — authenticated PDS access remains forbidden; rendering avatars from public ATProto data is allowed. A new requirement covers avatar display and fallback behavior. + +## Impact + +- `src/routes/(app)/+layout.svelte` — nav chip gains an avatar image. +- `src/routes/(app)/ledger/+page.svelte` — provenance actor display becomes avatar + popover. +- Possibly a small shared `Avatar` component in `src/lib/components/`. +- No schema changes (avatars are keyed by handle, which `users` already stores and refreshes at login); no server-side fetching; no new dependencies. +- External: relies on a public avatar-resolution endpoint (atp.pics) at image-load time; the design records this choice and the fallback contract. diff --git a/openspec/changes/profile-avatars/specs/auth/spec.md b/openspec/changes/profile-avatars/specs/auth/spec.md new file mode 100644 index 0000000..d44eef0 --- /dev/null +++ b/openspec/changes/profile-avatars/specs/auth/spec.md @@ -0,0 +1,36 @@ +# auth — Delta for profile-avatars + +## MODIFIED Requirements + +### Requirement: ATProto OAuth login +The system SHALL authenticate users via AT Protocol OAuth using handle-based login. The user enters their handle (or DID); the system resolves it, performs the OAuth authorization flow (PAR, PKCE, DPoP) against the user's authorization server, and establishes an application session on success. The system SHALL request only the `atproto` scope and SHALL NOT make authenticated requests to the user's PDS after authentication. Resolving and rendering profile pictures from public ATProto profile data — without using the OAuth session or any application credential — is permitted. + +#### Scenario: Successful login with allowlisted handle +- **WHEN** a user whose DID is in the allowlist completes the OAuth flow +- **THEN** the system creates an application session and sets an HTTP-only, Secure session cookie +- **AND** the user is redirected to the dashboard + +#### Scenario: Login with unknown handle +- **WHEN** a user submits a handle that cannot be resolved to a DID +- **THEN** the system shows an error on the login page without starting the OAuth flow + +## ADDED Requirements + +### Requirement: Profile picture display +The system SHALL display user profile pictures resolved from public ATProto profile data, keyed by the user's current handle and fetched by the browser without application credentials. Avatars SHALL appear in the navigation user chip alongside the handle, and in categorization provenance displays in place of the handle text, where the handle SHALL be revealed in a popover on hover or keyboard focus and the DID SHALL remain available via an accessible label. When an avatar cannot be loaded or does not exist, the UI SHALL fall back to the textual handle presentation. + +#### Scenario: Nav chip shows avatar +- **WHEN** an authenticated user views any app page +- **THEN** the navigation user chip shows their avatar together with their handle + +#### Scenario: Provenance avatar with popover +- **WHEN** a user hovers over or keyboard-focuses the actor avatar in a transaction's categorization history +- **THEN** a popover reveals the actor's handle, and the DID is exposed via an accessible label + +#### Scenario: Avatar unavailable +- **WHEN** an avatar image fails to load or the actor has no profile picture +- **THEN** the UI renders the actor's handle as text, and no provenance information is lost + +#### Scenario: Server fetches no profile data +- **WHEN** pages containing avatars are rendered +- **THEN** all profile-image requests originate from the browser against public endpoints, and the application server performs no profile-data requests diff --git a/openspec/changes/profile-avatars/tasks.md b/openspec/changes/profile-avatars/tasks.md new file mode 100644 index 0000000..1a6541d --- /dev/null +++ b/openspec/changes/profile-avatars/tasks.md @@ -0,0 +1,17 @@ +# Tasks — profile-avatars + +## 1. Avatar component + +- [ ] 1.1 Create `src/lib/components/Avatar.svelte`: renders the avatar image for a handle (resolution URL per design D1, size prop), falling back to the textual handle presentation on load error or missing handle +- [ ] 1.2 Add the popover variant for provenance use: focusable trigger, popover with handle on hover/focus (tap-toggle on touch), DID via accessible label/title + +## 2. Integration + +- [ ] 2.1 Nav "who" chip in `src/routes/(app)/+layout.svelte`: avatar beside the handle, keeping the existing `title={did}` +- [ ] 2.2 Categorization provenance in `src/routes/(app)/ledger/+page.svelte`: replace actor handle text with the popover avatar (fallback preserves today's text) + +## 3. Verification + +- [ ] 3.1 Verify in the browser: avatar renders in nav and provenance history; popover works via mouse hover and keyboard focus; DID present in accessible attributes +- [ ] 3.2 Verify fallback: with the avatar URL unreachable (blocked/bogus handle), text presentation returns and no layout breaks +- [ ] 3.3 Confirm no server-side profile fetches were introduced (avatar requests appear only in browser network log) diff --git a/openspec/changes/rules-workbench/.openspec.yaml b/openspec/changes/rules-workbench/.openspec.yaml new file mode 100644 index 0000000..64105fc --- /dev/null +++ b/openspec/changes/rules-workbench/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-14 diff --git a/openspec/changes/rules-workbench/design.md b/openspec/changes/rules-workbench/design.md new file mode 100644 index 0000000..2e2a0ee --- /dev/null +++ b/openspec/changes/rules-workbench/design.md @@ -0,0 +1,46 @@ +# Rules Workbench — Design + +## Context + +Rules today are `{matchType: exact|contains, pattern, categoryId, createdByDid, active}` (`src/lib/server/services/rules.ts`), matched case-insensitively against description/payee/memo with deterministic precedence (exact > contains > longer > newer). Application flows through the append-only `categorization_events` log with `rule_id` recorded, and the invariant "rules never overwrite a human decision" is enforced structurally (`applyRulesToUncategorized` only touches uncategorized transactions whose latest event is not manual). Management UI is a small form in Settings. `countRuleMatches` probes prospective matches over uncategorized transactions only. The ledger (`ledger.ts`) supports structured filters (account/category/month/pending) but no free text, and its query already joins the categorizing rule for provenance display. + +## Goals / Non-Goals + +**Goals:** +- Rules as a first-class surface: dedicated page, search, per-rule inspection. +- In-context rule creation from any ledger transaction or search. +- Amount as an exact-match conjunct; display-name rename as a read-time overlay. +- Ledger free-text search that matches what the user sees. +- Lay the substrate for future subscription tracking without building it. + +**Non-Goals:** +- Subscription tracking itself (schedules, expected-charge alerts). +- Amount ranges/tolerances — deliberately rejected for now: Copilot's range UX proved noisy and confusing; exact-with-visibility is the bet. +- Manual per-transaction rename (a future feature; would take precedence over rule renames, purely additively). +- Rule editing beyond today's enable/disable (unchanged scope), reordering, or manual priorities. + +## Decisions + +- **D1 — Amount is a conjunct, not a match type.** `rules.amount_cents` (nullable INTEGER, signed). A rule matches when its text pattern matches (unchanged semantics) AND, if `amount_cents` is set, the transaction's amount equals it exactly. Rationale: the driving case is "payee contains NETFLIX and amount = −1549" — amount narrows a text match. Whether pattern-less amount-only rules are allowed: no — a text pattern remains required, keeping every rule human-readable and avoiding accidental broad matches on common amounts. +- **D2 — Precedence: amount-constrained wins first.** New tiebreak order: amount-constrained beats unconstrained, then exact beats contains, then longer pattern, then newer rule. An amount conjunct is a strictly stronger statement of intent than any text-only refinement, so it outranks exactness. Precedence changes affect only future/retroactive applications; historical events are immutable. +- **D3 — Rename is a read-time overlay sourced from the categorizing rule.** `rules.display_name` (nullable TEXT). The ledger query already joins the rule referenced by the transaction's latest event; the overlay is `COALESCE(rule.display_name, payee-or-description)` in that same join — no per-transaction storage, no rename events, and editing a rule re-renames everywhere instantly. Consequence (accepted): the rename applies to transactions *this rule categorized*; a manually recategorized transaction loses the overlay along with the rule's categorization. That keeps rename provenance identical to category provenance and avoids a second live-matching pass at read time. Raw description/payee stay canonical, stored untouched, and visible in the transaction detail view. +- **D4 — Match health is derived, not stored.** The rule detail view derives from the event log and live matching: last-fired time, fire counts by month, and — the subscription-tracking seed — recent transactions that matched the rule's text pattern but failed its amount conjunct ("pattern hit, amount differs"), which is exactly the price-change signal. No new tables; all queries are per-rule and on demand. +- **D5 — Rule inspection separates "did" from "would".** *Did:* transactions whose events reference the rule (historical fact, from `categorization_events.rule_id`). *Would:* a live probe across all non-removed transactions of non-hidden accounts (widened from today's uncategorized-only `countRuleMatches`), with each hit labeled by its current status: uncategorized / categorized by this rule / categorized by another rule / manual. The probe never mutates anything. +- **D6 — Slide-over tray for creation, one component, two entry points.** From a ledger row: pre-filled with payee (preferred) or description as pattern, `contains` match, the transaction's amount (opt-in checkbox — default off so text-only rules stay the norm), and optional display name. From ledger search: the search term becomes the pattern candidate. On save, the existing retroactive-apply offer semantics run unchanged. The tray posts to the rules route's actions; the ledger page never grows rule logic. +- **D7 — Ledger search is `LIKE` over raw fields plus overlay name.** Case-insensitive substring over description, payee, memo, and the effective display name (via the existing provenance rule join) — "search matches what the ledger displays." Composable with all existing filters as one more `WHERE` conjunct. FTS5 rejected: personal-scale row counts make `LIKE` fine, and rules match with the same substring semantics, keeping search results an honest preview of rule hits. +- **D8 — `/rules` page replaces the Settings section.** Route `src/routes/(app)/rules/` with list + search (over pattern, display name, category) and per-rule detail; nav gains a Rules link; Settings keeps connections and categories. Rule search is server-side for symmetry with ledger search, though the row count would permit client-side. + +## Risks / Trade-offs + +- [Exact amount silently stops matching when a subscription price changes] → Deliberate: the transaction lands uncategorized (already surfaced by reporting), and D4's "pattern hit, amount differs" view names the cause. Revisit ranges later if this proves noisy in practice. +- [Precedence change reorders winners among existing overlapping rules] → Only two rules exist in practice today and events are immutable; retroactive application remains opt-in. +- [Overlay hides raw descriptions users might search by memory] → D7 searches raw fields *and* overlay, so both vocabularies find the transaction. +- [Widened would-hit probe scans all transactions] → Per-rule, on-demand, indexed personal-scale SQLite; cap the displayed list and show counts. + +## Migration Plan + +One migration: `ALTER TABLE rules ADD COLUMN amount_cents INTEGER; ALTER TABLE rules ADD COLUMN display_name TEXT;` — both nullable, existing rows keep exact behavior. UI moves are code-only. Rollback = revert code; the extra columns are inert. + +## Open Questions + +None blocking. Post-MVP candidates: amount ranges (revisit), rule editing in place, manual rename overlay outranking rule renames. diff --git a/openspec/changes/rules-workbench/proposal.md b/openspec/changes/rules-workbench/proposal.md new file mode 100644 index 0000000..ff40e8a --- /dev/null +++ b/openspec/changes/rules-workbench/proposal.md @@ -0,0 +1,36 @@ +# Rules Workbench + +## Why + +Categorization rules are currently a small form buried in Settings: creation requires retyping patterns by hand, there is no way to see what a rule has done or would do, and rules can only assign categories. Rules are becoming the primary automation surface of the app — they deserve a dedicated page, an in-context creation flow from the ledger, and richer matching (amount) and effects (display-name rename) that pave the way for subscription tracking. + +## What Changes + +- **Rules page**: move rule management out of Settings into a dedicated `/rules` page with rule search and per-rule inspection. +- **Rule inspection**: a rule detail view shows *what the rule did* (transactions it categorized, from the append-only event log) and *what it would hit* (live match preview across all non-removed transactions, not just uncategorized ones). +- **Create rule from transaction**: a slide-over tray, openable from any ledger transaction (and from ledger search results), pre-filled with the transaction's pattern candidates and amount; rules no longer require a trip to Settings. +- **Amount matching**: a rule may additionally constrain on an exact amount (integer cents, signed). Amount is a conjunct on top of the text pattern, not a new match type. Match-health visibility (e.g., "this rule stopped matching", "amount changed") is surfaced on the rule detail view rather than via tolerant/range matching. +- **Rename effect**: a rule may set a display name for matched transactions. This is a read-time presentation overlay — the raw description/payee remain canonical and stored; no per-transaction rename events are recorded, and editing the rule re-renames everywhere instantly. +- **Precedence**: amount-constrained rules are more specific than otherwise-equal unconstrained rules and win ties. +- **Ledger search**: free-text search over the ledger (description, payee, memo, and effective rule-applied display name), composable with existing account/category/month/pending filters. Search results feed the create-rule slide-over with the search term as the pattern candidate. +- **BREAKING** (UI only): the rules section is removed from Settings; Settings retains connection and category management. + +## Capabilities + +### New Capabilities + +None — all changes extend existing capabilities. + +### Modified Capabilities + +- `categorization`: rules gain an optional exact-amount conjunct, an optional display-name effect (read-time overlay), extended precedence, a dedicated management page with search and inspection, and an in-ledger creation flow. +- `reporting`: the transaction ledger gains free-text search (matching displayed names, not just raw fields), and displayed transaction names reflect rule rename overlays. + +## Impact + +- Schema: `rules` table gains `amount_cents` (nullable) and `display_name` (nullable) columns — one migration; existing rows unaffected. +- `src/lib/server/services/rules.ts` — matching, precedence, prospective-probe widening, match-health queries. +- `src/lib/server/services/ledger.ts` — search filter, display-name overlay in query results. +- New route `src/routes/(app)/rules/` (page + server loader); `settings/+page.svelte` sheds its rules section; `ledger/+page.svelte` gains search input and the slide-over tray (new component). +- Nav gains a Rules link (`(app)/+layout.svelte`). +- Future-facing: exact-amount rules plus event-log history are the substrate for later subscription tracking (not in scope here). diff --git a/openspec/changes/rules-workbench/specs/categorization/spec.md b/openspec/changes/rules-workbench/specs/categorization/spec.md new file mode 100644 index 0000000..3a7e64b --- /dev/null +++ b/openspec/changes/rules-workbench/specs/categorization/spec.md @@ -0,0 +1,84 @@ +# categorization — Delta for rules-workbench + +## MODIFIED Requirements + +### Requirement: Rule-based auto-categorization +The system SHALL support categorization rules with match types `exact` and `contains`, matched case-insensitively against the raw transaction description and, when the provider supplies them, the payee and memo fields (a rule fires if any of these matches). A rule MAY additionally specify an exact amount in signed integer cents; such a rule fires only when the text pattern matches AND the transaction's amount equals the rule's amount exactly. A text pattern is always required — amount-only rules SHALL NOT be permitted. Rules record their creator's DID and creation time. When multiple rules match one transaction, precedence SHALL be deterministic: amount-constrained beats unconstrained, then `exact` beats `contains`, then longer pattern beats shorter, then newer rule beats older. Rule application SHALL append a `rule` event recording the winning rule's id. + +#### Scenario: Rule fires on new transaction at sync +- **WHEN** a sync ingests an uncategorized transaction whose description matches an active rule +- **THEN** the winning rule's category is applied via a `rule` event referencing that rule + +#### Scenario: Precedence between overlapping rules +- **WHEN** a description matches both `contains "AMAZON"` and `contains "AMAZON PRIME"` +- **THEN** the longer pattern's rule wins and the fired rule id is recorded on the event + +#### Scenario: Rule matches the payee or memo field +- **WHEN** a transaction's description is a terse bank label but its provider-supplied payee or memo matches an active rule +- **THEN** the rule fires exactly as if the description had matched + +#### Scenario: Amount conjunct narrows a match +- **WHEN** a transaction matches a rule's text pattern but its amount differs from the rule's specified amount +- **THEN** that rule does not fire for the transaction + +#### Scenario: Amount-constrained rule outranks unconstrained +- **WHEN** a transaction matches both a `contains` rule with a matching amount constraint and an `exact` rule with no amount constraint +- **THEN** the amount-constrained rule wins and its id is recorded on the event + +## ADDED Requirements + +### Requirement: Rule display-name overlay +A rule MAY specify a display name. Wherever the system displays a transaction whose current category was assigned by that rule, it SHALL show the rule's display name in place of the raw payee/description. The overlay SHALL be applied at read time: the stored transaction fields remain canonical and unmodified, no per-transaction rename events are recorded, and changing or deactivating the rule's display name SHALL be reflected everywhere immediately. The raw description SHALL remain accessible in the transaction's detail view. + +#### Scenario: Renamed transaction display +- **WHEN** a rule with display name "Netflix" categorized a transaction described "NETFLIX.COM 866-579-7172" +- **THEN** ledger and dashboard listings show "Netflix", and the transaction's detail view still shows the raw description + +#### Scenario: Rule edit re-renames instantly +- **WHEN** a user changes a rule's display name +- **THEN** every transaction currently categorized by that rule reflects the new name on next render, with no data migration or new events + +#### Scenario: Manual recategorization sheds the overlay +- **WHEN** a user manually recategorizes a transaction that a renaming rule had categorized +- **THEN** the transaction's displayed name reverts to its raw payee/description, consistent with the rule no longer being its provenance + +### Requirement: Rule management page +The system SHALL provide a dedicated rules page, linked from the primary navigation, replacing rule management in Settings. The page SHALL list all rules with their pattern, match type, amount constraint, display name, target category, and active state, and SHALL provide rule search over these fields. Rule creation, enable, and disable SHALL be available from this page with unchanged semantics (including the retroactive-apply offer on creation). + +#### Scenario: Rules relocated +- **WHEN** a user opens Settings +- **THEN** rule management is no longer present, and the rules page is reachable from the primary navigation + +#### Scenario: Rule search +- **WHEN** a user searches the rules page for "netflix" +- **THEN** rules whose pattern, display name, or category name match are listed + +### Requirement: Rule inspection +The system SHALL provide a per-rule detail view showing: (1) what the rule did — transactions whose categorization events reference the rule, from the append-only log; (2) what the rule would hit — a live, read-only probe across all non-removed transactions of non-hidden accounts, each hit labeled with its current categorization status (uncategorized, this rule, another rule, or manual); and (3) match health — when the rule last fired, and for amount-constrained rules, recent transactions that matched the text pattern but not the amount. The probe SHALL NOT modify any transaction or event. + +#### Scenario: Historical fires +- **WHEN** a user opens a rule's detail view +- **THEN** transactions the rule categorized are listed from the event log, including ones later recategorized + +#### Scenario: Prospective matches labeled +- **WHEN** a user views a rule's would-hit preview +- **THEN** matching transactions are shown with their current status, and none are modified + +#### Scenario: Amount drift surfaced +- **WHEN** an amount-constrained rule's text pattern matches recent transactions at a different amount +- **THEN** the detail view surfaces those transactions as pattern-hit/amount-miss, indicating a probable price change + +### Requirement: Rule creation from the ledger +The system SHALL allow creating a rule from any ledger transaction via a slide-over tray, without leaving the ledger. The tray SHALL be pre-filled from the transaction: its payee (preferred) or description as the pattern, with the transaction's amount available as an opt-in constraint and an optional display name. When opened from an active ledger search, the search term SHALL be offered as the pattern. Saving SHALL create the rule with unchanged creation semantics, including the offer to retroactively apply it to matching uncategorized transactions. + +#### Scenario: Create rule from a transaction +- **WHEN** a user opens the rule tray from a ledger row and saves +- **THEN** a rule is created pre-filled from that transaction without navigating away, and the retroactive-apply offer is presented + +#### Scenario: Amount opt-in +- **WHEN** a user opens the rule tray from a transaction +- **THEN** the amount constraint is offered but not enabled by default + +#### Scenario: Search term becomes pattern +- **WHEN** a user opens the rule tray while a ledger text search is active +- **THEN** the search term is pre-filled as the rule pattern diff --git a/openspec/changes/rules-workbench/specs/reporting/spec.md b/openspec/changes/rules-workbench/specs/reporting/spec.md new file mode 100644 index 0000000..cc2121b --- /dev/null +++ b/openspec/changes/rules-workbench/specs/reporting/spec.md @@ -0,0 +1,22 @@ +# reporting — Delta for rules-workbench + +## MODIFIED Requirements + +### Requirement: Transaction ledger +The system SHALL provide a ledger view of transactions filterable by account, category (including uncategorized), month, and pending status, and searchable by free text. Text search SHALL match case-insensitively against the raw description, payee, and memo fields and against the effective displayed name produced by rule display-name overlays, so search finds what the user sees. Search SHALL compose with all structured filters. The ledger shows date, account, displayed name (rule overlay applied when present), amount, category, and a provenance indicator (rule vs. person). The ledger is the surface for manual categorization and for creating rules from transactions. + +#### Scenario: Filter to uncategorized +- **WHEN** a user filters the ledger to uncategorized transactions +- **THEN** only transactions with no current category are listed, ready for manual assignment + +#### Scenario: Provenance indicator +- **WHEN** a categorized transaction is displayed +- **THEN** the row indicates whether the category came from a rule, a person (with their identity), or reconciliation carry-forward + +#### Scenario: Search matches a renamed transaction +- **WHEN** a rule renames "ACH TRANSFER 4417" to display as "Rent" and a user searches the ledger for "rent" +- **THEN** the transaction is found, even though no raw field contains "rent" + +#### Scenario: Search composes with filters +- **WHEN** a user searches for "netflix" with a month filter active +- **THEN** only that month's transactions matching the text (raw fields or displayed name) are listed diff --git a/openspec/changes/rules-workbench/tasks.md b/openspec/changes/rules-workbench/tasks.md new file mode 100644 index 0000000..74e5466 --- /dev/null +++ b/openspec/changes/rules-workbench/tasks.md @@ -0,0 +1,37 @@ +# Tasks — rules-workbench + +## 1. Schema & matching + +- [ ] 1.1 Migration: add nullable `amount_cents` (INTEGER) and `display_name` (TEXT) to `rules` +- [ ] 1.2 Extend the `Rule` model and `rules.ts` CRUD for the new fields; require a text pattern (reject amount-only rules) +- [ ] 1.3 Amount conjunct in `matches()` and precedence extension in `findWinningRule()` (amount-constrained > exact > longer > newer); unit tests for conjunct misses and the new tiebreak + +## 2. Ledger service + +- [ ] 2.1 Display-name overlay in `listLedger`: effective displayed name from the categorizing rule's `display_name` via the existing provenance join, raw fields still returned; tests incl. manual-recategorization shedding the overlay +- [ ] 2.2 Free-text search filter (`q`): case-insensitive LIKE over description/payee/memo and the overlay display name, composed with existing filters; tests incl. renamed-transaction hit + +## 3. Rule inspection queries + +- [ ] 3.1 "Did" query: transactions whose categorization events reference a rule id (including superseded assignments) +- [ ] 3.2 "Would" probe: widen prospective matching to all non-removed transactions of non-hidden accounts, labeling each hit's current status (uncategorized / this rule / other rule / manual); read-only, with tests +- [ ] 3.3 Match-health queries: last-fired time, per-month fire counts, and pattern-hit/amount-miss listing for amount-constrained rules; tests + +## 4. Rules page + +- [ ] 4.1 Route `src/routes/(app)/rules/`: list with search (pattern, display name, category), create/enable/disable actions moved from Settings (retroactive-apply offer unchanged) +- [ ] 4.2 Rule detail view: did / would / match-health sections with capped lists and counts +- [ ] 4.3 Add Rules to primary nav in `(app)/+layout.svelte`; remove the rules section from `settings/+page.svelte` (and its server actions), leaving connections and categories + +## 5. Ledger UI + +- [ ] 5.1 Search input on the ledger page wired to the `q` filter, composing with existing filter controls and preserved in the query string +- [ ] 5.2 Displayed-name overlay in ledger rows; raw description visible in the expanded detail view +- [ ] 5.3 Slide-over tray component for rule creation: prefill from transaction (payee-preferred pattern, opt-in amount, optional display name) or from the active search term; posts to rules actions; retroactive-apply offer on save; scroll position preserved +- [ ] 5.4 "Create rule" affordance on ledger rows (and visible while a search is active) + +## 6. Verification + +- [ ] 6.1 Full test suite passes; new tests cover conjunct matching, precedence, overlay, search, probes, and match health +- [ ] 6.2 Browser walkthrough: search → create rule from tray → retroactive apply → renamed rows appear → rule detail shows did/would/health → search finds the renamed transaction +- [ ] 6.3 Confirm Settings retains connections/categories only and existing rules behave identically post-migration -- 2.51.2