Feature: show automatic-categorization provenance on transactions
Author: joshiarnavCreated Sep 16, 2026Updated Sep 16, 2026
Summary
Sure already records who set a transaction's category: DataEnrichment(enrichable: transaction, attribute_name: "category_id", source: "ai" | "bayes" | "rule" | …) (Enrichable#log_enrichment). Since #3465 the auto-categorize flow first tries the local naive-Bayes classifier (source: "bayes") and only then the LLM (source: "ai"). None of this is visible in the Transactions UI, so after running auto-categorization users cannot tell which categories were chosen for them, or which of those they have since overridden.
Proposed UI
Transaction row (next to the existing Pending / Split / Possible duplicate pills, using DS::Pill):
AI— anai/bayescategory enrichment exists and its recorded value equals the currentcategory_id. Tooltip names the source: "Categorized by AI" / "Categorized by the local Bayes classifier".AI history— anai/bayescategory enrichment exists but the current category differs (user, rule or import changed it afterwards). Tooltip: "Was auto-categorized, then changed".- Nothing when there is no automatic category enrichment.
Transaction drawer (transactions/show): a compact read-only block in the Overview section, e.g.
Auto-categorization [AI]
Assigned "Groceries" by AI 2 hours ago.Auto-categorization [AI history]
Assigned "Groceries" by the Bayes classifier 3 days ago; the category was changed afterwards.Implementation notes
- Data source is the existing
DataEnrichmentrows; no new table. - Add a scoped association on
Transactionfor thecategory_idenrichments from automatic sources so the index canincludesit (no per-row query). Provenance logic lives in a small PORO/model method, not in ERB. - "Current" wins when both an
aiand abayesrow exist and either matches the current category; otherwise "history". Show the most recent matching row in the drawer. - Localize all strings; design tokens only.
Acceptance criteria
- Current auto-assignment →
AIpill; changed afterwards →AI history; no enrichment → no pill. - Drawer shows assigned category name, source, relative time, and whether it is still current.
- Category deleted since → drawer degrades gracefully ("a category that no longer exists").
- No N+1 on the Transactions index (query-count assertion).
- Semantics identical to the companion filter feature.
Source: we-promise/sure