diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..5fff52f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,24 @@ +# moneyinsight codebase context + +Flutter personal finance app. Riverpod state management. Drift (SQLite) is the +primary database — Firestore is auth + sync only. + +**Data flow:** screen (`ConsumerWidget`) → `ref.watch(provider)` → provider function +→ DAO method → Drift table + +**Before editing the data layer:** read `lib/core/database/tables.dart` for schemas. + +**Key files:** +- `lib/core/database/tables.dart` — all table definitions +- `lib/core/database/daos/` — 6 DAOs (accounts, budget, budget_snapshots, categories, recurring_queue, transactions) +- `lib/features/shell/main_shell.dart` — app navigation +- `lib/features/budget/budget_calculator.dart` — TBB and allocation logic (pure, no DB) +- `lib/features/sync/sync_service.dart` — Firestore sync bridge (local → cloud only) + +**Month scoping:** use `selectedMonthProvider` for budget views; pass explicit `DateTime` ranges to DAO methods for other queries. +**Never write to Drift directly from screens** — always go through a provider → DAO. +**Generated files (`*.g.dart`):** never edit manually — run `dart run build_runner build`. +**Account balance:** `balanceCents` is opening balance; running balance = `balanceCents + sum(transactionAmounts)` via `accountRunningBalancesProvider`. + +Full architecture docs: `.github/docs/README.md` +Symbol index (providers, DAOs, tables, screens): `.github/docs/symbol_index.md` diff --git a/.github/docs/README.md b/.github/docs/README.md new file mode 100644 index 0000000..8de95e1 --- /dev/null +++ b/.github/docs/README.md @@ -0,0 +1,76 @@ +# moneyinsight — Architecture Documentation + +Generated by the Deep Reverse Engineering Agent. +Run timestamp: 2026-03-20T17:53:43.645376+00:00 + +## Tech stack +- **Flutter + Dart** — cross-platform mobile UI framework +- **State management:** Riverpod (code-generated providers via `@riverpod` annotation) +- **Local database:** Drift (SQLite) — all primary data stored on-device +- **Auth:** Firebase Auth + Google Sign-In + Biometric (`local_auth`) +- **Cloud sync:** Firestore (sync layer only — not primary storage; local is source of truth) +- **Charts:** `fl_chart` + +## Architecture pattern +The app follows a layered architecture: + +``` +Screen (ConsumerWidget) + └─ watches Riverpod Provider + └─ reads/writes via DAO (Drift DatabaseAccessor) + └─ Drift Table (SQLite on device) + ↕ (one-directional, on demand) + Firestore Cloud +``` + +Business logic lives in calculator classes (pure functions) and service classes. +Providers expose reactive `Stream<>` data from DAO watch methods, keeping the UI +automatically up-to-date when the database changes. + +## Feature index +- [Account Management](features/accounts.md) — Allows users to create and manage their financial accounts (checking, savings, c... +- [Analytics & Budget History](features/analytics.md) — Provides charts and historical views of spending by category and income vs expen... +- [Authentication & Biometric Lock](features/auth.md) — Handles user identity via Firebase Auth (email/Google sign-in) and provides a lo... +- [Budget Management](features/budget.md) — Allows users to allocate available funds across spending categories for a given ... +- [Financial Goals](features/goals.md) — Lets users set savings goals on categories (target amount + target date). A calc... +- [Onboarding](features/onboarding.md) — First-run flow that walks new users through setting up their first account and i... +- [Settings](features/settings.md) — App settings screen. Allows users to configure preferences such as currency disp... +- [Navigation Shell](features/shell.md) — The main navigation scaffold housing the bottom navigation bar. Routes between A... +- [Cloud Sync](features/sync.md) — Bridges local Drift/SQLite data to Firestore for cloud backup. On sync trigger, ... +- [Transaction Management](features/transactions.md) — Core feature for recording and reviewing financial transactions. Users can add i... + +## Data model summary +See [data_model.md](data_model.md) for the full schema. + +Tables: +- `accounts` — user financial accounts (checking, savings, credit, cash) +- `category_groups` — logical groupings of spending categories +- `categories` — individual budget categories; may carry a savings goal +- `monthly_budgets` — per-category monthly allocation (envelope budgeting) +- `transactions` — all income, expense, and transfer transactions +- `net_worth_snapshots` — periodic net worth snapshots for trending +- `budget_snapshots` — end-of-month budget performance snapshots +- `pending_recurring_queue` — upcoming recurring transactions awaiting user review + +## User flows +- [User Adds an Account](flows/user-adds-account.md) +- [User Records a Transaction](flows/user-records-transaction.md) +- [User Budgets a Month](flows/user-budgets-month.md) +- [User Imports Transactions from CSV](flows/user-imports-csv.md) +- [User Syncs Data to Firestore](flows/user-syncs-data.md) +- [User Sets a Financial Goal](flows/user-sets-goal.md) + +## Key design decisions observed +- **Envelope budgeting (YNAB-style):** `monthly_budgets` stores per-category allocations. + The To Be Budgeted (TBB) value is calculated in `budget_calculator.dart` as: + `TBB = sum(account balances) − sum(category allocations for month)`. +- **Month boundaries:** `month_boundary_service.dart` runs on app startup to detect + month roll-overs, trigger budget snapshots, and optionally roll over unspent category + balances where `categories.rollover = true`. +- **Drift as source of truth:** All reads go to local SQLite via Drift. Firestore is + written to on demand (sync button) and is never read back — no conflict resolution needed. +- **Biometric lock:** Applied at the `MaterialApp` home widget level via + `BiometricLockScreen`, which wraps the entire app and re-locks on background. +- **Recurring transactions:** Template transactions with `recurring = true` generate + `pending_recurring_queue` entries; the `RecurringDueBanner` prompts users to review + and post them each month. diff --git a/.github/docs/_inventory.json b/.github/docs/_inventory.json new file mode 100644 index 0000000..4d65818 --- /dev/null +++ b/.github/docs/_inventory.json @@ -0,0 +1,678 @@ +[ + { + "path": "lib/core/csv/csv_parser.dart", + "role": [ + "parser" + ], + "cluster": "csv", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/daos/accounts_dao.dart", + "role": [ + "dao" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/daos/budget_dao.dart", + "role": [ + "dao" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/daos/budget_snapshots_dao.dart", + "role": [ + "dao" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/daos/categories_dao.dart", + "role": [ + "dao" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/daos/recurring_queue_dao.dart", + "role": [ + "dao" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/daos/transactions_dao.dart", + "role": [ + "dao" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/database.dart", + "role": [ + "config" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/providers.dart", + "role": [ + "other" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/database/tables.dart", + "role": [ + "model" + ], + "cluster": "database", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/services/month_boundary_provider.dart", + "role": [ + "provider" + ], + "cluster": "services", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/services/month_boundary_service.dart", + "role": [ + "service" + ], + "cluster": "services", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/services/rollover_provider.dart", + "role": [ + "provider" + ], + "cluster": "services", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/theme/app_theme.dart", + "role": [ + "config" + ], + "cluster": "theme", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/core/utils/currency_formatter.dart", + "role": [ + "util" + ], + "cluster": "utils", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/accounts/account_detail_screen.dart", + "role": [ + "screen" + ], + "cluster": "accounts", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/accounts/account_providers.dart", + "role": [ + "provider" + ], + "cluster": "accounts", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/accounts/accounts_screen.dart", + "role": [ + "screen" + ], + "cluster": "accounts", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/accounts/add_account_screen.dart", + "role": [ + "screen" + ], + "cluster": "accounts", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/analytics/analytics_providers.dart", + "role": [ + "provider" + ], + "cluster": "analytics", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/analytics/analytics_screen.dart", + "role": [ + "screen" + ], + "cluster": "analytics", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/analytics/budget_history_providers.dart", + "role": [ + "provider" + ], + "cluster": "analytics", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/analytics/budget_history_screen.dart", + "role": [ + "screen" + ], + "cluster": "analytics", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/analytics/widgets/income_vs_expenses_chart.dart", + "role": [ + "widget" + ], + "cluster": "analytics", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/analytics/widgets/spending_by_category_chart.dart", + "role": [ + "widget" + ], + "cluster": "analytics", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/auth/auth_providers.dart", + "role": [ + "provider" + ], + "cluster": "auth", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/auth/biometric_lock_screen.dart", + "role": [ + "screen" + ], + "cluster": "auth", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/auth/firebase_auth_service.dart", + "role": [ + "service" + ], + "cluster": "auth", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/budget_calculator.dart", + "role": [ + "calculator" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/budget_providers.dart", + "role": [ + "provider" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/budget_screen.dart", + "role": [ + "screen" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/rebalance_provider.dart", + "role": [ + "provider" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/recurring_providers.dart", + "role": [ + "provider" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/widgets/category_row.dart", + "role": [ + "widget" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/widgets/rebalance_sheet.dart", + "role": [ + "widget" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/widgets/recurring_due_banner.dart", + "role": [ + "widget" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/widgets/recurring_review_sheet.dart", + "role": [ + "widget" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/budget/widgets/tbb_banner.dart", + "role": [ + "widget" + ], + "cluster": "budget", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/goals/add_goal_screen.dart", + "role": [ + "screen" + ], + "cluster": "goals", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/goals/goal_calculator.dart", + "role": [ + "calculator" + ], + "cluster": "goals", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/goals/goals_providers.dart", + "role": [ + "provider" + ], + "cluster": "goals", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/goals/goals_screen.dart", + "role": [ + "screen" + ], + "cluster": "goals", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/onboarding/onboarding_providers.dart", + "role": [ + "provider" + ], + "cluster": "onboarding", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/onboarding/onboarding_screen.dart", + "role": [ + "screen" + ], + "cluster": "onboarding", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/settings/settings_screen.dart", + "role": [ + "screen" + ], + "cluster": "settings", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/shell/main_shell.dart", + "role": [ + "router" + ], + "cluster": "shell", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/sync/sync_service.dart", + "role": [ + "service" + ], + "cluster": "sync", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": true, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/transactions/add_transaction_screen.dart", + "role": [ + "screen" + ], + "cluster": "transactions", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/transactions/import_csv_screen.dart", + "role": [ + "screen" + ], + "cluster": "transactions", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/transactions/transactions_screen.dart", + "role": [ + "screen" + ], + "cluster": "transactions", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/features/transactions/widgets/transaction_tile.dart", + "role": [ + "widget" + ], + "cluster": "transactions", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + }, + { + "path": "lib/main.dart", + "role": [ + "entry" + ], + "cluster": "root", + "p1_inventory": true, + "p2_signatures": true, + "p3_dependencies": true, + "p4_data_models": false, + "p5_logic_summary": true, + "p6_business_intent": true + } +] diff --git a/.github/docs/_manifest.json b/.github/docs/_manifest.json new file mode 100644 index 0000000..bac3bd8 --- /dev/null +++ b/.github/docs/_manifest.json @@ -0,0 +1,237 @@ +{ + "repo": "moneyinsight", + "tier": "XS", + "primary_language": "dart", + "framework": "flutter", + "state_management": "riverpod", + "database": { + "primary": "drift_sqlite", + "sync": "cloud_firestore" + }, + "firebase_role": "auth_and_sync_only", + "features": [ + "accounts", + "analytics", + "auth", + "budget", + "goals", + "onboarding", + "settings", + "shell", + "sync", + "transactions" + ], + "entry_point": "lib/main.dart", + "dart_file_count": 52, + "dart_files": [ + { + "path": "lib/core/csv/csv_parser.dart", + "lines": 124 + }, + { + "path": "lib/core/database/daos/accounts_dao.dart", + "lines": 35 + }, + { + "path": "lib/core/database/daos/budget_dao.dart", + "lines": 38 + }, + { + "path": "lib/core/database/daos/budget_snapshots_dao.dart", + "lines": 42 + }, + { + "path": "lib/core/database/daos/categories_dao.dart", + "lines": 69 + }, + { + "path": "lib/core/database/daos/recurring_queue_dao.dart", + "lines": 33 + }, + { + "path": "lib/core/database/daos/transactions_dao.dart", + "lines": 85 + }, + { + "path": "lib/core/database/database.dart", + "lines": 66 + }, + { + "path": "lib/core/database/providers.dart", + "lines": 8 + }, + { + "path": "lib/core/database/tables.dart", + "lines": 122 + }, + { + "path": "lib/core/services/month_boundary_provider.dart", + "lines": 10 + }, + { + "path": "lib/core/services/month_boundary_service.dart", + "lines": 150 + }, + { + "path": "lib/core/services/rollover_provider.dart", + "lines": 27 + }, + { + "path": "lib/core/theme/app_theme.dart", + "lines": 30 + }, + { + "path": "lib/core/utils/currency_formatter.dart", + "lines": 23 + }, + { + "path": "lib/features/accounts/account_detail_screen.dart", + "lines": 123 + }, + { + "path": "lib/features/accounts/account_providers.dart", + "lines": 42 + }, + { + "path": "lib/features/accounts/accounts_screen.dart", + "lines": 203 + }, + { + "path": "lib/features/accounts/add_account_screen.dart", + "lines": 210 + }, + { + "path": "lib/features/analytics/analytics_providers.dart", + "lines": 94 + }, + { + "path": "lib/features/analytics/analytics_screen.dart", + "lines": 99 + }, + { + "path": "lib/features/analytics/budget_history_providers.dart", + "lines": 62 + }, + { + "path": "lib/features/analytics/budget_history_screen.dart", + "lines": 272 + }, + { + "path": "lib/features/analytics/widgets/income_vs_expenses_chart.dart", + "lines": 142 + }, + { + "path": "lib/features/analytics/widgets/spending_by_category_chart.dart", + "lines": 119 + }, + { + "path": "lib/features/auth/auth_providers.dart", + "lines": 23 + }, + { + "path": "lib/features/auth/biometric_lock_screen.dart", + "lines": 149 + }, + { + "path": "lib/features/auth/firebase_auth_service.dart", + "lines": 80 + }, + { + "path": "lib/features/budget/budget_calculator.dart", + "lines": 38 + }, + { + "path": "lib/features/budget/budget_providers.dart", + "lines": 84 + }, + { + "path": "lib/features/budget/budget_screen.dart", + "lines": 525 + }, + { + "path": "lib/features/budget/rebalance_provider.dart", + "lines": 117 + }, + { + "path": "lib/features/budget/recurring_providers.dart", + "lines": 10 + }, + { + "path": "lib/features/budget/widgets/category_row.dart", + "lines": 77 + }, + { + "path": "lib/features/budget/widgets/rebalance_sheet.dart", + "lines": 183 + }, + { + "path": "lib/features/budget/widgets/recurring_due_banner.dart", + "lines": 44 + }, + { + "path": "lib/features/budget/widgets/recurring_review_sheet.dart", + "lines": 167 + }, + { + "path": "lib/features/budget/widgets/tbb_banner.dart", + "lines": 55 + }, + { + "path": "lib/features/goals/add_goal_screen.dart", + "lines": 178 + }, + { + "path": "lib/features/goals/goal_calculator.dart", + "lines": 26 + }, + { + "path": "lib/features/goals/goals_providers.dart", + "lines": 65 + }, + { + "path": "lib/features/goals/goals_screen.dart", + "lines": 199 + }, + { + "path": "lib/features/onboarding/onboarding_providers.dart", + "lines": 24 + }, + { + "path": "lib/features/onboarding/onboarding_screen.dart", + "lines": 691 + }, + { + "path": "lib/features/settings/settings_screen.dart", + "lines": 244 + }, + { + "path": "lib/features/shell/main_shell.dart", + "lines": 66 + }, + { + "path": "lib/features/sync/sync_service.dart", + "lines": 117 + }, + { + "path": "lib/features/transactions/add_transaction_screen.dart", + "lines": 420 + }, + { + "path": "lib/features/transactions/import_csv_screen.dart", + "lines": 276 + }, + { + "path": "lib/features/transactions/transactions_screen.dart", + "lines": 413 + }, + { + "path": "lib/features/transactions/widgets/transaction_tile.dart", + "lines": 124 + }, + { + "path": "lib/main.dart", + "lines": 58 + } + ], + "run_timestamp": "2026-03-20T17:53:43.231245+00:00" +} diff --git a/.github/docs/brownfield_context.md b/.github/docs/brownfield_context.md new file mode 100644 index 0000000..56fe849 --- /dev/null +++ b/.github/docs/brownfield_context.md @@ -0,0 +1,114 @@ +# Brownfield Development Context — moneyinsight + +_Generated by the Deep Reverse Engineering Agent — Stage 8._ +_Copy this file into your context when starting work on a new feature._ + +--- + +## Existing features (do not duplicate) +- **Account Management** (`lib/features/accounts/`): Allows users to create and manage their financial accounts (checking, savings, credit card... +- **Analytics & Budget History** (`lib/features/analytics/`): Provides charts and historical views of spending by category and income vs expenses over t... +- **Authentication & Biometric Lock** (`lib/features/auth/`): Handles user identity via Firebase Auth (email/Google sign-in) and provides a local biomet... +- **Budget Management** (`lib/features/budget/`): Allows users to allocate available funds across spending categories for a given month usin... +- **Financial Goals** (`lib/features/goals/`): Lets users set savings goals on categories (target amount + target date). A calculator com... +- **Onboarding** (`lib/features/onboarding/`): First-run flow that walks new users through setting up their first account and initial bud... +- **Settings** (`lib/features/settings/`): App settings screen. Allows users to configure preferences such as currency display, theme... +- **Navigation Shell** (`lib/features/shell/`): The main navigation scaffold housing the bottom navigation bar. Routes between Accounts, B... +- **Cloud Sync** (`lib/features/sync/`): Bridges local Drift/SQLite data to Firestore for cloud backup. On sync trigger, reads all ... +- **Transaction Management** (`lib/features/transactions/`): Core feature for recording and reviewing financial transactions. Users can add income, exp... + +--- + +## How to add a new feature — pattern to follow + +1. Create `lib/features/{feature}/` directory +2. Add a `{feature}_screen.dart` extending `ConsumerWidget` +3. Add a `{feature}_providers.dart` with provider declarations +4. If new data is needed: + - Add table class to `lib/core/database/tables.dart` + - Create `lib/core/database/daos/{feature}_dao.dart` with `@DriftAccessor` + - Register the DAO in `lib/core/database/database.dart` (add to `@DriftDatabase(tables:[...], daos:[...])`) + - Run `dart run build_runner build --delete-conflicting-outputs` +5. Add a route/tab in `lib/features/shell/main_shell.dart` +6. If data should sync to Firestore: add collection handling in `lib/features/sync/sync_service.dart` + +--- + +## Current data model (tables that exist — do not recreate) +- `accounts` — user financial accounts; opening balance + soft-delete +- `category_groups` — groupings of spending categories (e.g. Housing, Food) +- `categories` — individual budget categories; carries optional savings goal columns +- `monthly_budgets` — per-category per-month envelope allocation (YYYY-MM key) +- `transactions` — all income / expense / transfer entries; links account + category +- `net_worth_snapshots` — periodic net-worth snapshots for trend charts +- `budget_snapshots` — end-of-month assigned/spent snapshot per category +- `pending_recurring_queue` — upcoming recurring transactions awaiting user review + +--- + +## Provider names already in use (do not clash) +- `accountsProvider` +- `accountRunningBalancesProvider` +- `netWorthProvider` +- `selectedMonthProvider` +- `categoryGroupsProvider` +- `allCategoriesProvider` +- `monthlyBudgetsProvider` +- `transactionsForMonthProvider` +- `monthlyIncomeProvider` +- `totalAssignedProvider` +- `rolloverAmountsProvider` +- `rebalanceSuggestionsProvider` +- `pendingRecurringProvider` +- `spendingByCategoryProvider` +- `monthlyTotalsProvider` +- `budgetHistoryProvider` +- `goalsProvider` +- `localAuthProvider` +- `biometricEnabledProvider` +- `isUnlockedProvider` +- `onboardingCompleteProvider` +- `monthBoundaryServiceProvider` +- `databaseProvider` +- `syncServiceProvider` +- `lastSyncProvider` + +--- + +## Known extension points +- **Onboarding**: Exact onboarding steps depend on screen implementation detail +- **Settings**: Settings screen detail depends on runtime implementation +- **Net Worth**: `net_worth_snapshots` table exists but no screen writes to it yet +- **Recurring transactions**: `pending_recurring_queue` populated by `MonthBoundaryService`; no user-facing management screen for the templates +- **Cloud pull / merge**: sync is local → cloud only; no conflict resolution or pull-from-cloud implemented +- **Multi-currency**: `balanceCents` is stored as integer cents; no currency column — multi-currency would require a schema change +- **Notifications**: no push notification or local notification integration exists + +--- + +## Test coverage +### Unit tests + +- `test/core/csv_parser_test.dart` +- `test/core/currency_formatter_test.dart` +- `test/core/database/budget_snapshots_dao_test.dart` +- `test/core/database_test.dart` +- `test/core/services/month_boundary_service_test.dart` +- `test/features/budget/budget_calculator_test.dart` +- `test/features/budget/budget_providers_test.dart` +- `test/features/budget/rebalance_provider_test.dart` +- `test/features/goals/goal_calculator_test.dart` +- `test/features/transactions/add_transaction_test.dart` +- `test/widget_test.dart` + +### Integration tests + +- `integration_test/budget_flow_test.dart` + +### Coverage gaps +- `auth` — no unit tests for `firebase_auth_service.dart` +- `analytics` — no unit tests for provider logic +- `onboarding` — no unit tests for screen flow +- `settings` — no tests +- `sync` — no unit tests for `SyncService` (requires Firestore mock) +- `shell` — no tests diff --git a/.github/docs/data_model.md b/.github/docs/data_model.md new file mode 100644 index 0000000..bf23da3 --- /dev/null +++ b/.github/docs/data_model.md @@ -0,0 +1,124 @@ +# Data Model + +## Local Database (SQLite via Drift) + +### `accounts` (`Accounts`) +**DAO:** `AccountsDao` — `lib/core/database/daos/accounts_dao.dart` +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| name | text | | | | +| type | text | | | | +| balanceCents | integer | | | | +| institution | text | | ✓ | | +| createdAt | datetime | | | | +| updatedAt | datetime | | | | +| isDeleted | boolean | | | | + +### `category_groups` (`CategoryGroups`) +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| name | text | | | | +| sortOrder | integer | | | | +| isDeleted | boolean | | | | + +### `categories` (`Categories`) +**DAO:** `CategoriesDao` — `lib/core/database/daos/categories_dao.dart` +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| groupId | integer | | | CategoryGroups | +| name | text | | | | +| rollover | boolean | | | | +| goalAmountCents | integer | | ✓ | | +| goalDate | datetime | | ✓ | | +| goalType | text | | ✓ | | +| sortOrder | integer | | | | +| createdAt | datetime | | | | +| updatedAt | datetime | | | | +| isDeleted | boolean | | | | + +### `monthly_budgets` (`MonthlyBudgets`) +**DAO:** `BudgetDao` — `lib/core/database/daos/budget_dao.dart` +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| categoryId | integer | | | Categories | +| month | text | | | | +| assignedCents | integer | | | | +| rolledOverCents | integer | | | | +| updatedAt | datetime | | | | + +### `transactions` (`Transactions`) +**DAO:** `TransactionsDao` — `lib/core/database/daos/transactions_dao.dart` +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| accountId | integer | | | Accounts | +| categoryId | integer | | ✓ | Categories | +| amountCents | integer | | | | +| payee | text | | | | +| date | datetime | | | | +| memo | text | | ✓ | | +| type | text | | | | +| cleared | boolean | | | | +| recurring | boolean | | | | +| recurringInterval | text | | ✓ | | +| nextDueDate | datetime | | ✓ | | +| importedFrom | text | | ✓ | | +| createdAt | datetime | | | | +| updatedAt | datetime | | | | +| toAccountId | integer | | ✓ | Accounts | +| isDeleted | boolean | | | | + +### `net_worth_snapshots` (`NetWorthSnapshots`) +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| date | datetime | | | | +| totalAssetsCents | integer | | | | +| totalLiabilitiesCents | integer | | | | +| netWorthCents | integer | | | | + +### `budget_snapshots` (`BudgetSnapshots`) +**DAO:** `BudgetSnapshotsDao` — `lib/core/database/daos/budget_snapshots_dao.dart` +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| categoryId | integer | | | Categories | +| month | text | | | | +| assignedCents | integer | | | | +| spentCents | integer | | | | +| createdAt | datetime | | | | + +### `pending_recurring_queue` (`PendingRecurringQueue`) +**DAO:** `RecurringQueueDao` — `lib/core/database/daos/recurring_queue_dao.dart` +| Column | Type | PK | Nullable | References | +| --- | --- | --- | --- | --- | +| id | integer | ✓ | | | +| sourceTransactionId | integer | | | Transactions | +| dueDate | datetime | | | | +| createdAt | datetime | | | | + + +## Firestore Sync + +Sync is **one-directional: local → cloud** (no merge/pull from Firestore). + +| Firestore Collection | Local Table | Sync Direction | +| --- | --- | --- | +| accounts | accounts | local_to_cloud | +| categoryGroups | category_groups | local_to_cloud | +| categories | categories | local_to_cloud | +| transactions | transactions | local_to_cloud | + + +## Entity Relationships + +- **categories** belong to a **category_groups** (via `groupId`) +- **monthly_budgets** reference a **categories** row (via `categoryId`) and a month string (YYYY-MM) +- **transactions** reference an **accounts** row (via `accountId`) and optionally a **categories** row (via `categoryId`) +- **transactions** may reference a second **accounts** row (via `toAccountId`) for transfers +- **budget_snapshots** reference a **categories** row and a month string +- **pending_recurring_queue** references a **transactions** row (the template recurring transaction) diff --git a/.github/docs/features/accounts.md b/.github/docs/features/accounts.md new file mode 100644 index 0000000..0b0cc2d --- /dev/null +++ b/.github/docs/features/accounts.md @@ -0,0 +1,37 @@ +# Account Management + +## What it does +Allows users to create and manage their financial accounts (checking, savings, credit cards, cash). Users can add accounts with opening balances, view account details and transaction history, and soft-delete accounts they no longer need. + +## User actions +- Add a new account with name, type, and opening balance +- View account details and running balance +- View transactions for a specific account +- Edit or delete an account + +## Screens and widgets +- `lib/features/accounts/account_detail_screen.dart` +- `lib/features/accounts/accounts_screen.dart` +- `lib/features/accounts/add_account_screen.dart` + +## Providers +- `lib/features/accounts/account_providers.dart` + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`accounts_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| accounts | read+write | yes | +| transactions | read | yes | + +## Dependencies on other features +_none_ + +## Known gaps +_none identified_ diff --git a/.github/docs/features/analytics.md b/.github/docs/features/analytics.md new file mode 100644 index 0000000..a5f3f8a --- /dev/null +++ b/.github/docs/features/analytics.md @@ -0,0 +1,38 @@ +# Analytics & Budget History + +## What it does +Provides charts and historical views of spending by category and income vs expenses over time. Also shows monthly budget snapshots so users can track their budgeting performance across months. + +## User actions +- View spending breakdown by category (pie/bar chart) +- View income vs expense chart +- Browse historical monthly budget snapshots + +## Screens and widgets +- `lib/features/analytics/analytics_screen.dart` +- `lib/features/analytics/budget_history_screen.dart` + +## Providers +- `lib/features/analytics/analytics_providers.dart` +- `lib/features/analytics/budget_history_providers.dart` + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`analytics_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| transactions | read | yes | +| categories | read | yes | +| budget_snapshots | read | yes | +| monthly_budgets | read | yes | + +## Dependencies on other features +`budget`, `transactions` + +## Known gaps +_none identified_ diff --git a/.github/docs/features/auth.md b/.github/docs/features/auth.md new file mode 100644 index 0000000..545c8f2 --- /dev/null +++ b/.github/docs/features/auth.md @@ -0,0 +1,31 @@ +# Authentication & Biometric Lock + +## What it does +Handles user identity via Firebase Auth (email/Google sign-in) and provides a local biometric lock screen that gates access to the app on app launch or resume. Auth state is exposed as a Riverpod provider. + +## User actions +- Sign in with Google or email +- Sign out +- Unlock app with biometrics (fingerprint / face ID) + +## Screens and widgets +- `lib/features/auth/biometric_lock_screen.dart` + +## Providers +- `lib/features/auth/auth_providers.dart` + +## Services +- `lib/features/auth/firebase_auth_service.dart` + +## Provider chain +Data flows from screen → provider (`auth_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +_no direct table access_ + +## Dependencies on other features +_none_ + +## Known gaps +_none identified_ diff --git a/.github/docs/features/budget.md b/.github/docs/features/budget.md new file mode 100644 index 0000000..888d27e --- /dev/null +++ b/.github/docs/features/budget.md @@ -0,0 +1,41 @@ +# Budget Management + +## What it does +Allows users to allocate available funds across spending categories for a given month using an envelope-budgeting (YNAB-style) approach. Users assign amounts to categories, see their To Be Budgeted balance update in real time, rebalance overspent categories, and review recurring transactions due this month. + +## User actions +- Assign funds to a spending category +- Rebalance overspent categories +- Review and approve recurring transactions +- View remaining To Be Budgeted amount +- Navigate between months + +## Screens and widgets +- `lib/features/budget/budget_screen.dart` + +## Providers +- `lib/features/budget/budget_providers.dart` +- `lib/features/budget/rebalance_provider.dart` +- `lib/features/budget/recurring_providers.dart` + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`budget_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| monthly_budgets | read+write | yes | +| categories | read | yes | +| transactions | read | yes | +| accounts | read | yes | +| budget_snapshots | read+write | no | + +## Dependencies on other features +`accounts`, `transactions` + +## Known gaps +_none identified_ diff --git a/.github/docs/features/goals.md b/.github/docs/features/goals.md new file mode 100644 index 0000000..a4b72f7 --- /dev/null +++ b/.github/docs/features/goals.md @@ -0,0 +1,35 @@ +# Financial Goals + +## What it does +Lets users set savings goals on categories (target amount + target date). A calculator computes progress and estimated monthly contribution required to reach each goal. + +## User actions +- Create a savings goal on a category +- View goal progress and projected completion date +- Edit or remove a goal + +## Screens and widgets +- `lib/features/goals/add_goal_screen.dart` +- `lib/features/goals/goals_screen.dart` + +## Providers +- `lib/features/goals/goals_providers.dart` + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`goals_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| categories | read+write | yes | +| monthly_budgets | read | yes | + +## Dependencies on other features +`budget` + +## Known gaps +_none identified_ diff --git a/.github/docs/features/onboarding.md b/.github/docs/features/onboarding.md new file mode 100644 index 0000000..f0ad7c6 --- /dev/null +++ b/.github/docs/features/onboarding.md @@ -0,0 +1,34 @@ +# Onboarding + +## What it does +First-run flow that walks new users through setting up their first account and initial budget categories so the app is ready to use. Completion state is persisted and controls whether the app shows the main shell or the onboarding screen. + +## User actions +- Complete first-time setup wizard +- Add first account +- Accept default budget categories + +## Screens and widgets +- `lib/features/onboarding/onboarding_screen.dart` + +## Providers +- `lib/features/onboarding/onboarding_providers.dart` + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`onboarding_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| accounts | read+write | no | +| categories | read+write | no | + +## Dependencies on other features +`accounts` + +## Known gaps +- Exact onboarding steps depend on screen implementation detail diff --git a/.github/docs/features/settings.md b/.github/docs/features/settings.md new file mode 100644 index 0000000..bb00f58 --- /dev/null +++ b/.github/docs/features/settings.md @@ -0,0 +1,32 @@ +# Settings + +## What it does +App settings screen. Allows users to configure preferences such as currency display, theme, and manage their account (sign out, delete data). + +## User actions +- Change currency or number format +- Toggle dark/light theme +- Sign out +- Trigger manual data sync + +## Screens and widgets +- `lib/features/settings/settings_screen.dart` + +## Providers +_none_ + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`settings_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +_no direct table access_ + +## Dependencies on other features +`auth`, `sync` + +## Known gaps +- Settings screen detail depends on runtime implementation diff --git a/.github/docs/features/shell.md b/.github/docs/features/shell.md new file mode 100644 index 0000000..95b1b7e --- /dev/null +++ b/.github/docs/features/shell.md @@ -0,0 +1,29 @@ +# Navigation Shell + +## What it does +The main navigation scaffold housing the bottom navigation bar. Routes between Accounts, Budget, Transactions, Analytics, and Goals features. + +## User actions +- Switch between main app sections via bottom navigation + +## Screens and widgets +- `lib/features/shell/main_shell.dart` + +## Providers +_none_ + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`shell_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +_no direct table access_ + +## Dependencies on other features +`accounts`, `budget`, `transactions`, `analytics`, `goals` + +## Known gaps +_none identified_ diff --git a/.github/docs/features/sync.md b/.github/docs/features/sync.md new file mode 100644 index 0000000..953e673 --- /dev/null +++ b/.github/docs/features/sync.md @@ -0,0 +1,36 @@ +# Cloud Sync + +## What it does +Bridges local Drift/SQLite data to Firestore for cloud backup. On sync trigger, reads all accounts, category groups, categories, monthly budgets, and transactions from the local DB and batch-writes them to a per-user Firestore document tree. One-directional (local → cloud). + +## User actions +- Trigger manual sync from settings +- View last sync timestamp + +## Screens and widgets +_none_ + +## Providers +_none_ + +## Services +- `lib/features/sync/sync_service.dart` + +## Provider chain +Data flows from screen → provider (`sync_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| accounts | read | yes | +| category_groups | read | yes | +| categories | read | yes | +| monthly_budgets | read | yes | +| transactions | read | yes | + +## Dependencies on other features +`accounts`, `budget`, `transactions` + +## Known gaps +_none identified_ diff --git a/.github/docs/features/transactions.md b/.github/docs/features/transactions.md new file mode 100644 index 0000000..1d44df0 --- /dev/null +++ b/.github/docs/features/transactions.md @@ -0,0 +1,40 @@ +# Transaction Management + +## What it does +Core feature for recording and reviewing financial transactions. Users can add income, expense, and transfer transactions, categorise them, mark them as cleared, and import bulk transactions from CSV files. Transaction list is filterable by account and month. + +## User actions +- Add a new income, expense, or transfer transaction +- Assign a category to a transaction +- Mark a transaction as cleared +- Import transactions from a CSV file +- View transaction list filtered by account or month +- Edit or delete an existing transaction + +## Screens and widgets +- `lib/features/transactions/add_transaction_screen.dart` +- `lib/features/transactions/import_csv_screen.dart` +- `lib/features/transactions/transactions_screen.dart` + +## Providers +_none_ + +## Services +_none_ + +## Provider chain +Data flows from screen → provider (`transactions_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +| Table | Operations | Reactive? | +| --- | --- | --- | +| transactions | read+write | yes | +| accounts | read+write | yes | +| categories | read | yes | + +## Dependencies on other features +`accounts` + +## Known gaps +_none identified_ diff --git a/.github/docs/files.md b/.github/docs/files.md new file mode 100644 index 0000000..c839f1b --- /dev/null +++ b/.github/docs/files.md @@ -0,0 +1,389 @@ +# File Reference + +Generated by the Deep Reverse Engineering Agent. + + +## Cluster: `accounts` + +### `lib/features/accounts/account_detail_screen.dart` +**Role:** screen +**Cluster:** accounts +**Classes:** AccountDetailScreen +**Public methods:** build +**Complexity:** low + +### `lib/features/accounts/account_providers.dart` +**Role:** provider +**Cluster:** accounts +**Complexity:** low + +### `lib/features/accounts/accounts_screen.dart` +**Role:** screen +**Cluster:** accounts +**Classes:** AccountsScreen +**Public methods:** build, Expanded +**Complexity:** low + +### `lib/features/accounts/add_account_screen.dart` +**Role:** screen +**Cluster:** accounts +**Classes:** AddAccountScreen, _AddAccountScreenState +**Public methods:** initState, dispose, build +**Complexity:** low + + +## Cluster: `analytics` + +### `lib/features/analytics/analytics_providers.dart` +**Role:** provider +**Cluster:** analytics +**Classes:** CategorySpending, MonthlyTotal +**Complexity:** low + +### `lib/features/analytics/analytics_screen.dart` +**Role:** screen +**Cluster:** analytics +**Classes:** AnalyticsScreen, _SpendingTab, _IncomeVsExpensesTab +**Public methods:** build, build, build +**Complexity:** low + +### `lib/features/analytics/budget_history_providers.dart` +**Role:** provider +**Cluster:** analytics +**Classes:** CategoryMonthHistory, MonthBudgetHistory +**Complexity:** low + +### `lib/features/analytics/budget_history_screen.dart` +**Role:** screen +**Cluster:** analytics +**Classes:** BudgetHistoryScreen, _BudgetTrendChart, _MonthTile +**Public methods:** build, build, build +**Complexity:** low + +### `lib/features/analytics/widgets/income_vs_expenses_chart.dart` +**Role:** widget +**Cluster:** analytics +**Classes:** IncomeVsExpensesChart, _Legend +**Public methods:** build, Column, build +**Complexity:** low + +### `lib/features/analytics/widgets/spending_by_category_chart.dart` +**Role:** widget +**Cluster:** analytics +**Classes:** SpendingByCategoryChart, _SpendingByCategoryChartState +**Public methods:** build, Column, setState +**Complexity:** low + + +## Cluster: `auth` + +### `lib/features/auth/auth_providers.dart` +**Role:** provider +**Cluster:** auth +**Public methods:** setBiometricEnabled +**Complexity:** low + +### `lib/features/auth/biometric_lock_screen.dart` +**Role:** screen +**Cluster:** auth +**Classes:** BiometricLockScreen, _LockScreen, _LockScreenState +**Public methods:** build, initState, setState, setState, setState, build +**Complexity:** low + +### `lib/features/auth/firebase_auth_service.dart` +**Role:** service +**Cluster:** auth +**Classes:** FirebaseAuthService +**Public methods:** signInWithGoogle, signInAnonymously, signOut +**Complexity:** low + + +## Cluster: `budget` + +### `lib/features/budget/budget_calculator.dart` +**Role:** calculator +**Cluster:** budget +**Classes:** BudgetCalculator +**Public methods:** available, toBeBudgeted, ageOfMoney +**Complexity:** low + +### `lib/features/budget/budget_providers.dart` +**Role:** provider +**Cluster:** budget +**Complexity:** low + +### `lib/features/budget/budget_screen.dart` +**Role:** screen +**Cluster:** budget +**Classes:** BudgetScreen, _GroupTile +**Public methods:** build, Consumer, Expanded, FilledButton, build, FilledButton +**Complexity:** medium + +### `lib/features/budget/rebalance_provider.dart` +**Role:** provider +**Cluster:** budget +**Classes:** CategoryBudgetData, RebalanceSuggestion +**Public methods:** computeRebalanceSuggestions +**Complexity:** low + +### `lib/features/budget/recurring_providers.dart` +**Role:** provider +**Cluster:** budget +**Complexity:** low + +### `lib/features/budget/widgets/category_row.dart` +**Role:** widget +**Cluster:** budget +**Classes:** CategoryRow +**Public methods:** build +**Complexity:** low + +### `lib/features/budget/widgets/rebalance_sheet.dart` +**Role:** widget +**Cluster:** budget +**Classes:** RebalanceSheet, _RebalanceSheetState +**Public methods:** build, Expanded +**Complexity:** low + +### `lib/features/budget/widgets/recurring_due_banner.dart` +**Role:** widget +**Cluster:** budget +**Classes:** RecurringDueBanner +**Public methods:** build +**Complexity:** low + +### `lib/features/budget/widgets/recurring_review_sheet.dart` +**Role:** widget +**Cluster:** budget +**Classes:** RecurringReviewSheet +**Public methods:** build, Expanded +**Complexity:** low + +### `lib/features/budget/widgets/tbb_banner.dart` +**Role:** widget +**Cluster:** budget +**Classes:** ToBeBudgetedBanner +**Public methods:** build +**Complexity:** low + + +## Cluster: `csv` + +### `lib/core/csv/csv_parser.dart` +**Role:** parser +**Cluster:** csv +**Classes:** ParsedTransaction, ValidationResult, CsvParser +**Public methods:** parse, detectHeaders, toTransactionData, validate +**Complexity:** low + + +## Cluster: `database` + +### `lib/core/database/daos/accounts_dao.dart` +**Role:** dao +**Cluster:** database +**Complexity:** low + +### `lib/core/database/daos/budget_dao.dart` +**Role:** dao +**Cluster:** database +**Complexity:** low + +### `lib/core/database/daos/budget_snapshots_dao.dart` +**Role:** dao +**Cluster:** database +**Public methods:** getSnapshotMonths, hasSnapshot +**Complexity:** low + +### `lib/core/database/daos/categories_dao.dart` +**Role:** dao +**Cluster:** database +**Public methods:** softDeleteGroup +**Complexity:** low + +### `lib/core/database/daos/recurring_queue_dao.dart` +**Role:** dao +**Cluster:** database +**Public methods:** isEnqueued +**Complexity:** low + +### `lib/core/database/daos/transactions_dao.dart` +**Role:** dao +**Cluster:** database +**Public methods:** watchTransactionsForMonth +**Complexity:** low + +### `lib/core/database/database.dart` +**Role:** config +**Cluster:** database +**Public methods:** MigrationStrategy, LazyDatabase +**Complexity:** low + +### `lib/core/database/providers.dart` +**Role:** other +**Cluster:** database +**Complexity:** low + +### `lib/core/database/tables.dart` +**Role:** model +**Cluster:** database +**Classes:** Accounts, CategoryGroups, Categories, MonthlyBudgets, Transactions +**Complexity:** low + + +## Cluster: `goals` + +### `lib/features/goals/add_goal_screen.dart` +**Role:** screen +**Cluster:** goals +**Classes:** AddGoalScreen, _AddGoalScreenState +**Public methods:** dispose, build +**Complexity:** low + +### `lib/features/goals/goal_calculator.dart` +**Role:** calculator +**Cluster:** goals +**Classes:** GoalCalculator +**Public methods:** progressPercent, projectedDate +**Complexity:** low + +### `lib/features/goals/goals_providers.dart` +**Role:** provider +**Cluster:** goals +**Classes:** GoalProgress +**Complexity:** low + +### `lib/features/goals/goals_screen.dart` +**Role:** screen +**Cluster:** goals +**Classes:** GoalsScreen, _EmptyState, _GoalCard +**Public methods:** build, build, build +**Complexity:** low + + +## Cluster: `onboarding` + +### `lib/features/onboarding/onboarding_providers.dart` +**Role:** provider +**Cluster:** onboarding +**Public methods:** setOnboardingComplete, saveMonthlyIncome +**Complexity:** low + +### `lib/features/onboarding/onboarding_screen.dart` +**Role:** screen +**Cluster:** onboarding +**Classes:** OnboardingScreen, _OnboardingScreenState, _WelcomePage, _AccountDraft, _AccountsPage +**Public methods:** dispose, build, setState, setState, setState, build, build, build +**Complexity:** medium + + +## Cluster: `root` + +### `lib/main.dart` +**Role:** entry +**Cluster:** root +**Classes:** MoneyInSightApp, _AppStartup +**Public methods:** main, build, build +**Complexity:** low + + +## Cluster: `services` + +### `lib/core/services/month_boundary_provider.dart` +**Role:** provider +**Cluster:** services +**Complexity:** low + +### `lib/core/services/month_boundary_service.dart` +**Role:** service +**Cluster:** services +**Classes:** MonthBoundaryService +**Public methods:** computeRolloverCents, computeNextDueDate, run +**Complexity:** low + +### `lib/core/services/rollover_provider.dart` +**Role:** provider +**Cluster:** services +**Classes:** GlobalRolloverNotifier +**Public methods:** build, setEnabled +**Complexity:** low + + +## Cluster: `settings` + +### `lib/features/settings/settings_screen.dart` +**Role:** screen +**Cluster:** settings +**Classes:** SettingsScreen, _SettingsScreenState, _SectionHeader +**Public methods:** build, build +**Complexity:** low + + +## Cluster: `shell` + +### `lib/features/shell/main_shell.dart` +**Role:** router +**Cluster:** shell +**Classes:** MainShell, _MainShellState +**Public methods:** build +**Complexity:** low + + +## Cluster: `sync` + +### `lib/features/sync/sync_service.dart` +**Role:** service +**Cluster:** sync +**Classes:** SyncService +**Public methods:** syncToFirestore +**Complexity:** low + + +## Cluster: `theme` + +### `lib/core/theme/app_theme.dart` +**Role:** config +**Cluster:** theme +**Classes:** AppTheme +**Complexity:** low + + +## Cluster: `transactions` + +### `lib/features/transactions/add_transaction_screen.dart` +**Role:** screen +**Cluster:** transactions +**Classes:** AddTransactionScreen, _AddTransactionScreenState +**Public methods:** initState, dispose, build +**Complexity:** low + +### `lib/features/transactions/import_csv_screen.dart` +**Role:** screen +**Cluster:** transactions +**Classes:** ImportCsvScreen, _ImportCsvScreenState +**Public methods:** setState, setState, build +**Complexity:** low + +### `lib/features/transactions/transactions_screen.dart` +**Role:** screen +**Cluster:** transactions +**Classes:** TransactionsScreen, _TransactionsScreenState +**Public methods:** build, Row, setState, Expanded, setState +**Complexity:** medium + +### `lib/features/transactions/widgets/transaction_tile.dart` +**Role:** widget +**Cluster:** transactions +**Classes:** TransactionTile +**Public methods:** build +**Complexity:** low + + +## Cluster: `utils` + +### `lib/core/utils/currency_formatter.dart` +**Role:** util +**Cluster:** utils +**Classes:** CurrencyFormatter +**Public methods:** format +**Complexity:** low diff --git a/.github/docs/flows/user-adds-account.md b/.github/docs/flows/user-adds-account.md new file mode 100644 index 0000000..67be5d6 --- /dev/null +++ b/.github/docs/flows/user-adds-account.md @@ -0,0 +1,23 @@ +# User Adds an Account + +## Actor +App user + +## Trigger +User taps 'Add Account' button on the Accounts screen + +## Steps +| Step | Action | Screen/Widget | Provider | DAO | Tables written | +| --- | --- | --- | --- | --- | --- | +| 1 | Tap 'Add Account' | AccountsScreen | accountsProvider | _ | _ | +| 2 | Enter name, type, opening balance | AddAccountScreen | _ | _ | _ | +| 3 | Tap 'Save' | AddAccountScreen | accountsProvider | AccountsDao.insertAccount | accounts | +| 4 | Account list refreshes via stream | AccountsScreen | accountsProvider (stream) | AccountsDao.watchAllAccounts | _ | + +## Happy path end state +A new row is inserted into the `accounts` table. The account list screen shows the new account immediately via the reactive stream. + +## Edge cases +- Name must be at least 1 character (Drift validation) +- Opening balance defaults to 0 if left blank +- Offline: account is saved locally only; sync to Firestore on next manual sync diff --git a/.github/docs/flows/user-budgets-month.md b/.github/docs/flows/user-budgets-month.md new file mode 100644 index 0000000..e2e40d1 --- /dev/null +++ b/.github/docs/flows/user-budgets-month.md @@ -0,0 +1,24 @@ +# User Budgets a Month + +## Actor +App user + +## Trigger +User opens the Budget screen + +## Steps +| Step | Action | Screen/Widget | Provider | DAO | Tables written | +| --- | --- | --- | --- | --- | --- | +| 1 | Open Budget screen | BudgetScreen | budgetProvider | BudgetDao.watchMonthlyBudgets | _ | +| 2 | TBB banner shows available funds | TbbBanner | budgetCalculatorProvider | _ | _ | +| 3 | Tap a category row, enter assigned amount | CategoryRow | budgetProvider | BudgetDao.upsertMonthlyBudget | monthly_budgets | +| 4 | TBB updates in real time | TbbBanner | budgetCalculatorProvider (stream) | _ | _ | +| 5 | Navigate to previous/next month | BudgetScreen | monthBoundaryProvider | _ | _ | + +## Happy path end state +A `monthly_budgets` row is created or updated for the category + month key. TBB (To Be Budgeted) value decreases by the assigned amount. + +## Edge cases +- Over-assignment makes TBB negative — user is warned via TBB banner colour change +- Month rollover: remaining budget can roll over to next month if category has rollover=true +- Recurring transactions due this month shown via RecurringDueBanner diff --git a/.github/docs/flows/user-imports-csv.md b/.github/docs/flows/user-imports-csv.md new file mode 100644 index 0000000..aed1a9c --- /dev/null +++ b/.github/docs/flows/user-imports-csv.md @@ -0,0 +1,25 @@ +# User Imports Transactions from CSV + +## Actor +App user + +## Trigger +User navigates to Import CSV screen and selects a file + +## Steps +| Step | Action | Screen/Widget | Provider | DAO | Tables written | +| --- | --- | --- | --- | --- | --- | +| 1 | Open Import CSV screen | ImportCsvScreen | _ | _ | _ | +| 2 | Select CSV file from device | ImportCsvScreen (file picker) | _ | _ | _ | +| 3 | CSV parsed into transaction objects | ImportCsvScreen | _ | CsvParser.parse | _ | +| 4 | Preview shown, user confirms | ImportCsvScreen | _ | _ | _ | +| 5 | Transactions inserted in batch | ImportCsvScreen | transactionsProvider | TransactionsDao.insertTransaction (×N) | transactions | +| 6 | Transaction list refreshes | TransactionsScreen | transactionsProvider (stream) | _ | _ | + +## Happy path end state +N new transaction rows inserted into `transactions`, each with `importedFrom` set to the CSV filename. Account balances updated. + +## Edge cases +- Invalid CSV format: parser returns error, no rows inserted +- Duplicate detection: `importedFrom` field identifies source file but no deduplication is enforced by default +- Missing category: rows import without a category (uncategorised) diff --git a/.github/docs/flows/user-records-transaction.md b/.github/docs/flows/user-records-transaction.md new file mode 100644 index 0000000..40c2570 --- /dev/null +++ b/.github/docs/flows/user-records-transaction.md @@ -0,0 +1,24 @@ +# User Records a Transaction + +## Actor +App user + +## Trigger +User taps 'Add Transaction' button on the Transactions screen + +## Steps +| Step | Action | Screen/Widget | Provider | DAO | Tables written | +| --- | --- | --- | --- | --- | --- | +| 1 | Tap 'Add Transaction' | TransactionsScreen | _ | _ | _ | +| 2 | Select account, type (income/expense/transfer), amount, payee, category | AddTransactionScreen | _ | _ | _ | +| 3 | Tap 'Save' | AddTransactionScreen | transactionsProvider | TransactionsDao.insertTransaction | transactions | +| 4 | Account balance updated | _ | accountsProvider | AccountsDao.updateAccount | accounts | +| 5 | Transaction list refreshes | TransactionsScreen | transactionsProvider (stream) | TransactionsDao.watchTransactions | _ | + +## Happy path end state +New transaction row in `transactions`. Account `balanceCents` updated. Reactive streams update all watchers. + +## Edge cases +- Transfer type requires selecting a destination account +- Category is optional for income/expense (uncategorised transactions are allowed) +- Recurring flag schedules a future entry in pending_recurring_queue diff --git a/.github/docs/flows/user-sets-goal.md b/.github/docs/flows/user-sets-goal.md new file mode 100644 index 0000000..53eeeb9 --- /dev/null +++ b/.github/docs/flows/user-sets-goal.md @@ -0,0 +1,25 @@ +# User Sets a Financial Goal + +## Actor +App user + +## Trigger +User taps 'Add Goal' on the Goals screen or edits a category's goal + +## Steps +| Step | Action | Screen/Widget | Provider | DAO | Tables written | +| --- | --- | --- | --- | --- | --- | +| 1 | Open Goals screen | GoalsScreen | goalsProvider | CategoriesDao.watchAllCategories | _ | +| 2 | Tap 'Add Goal' | GoalsScreen | _ | _ | _ | +| 3 | Select category, enter target amount and date | AddGoalScreen | _ | _ | _ | +| 4 | Tap 'Save' | AddGoalScreen | goalsProvider | CategoriesDao.updateCategory | categories | +| 5 | Goal progress calculated | GoalsScreen | goalCalculatorProvider | _ | _ | +| 6 | Goals list refreshes | GoalsScreen | goalsProvider (stream) | _ | _ | + +## Happy path end state +The `categories` row for the selected category is updated with `goalAmountCents`, `goalDate`, and `goalType`. Goal progress is computed and displayed. + +## Edge cases +- Goal date must be in the future +- Target amount must be > 0 +- If no monthly budget is assigned, goal completion will take longer than projected diff --git a/.github/docs/flows/user-syncs-data.md b/.github/docs/flows/user-syncs-data.md new file mode 100644 index 0000000..658fe07 --- /dev/null +++ b/.github/docs/flows/user-syncs-data.md @@ -0,0 +1,26 @@ +# User Syncs Data to Firestore + +## Actor +App user + +## Trigger +User taps 'Sync Now' in Settings screen + +## Steps +| Step | Action | Screen/Widget | Provider | DAO | Tables written | +| --- | --- | --- | --- | --- | --- | +| 1 | Tap 'Sync Now' | SettingsScreen | syncServiceProvider | _ | _ | +| 2 | Auth check: user must be signed in | _ | SyncService | _ | _ | +| 3 | Read all accounts from local DB | _ | SyncService | AccountsDao.getAllAccounts | accounts | +| 4 | Read all categories from local DB | _ | SyncService | CategoriesDao.getAllCategories | categories | +| 5 | Read all transactions from local DB | _ | SyncService | TransactionsDao.getAllTransactions | transactions | +| 6 | Batch write to Firestore under users/{uid}/... | _ | SyncService (Firestore batch) | _ | _ | +| 7 | Last sync timestamp stored in secure storage | _ | lastSyncProvider | _ | _ | + +## Happy path end state +Firestore collections `accounts`, `categoryGroups`, `categories`, `transactions` under `users/{uid}` are overwritten with current local state. `last_sync_timestamp` updated in secure storage. + +## Edge cases +- Not signed in: sync returns false, no data written +- Firebase unavailable (offline): Firestore SDK queues writes for later delivery +- Large dataset: Firestore batch limit is 500 operations — may require chunking for large transaction sets diff --git a/.github/docs/intermediate/business_intent.json b/.github/docs/intermediate/business_intent.json new file mode 100644 index 0000000..3ad5acc --- /dev/null +++ b/.github/docs/intermediate/business_intent.json @@ -0,0 +1,254 @@ +[ + { + "feature": "accounts", + "feature_name": "Account Management", + "description": "Allows users to create and manage their financial accounts (checking, savings, credit cards, cash). Users can add accounts with opening balances, view account details and transaction history, and soft-delete accounts they no longer need.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Add a new account with name, type, and opening balance", + "View account details and running balance", + "View transactions for a specific account", + "Edit or delete an account" + ], + "tables_read": [ + "accounts", + "transactions" + ], + "tables_written": [ + "accounts" + ], + "depends_on_features": [], + "confidence": "high", + "gaps": [] + }, + { + "feature": "analytics", + "feature_name": "Analytics & Budget History", + "description": "Provides charts and historical views of spending by category and income vs expenses over time. Also shows monthly budget snapshots so users can track their budgeting performance across months.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "View spending breakdown by category (pie/bar chart)", + "View income vs expense chart", + "Browse historical monthly budget snapshots" + ], + "tables_read": [ + "transactions", + "categories", + "budget_snapshots", + "monthly_budgets" + ], + "tables_written": [], + "depends_on_features": [ + "budget", + "transactions" + ], + "confidence": "high", + "gaps": [] + }, + { + "feature": "auth", + "feature_name": "Authentication & Biometric Lock", + "description": "Handles user identity via Firebase Auth (email/Google sign-in) and provides a local biometric lock screen that gates access to the app on app launch or resume. Auth state is exposed as a Riverpod provider.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Sign in with Google or email", + "Sign out", + "Unlock app with biometrics (fingerprint / face ID)" + ], + "tables_read": [], + "tables_written": [], + "depends_on_features": [], + "confidence": "high", + "gaps": [] + }, + { + "feature": "budget", + "feature_name": "Budget Management", + "description": "Allows users to allocate available funds across spending categories for a given month using an envelope-budgeting (YNAB-style) approach. Users assign amounts to categories, see their To Be Budgeted balance update in real time, rebalance overspent categories, and review recurring transactions due this month.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Assign funds to a spending category", + "Rebalance overspent categories", + "Review and approve recurring transactions", + "View remaining To Be Budgeted amount", + "Navigate between months" + ], + "tables_read": [ + "monthly_budgets", + "categories", + "transactions", + "accounts" + ], + "tables_written": [ + "monthly_budgets", + "budget_snapshots" + ], + "depends_on_features": [ + "accounts", + "transactions" + ], + "confidence": "high", + "gaps": [] + }, + { + "feature": "goals", + "feature_name": "Financial Goals", + "description": "Lets users set savings goals on categories (target amount + target date). A calculator computes progress and estimated monthly contribution required to reach each goal.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Create a savings goal on a category", + "View goal progress and projected completion date", + "Edit or remove a goal" + ], + "tables_read": [ + "categories", + "monthly_budgets" + ], + "tables_written": [ + "categories" + ], + "depends_on_features": [ + "budget" + ], + "confidence": "high", + "gaps": [] + }, + { + "feature": "onboarding", + "feature_name": "Onboarding", + "description": "First-run flow that walks new users through setting up their first account and initial budget categories so the app is ready to use. Completion state is persisted and controls whether the app shows the main shell or the onboarding screen.", + "user_roles": [ + "new user" + ], + "user_actions": [ + "Complete first-time setup wizard", + "Add first account", + "Accept default budget categories" + ], + "tables_read": [], + "tables_written": [ + "accounts", + "categories" + ], + "depends_on_features": [ + "accounts" + ], + "confidence": "medium", + "gaps": [ + "Exact onboarding steps depend on screen implementation detail" + ] + }, + { + "feature": "settings", + "feature_name": "Settings", + "description": "App settings screen. Allows users to configure preferences such as currency display, theme, and manage their account (sign out, delete data).", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Change currency or number format", + "Toggle dark/light theme", + "Sign out", + "Trigger manual data sync" + ], + "tables_read": [], + "tables_written": [], + "depends_on_features": [ + "auth", + "sync" + ], + "confidence": "medium", + "gaps": [ + "Settings screen detail depends on runtime implementation" + ] + }, + { + "feature": "shell", + "feature_name": "Navigation Shell", + "description": "The main navigation scaffold housing the bottom navigation bar. Routes between Accounts, Budget, Transactions, Analytics, and Goals features.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Switch between main app sections via bottom navigation" + ], + "tables_read": [], + "tables_written": [], + "depends_on_features": [ + "accounts", + "budget", + "transactions", + "analytics", + "goals" + ], + "confidence": "high", + "gaps": [] + }, + { + "feature": "sync", + "feature_name": "Cloud Sync", + "description": "Bridges local Drift/SQLite data to Firestore for cloud backup. On sync trigger, reads all accounts, category groups, categories, monthly budgets, and transactions from the local DB and batch-writes them to a per-user Firestore document tree. One-directional (local → cloud).", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Trigger manual sync from settings", + "View last sync timestamp" + ], + "tables_read": [ + "accounts", + "category_groups", + "categories", + "monthly_budgets", + "transactions" + ], + "tables_written": [], + "depends_on_features": [ + "accounts", + "budget", + "transactions" + ], + "confidence": "high", + "gaps": [] + }, + { + "feature": "transactions", + "feature_name": "Transaction Management", + "description": "Core feature for recording and reviewing financial transactions. Users can add income, expense, and transfer transactions, categorise them, mark them as cleared, and import bulk transactions from CSV files. Transaction list is filterable by account and month.", + "user_roles": [ + "app user" + ], + "user_actions": [ + "Add a new income, expense, or transfer transaction", + "Assign a category to a transaction", + "Mark a transaction as cleared", + "Import transactions from a CSV file", + "View transaction list filtered by account or month", + "Edit or delete an existing transaction" + ], + "tables_read": [ + "transactions", + "accounts", + "categories" + ], + "tables_written": [ + "transactions", + "accounts" + ], + "depends_on_features": [ + "accounts" + ], + "confidence": "high", + "gaps": [] + } +] diff --git a/.github/docs/intermediate/data_model.json b/.github/docs/intermediate/data_model.json new file mode 100644 index 0000000..142200a --- /dev/null +++ b/.github/docs/intermediate/data_model.json @@ -0,0 +1,775 @@ +{ + "local_database": "sqlite_via_drift", + "tables": [ + { + "drift_class": "Accounts", + "table_name": "accounts", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "name", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "type", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "balanceCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "institution", + "type": "TextColumn", + "drift_type": "text", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "createdAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "updatedAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "isDeleted", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [], + "dao": "AccountsDao", + "dao_file": "lib/core/database/daos/accounts_dao.dart", + "queries": [ + { + "method": "insertAccount", + "operation": "INSERT", + "returns": "Future", + "reactive": false, + "params": "AccountsCompanion entry" + }, + { + "method": "getAccount", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "int id" + }, + { + "method": "updateAccount", + "operation": "UPDATE", + "returns": "Future", + "reactive": false, + "params": "AccountsCompanion entry" + }, + { + "method": "softDeleteAccount", + "operation": "DELETE", + "returns": "Future", + "reactive": false, + "params": "int id" + } + ], + "firestore_collection": "accounts", + "sync_direction": "local_to_cloud" + }, + { + "drift_class": "CategoryGroups", + "table_name": "category_groups", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "name", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "sortOrder", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "isDeleted", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [], + "dao": null, + "dao_file": null, + "queries": [], + "firestore_collection": "categoryGroups", + "sync_direction": "local_to_cloud" + }, + { + "drift_class": "Categories", + "table_name": "categories", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "groupId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": "CategoryGroups" + }, + { + "name": "name", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "rollover", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "goalAmountCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "goalDate", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "goalType", + "type": "TextColumn", + "drift_type": "text", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "sortOrder", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "createdAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "updatedAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "isDeleted", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [], + "dao": "CategoriesDao", + "dao_file": "lib/core/database/daos/categories_dao.dart", + "queries": [ + { + "method": "insertGroup", + "operation": "INSERT", + "returns": "Future", + "reactive": false, + "params": "String name" + }, + { + "method": "insertCategory", + "operation": "INSERT", + "returns": "Future", + "reactive": false, + "params": "CategoriesCompanion entry" + }, + { + "method": "getCategory", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "int id" + }, + { + "method": "softDeleteCategory", + "operation": "DELETE", + "returns": "Future", + "reactive": false, + "params": "int id" + }, + { + "method": "updateRollover", + "operation": "UPDATE", + "returns": "Future", + "reactive": false, + "params": "int categoryId, bool rollover" + }, + { + "method": "softDeleteGroup", + "operation": "DELETE", + "returns": "Future", + "reactive": false, + "params": "int id" + } + ], + "firestore_collection": "categories", + "sync_direction": "local_to_cloud" + }, + { + "drift_class": "MonthlyBudgets", + "table_name": "monthly_budgets", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "categoryId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": "Categories" + }, + { + "name": "month", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "assignedCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "rolledOverCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "updatedAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [ + "categoryId, month" + ], + "dao": "BudgetDao", + "dao_file": "lib/core/database/daos/budget_dao.dart", + "queries": [ + { + "method": "getBudgetForCategoryMonth", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "int categoryId,\n String month," + }, + { + "method": "upsertBudget", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "MonthlyBudgetsCompanion entry" + } + ], + "firestore_collection": null, + "sync_direction": "none" + }, + { + "drift_class": "Transactions", + "table_name": "transactions", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "accountId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": "Accounts" + }, + { + "name": "categoryId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": true, + "primary_key": false, + "references": "Categories" + }, + { + "name": "amountCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "payee", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "date", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "memo", + "type": "TextColumn", + "drift_type": "text", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "type", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "cleared", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "recurring", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "recurringInterval", + "type": "TextColumn", + "drift_type": "text", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "nextDueDate", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "importedFrom", + "type": "TextColumn", + "drift_type": "text", + "nullable": true, + "primary_key": false, + "references": null + }, + { + "name": "createdAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "updatedAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "toAccountId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": true, + "primary_key": false, + "references": "Accounts" + }, + { + "name": "isDeleted", + "type": "BoolColumn", + "drift_type": "boolean", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [], + "dao": "TransactionsDao", + "dao_file": "lib/core/database/daos/transactions_dao.dart", + "queries": [ + { + "method": "insertTransaction", + "operation": "INSERT", + "returns": "Future", + "reactive": false, + "params": "TransactionsCompanion entry" + }, + { + "method": "getTransaction", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "int id" + }, + { + "method": "updateTransaction", + "operation": "UPDATE", + "returns": "Future", + "reactive": false, + "params": "int id, TransactionsCompanion data" + }, + { + "method": "softDelete", + "operation": "DELETE", + "returns": "Future", + "reactive": false, + "params": "int id" + } + ], + "firestore_collection": "transactions", + "sync_direction": "local_to_cloud" + }, + { + "drift_class": "NetWorthSnapshots", + "table_name": "net_worth_snapshots", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "date", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "totalAssetsCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "totalLiabilitiesCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "netWorthCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [], + "dao": null, + "dao_file": null, + "queries": [], + "firestore_collection": null, + "sync_direction": "none" + }, + { + "drift_class": "BudgetSnapshots", + "table_name": "budget_snapshots", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "categoryId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": "Categories" + }, + { + "name": "month", + "type": "TextColumn", + "drift_type": "text", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "assignedCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "spentCents", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "createdAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [ + "categoryId, month" + ], + "dao": "BudgetSnapshotsDao", + "dao_file": "lib/core/database/daos/budget_snapshots_dao.dart", + "queries": [ + { + "method": "upsertSnapshot", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "BudgetSnapshotsCompanion entry" + }, + { + "method": "hasSnapshot", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "int categoryId, String month" + } + ], + "firestore_collection": null, + "sync_direction": "none" + }, + { + "drift_class": "PendingRecurringQueue", + "table_name": "pending_recurring_queue", + "columns": [ + { + "name": "id", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": true, + "references": null + }, + { + "name": "sourceTransactionId", + "type": "IntColumn", + "drift_type": "integer", + "nullable": false, + "primary_key": false, + "references": "Transactions" + }, + { + "name": "dueDate", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + }, + { + "name": "createdAt", + "type": "DateTimeColumn", + "drift_type": "datetime", + "nullable": false, + "primary_key": false, + "references": null + } + ], + "unique_keys": [], + "dao": "RecurringQueueDao", + "dao_file": "lib/core/database/daos/recurring_queue_dao.dart", + "queries": [ + { + "method": "enqueue", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "PendingRecurringQueueCompanion entry" + }, + { + "method": "removeFromQueue", + "operation": "DELETE", + "returns": "Future", + "reactive": false, + "params": "int id" + }, + { + "method": "clearAll", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": null + }, + { + "method": "isEnqueued", + "operation": "SELECT", + "returns": "Future", + "reactive": false, + "params": "int sourceTransactionId" + } + ], + "firestore_collection": null, + "sync_direction": "none" + } + ], + "firestore_collections": [ + { + "collection": "accounts", + "maps_to_table": "accounts", + "sync_direction": "local_to_cloud" + }, + { + "collection": "categoryGroups", + "maps_to_table": "category_groups", + "sync_direction": "local_to_cloud" + }, + { + "collection": "categories", + "maps_to_table": "categories", + "sync_direction": "local_to_cloud" + }, + { + "collection": "transactions", + "maps_to_table": "transactions", + "sync_direction": "local_to_cloud" + } + ] +} diff --git a/.github/docs/intermediate/dependency_graph.json b/.github/docs/intermediate/dependency_graph.json new file mode 100644 index 0000000..594ba50 --- /dev/null +++ b/.github/docs/intermediate/dependency_graph.json @@ -0,0 +1,184 @@ +{ + "critical_path_files": [ + "lib/core/database/database.dart", + "lib/core/database/providers.dart", + "lib/core/utils/currency_formatter.dart", + "lib/features/budget/budget_providers.dart", + "lib/core/database/tables.dart", + "lib/features/accounts/account_providers.dart", + "lib/features/analytics/analytics_providers.dart", + "lib/core/services/month_boundary_service.dart" + ], + "cross_feature_imports": [ + { + "from": "lib/features/accounts/account_detail_screen.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/accounts/account_detail_screen.dart", + "to": "lib/features/transactions/add_transaction_screen.dart" + }, + { + "from": "lib/features/accounts/account_detail_screen.dart", + "to": "lib/features/transactions/widgets/transaction_tile.dart" + }, + { + "from": "lib/features/accounts/accounts_screen.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/accounts/add_account_screen.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/analytics/analytics_providers.dart", + "to": "lib/features/budget/budget_providers.dart" + }, + { + "from": "lib/features/analytics/budget_history_screen.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/analytics/widgets/income_vs_expenses_chart.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/analytics/widgets/spending_by_category_chart.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/budget/budget_screen.dart", + "to": "lib/features/goals/goals_screen.dart" + }, + { + "from": "lib/features/budget/budget_screen.dart", + "to": "lib/core/services/rollover_provider.dart" + }, + { + "from": "lib/features/budget/widgets/category_row.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/budget/widgets/recurring_review_sheet.dart", + "to": "lib/core/services/month_boundary_service.dart" + }, + { + "from": "lib/features/budget/widgets/recurring_review_sheet.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/budget/widgets/tbb_banner.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/core/csv/csv_parser.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/goals/add_goal_screen.dart", + "to": "lib/features/budget/budget_providers.dart" + }, + { + "from": "lib/features/goals/goals_screen.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/onboarding/onboarding_screen.dart", + "to": "lib/features/shell/main_shell.dart" + }, + { + "from": "lib/features/settings/settings_screen.dart", + "to": "lib/core/services/rollover_provider.dart" + }, + { + "from": "lib/features/settings/settings_screen.dart", + "to": "lib/features/auth/auth_providers.dart" + }, + { + "from": "lib/features/settings/settings_screen.dart", + "to": "lib/features/auth/firebase_auth_service.dart" + }, + { + "from": "lib/features/settings/settings_screen.dart", + "to": "lib/features/sync/sync_service.dart" + }, + { + "from": "lib/features/shell/main_shell.dart", + "to": "lib/features/budget/budget_screen.dart" + }, + { + "from": "lib/features/shell/main_shell.dart", + "to": "lib/features/transactions/transactions_screen.dart" + }, + { + "from": "lib/features/shell/main_shell.dart", + "to": "lib/features/accounts/accounts_screen.dart" + }, + { + "from": "lib/features/shell/main_shell.dart", + "to": "lib/features/analytics/analytics_screen.dart" + }, + { + "from": "lib/features/shell/main_shell.dart", + "to": "lib/features/settings/settings_screen.dart" + }, + { + "from": "lib/features/transactions/add_transaction_screen.dart", + "to": "lib/core/services/month_boundary_service.dart" + }, + { + "from": "lib/features/transactions/add_transaction_screen.dart", + "to": "lib/core/utils/currency_formatter.dart" + }, + { + "from": "lib/features/transactions/add_transaction_screen.dart", + "to": "lib/features/budget/budget_providers.dart" + }, + { + "from": "lib/features/transactions/import_csv_screen.dart", + "to": "lib/core/csv/csv_parser.dart" + }, + { + "from": "lib/features/transactions/transactions_screen.dart", + "to": "lib/features/accounts/account_providers.dart" + }, + { + "from": "lib/features/transactions/transactions_screen.dart", + "to": "lib/features/budget/budget_providers.dart" + }, + { + "from": "lib/features/transactions/widgets/transaction_tile.dart", + "to": "lib/core/utils/currency_formatter.dart" + } + ], + "provider_chains": [ + { + "screen": "lib/features/accounts/account_detail_screen.dart", + "watches": [ + "accountsProvider", + "accountRunningBalancesProvider", + "txStream" + ] + } + ], + "sync_boundary": { + "file": "lib/features/sync/sync_service.dart", + "synced_collections": [ + "accounts", + "categoryGroups", + "categories", + "transactions" + ], + "synced_tables": [ + "accounts", + "category_groups", + "categories", + "monthly_budgets", + "transactions" + ], + "synced_providers": [ + "syncServiceProvider", + "lastSyncProvider" + ] + } +} diff --git a/.github/docs/intermediate/logic_summaries/accounts.json b/.github/docs/intermediate/logic_summaries/accounts.json new file mode 100644 index 0000000..79b82d9 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/accounts.json @@ -0,0 +1,81 @@ +[ + { + "file": "lib/features/accounts/account_detail_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: AccountDetailScreen. Providers: txStream.", + "responsibilities": [ + "Defines class(es): AccountDetailScreen", + "Exposes methods: build" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "txStreamProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/accounts/account_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Providers: accountsProvider, _allTransactionsStreamProvider, accountRunningBalancesProvider.", + "responsibilities": [ + "Provides functionality for the account_providers.dart module" + ], + "inputs": [ + "ref", + "ref", + "ref" + ], + "outputs": [ + "accountsProviderProvider", + "_allTransactionsStreamProviderProvider", + "accountRunningBalancesProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/accounts/accounts_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: AccountsScreen.", + "responsibilities": [ + "Defines class(es): AccountsScreen", + "Exposes methods: build, Expanded" + ], + "inputs": [], + "outputs": [ + "Widget", + ",", + "IconData" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/accounts/add_account_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: AddAccountScreen, _AddAccountScreenState.", + "responsibilities": [ + "Defines class(es): AddAccountScreen, _AddAccountScreenState", + "Exposes methods: initState, dispose, build" + ], + "inputs": [], + "outputs": [ + "void", + "void", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/analytics.json b/.github/docs/intermediate/logic_summaries/analytics.json new file mode 100644 index 0000000..c7e0594 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/analytics.json @@ -0,0 +1,121 @@ +[ + { + "file": "lib/features/analytics/analytics_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Defines: CategorySpending, MonthlyTotal. Providers: spendingByCategoryProvider, monthlyTotalsProvider.", + "responsibilities": [ + "Defines class(es): CategorySpending, MonthlyTotal" + ], + "inputs": [ + "ref", + "ref" + ], + "outputs": [ + "spendingByCategoryProviderProvider", + "monthlyTotalsProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/analytics/analytics_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: AnalyticsScreen, _SpendingTab, _IncomeVsExpensesTab.", + "responsibilities": [ + "Defines class(es): AnalyticsScreen, _SpendingTab, _IncomeVsExpensesTab", + "Exposes methods: build, build, build" + ], + "inputs": [], + "outputs": [ + "Widget", + "Widget", + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/analytics/budget_history_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Defines: CategoryMonthHistory, MonthBudgetHistory. Providers: budgetHistoryProvider.", + "responsibilities": [ + "Defines class(es): CategoryMonthHistory, MonthBudgetHistory" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "budgetHistoryProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/analytics/budget_history_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: BudgetHistoryScreen, _BudgetTrendChart, _MonthTile.", + "responsibilities": [ + "Defines class(es): BudgetHistoryScreen, _BudgetTrendChart, _MonthTile", + "Exposes methods: build, build, build" + ], + "inputs": [], + "outputs": [ + "Widget", + "Widget", + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:fl_chart/fl_chart.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/analytics/widgets/income_vs_expenses_chart.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: IncomeVsExpensesChart, _Legend.", + "responsibilities": [ + "Defines class(es): IncomeVsExpensesChart, _Legend", + "Exposes methods: build, Column, build" + ], + "inputs": [], + "outputs": [ + "Widget", + "return", + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:fl_chart/fl_chart.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/analytics/widgets/spending_by_category_chart.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: SpendingByCategoryChart, _SpendingByCategoryChartState.", + "responsibilities": [ + "Defines class(es): SpendingByCategoryChart, _SpendingByCategoryChartState", + "Exposes methods: build, Column, setState" + ], + "inputs": [], + "outputs": [ + "Widget", + "return" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:fl_chart/fl_chart.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/auth.json b/.github/docs/intermediate/logic_summaries/auth.json new file mode 100644 index 0000000..7ecb7ec --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/auth.json @@ -0,0 +1,72 @@ +[ + { + "file": "lib/features/auth/auth_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Providers: localAuthProvider, biometricEnabledProvider.", + "responsibilities": [ + "Exposes methods: setBiometricEnabled" + ], + "inputs": [ + "ref", + "ref" + ], + "outputs": [ + "localAuthProviderProvider", + "biometricEnabledProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:flutter_secure_storage/flutter_secure_storage.dart", + "package:local_auth/local_auth.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/auth/biometric_lock_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: BiometricLockScreen, _LockScreen, _LockScreenState.", + "responsibilities": [ + "Defines class(es): BiometricLockScreen, _LockScreen, _LockScreenState", + "Exposes methods: build, initState, setState, setState, setState" + ], + "inputs": [], + "outputs": [ + "Widget", + "void", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:local_auth/local_auth.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/auth/firebase_auth_service.dart", + "summary": "Domain service that encapsulates business logic or integration with an external system. Invoked by providers or other services. Defines: FirebaseAuthService. Providers: firebaseUserProvider, isSignedInProvider, firebaseAuthServiceProvider.", + "responsibilities": [ + "Defines class(es): FirebaseAuthService", + "Exposes methods: signInWithGoogle, signInAnonymously, signOut" + ], + "inputs": [ + "ref", + "ref", + "ref" + ], + "outputs": [ + "firebaseUserProviderProvider", + "isSignedInProviderProvider", + "firebaseAuthServiceProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:firebase_auth/firebase_auth.dart", + "package:google_sign_in/google_sign_in.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/budget.json b/.github/docs/intermediate/logic_summaries/budget.json new file mode 100644 index 0000000..5486269 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/budget.json @@ -0,0 +1,188 @@ +[ + { + "file": "lib/features/budget/budget_calculator.dart", + "summary": "Pure computation class. Takes data as input parameters and returns computed values. Contains no side effects, database access, or UI concerns. Defines: BudgetCalculator.", + "responsibilities": [ + "Defines class(es): BudgetCalculator", + "Exposes methods: available, toBeBudgeted, ageOfMoney" + ], + "inputs": [], + "outputs": [ + "static int", + "static int", + "static int" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/budget_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Providers: categoryGroupsProvider, allCategoriesProvider, monthlyBudgetsProvider.", + "responsibilities": [ + "Provides functionality for the budget_providers.dart module" + ], + "inputs": [ + "ref", + "ref", + "ref" + ], + "outputs": [ + "categoryGroupsProviderProvider", + "allCategoriesProviderProvider", + "monthlyBudgetsProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/budget_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: BudgetScreen, _GroupTile.", + "responsibilities": [ + "Defines class(es): BudgetScreen, _GroupTile", + "Exposes methods: build, Consumer, Expanded, FilledButton, build" + ], + "inputs": [], + "outputs": [ + "Widget", + "[", + "," + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "medium", + "confidence": "medium" + }, + { + "file": "lib/features/budget/rebalance_provider.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Defines: CategoryBudgetData, RebalanceSuggestion. Providers: rebalanceSuggestionsProvider.", + "responsibilities": [ + "Defines class(es): CategoryBudgetData, RebalanceSuggestion", + "Exposes methods: computeRebalanceSuggestions" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "rebalanceSuggestionsProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/recurring_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Providers: pendingRecurringProvider.", + "responsibilities": [ + "Provides functionality for the recurring_providers.dart module" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "pendingRecurringProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/widgets/category_row.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: CategoryRow.", + "responsibilities": [ + "Defines class(es): CategoryRow", + "Exposes methods: build" + ], + "inputs": [], + "outputs": [ + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/widgets/rebalance_sheet.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: RebalanceSheet, _RebalanceSheetState.", + "responsibilities": [ + "Defines class(es): RebalanceSheet, _RebalanceSheetState", + "Exposes methods: build, Expanded" + ], + "inputs": [], + "outputs": [ + "Widget", + ",", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/widgets/recurring_due_banner.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: RecurringDueBanner.", + "responsibilities": [ + "Defines class(es): RecurringDueBanner", + "Exposes methods: build" + ], + "inputs": [], + "outputs": [ + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/widgets/recurring_review_sheet.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: RecurringReviewSheet.", + "responsibilities": [ + "Defines class(es): RecurringReviewSheet", + "Exposes methods: build, Expanded" + ], + "inputs": [], + "outputs": [ + "Future", + "Future", + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/budget/widgets/tbb_banner.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: ToBeBudgetedBanner.", + "responsibilities": [ + "Defines class(es): ToBeBudgetedBanner", + "Exposes methods: build" + ], + "inputs": [], + "outputs": [ + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/csv.json b/.github/docs/intermediate/logic_summaries/csv.json new file mode 100644 index 0000000..9dd2940 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/csv.json @@ -0,0 +1,23 @@ +[ + { + "file": "lib/core/csv/csv_parser.dart", + "summary": "File or data parser. Reads raw input (CSV, JSON) and transforms it into typed Dart objects suitable for database insertion. Defines: ParsedTransaction, ValidationResult, CsvParser.", + "responsibilities": [ + "Defines class(es): ParsedTransaction, ValidationResult, CsvParser", + "Exposes methods: parse, detectHeaders, toTransactionData, validate" + ], + "inputs": [], + "outputs": [ + "static List>", + "static List", + "static ParsedTransaction" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:csv/csv.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/database.json b/.github/docs/intermediate/logic_summaries/database.json new file mode 100644 index 0000000..e61e487 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/database.json @@ -0,0 +1,160 @@ +[ + { + "file": "lib/core/database/daos/accounts_dao.dart", + "summary": "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot).", + "responsibilities": [ + "Provides functionality for the accounts_dao.dart module" + ], + "inputs": [], + "outputs": [], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/daos/budget_dao.dart", + "summary": "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot).", + "responsibilities": [ + "Provides functionality for the budget_dao.dart module" + ], + "inputs": [], + "outputs": [], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/daos/budget_snapshots_dao.dart", + "summary": "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot).", + "responsibilities": [ + "Exposes methods: getSnapshotMonths, hasSnapshot" + ], + "inputs": [], + "outputs": [ + "Future>", + "Future" + ], + "database_operations": [ + "getSnapshotMonths", + "hasSnapshot" + ], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/daos/categories_dao.dart", + "summary": "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot).", + "responsibilities": [ + "Exposes methods: softDeleteGroup" + ], + "inputs": [], + "outputs": [ + "Future" + ], + "database_operations": [ + "softDeleteGroup" + ], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/daos/recurring_queue_dao.dart", + "summary": "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot).", + "responsibilities": [ + "Exposes methods: isEnqueued" + ], + "inputs": [], + "outputs": [ + "Future" + ], + "database_operations": [ + "isEnqueued" + ], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/daos/transactions_dao.dart", + "summary": "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot).", + "responsibilities": [ + "Exposes methods: watchTransactionsForMonth", + "Reactive streams: watchTransactionsForMonth" + ], + "inputs": [], + "outputs": [ + "Stream>" + ], + "database_operations": [ + "watchTransactionsForMonth" + ], + "reactive_streams": [ + "watchTransactionsForMonth" + ], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/database.dart", + "summary": "Application-level configuration. Provides theme definitions, database instance, or Firebase options. Referenced globally throughout the app.", + "responsibilities": [ + "Exposes methods: MigrationStrategy, LazyDatabase" + ], + "inputs": [], + "outputs": [ + ">", + "LazyDatabase", + "return" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:path/path.dart", + "package:path_provider/path_provider.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/providers.dart", + "summary": "Source file in the moneyinsight application. Providers: databaseProvider.", + "responsibilities": [ + "Provides functionality for the providers.dart module" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "databaseProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/database/tables.dart", + "summary": "Data model or Drift table definition. Describes the shape of a data entity including column types, constraints, foreign keys, and indexes. Defines: Accounts, CategoryGroups, Categories.", + "responsibilities": [ + "Defines class(es): Accounts, CategoryGroups, Categories" + ], + "inputs": [], + "outputs": [], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/goals.json b/.github/docs/intermediate/logic_summaries/goals.json new file mode 100644 index 0000000..9928f45 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/goals.json @@ -0,0 +1,78 @@ +[ + { + "file": "lib/features/goals/add_goal_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: AddGoalScreen, _AddGoalScreenState.", + "responsibilities": [ + "Defines class(es): AddGoalScreen, _AddGoalScreenState", + "Exposes methods: dispose, build" + ], + "inputs": [], + "outputs": [ + "void", + "Future", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/goals/goal_calculator.dart", + "summary": "Pure computation class. Takes data as input parameters and returns computed values. Contains no side effects, database access, or UI concerns. Defines: GoalCalculator.", + "responsibilities": [ + "Defines class(es): GoalCalculator", + "Exposes methods: progressPercent, projectedDate" + ], + "inputs": [], + "outputs": [ + "static double", + "static DateTime" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/goals/goals_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Defines: GoalProgress. Providers: goalsProvider.", + "responsibilities": [ + "Defines class(es): GoalProgress" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "goalsProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/goals/goals_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: GoalsScreen, _EmptyState, _GoalCard.", + "responsibilities": [ + "Defines class(es): GoalsScreen, _EmptyState, _GoalCard", + "Exposes methods: build, build, build" + ], + "inputs": [], + "outputs": [ + "Widget", + "Widget", + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:intl/intl.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/onboarding.json b/.github/docs/intermediate/logic_summaries/onboarding.json new file mode 100644 index 0000000..6ec705f --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/onboarding.json @@ -0,0 +1,41 @@ +[ + { + "file": "lib/features/onboarding/onboarding_providers.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Providers: onboardingCompleteProvider.", + "responsibilities": [ + "Exposes methods: setOnboardingComplete, saveMonthlyIncome" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "onboardingCompleteProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:flutter_secure_storage/flutter_secure_storage.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/onboarding/onboarding_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: OnboardingScreen, _OnboardingScreenState, _WelcomePage.", + "responsibilities": [ + "Defines class(es): OnboardingScreen, _OnboardingScreenState, _WelcomePage", + "Exposes methods: dispose, build, setState, setState, setState" + ], + "inputs": [], + "outputs": [ + "void", + "void", + "void" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "medium", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/root.json b/.github/docs/intermediate/logic_summaries/root.json new file mode 100644 index 0000000..4d513de --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/root.json @@ -0,0 +1,18 @@ +[ + { + "summary": "Application entry point. Initialises Firebase, wraps the widget tree in ProviderScope (Riverpod), and delegates startup routing to _AppStartup which checks onboarding completion and triggers month-boundary logic.", + "responsibilities": [ + "Bootstrap Firebase", + "Wrap app in ProviderScope", + "Route to onboarding or main shell", + "Trigger month boundary checks" + ], + "inputs": [], + "outputs": [ + "Widget tree" + ], + "complexity_flag": "low", + "confidence": "high", + "file": "lib/main.dart" + } +] diff --git a/.github/docs/intermediate/logic_summaries/services.json b/.github/docs/intermediate/logic_summaries/services.json new file mode 100644 index 0000000..01426ae --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/services.json @@ -0,0 +1,63 @@ +[ + { + "file": "lib/core/services/month_boundary_provider.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Providers: monthBoundaryServiceProvider.", + "responsibilities": [ + "Provides functionality for the month_boundary_provider.dart module" + ], + "inputs": [ + "ref" + ], + "outputs": [ + "monthBoundaryServiceProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:flutter_secure_storage/flutter_secure_storage.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/services/month_boundary_service.dart", + "summary": "Domain service that encapsulates business logic or integration with an external system. Invoked by providers or other services. Defines: MonthBoundaryService.", + "responsibilities": [ + "Defines class(es): MonthBoundaryService", + "Exposes methods: computeRolloverCents, computeNextDueDate, run" + ], + "inputs": [], + "outputs": [ + "static int", + "static DateTime", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:flutter_secure_storage/flutter_secure_storage.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/core/services/rollover_provider.dart", + "summary": "Riverpod state management unit. Exposes reactive data or async results to the UI layer. Typically reads from a DAO or another provider and transforms the data. Defines: GlobalRolloverNotifier.", + "responsibilities": [ + "Defines class(es): GlobalRolloverNotifier", + "Exposes methods: build, setEnabled" + ], + "inputs": [], + "outputs": [ + "Future", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:flutter_secure_storage/flutter_secure_storage.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/settings.json b/.github/docs/intermediate/logic_summaries/settings.json new file mode 100644 index 0000000..a2a0989 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/settings.json @@ -0,0 +1,23 @@ +[ + { + "file": "lib/features/settings/settings_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: SettingsScreen, _SettingsScreenState, _SectionHeader.", + "responsibilities": [ + "Defines class(es): SettingsScreen, _SettingsScreenState, _SectionHeader", + "Exposes methods: build, build" + ], + "inputs": [], + "outputs": [ + "Future", + "Future", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:intl/intl.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/shell.json b/.github/docs/intermediate/logic_summaries/shell.json new file mode 100644 index 0000000..d01299b --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/shell.json @@ -0,0 +1,19 @@ +[ + { + "file": "lib/features/shell/main_shell.dart", + "summary": "Navigation scaffold. Defines the top-level navigation structure (bottom nav, tab bar) and controls which feature screen is shown based on the selected destination. Defines: MainShell, _MainShellState.", + "responsibilities": [ + "Defines class(es): MainShell, _MainShellState", + "Exposes methods: build" + ], + "inputs": [], + "outputs": [ + "Widget" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/sync.json b/.github/docs/intermediate/logic_summaries/sync.json new file mode 100644 index 0000000..080f285 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/sync.json @@ -0,0 +1,27 @@ +[ + { + "file": "lib/features/sync/sync_service.dart", + "summary": "Domain service that encapsulates business logic or integration with an external system. Invoked by providers or other services. Defines: SyncService. Providers: lastSyncProvider, syncServiceProvider.", + "responsibilities": [ + "Defines class(es): SyncService", + "Exposes methods: syncToFirestore" + ], + "inputs": [ + "ref", + "ref" + ], + "outputs": [ + "lastSyncProviderProvider", + "syncServiceProviderProvider" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:flutter_secure_storage/flutter_secure_storage.dart", + "package:firebase_auth/firebase_auth.dart", + "package:cloud_firestore/cloud_firestore.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/theme.json b/.github/docs/intermediate/logic_summaries/theme.json new file mode 100644 index 0000000..593ce98 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/theme.json @@ -0,0 +1,16 @@ +[ + { + "file": "lib/core/theme/app_theme.dart", + "summary": "Application-level configuration. Provides theme definitions, database instance, or Firebase options. Referenced globally throughout the app. Defines: AppTheme.", + "responsibilities": [ + "Defines class(es): AppTheme" + ], + "inputs": [], + "outputs": [], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/transactions.json b/.github/docs/intermediate/logic_summaries/transactions.json new file mode 100644 index 0000000..e7e1a4b --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/transactions.json @@ -0,0 +1,79 @@ +[ + { + "file": "lib/features/transactions/add_transaction_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: AddTransactionScreen, _AddTransactionScreenState.", + "responsibilities": [ + "Defines class(es): AddTransactionScreen, _AddTransactionScreenState", + "Exposes methods: initState, dispose, build" + ], + "inputs": [], + "outputs": [ + "void", + "void", + "Future" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/transactions/import_csv_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: ImportCsvScreen, _ImportCsvScreenState.", + "responsibilities": [ + "Defines class(es): ImportCsvScreen, _ImportCsvScreenState", + "Exposes methods: setState, setState, build" + ], + "inputs": [], + "outputs": [ + "Future", + "String?" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:file_picker/file_picker.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + }, + { + "file": "lib/features/transactions/transactions_screen.dart", + "summary": "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, dispatches user actions back to providers, and composes reusable widgets. Defines: TransactionsScreen, _TransactionsScreenState.", + "responsibilities": [ + "Defines class(es): TransactionsScreen, _TransactionsScreenState", + "Exposes methods: build, Row, setState, Expanded, setState" + ], + "inputs": [], + "outputs": [ + "Widget", + "Widget", + "void" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "medium", + "confidence": "medium" + }, + { + "file": "lib/features/transactions/widgets/transaction_tile.dart", + "summary": "Reusable UI component. Accepts typed parameters and renders a portion of a screen. May watch providers directly or receive data from a parent screen. Defines: TransactionTile.", + "responsibilities": [ + "Defines class(es): TransactionTile", + "Exposes methods: build" + ], + "inputs": [], + "outputs": [ + "Widget", + "String", + "String" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/logic_summaries/utils.json b/.github/docs/intermediate/logic_summaries/utils.json new file mode 100644 index 0000000..31629d5 --- /dev/null +++ b/.github/docs/intermediate/logic_summaries/utils.json @@ -0,0 +1,21 @@ +[ + { + "file": "lib/core/utils/currency_formatter.dart", + "summary": "Utility / formatting helper. Provides stateless functions for formatting, conversion, or string manipulation used across multiple features. Defines: CurrencyFormatter.", + "responsibilities": [ + "Defines class(es): CurrencyFormatter", + "Exposes methods: format" + ], + "inputs": [], + "outputs": [ + "static String" + ], + "database_operations": [], + "reactive_streams": [], + "external_calls": [ + "package:intl/intl.dart" + ], + "complexity_flag": "low", + "confidence": "medium" + } +] diff --git a/.github/docs/intermediate/signatures/accounts.json b/.github/docs/intermediate/signatures/accounts.json new file mode 100644 index 0000000..15be546 --- /dev/null +++ b/.github/docs/intermediate/signatures/accounts.json @@ -0,0 +1,337 @@ +[ + { + "path": "lib/features/accounts/account_detail_screen.dart", + "cluster": "accounts", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "../transactions/add_transaction_screen.dart", + "kind": "internal" + }, + { + "uri": "../transactions/widgets/transaction_tile.dart", + "kind": "internal" + }, + { + "uri": "account_providers.dart", + "kind": "internal" + }, + { + "uri": "add_account_screen.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "AccountDetailScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [ + { + "name": "txStream", + "type": "StreamProvider", + "watches": [ + "accountsProvider", + "accountRunningBalancesProvider", + "txStream" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "Account", + "name": "account" + } + ] + }, + { + "path": "lib/features/accounts/account_providers.dart", + "cluster": "accounts", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [ + { + "name": "accountsProvider", + "type": "StreamProvider", + "watches": [ + "databaseProvider" + ], + "reads": [] + }, + { + "name": "_allTransactionsStreamProvider", + "type": "StreamProvider", + "watches": [ + "accountsProvider", + "databaseProvider", + "_allTransactionsStreamProvider" + ], + "reads": [] + }, + { + "name": "accountRunningBalancesProvider", + "type": "Provider", + "watches": [ + "accountsProvider", + "_allTransactionsStreamProvider" + ], + "reads": [] + }, + { + "name": "netWorthProvider", + "type": "Provider", + "watches": [ + "accountRunningBalancesProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/accounts/accounts_screen.dart", + "cluster": "accounts", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "account_detail_screen.dart", + "kind": "internal" + }, + { + "uri": "account_providers.dart", + "kind": "internal" + }, + { + "uri": "add_account_screen.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "AccountsScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Expanded", + "return_type": ",", + "params": [ + "child: accountsAsync.when(\n data: (accounts" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_accountIcon", + "return_type": "IconData", + "params": [ + "String type" + ], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/accounts/add_account_screen.dart", + "cluster": "accounts", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../../core/utils/currency_formatter.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "AddAccountScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_AddAccountScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "initState", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "dispose", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "_save", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_confirmDelete", + "return_type": "Future", + "params": [ + "BuildContext context" + ], + "async": true, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "Account?", + "name": "initial" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/analytics.json b/.github/docs/intermediate/signatures/analytics.json new file mode 100644 index 0000000..54554ad --- /dev/null +++ b/.github/docs/intermediate/signatures/analytics.json @@ -0,0 +1,547 @@ +[ + { + "path": "lib/features/analytics/analytics_providers.dart", + "cluster": "analytics", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../budget/budget_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "CategorySpending", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "MonthlyTotal", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [], + "providers": [ + { + "name": "spendingByCategoryProvider", + "type": "FutureProvider", + "watches": [ + "databaseProvider", + "selectedMonthProvider" + ], + "reads": [] + }, + { + "name": "monthlyTotalsProvider", + "type": "FutureProvider", + "watches": [ + "databaseProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "String", + "name": "categoryName" + }, + { + "modifier": "final", + "type": "int", + "name": "spentCents" + }, + { + "modifier": "final", + "type": "String", + "name": "month" + }, + { + "modifier": "final", + "type": "int", + "name": "incomeCents" + }, + { + "modifier": "final", + "type": "int", + "name": "expenseCents" + } + ] + }, + { + "path": "lib/features/analytics/analytics_screen.dart", + "cluster": "analytics", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "analytics_providers.dart", + "kind": "internal" + }, + { + "uri": "budget_history_screen.dart", + "kind": "internal" + }, + { + "uri": "widgets/income_vs_expenses_chart.dart", + "kind": "internal" + }, + { + "uri": "widgets/spending_by_category_chart.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "AnalyticsScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + }, + { + "name": "_SpendingTab", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + }, + { + "name": "_IncomeVsExpensesTab", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/analytics/budget_history_providers.dart", + "cluster": "analytics", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "CategoryMonthHistory", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "MonthBudgetHistory", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [], + "providers": [ + { + "name": "budgetHistoryProvider", + "type": "FutureProvider", + "watches": [ + "databaseProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "String", + "name": "categoryName" + }, + { + "modifier": "final", + "type": "int", + "name": "assignedCents" + }, + { + "modifier": "final", + "type": "int", + "name": "spentCents" + }, + { + "modifier": "final", + "type": "String", + "name": "month" + }, + { + "modifier": "final", + "type": "List", + "name": "categories" + } + ] + }, + { + "path": "lib/features/analytics/budget_history_screen.dart", + "cluster": "analytics", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:fl_chart/fl_chart.dart", + "kind": "third_party" + }, + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "budget_history_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "BudgetHistoryScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + }, + { + "name": "_BudgetTrendChart", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_MonthTile", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "List", + "name": "months" + }, + { + "modifier": "final", + "type": "MonthBudgetHistory", + "name": "month" + } + ] + }, + { + "path": "lib/features/analytics/widgets/income_vs_expenses_chart.dart", + "cluster": "analytics", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:fl_chart/fl_chart.dart", + "kind": "third_party" + }, + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "../../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "../analytics_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "IncomeVsExpensesChart", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_Legend", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Column", + "return_type": "return", + "params": [ + "children: [\n SizedBox(\n height: 220", + "child: BarChart(\n BarChartData(\n maxY: maxValue * 1.15", + "barTouchData: BarTouchData(\n touchTooltipData: BarTouchTooltipData(\n getTooltipItem: (group", + "groupIndex", + "rod", + "rodIndex" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "List", + "name": "data" + }, + { + "modifier": "final", + "type": "Color", + "name": "color" + }, + { + "modifier": "final", + "type": "String", + "name": "label" + } + ] + }, + { + "path": "lib/features/analytics/widgets/spending_by_category_chart.dart", + "cluster": "analytics", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:fl_chart/fl_chart.dart", + "kind": "third_party" + }, + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "../../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "../analytics_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "SpendingByCategoryChart", + "extends": "StatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "StatefulWidget", + "notifier_type": null + }, + { + "name": "_SpendingByCategoryChartState", + "extends": "State", + "mixins": null, + "implements": null, + "widget_type": "State", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Column", + "return_type": "return", + "params": [ + "children: [\n SizedBox(\n height: 220", + "child: PieChart(\n PieChartData(\n pieTouchData: PieTouchData(\n touchCallback: (event", + "response" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "List", + "name": "data" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/auth.json b/.github/docs/intermediate/signatures/auth.json new file mode 100644 index 0000000..03f327f --- /dev/null +++ b/.github/docs/intermediate/signatures/auth.json @@ -0,0 +1,279 @@ +[ + { + "path": "lib/features/auth/auth_providers.dart", + "cluster": "auth", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:flutter_secure_storage/flutter_secure_storage.dart", + "kind": "third_party" + }, + { + "uri": "package:local_auth/local_auth.dart", + "kind": "third_party" + } + ], + "classes": [], + "methods": [ + { + "name": "setBiometricEnabled", + "return_type": "Future", + "params": [ + "bool enabled" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [ + { + "name": "localAuthProvider", + "type": "Provider", + "watches": [], + "reads": [] + }, + { + "name": "biometricEnabledProvider", + "type": "FutureProvider", + "watches": [], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/auth/biometric_lock_screen.dart", + "cluster": "auth", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:local_auth/local_auth.dart", + "kind": "third_party" + }, + { + "uri": "auth_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "BiometricLockScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + }, + { + "name": "_LockScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_LockScreenState", + "extends": "ConsumerState<_LockScreen>", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "initState", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "_authenticate", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "Widget", + "name": "child" + } + ] + }, + { + "path": "lib/features/auth/firebase_auth_service.dart", + "cluster": "auth", + "role": [ + "service" + ], + "imports": [ + { + "uri": "package:firebase_auth/firebase_auth.dart", + "kind": "firebase" + }, + { + "uri": "package:flutter/foundation.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:google_sign_in/google_sign_in.dart", + "kind": "third_party" + } + ], + "classes": [ + { + "name": "FirebaseAuthService", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "signInWithGoogle", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": false + }, + { + "name": "signInAnonymously", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": false + }, + { + "name": "signOut", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": false + } + ], + "providers": [ + { + "name": "firebaseUserProvider", + "type": "StreamProvider", + "watches": [ + "firebaseUserProvider" + ], + "reads": [] + }, + { + "name": "isSignedInProvider", + "type": "Provider", + "watches": [ + "firebaseUserProvider" + ], + "reads": [] + }, + { + "name": "firebaseAuthServiceProvider", + "type": "Provider", + "watches": [], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "FirebaseAuth", + "name": "_auth" + }, + { + "modifier": "final", + "type": "GoogleSignIn", + "name": "_googleSignIn" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/budget.json b/.github/docs/intermediate/signatures/budget.json new file mode 100644 index 0000000..7354f90 --- /dev/null +++ b/.github/docs/intermediate/signatures/budget.json @@ -0,0 +1,887 @@ +[ + { + "path": "lib/features/budget/budget_calculator.dart", + "cluster": "budget", + "role": [ + "calculator" + ], + "imports": [], + "classes": [ + { + "name": "BudgetCalculator", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "available", + "return_type": "static int", + "params": [ + "required int assignedCents", + "required int spentCents", + "required int rolledOverCents", + "required bool rollover" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "toBeBudgeted", + "return_type": "static int", + "params": [ + "required int totalIncomeCents", + "required int totalAssignedCents" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "ageOfMoney", + "return_type": "static int", + "params": [ + "required DateTime incomeDate", + "required DateTime spendingDate" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/budget/budget_providers.dart", + "cluster": "budget", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [ + { + "name": "categoryGroupsProvider", + "type": "StreamProvider", + "watches": [ + "databaseProvider" + ], + "reads": [] + }, + { + "name": "allCategoriesProvider", + "type": "FutureProvider", + "watches": [ + "databaseProvider", + "selectedMonthProvider" + ], + "reads": [] + }, + { + "name": "monthlyBudgetsProvider", + "type": "StreamProvider", + "watches": [ + "databaseProvider", + "selectedMonthProvider" + ], + "reads": [] + }, + { + "name": "transactionsForMonthProvider", + "type": "StreamProvider", + "watches": [ + "databaseProvider", + "selectedMonthProvider", + "transactionsForMonthProvider" + ], + "reads": [] + }, + { + "name": "monthlyIncomeProvider", + "type": "Provider", + "watches": [ + "monthlyBudgetsProvider", + "transactionsForMonthProvider" + ], + "reads": [] + }, + { + "name": "totalAssignedProvider", + "type": "Provider", + "watches": [ + "monthlyBudgetsProvider" + ], + "reads": [] + }, + { + "name": "rolloverAmountsProvider", + "type": "FutureProvider", + "watches": [ + "databaseProvider", + "selectedMonthProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/budget/budget_screen.dart", + "cluster": "budget", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../goals/goals_screen.dart", + "kind": "internal" + }, + { + "uri": "budget_calculator.dart", + "kind": "internal" + }, + { + "uri": "budget_providers.dart", + "kind": "internal" + }, + { + "uri": "../../core/services/rollover_provider.dart", + "kind": "internal" + }, + { + "uri": "rebalance_provider.dart", + "kind": "internal" + }, + { + "uri": "widgets/category_row.dart", + "kind": "internal" + }, + { + "uri": "widgets/rebalance_sheet.dart", + "kind": "internal" + }, + { + "uri": "widgets/recurring_due_banner.dart", + "kind": "internal" + }, + { + "uri": "widgets/tbb_banner.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "BudgetScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + }, + { + "name": "_GroupTile", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Consumer", + "return_type": "[", + "params": [ + "builder: (context", + "ref", + "_" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Expanded", + "return_type": ",", + "params": [ + "child: groupsAsync.when(\n data: (groups" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_showAddCategoryDialog", + "return_type": "Future", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "FilledButton", + "return_type": ",", + "params": [ + "onPressed: (" + ], + "async": true, + "override": false, + "private": false + }, + { + "name": "_monthName", + "return_type": "String", + "params": [ + "int m" + ], + "async": false, + "override": false, + "private": true + }, + { + "name": "_confirmDeleteGroup", + "return_type": "Future", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "_spentForCategory", + "return_type": "int", + "params": [ + "int categoryId" + ], + "async": false, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_showBudgetDialog", + "return_type": "Future", + "params": [ + "BuildContext context", + "WidgetRef ref", + "Category cat", + "int currentCents" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "FilledButton", + "return_type": ",", + "params": [ + "onPressed: (" + ], + "async": true, + "override": false, + "private": false + }, + { + "name": "_confirmDeleteCategory", + "return_type": "Future", + "params": [ + "BuildContext context", + "WidgetRef ref", + "Category cat" + ], + "async": true, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "CategoryGroup", + "name": "group" + }, + { + "modifier": "final", + "type": "List", + "name": "budgets" + }, + { + "modifier": "final", + "type": "List", + "name": "transactions" + }, + { + "modifier": "final", + "type": "String", + "name": "month" + } + ] + }, + { + "path": "lib/features/budget/rebalance_provider.dart", + "cluster": "budget", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "budget_providers.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "CategoryBudgetData", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "RebalanceSuggestion", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "computeRebalanceSuggestions", + "return_type": "List", + "params": [ + "required List categoryData" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [ + { + "name": "rebalanceSuggestionsProvider", + "type": "FutureProvider", + "watches": [ + "databaseProvider", + "selectedMonthProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "int", + "name": "id" + }, + { + "modifier": "final", + "type": "String", + "name": "name" + }, + { + "modifier": "final", + "type": "int", + "name": "assigned" + }, + { + "modifier": "final", + "type": "int", + "name": "spent" + }, + { + "modifier": "final", + "type": "int", + "name": "fromCategoryId" + }, + { + "modifier": "final", + "type": "String", + "name": "fromCategoryName" + }, + { + "modifier": "final", + "type": "int", + "name": "toCategoryId" + }, + { + "modifier": "final", + "type": "String", + "name": "toCategoryName" + } + ] + }, + { + "path": "lib/features/budget/recurring_providers.dart", + "cluster": "budget", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [ + { + "name": "pendingRecurringProvider", + "type": "StreamProvider", + "watches": [ + "databaseProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/budget/widgets/category_row.dart", + "cluster": "budget", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "../../../core/utils/currency_formatter.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "CategoryRow", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "String", + "name": "name" + }, + { + "modifier": "final", + "type": "int", + "name": "assignedCents" + }, + { + "modifier": "final", + "type": "int", + "name": "spentCents" + }, + { + "modifier": "final", + "type": "int", + "name": "availableCents" + }, + { + "modifier": "final", + "type": "VoidCallback?", + "name": "onTap" + }, + { + "modifier": "final", + "type": "VoidCallback?", + "name": "onLongPress" + } + ] + }, + { + "path": "lib/features/budget/widgets/rebalance_sheet.dart", + "cluster": "budget", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../budget_providers.dart", + "kind": "internal" + }, + { + "uri": "../rebalance_provider.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "RebalanceSheet", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_RebalanceSheetState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Expanded", + "return_type": ",", + "params": [ + "child: suggestionsAsync.when(\n data: (suggestions" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_applyAll", + "return_type": "Future", + "params": [ + "BuildContext context" + ], + "async": true, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/budget/widgets/recurring_due_banner.dart", + "cluster": "budget", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../recurring_providers.dart", + "kind": "internal" + }, + { + "uri": "recurring_review_sheet.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "RecurringDueBanner", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/budget/widgets/recurring_review_sheet.dart", + "cluster": "budget", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../../../core/services/month_boundary_service.dart", + "kind": "internal" + }, + { + "uri": "../../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "../recurring_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "RecurringReviewSheet", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "_confirm", + "return_type": "Future", + "params": [ + "BuildContext context", + "WidgetRef ref", + "PendingRecurringQueueData item", + "Transaction sourceTx" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "_skip", + "return_type": "Future", + "params": [ + "WidgetRef ref", + "PendingRecurringQueueData item", + "Transaction sourceTx" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Expanded", + "return_type": ",", + "params": [ + "child: pendingAsync.when(\n data: (pending" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "late", + "type": "final", + "name": "nextDue" + } + ] + }, + { + "path": "lib/features/budget/widgets/tbb_banner.dart", + "cluster": "budget", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "../../../core/utils/currency_formatter.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "ToBeBudgetedBanner", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "int", + "name": "tbbCents" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/csv.json b/.github/docs/intermediate/signatures/csv.json new file mode 100644 index 0000000..9420c5b --- /dev/null +++ b/.github/docs/intermediate/signatures/csv.json @@ -0,0 +1,149 @@ +[ + { + "path": "lib/core/csv/csv_parser.dart", + "cluster": "csv", + "role": [ + "parser" + ], + "imports": [ + { + "uri": "package:csv/csv.dart", + "kind": "third_party" + }, + { + "uri": "../utils/currency_formatter.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "ParsedTransaction", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "ValidationResult", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "CsvParser", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "parse", + "return_type": "static List>", + "params": [ + "String csvContent" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "detectHeaders", + "return_type": "static List", + "params": [ + "String csvContent" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "toTransactionData", + "return_type": "static ParsedTransaction", + "params": [ + "required Map row", + "required String dateColumn", + "required String amountColumn", + "required String payeeColumn", + "String? memoColumn" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "validate", + "return_type": "static ValidationResult", + "params": [ + "Map row", + "required String dateColumn", + "required String amountColumn" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_parseDate", + "return_type": "static DateTime?", + "params": [ + "String s" + ], + "async": false, + "override": false, + "private": true + }, + { + "name": "_tryUsFormat", + "return_type": "static DateTime?", + "params": [ + "String s" + ], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "String", + "name": "payee" + }, + { + "modifier": "final", + "type": "int", + "name": "amountCents" + }, + { + "modifier": "final", + "type": "DateTime", + "name": "date" + }, + { + "modifier": "final", + "type": "String?", + "name": "memo" + }, + { + "modifier": "final", + "type": "bool", + "name": "isValid" + }, + { + "modifier": "final", + "type": "String?", + "name": "error" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/database.json b/.github/docs/intermediate/signatures/database.json new file mode 100644 index 0000000..6bc6d11 --- /dev/null +++ b/.github/docs/intermediate/signatures/database.json @@ -0,0 +1,443 @@ +[ + { + "path": "lib/core/database/daos/accounts_dao.dart", + "cluster": "database", + "role": [ + "dao" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "../database.dart", + "kind": "internal" + }, + { + "uri": "../tables.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [], + "drift_dao": { + "tables": [ + "Accounts" + ] + }, + "fields": [] + }, + { + "path": "lib/core/database/daos/budget_dao.dart", + "cluster": "database", + "role": [ + "dao" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "../database.dart", + "kind": "internal" + }, + { + "uri": "../tables.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [], + "drift_dao": { + "tables": [ + "MonthlyBudgets" + ] + }, + "fields": [] + }, + { + "path": "lib/core/database/daos/budget_snapshots_dao.dart", + "cluster": "database", + "role": [ + "dao" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "../database.dart", + "kind": "internal" + }, + { + "uri": "../tables.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [ + { + "name": "getSnapshotMonths", + "return_type": "Future>", + "params": [], + "async": true, + "override": false, + "private": false + }, + { + "name": "hasSnapshot", + "return_type": "Future", + "params": [ + "int categoryId", + "String month" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": { + "tables": [ + "BudgetSnapshots" + ] + }, + "fields": [] + }, + { + "path": "lib/core/database/daos/categories_dao.dart", + "cluster": "database", + "role": [ + "dao" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "../database.dart", + "kind": "internal" + }, + { + "uri": "../tables.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [ + { + "name": "softDeleteGroup", + "return_type": "Future", + "params": [ + "int id" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": { + "tables": [ + "CategoryGroups", + "Categories" + ] + }, + "fields": [] + }, + { + "path": "lib/core/database/daos/recurring_queue_dao.dart", + "cluster": "database", + "role": [ + "dao" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "../database.dart", + "kind": "internal" + }, + { + "uri": "../tables.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [ + { + "name": "isEnqueued", + "return_type": "Future", + "params": [ + "int sourceTransactionId" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": { + "tables": [ + "PendingRecurringQueue", + "Transactions" + ] + }, + "fields": [] + }, + { + "path": "lib/core/database/daos/transactions_dao.dart", + "cluster": "database", + "role": [ + "dao" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "../database.dart", + "kind": "internal" + }, + { + "uri": "../tables.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [ + { + "name": "watchTransactionsForMonth", + "return_type": "Stream>", + "params": [ + "int year", + "int month" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": { + "tables": [ + "Transactions" + ] + }, + "fields": [] + }, + { + "path": "lib/core/database/database.dart", + "cluster": "database", + "role": [ + "config" + ], + "imports": [ + { + "uri": "dart:io", + "kind": "sdk" + }, + { + "uri": "package:drift/drift.dart", + "kind": "drift" + }, + { + "uri": "package:drift/native.dart", + "kind": "drift" + }, + { + "uri": "package:path_provider/path_provider.dart", + "kind": "third_party" + }, + { + "uri": "package:path/path.dart", + "kind": "third_party" + }, + { + "uri": "tables.dart", + "kind": "internal" + }, + { + "uri": "daos/accounts_dao.dart", + "kind": "internal" + }, + { + "uri": "daos/categories_dao.dart", + "kind": "internal" + }, + { + "uri": "daos/transactions_dao.dart", + "kind": "internal" + }, + { + "uri": "daos/budget_dao.dart", + "kind": "internal" + }, + { + "uri": "daos/budget_snapshots_dao.dart", + "kind": "internal" + }, + { + "uri": "daos/recurring_queue_dao.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [ + { + "name": "MigrationStrategy", + "return_type": ">", + "params": [ + "onUpgrade: (m", + "from", + "to" + ], + "async": true, + "override": false, + "private": false + }, + { + "name": "_openConnection", + "return_type": "LazyDatabase", + "params": [], + "async": false, + "override": false, + "private": true + }, + { + "name": "LazyDatabase", + "return_type": "return", + "params": [ + "(" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/core/database/providers.dart", + "cluster": "database", + "role": [ + "other" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "database.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [ + { + "name": "databaseProvider", + "type": "Provider", + "watches": [], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/core/database/tables.dart", + "cluster": "database", + "role": [ + "model" + ], + "imports": [ + { + "uri": "package:drift/drift.dart", + "kind": "drift" + } + ], + "classes": [ + { + "name": "Accounts", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "CategoryGroups", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "Categories", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "MonthlyBudgets", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "Transactions", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "NetWorthSnapshots", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "BudgetSnapshots", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "PendingRecurringQueue", + "extends": "Table", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [], + "providers": [], + "drift_dao": null, + "fields": [] + } +] diff --git a/.github/docs/intermediate/signatures/goals.json b/.github/docs/intermediate/signatures/goals.json new file mode 100644 index 0000000..8337a6d --- /dev/null +++ b/.github/docs/intermediate/signatures/goals.json @@ -0,0 +1,331 @@ +[ + { + "path": "lib/features/goals/add_goal_screen.dart", + "cluster": "goals", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../budget/budget_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "AddGoalScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_AddGoalScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "dispose", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "_pickDate", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "_save", + "return_type": "Future", + "params": [ + "List categories" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_buildForm", + "return_type": "Widget", + "params": [ + "List categories" + ], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/goals/goal_calculator.dart", + "cluster": "goals", + "role": [ + "calculator" + ], + "imports": [], + "classes": [ + { + "name": "GoalCalculator", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "progressPercent", + "return_type": "static double", + "params": [ + "required int currentCents", + "required int goalCents" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "projectedDate", + "return_type": "static DateTime", + "params": [ + "required int currentCents", + "required int goalCents", + "required int monthlyContributionCents" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/goals/goals_providers.dart", + "cluster": "goals", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "goal_calculator.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "GoalProgress", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [], + "providers": [ + { + "name": "goalsProvider", + "type": "StreamProvider", + "watches": [ + "databaseProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "Category", + "name": "category" + }, + { + "modifier": "final", + "type": "int", + "name": "totalAssignedCents" + }, + { + "modifier": "final", + "type": "int", + "name": "lastMonthAssignedCents" + }, + { + "modifier": "final", + "type": "double", + "name": "progressPercent" + }, + { + "modifier": "final", + "type": "DateTime", + "name": "projectedDate" + } + ] + }, + { + "path": "lib/features/goals/goals_screen.dart", + "cluster": "goals", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:intl/intl.dart", + "kind": "third_party" + }, + { + "uri": "../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "goals_providers.dart", + "kind": "internal" + }, + { + "uri": "add_goal_screen.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "GoalsScreen", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + }, + { + "name": "_EmptyState", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_GoalCard", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_projectedLabel", + "return_type": "String", + "params": [ + "GoalProgress goal" + ], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "GoalProgress", + "name": "goal" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/onboarding.json b/.github/docs/intermediate/signatures/onboarding.json new file mode 100644 index 0000000..7c0c0bc --- /dev/null +++ b/.github/docs/intermediate/signatures/onboarding.json @@ -0,0 +1,397 @@ +[ + { + "path": "lib/features/onboarding/onboarding_providers.dart", + "cluster": "onboarding", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:flutter_secure_storage/flutter_secure_storage.dart", + "kind": "third_party" + } + ], + "classes": [], + "methods": [ + { + "name": "setOnboardingComplete", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": false + }, + { + "name": "saveMonthlyIncome", + "return_type": "Future", + "params": [ + "int cents" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [ + { + "name": "onboardingCompleteProvider", + "type": "FutureProvider", + "watches": [], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/onboarding/onboarding_screen.dart", + "cluster": "onboarding", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../shell/main_shell.dart", + "kind": "internal" + }, + { + "uri": "onboarding_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "OnboardingScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_OnboardingScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + }, + { + "name": "_WelcomePage", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_AccountDraft", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "_AccountsPage", + "extends": "StatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "StatefulWidget", + "notifier_type": null + }, + { + "name": "_AccountsPageState", + "extends": "State<_AccountsPage>", + "mixins": null, + "implements": null, + "widget_type": "State", + "notifier_type": null + }, + { + "name": "_IncomePage", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_CategoriesPage", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_GoalDraft", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + }, + { + "name": "_GoalsPage", + "extends": "StatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "StatefulWidget", + "notifier_type": null + }, + { + "name": "_GoalsPageState", + "extends": "State<_GoalsPage>", + "mixins": null, + "implements": null, + "widget_type": "State", + "notifier_type": null + }, + { + "name": "_DonePage", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "dispose", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "_nextPage", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": true + }, + { + "name": "_prevPage", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": true + }, + { + "name": "_finish", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": ">", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": ">", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": ">", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_addAccount", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "List<_AccountDraft>", + "name": "_accounts" + }, + { + "modifier": "final", + "type": "Set", + "name": "_selectedCategories" + }, + { + "modifier": "final", + "type": "List<_GoalDraft>", + "name": "_goals" + }, + { + "modifier": "final", + "type": "VoidCallback", + "name": "onNext" + }, + { + "modifier": "final", + "type": "List<_AccountDraft>", + "name": "accounts" + }, + { + "modifier": "final", + "type": "VoidCallback", + "name": "onChanged" + }, + { + "modifier": "final", + "type": "TextEditingController", + "name": "controller" + }, + { + "modifier": "final", + "type": "List", + "name": "defaults" + }, + { + "modifier": "final", + "type": "Set", + "name": "selected" + }, + { + "modifier": "final", + "type": "TextEditingController", + "name": "customController" + }, + { + "modifier": "final", + "type": "VoidCallback", + "name": "onChanged" + }, + { + "modifier": "final", + "type": "List<_GoalDraft>", + "name": "goals" + }, + { + "modifier": "final", + "type": "VoidCallback", + "name": "onChanged" + }, + { + "modifier": "final", + "type": "VoidCallback?", + "name": "onFinish" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/root.json b/.github/docs/intermediate/signatures/root.json new file mode 100644 index 0000000..03289d4 --- /dev/null +++ b/.github/docs/intermediate/signatures/root.json @@ -0,0 +1,99 @@ +[ + { + "path": "lib/main.dart", + "cluster": "root", + "role": [ + "entry" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:firebase_core/firebase_core.dart", + "kind": "firebase" + }, + { + "uri": "core/theme/app_theme.dart", + "kind": "internal" + }, + { + "uri": "core/services/month_boundary_provider.dart", + "kind": "internal" + }, + { + "uri": "features/auth/biometric_lock_screen.dart", + "kind": "internal" + }, + { + "uri": "features/onboarding/onboarding_providers.dart", + "kind": "internal" + }, + { + "uri": "features/onboarding/onboarding_screen.dart", + "kind": "internal" + }, + { + "uri": "features/shell/main_shell.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "MoneyInSightApp", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + }, + { + "name": "_AppStartup", + "extends": "ConsumerWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "main", + "return_type": "void", + "params": [], + "async": true, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context", + "WidgetRef ref" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + } +] diff --git a/.github/docs/intermediate/signatures/services.json b/.github/docs/intermediate/signatures/services.json new file mode 100644 index 0000000..8c2d06b --- /dev/null +++ b/.github/docs/intermediate/signatures/services.json @@ -0,0 +1,197 @@ +[ + { + "path": "lib/core/services/month_boundary_provider.dart", + "cluster": "services", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:flutter_secure_storage/flutter_secure_storage.dart", + "kind": "third_party" + }, + { + "uri": "../database/providers.dart", + "kind": "internal" + }, + { + "uri": "month_boundary_service.dart", + "kind": "internal" + } + ], + "classes": [], + "methods": [], + "providers": [ + { + "name": "monthBoundaryServiceProvider", + "type": "Provider", + "watches": [ + "databaseProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/core/services/month_boundary_service.dart", + "cluster": "services", + "role": [ + "service" + ], + "imports": [ + { + "uri": "package:flutter_secure_storage/flutter_secure_storage.dart", + "kind": "third_party" + }, + { + "uri": "../database/database.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "MonthBoundaryService", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "computeRolloverCents", + "return_type": "static int", + "params": [ + "required int assignedCents", + "required int spentCents" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "computeNextDueDate", + "return_type": "static DateTime", + "params": [ + "DateTime from", + "String interval" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "run", + "return_type": "Future", + "params": [ + "DateTime? now" + ], + "async": true, + "override": false, + "private": false + }, + { + "name": "_snapshotMonth", + "return_type": "Future", + "params": [ + "String month" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "_applyRollovers", + "return_type": "Future", + "params": [ + "String previousMonth", + "String currentMonth" + ], + "async": true, + "override": false, + "private": true + }, + { + "name": "_enqueueRecurring", + "return_type": "Future", + "params": [ + "DateTime today" + ], + "async": true, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "AppDatabase", + "name": "db" + }, + { + "modifier": "final", + "type": "FlutterSecureStorage", + "name": "storage" + } + ] + }, + { + "path": "lib/core/services/rollover_provider.dart", + "cluster": "services", + "role": [ + "provider" + ], + "imports": [ + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:flutter_secure_storage/flutter_secure_storage.dart", + "kind": "third_party" + } + ], + "classes": [ + { + "name": "GlobalRolloverNotifier", + "extends": "AsyncNotifier", + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": "AsyncNotifier" + } + ], + "methods": [ + { + "name": "build", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": false + }, + { + "name": "setEnabled", + "return_type": "Future", + "params": [ + "bool value" + ], + "async": true, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + } +] diff --git a/.github/docs/intermediate/signatures/settings.json b/.github/docs/intermediate/signatures/settings.json new file mode 100644 index 0000000..e445a92 --- /dev/null +++ b/.github/docs/intermediate/signatures/settings.json @@ -0,0 +1,120 @@ +[ + { + "path": "lib/features/settings/settings_screen.dart", + "cluster": "settings", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:intl/intl.dart", + "kind": "third_party" + }, + { + "uri": "../../core/services/rollover_provider.dart", + "kind": "internal" + }, + { + "uri": "../auth/auth_providers.dart", + "kind": "internal" + }, + { + "uri": "../auth/firebase_auth_service.dart", + "kind": "internal" + }, + { + "uri": "../sync/sync_service.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "SettingsScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_SettingsScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + }, + { + "name": "_SectionHeader", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "_handleSync", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "_handleSignIn", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "_handleSignOut", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "String", + "name": "title" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/shell.json b/.github/docs/intermediate/signatures/shell.json new file mode 100644 index 0000000..994c03a --- /dev/null +++ b/.github/docs/intermediate/signatures/shell.json @@ -0,0 +1,68 @@ +[ + { + "path": "lib/features/shell/main_shell.dart", + "cluster": "shell", + "role": [ + "router" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "../budget/budget_screen.dart", + "kind": "internal" + }, + { + "uri": "../transactions/transactions_screen.dart", + "kind": "internal" + }, + { + "uri": "../accounts/accounts_screen.dart", + "kind": "internal" + }, + { + "uri": "../analytics/analytics_screen.dart", + "kind": "internal" + }, + { + "uri": "../settings/settings_screen.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "MainShell", + "extends": "StatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "StatefulWidget", + "notifier_type": null + }, + { + "name": "_MainShellState", + "extends": "State", + "mixins": null, + "implements": null, + "widget_type": "State", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + } +] diff --git a/.github/docs/intermediate/signatures/sync.json b/.github/docs/intermediate/signatures/sync.json new file mode 100644 index 0000000..8fb8d7d --- /dev/null +++ b/.github/docs/intermediate/signatures/sync.json @@ -0,0 +1,93 @@ +[ + { + "path": "lib/features/sync/sync_service.dart", + "cluster": "sync", + "role": [ + "service" + ], + "imports": [ + { + "uri": "package:cloud_firestore/cloud_firestore.dart", + "kind": "firebase" + }, + { + "uri": "package:firebase_auth/firebase_auth.dart", + "kind": "firebase" + }, + { + "uri": "package:flutter/foundation.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "package:flutter_secure_storage/flutter_secure_storage.dart", + "kind": "third_party" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "SyncService", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "syncToFirestore", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": false + } + ], + "providers": [ + { + "name": "lastSyncProvider", + "type": "FutureProvider", + "watches": [], + "reads": [] + }, + { + "name": "syncServiceProvider", + "type": "Provider", + "watches": [ + "databaseProvider" + ], + "reads": [] + } + ], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "AppDatabase", + "name": "_db" + }, + { + "modifier": "final", + "type": "FirebaseFirestore", + "name": "_firestore" + }, + { + "modifier": "final", + "type": "FirebaseAuth", + "name": "_auth" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/theme.json b/.github/docs/intermediate/signatures/theme.json new file mode 100644 index 0000000..8d8a678 --- /dev/null +++ b/.github/docs/intermediate/signatures/theme.json @@ -0,0 +1,29 @@ +[ + { + "path": "lib/core/theme/app_theme.dart", + "cluster": "theme", + "role": [ + "config" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + } + ], + "classes": [ + { + "name": "AppTheme", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [], + "providers": [], + "drift_dao": null, + "fields": [] + } +] diff --git a/.github/docs/intermediate/signatures/transactions.json b/.github/docs/intermediate/signatures/transactions.json new file mode 100644 index 0000000..809d19c --- /dev/null +++ b/.github/docs/intermediate/signatures/transactions.json @@ -0,0 +1,474 @@ +[ + { + "path": "lib/features/transactions/add_transaction_screen.dart", + "cluster": "transactions", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../../core/services/month_boundary_service.dart", + "kind": "internal" + }, + { + "uri": "../../core/utils/currency_formatter.dart", + "kind": "internal" + }, + { + "uri": "../budget/budget_providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "AddTransactionScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_AddTransactionScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "initState", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "dispose", + "return_type": "void", + "params": [], + "async": false, + "override": false, + "private": false + }, + { + "name": "_save", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "Transaction?", + "name": "initial" + }, + { + "modifier": "final", + "type": "int?", + "name": "initialAccountId" + } + ] + }, + { + "path": "lib/features/transactions/import_csv_screen.dart", + "cluster": "transactions", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "dart:io", + "kind": "sdk" + }, + { + "uri": "package:file_picker/file_picker.dart", + "kind": "third_party" + }, + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/csv/csv_parser.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "ImportCsvScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_ImportCsvScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "_pickFile", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_findHeader", + "return_type": "String?", + "params": [ + "List candidates" + ], + "async": false, + "override": false, + "private": true + }, + { + "name": "_import", + "return_type": "Future", + "params": [], + "async": true, + "override": false, + "private": true + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_columnDropdown", + "return_type": "Widget", + "params": [ + "String label", + "String? value", + "ValueChanged onChanged" + ], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/transactions/transactions_screen.dart", + "cluster": "transactions", + "role": [ + "screen" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "package:flutter_riverpod/flutter_riverpod.dart", + "kind": "riverpod" + }, + { + "uri": "../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../core/database/providers.dart", + "kind": "internal" + }, + { + "uri": "../accounts/account_providers.dart", + "kind": "internal" + }, + { + "uri": "../budget/budget_providers.dart", + "kind": "internal" + }, + { + "uri": "add_transaction_screen.dart", + "kind": "internal" + }, + { + "uri": "import_csv_screen.dart", + "kind": "internal" + }, + { + "uri": "widgets/transaction_tile.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "TransactionsScreen", + "extends": "ConsumerStatefulWidget", + "mixins": null, + "implements": null, + "widget_type": "ConsumerStatefulWidget", + "notifier_type": null + }, + { + "name": "_TransactionsScreenState", + "extends": "ConsumerState", + "mixins": null, + "implements": null, + "widget_type": "ConsumerState", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_filterChip", + "return_type": "Widget", + "params": [ + "String label", + "VoidCallback onRemove" + ], + "async": false, + "override": false, + "private": true + }, + { + "name": "_showFilterSheet", + "return_type": "void", + "params": [ + "BuildContext context", + "AsyncValue> accountsAsync", + "AsyncValue> categoriesAsync" + ], + "async": false, + "override": false, + "private": true + }, + { + "name": "Row", + "return_type": ",", + "params": [ + "children: [\n Expanded(\n child: OutlinedButton(\n onPressed: (" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "Expanded", + "return_type": ",", + "params": [ + "child: FilledButton(\n onPressed: (" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "setState", + "return_type": "", + "params": [ + "(" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_monthName", + "return_type": "String", + "params": [ + "int m" + ], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + }, + { + "path": "lib/features/transactions/widgets/transaction_tile.dart", + "cluster": "transactions", + "role": [ + "widget" + ], + "imports": [ + { + "uri": "package:flutter/material.dart", + "kind": "flutter" + }, + { + "uri": "../../../core/database/database.dart", + "kind": "internal" + }, + { + "uri": "../../../core/utils/currency_formatter.dart", + "kind": "internal" + } + ], + "classes": [ + { + "name": "TransactionTile", + "extends": "StatelessWidget", + "mixins": null, + "implements": null, + "widget_type": "StatelessWidget", + "notifier_type": null + } + ], + "methods": [ + { + "name": "build", + "return_type": "Widget", + "params": [ + "BuildContext context" + ], + "async": false, + "override": false, + "private": false + }, + { + "name": "_subtitleText", + "return_type": "String", + "params": [], + "async": false, + "override": false, + "private": true + }, + { + "name": "_dateLabel", + "return_type": "String", + "params": [], + "async": false, + "override": false, + "private": true + } + ], + "providers": [], + "drift_dao": null, + "fields": [ + { + "modifier": "final", + "type": "Transaction", + "name": "transaction" + }, + { + "modifier": "final", + "type": "VoidCallback", + "name": "onDelete" + }, + { + "modifier": "final", + "type": "VoidCallback?", + "name": "onTap" + }, + { + "modifier": "final", + "type": "String?", + "name": "accountName" + }, + { + "modifier": "final", + "type": "String?", + "name": "toAccountName" + } + ] + } +] diff --git a/.github/docs/intermediate/signatures/utils.json b/.github/docs/intermediate/signatures/utils.json new file mode 100644 index 0000000..c125365 --- /dev/null +++ b/.github/docs/intermediate/signatures/utils.json @@ -0,0 +1,40 @@ +[ + { + "path": "lib/core/utils/currency_formatter.dart", + "cluster": "utils", + "role": [ + "util" + ], + "imports": [ + { + "uri": "package:intl/intl.dart", + "kind": "third_party" + } + ], + "classes": [ + { + "name": "CurrencyFormatter", + "extends": null, + "mixins": null, + "implements": null, + "widget_type": null, + "notifier_type": null + } + ], + "methods": [ + { + "name": "format", + "return_type": "static String", + "params": [ + "int cents" + ], + "async": false, + "override": false, + "private": false + } + ], + "providers": [], + "drift_dao": null, + "fields": [] + } +] diff --git a/.github/docs/symbol_index.md b/.github/docs/symbol_index.md new file mode 100644 index 0000000..22e9b0f --- /dev/null +++ b/.github/docs/symbol_index.md @@ -0,0 +1,128 @@ +# Symbol Index + +_Generated by the Deep Reverse Engineering Agent — Stage 8._ +_Use `grep` on this file to locate any provider, DAO method, table, or screen._ + +--- + +## Riverpod Providers +| Provider name | File | Provides | Watches | +| --- | --- | --- | --- | +| accountsProvider | lib/features/accounts/account_providers.dart | Stream> | AccountsDao.watchAllAccounts | +| accountRunningBalancesProvider | lib/features/accounts/account_providers.dart | AsyncValue> | accountsProvider, _allTransactionsStreamProvider | +| netWorthProvider | lib/features/accounts/account_providers.dart | AsyncValue | accountRunningBalancesProvider | +| selectedMonthProvider | lib/features/budget/budget_providers.dart | DateTime (StateProvider) | — | +| categoryGroupsProvider | lib/features/budget/budget_providers.dart | Stream> | CategoriesDao.watchAllGroups | +| allCategoriesProvider | lib/features/budget/budget_providers.dart | Future> | CategoriesDao.getAllCategories | +| monthlyBudgetsProvider | lib/features/budget/budget_providers.dart | Stream> | BudgetDao.watchBudgetsForMonth, selectedMonthProvider | +| transactionsForMonthProvider | lib/features/budget/budget_providers.dart | Stream> | TransactionsDao.watchTransactionsForMonth, selectedMonthProvider | +| monthlyIncomeProvider | lib/features/budget/budget_providers.dart | AsyncValue | transactionsForMonthProvider | +| totalAssignedProvider | lib/features/budget/budget_providers.dart | AsyncValue | monthlyBudgetsProvider | +| rolloverAmountsProvider | lib/features/budget/budget_providers.dart | Future> | BudgetDao.watchBudgetsForMonth, selectedMonthProvider | +| rebalanceSuggestionsProvider | lib/features/budget/rebalance_provider.dart | Future> | databaseProvider, selectedMonthProvider | +| pendingRecurringProvider | lib/features/budget/recurring_providers.dart | Stream> | RecurringQueueDao.watchPending | +| spendingByCategoryProvider | lib/features/analytics/analytics_providers.dart | Future> | TransactionsDao.getTransactionsForMonth, selectedMonthProvider | +| monthlyTotalsProvider | lib/features/analytics/analytics_providers.dart | Future> | TransactionsDao.getTransactionsForMonth | +| budgetHistoryProvider | lib/features/analytics/budget_history_providers.dart | Future> | BudgetSnapshotsDao, CategoriesDao | +| goalsProvider | lib/features/goals/goals_providers.dart | Stream> | CategoriesDao.watchCategoriesWithGoals, BudgetDao.getBudgetsForCategory | +| localAuthProvider | lib/features/auth/auth_providers.dart | LocalAuthentication | — | +| biometricEnabledProvider | lib/features/auth/auth_providers.dart | Future | — | +| isUnlockedProvider | lib/features/auth/auth_providers.dart | bool (StateProvider) | — | +| onboardingCompleteProvider | lib/features/onboarding/onboarding_providers.dart | Future | — | +| monthBoundaryServiceProvider | lib/core/services/month_boundary_provider.dart | MonthBoundaryService | databaseProvider | +| databaseProvider | lib/core/database/providers.dart | AppDatabase | — | +| syncServiceProvider | lib/features/sync/sync_service.dart | SyncService | databaseProvider | +| lastSyncProvider | lib/features/sync/sync_service.dart | Future | — | + +--- + +## DAO Methods +| Method | DAO | File | Operation | Returns | +| --- | --- | --- | --- | --- | +| insertAccount | AccountsDao | lib/core/database/daos/accounts_dao.dart | INSERT | Future | +| getAccount | AccountsDao | lib/core/database/daos/accounts_dao.dart | SELECT | Future | +| watchAllAccounts | AccountsDao | lib/core/database/daos/accounts_dao.dart | SELECT | Stream> | +| getAllAccounts | AccountsDao | lib/core/database/daos/accounts_dao.dart | SELECT | Future> | +| updateAccount | AccountsDao | lib/core/database/daos/accounts_dao.dart | UPDATE | Future | +| softDeleteAccount | AccountsDao | lib/core/database/daos/accounts_dao.dart | UPDATE | Future | +| getBudgetForCategoryMonth | BudgetDao | lib/core/database/daos/budget_dao.dart | SELECT | Future | +| upsertBudget | BudgetDao | lib/core/database/daos/budget_dao.dart | INSERT/UPDATE | Future | +| watchBudgetsForMonth | BudgetDao | lib/core/database/daos/budget_dao.dart | SELECT | Stream> | +| getBudgetsForCategory | BudgetDao | lib/core/database/daos/budget_dao.dart | SELECT | Future> | +| insertGroup | CategoriesDao | lib/core/database/daos/categories_dao.dart | INSERT | Future | +| insertCategory | CategoriesDao | lib/core/database/daos/categories_dao.dart | INSERT | Future | +| getCategory | CategoriesDao | lib/core/database/daos/categories_dao.dart | SELECT | Future | +| watchCategoriesForGroup | CategoriesDao | lib/core/database/daos/categories_dao.dart | SELECT | Stream> | +| watchAllGroups | CategoriesDao | lib/core/database/daos/categories_dao.dart | SELECT | Stream> | +| getAllCategories | CategoriesDao | lib/core/database/daos/categories_dao.dart | SELECT | Future> | +| watchCategoriesWithGoals | CategoriesDao | lib/core/database/daos/categories_dao.dart | SELECT | Stream> | +| softDeleteCategory | CategoriesDao | lib/core/database/daos/categories_dao.dart | UPDATE | Future | +| updateRollover | CategoriesDao | lib/core/database/daos/categories_dao.dart | UPDATE | Future | +| softDeleteGroup | CategoriesDao | lib/core/database/daos/categories_dao.dart | UPDATE | Future | +| insertTransaction | TransactionsDao | lib/core/database/daos/transactions_dao.dart | INSERT | Future | +| getTransaction | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Future | +| watchTransactionsForMonth | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Stream> | +| getTransactionsForMonth | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Future> | +| getTransactionsForAccount | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Future> | +| watchTransactionsForAccount | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Stream> | +| getAllTransactions | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Future> | +| watchAllTransactions | TransactionsDao | lib/core/database/daos/transactions_dao.dart | SELECT | Stream> | +| updateTransaction | TransactionsDao | lib/core/database/daos/transactions_dao.dart | UPDATE | Future | +| softDelete | TransactionsDao | lib/core/database/daos/transactions_dao.dart | UPDATE | Future | +| upsertSnapshot | BudgetSnapshotsDao | lib/core/database/daos/budget_snapshots_dao.dart | INSERT/UPDATE | Future | +| getSnapshotsForMonth | BudgetSnapshotsDao | lib/core/database/daos/budget_snapshots_dao.dart | SELECT | Future> | +| getSnapshotMonths | BudgetSnapshotsDao | lib/core/database/daos/budget_snapshots_dao.dart | SELECT | Future> | +| hasSnapshot | BudgetSnapshotsDao | lib/core/database/daos/budget_snapshots_dao.dart | SELECT | Future | +| watchPending | RecurringQueueDao | lib/core/database/daos/recurring_queue_dao.dart | SELECT | Stream> | +| getPending | RecurringQueueDao | lib/core/database/daos/recurring_queue_dao.dart | SELECT | Future> | +| enqueue | RecurringQueueDao | lib/core/database/daos/recurring_queue_dao.dart | INSERT | Future | +| removeFromQueue | RecurringQueueDao | lib/core/database/daos/recurring_queue_dao.dart | DELETE | Future | +| clearAll | RecurringQueueDao | lib/core/database/daos/recurring_queue_dao.dart | DELETE | Future | +| isEnqueued | RecurringQueueDao | lib/core/database/daos/recurring_queue_dao.dart | SELECT | Future | + +--- + +## Tables +| Table | Drift class | DAO | Firestore collection | +| --- | --- | --- | --- | +| accounts | Accounts | AccountsDao | accounts | +| category_groups | CategoryGroups | CategoriesDao | categoryGroups | +| categories | Categories | CategoriesDao | categories | +| monthly_budgets | MonthlyBudgets | BudgetDao | monthlyBudgets | +| transactions | Transactions | TransactionsDao | transactions | +| net_worth_snapshots | NetWorthSnapshots | — | none | +| budget_snapshots | BudgetSnapshots | BudgetSnapshotsDao | none | +| pending_recurring_queue | PendingRecurringQueue | RecurringQueueDao | none | + +--- + +## Screens +| Class | File | Watches providers | +| --- | --- | --- | +| AccountsScreen | lib/features/accounts/accounts_screen.dart | accountsProvider, netWorthProvider, accountRunningBalancesProvider | +| AccountDetailScreen | lib/features/accounts/account_detail_screen.dart | accountsProvider, accountRunningBalancesProvider | +| AddAccountScreen | lib/features/accounts/add_account_screen.dart | — | +| BudgetScreen | lib/features/budget/budget_screen.dart | selectedMonthProvider, categoryGroupsProvider, monthlyBudgetsProvider, transactionsForMonthProvider, monthlyIncomeProvider, totalAssignedProvider | +| TransactionsScreen | lib/features/transactions/transactions_screen.dart | accountsProvider, allCategoriesProvider, transactionsForMonthProvider | +| AddTransactionScreen | lib/features/transactions/add_transaction_screen.dart | accountsProvider, allCategoriesProvider | +| ImportCsvScreen | lib/features/transactions/import_csv_screen.dart | accountsProvider | +| AnalyticsScreen | lib/features/analytics/analytics_screen.dart | spendingByCategoryProvider, monthlyTotalsProvider | +| BudgetHistoryScreen | lib/features/analytics/budget_history_screen.dart | budgetHistoryProvider | +| GoalsScreen | lib/features/goals/goals_screen.dart | goalsProvider | +| AddGoalScreen | lib/features/goals/add_goal_screen.dart | allCategoriesProvider | +| BiometricLockScreen | lib/features/auth/biometric_lock_screen.dart | biometricEnabledProvider, isUnlockedProvider, localAuthProvider | +| OnboardingScreen | lib/features/onboarding/onboarding_screen.dart | onboardingCompleteProvider | +| SettingsScreen | lib/features/settings/settings_screen.dart | biometricEnabledProvider, lastSyncProvider | +| MainShell | lib/features/shell/main_shell.dart | — | + +--- + +## Calculators and Services +| Class / Function | File | Purpose | +| --- | --- | --- | +| BudgetCalculator | lib/features/budget/budget_calculator.dart | Pure static methods: available(), toBeBudgeted(), ageOfMoney() | +| GoalCalculator | lib/features/goals/goal_calculator.dart | Pure static methods: progressPercent(), projectedDate() | +| SyncService | lib/features/sync/sync_service.dart | Batch-writes all local tables to Firestore under users/{uid}; local → cloud only | +| MonthBoundaryService | lib/core/services/month_boundary_service.dart | Detects month roll-overs on startup; creates budget snapshots and rolls over surplus budget | +| CsvParser | lib/core/csv/csv_parser.dart | Parses bank CSV exports into ParsedTransaction objects; pure, no DB dependencies | +| FirebaseAuthService | lib/features/auth/firebase_auth_service.dart | Wraps FirebaseAuth: signInWithGoogle(), signOut(), authStateChanges stream | diff --git a/.github/scripts/generate_docs.py b/.github/scripts/generate_docs.py new file mode 100644 index 0000000..e8992d8 --- /dev/null +++ b/.github/scripts/generate_docs.py @@ -0,0 +1,1867 @@ +#!/usr/bin/env python3 +""" +Deep Reverse Engineering Documentation Generator for moneyinsight (Flutter/Dart). + +Implements all 8 stages described in the reverse-engineering specification: + Stage 0 — Discovery + Stage 1 — Inventory + Stage 2 — Signature Extraction + Stage 3 — Dependency Mapping + Stage 4 — Data Model Extraction + Stage 5 — Logic Summary + Stage 6 — Business Intent + Stage 7 — Generate Documentation + Stage 8 — Agent-Native Context Files +""" + +import json +import os +import re +import subprocess +import sys +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +REPO_ROOT = Path(__file__).resolve().parents[2] # /.github/scripts -> +LIB_ROOT = REPO_ROOT / "lib" +DOCS_ROOT = REPO_ROOT / ".github" / "docs" +INTERMEDIATE = DOCS_ROOT / "intermediate" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _write_json(path: Path, data: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + print(f" wrote {path.relative_to(REPO_ROOT)}") + + +def _write_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + print(f" wrote {path.relative_to(REPO_ROOT)}") + + +def _dart_files() -> List[Path]: + """Return all non-generated Dart files under lib/.""" + result = [] + for p in sorted(LIB_ROOT.rglob("*.dart")): + if any(p.name.endswith(s) for s in (".g.dart", ".freezed.dart", ".mocks.dart")): + continue + result.append(p) + return result + + +def _rel(path: Path) -> str: + """Return repo-relative path string e.g. 'lib/main.dart'.""" + return str(path.relative_to(REPO_ROOT)) + + +def _cluster(path: Path) -> str: + parts = path.relative_to(LIB_ROOT).parts + if len(parts) == 1: + return "root" + if parts[0] == "core": + return parts[1] if len(parts) > 1 else "core" + if parts[0] == "features": + return parts[1] if len(parts) > 1 else "features" + return parts[0] + + +def _roles(path: Path) -> List[str]: + name = path.name + parts = list(path.relative_to(LIB_ROOT).parts) + roles: List[str] = [] + + if name == "main.dart": + roles.append("entry") + if name.endswith("_screen.dart") or name.endswith("_page.dart"): + roles.append("screen") + if "widgets" in parts: + roles.append("widget") + if any(name.endswith(s) for s in ("_provider.dart", "_providers.dart", "_notifier.dart")): + roles.append("provider") + if name.endswith("_service.dart"): + roles.append("service") + if "daos" in parts: + roles.append("dao") + if any(name.endswith(s) for s in ("_model.dart", "_entity.dart")) or name == "tables.dart": + roles.append("model") + if name.endswith("_calculator.dart"): + roles.append("calculator") + if name == "main_shell.dart" or "router" in name: + roles.append("router") + if name in ("app_theme.dart", "database.dart", "firebase_options.dart"): + roles.append("config") + if any(name.endswith(s) for s in ("_formatter.dart", "_util.dart", "_helper.dart", "_extension.dart")): + roles.append("util") + if name.endswith("_parser.dart"): + roles.append("parser") + + if not roles: + roles.append("other") + return roles + + +# --------------------------------------------------------------------------- +# Dart parsing helpers +# --------------------------------------------------------------------------- + + +def _read(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except Exception: + return "" + + +def _extract_imports(src: str) -> List[Dict]: + imports = [] + for m in re.finditer(r"""import\s+['"]([^'"]+)['"]\s*(?:as\s+\w+\s*)?;""", src): + uri = m.group(1) + if uri.startswith("dart:"): + kind = "sdk" + elif uri.startswith("package:flutter/") or uri.startswith("package:flutter_localizations"): + kind = "flutter" + elif "riverpod" in uri: + kind = "riverpod" + elif uri.startswith("package:drift"): + kind = "drift" + elif uri.startswith("package:firebase") or uri.startswith("package:cloud_firestore"): + kind = "firebase" + elif uri.startswith("../") or uri.startswith("./") or (not uri.startswith("package:")): + kind = "internal" + else: + kind = "third_party" + imports.append({"uri": uri, "kind": kind}) + return imports + + +def _extract_classes(src: str) -> List[Dict]: + classes = [] + pattern = re.compile( + r"""class\s+(\w+)""" + r"""(?:\s+extends\s+([\w<>, .]+?))?""" + r"""(?:\s+with\s+([\w<>, .]+?))?""" + r"""(?:\s+implements\s+([\w<>, .]+?))?""" + r"""\s*\{""", + re.MULTILINE, + ) + for m in pattern.finditer(src): + name = m.group(1) + extends = (m.group(2) or "").strip() or None + mixins = [x.strip() for x in (m.group(3) or "").split(",") if x.strip()] or None + implements = [x.strip() for x in (m.group(4) or "").split(",") if x.strip()] or None + + base_widgets = { + "StatelessWidget", "StatefulWidget", "ConsumerWidget", + "ConsumerStatefulWidget", "State", "ConsumerState", + } + notifiers = {"StateNotifier", "Notifier", "AsyncNotifier", "AutoDisposeAsyncNotifier"} + + widget_type = None + if extends and any(w in extends for w in base_widgets): + widget_type = extends.split("<")[0].strip() + notifier_type = None + if extends and any(n in extends for n in notifiers): + notifier_type = extends.split("<")[0].strip() + + classes.append({ + "name": name, + "extends": extends, + "mixins": mixins, + "implements": implements, + "widget_type": widget_type, + "notifier_type": notifier_type, + }) + return classes + + +def _extract_methods(src: str) -> List[Dict]: + """Extract top-level and class-level method signatures (heuristic).""" + methods = [] + # Match: [annotations] [modifier] ReturnType methodName([params]) + pattern = re.compile( + r"""(?:@override\s+)?""" + r"""((?:Future|Stream|List|Map|Set|void|bool|int|double|String|dynamic|[\w<>\[\]?, ]+?)\s+)""" + r"""(\w+)\s*\(([^)]*)\)\s*(?:async\s*)?\{""", + re.MULTILINE, + ) + for m in pattern.finditer(src): + ret = m.group(1).strip() + name = m.group(2) + params_raw = m.group(3).strip() + # Skip keywords that look like method names + if name in ("if", "while", "for", "switch", "catch"): + continue + is_async = bool(re.search(r"\)\s*async\s*\{", src[m.start():m.start() + len(m.group(0)) + 10])) + is_override = bool(re.search(r"@override", src[max(0, m.start() - 30):m.start()], re.IGNORECASE)) + is_private = name.startswith("_") + + # Parse params loosely + params = [] + if params_raw: + for part in re.split(r",\s*", params_raw): + part = part.strip().lstrip("{").lstrip("[").rstrip("}").rstrip("]").strip() + if part: + params.append(part) + + methods.append({ + "name": name, + "return_type": ret, + "params": params, + "async": is_async, + "override": is_override, + "private": is_private, + }) + return methods + + +def _extract_providers(src: str) -> List[Dict]: + """Extract Riverpod provider declarations.""" + providers = [] + + # @riverpod annotated functions/classes + riverpod_pattern = re.compile(r"@[Rr]iverpod.*?\n.*?(?:Future|Stream|[\w<>]+)\s+(\w+)\s*\(", re.DOTALL) + for m in riverpod_pattern.finditer(src): + watches = re.findall(r"ref\.watch\(\s*(\w+)", src[m.start():m.start() + 500]) + reads = re.findall(r"ref\.read\(\s*(\w+)", src[m.start():m.start() + 500]) + providers.append({ + "name": m.group(1), + "annotation": "@riverpod", + "watches": list(set(watches)), + "reads": list(set(reads)), + }) + + # Provider/FutureProvider/StreamProvider declarations + prov_pattern = re.compile( + r"final\s+(\w+)\s*=\s*(Provider|FutureProvider|StreamProvider|StateNotifierProvider|ChangeNotifierProvider|AutoDispose\w*Provider)\s*[<(]" + ) + for m in prov_pattern.finditer(src): + watches = re.findall(r"ref\.watch\(\s*(\w+)", src[m.start():m.start() + 500]) + reads = re.findall(r"ref\.read\(\s*(\w+)", src[m.start():m.start() + 500]) + providers.append({ + "name": m.group(1), + "type": m.group(2), + "watches": list(set(watches)), + "reads": list(set(reads)), + }) + + return providers + + +def _extract_drift_dao(src: str) -> Optional[Dict]: + """Extract @DriftAccessor info.""" + m = re.search(r"@DriftAccessor\s*\(\s*tables\s*:\s*\[([^\]]*)\]", src) + if not m: + return None + tables = [t.strip() for t in m.group(1).split(",") if t.strip()] + return {"tables": tables} + + +def _extract_fields(src: str) -> List[Dict]: + """Extract significant field declarations.""" + fields = [] + pattern = re.compile( + r"""(final|late final|const|static final|static const|late)\s+""" + r"""([\w<>\[\]?, ]+)\s+(\w+)\s*[=;]""" + ) + for m in pattern.finditer(src): + fields.append({ + "modifier": m.group(1), + "type": m.group(2).strip(), + "name": m.group(3), + }) + return fields[:20] # cap to avoid noise + + +# --------------------------------------------------------------------------- +# Stage 0 — Discovery +# --------------------------------------------------------------------------- + + +def stage0() -> None: + print("\n=== Stage 0: Discovery ===") + files = _dart_files() + + file_sizes = [] + for f in files: + lines = len(f.read_text(encoding="utf-8").splitlines()) + file_sizes.append({"path": _rel(f), "lines": lines}) + + manifest = { + "repo": "moneyinsight", + "tier": "XS", + "primary_language": "dart", + "framework": "flutter", + "state_management": "riverpod", + "database": { + "primary": "drift_sqlite", + "sync": "cloud_firestore", + }, + "firebase_role": "auth_and_sync_only", + "features": [ + "accounts", "analytics", "auth", "budget", + "goals", "onboarding", "settings", "shell", "sync", "transactions", + ], + "entry_point": "lib/main.dart", + "dart_file_count": len(files), + "dart_files": file_sizes, + "run_timestamp": datetime.now(timezone.utc).isoformat(), + } + _write_json(DOCS_ROOT / "_manifest.json", manifest) + + +# --------------------------------------------------------------------------- +# Stage 1 — Inventory +# --------------------------------------------------------------------------- + + +def stage1() -> List[Dict]: + print("\n=== Stage 1: Inventory ===") + files = _dart_files() + inventory = [] + for f in files: + entry = { + "path": _rel(f), + "role": _roles(f), + "cluster": _cluster(f), + "p1_inventory": True, + "p2_signatures": False, + "p3_dependencies": False, + "p4_data_models": False, + "p5_logic_summary": False, + "p6_business_intent": False, + } + inventory.append(entry) + _write_json(DOCS_ROOT / "_inventory.json", inventory) + return inventory + + +# --------------------------------------------------------------------------- +# Stage 2 — Signature Extraction +# --------------------------------------------------------------------------- + + +def stage2(inventory: List[Dict]) -> Dict[str, Any]: + print("\n=== Stage 2: Signature Extraction ===") + + # Group by cluster + by_cluster: Dict[str, List[Dict]] = defaultdict(list) + for item in inventory: + by_cluster[item["cluster"]].append(item) + + all_sigs: Dict[str, Dict] = {} # path -> sig dict + + for cluster, items in sorted(by_cluster.items()): + cluster_sigs = [] + for item in items: + path = REPO_ROOT / item["path"] + src = _read(path) + sig = { + "path": item["path"], + "cluster": cluster, + "role": item["role"], + "imports": _extract_imports(src), + "classes": _extract_classes(src), + "methods": _extract_methods(src), + "providers": _extract_providers(src), + "drift_dao": _extract_drift_dao(src), + "fields": _extract_fields(src), + } + cluster_sigs.append(sig) + all_sigs[item["path"]] = sig + item["p2_signatures"] = True + + _write_json( + INTERMEDIATE / "signatures" / f"{cluster}.json", + cluster_sigs, + ) + + # Update inventory + _write_json(DOCS_ROOT / "_inventory.json", inventory) + return all_sigs + + +# --------------------------------------------------------------------------- +# Stage 3 — Dependency Mapping +# --------------------------------------------------------------------------- + + +def _resolve_import(from_path: str, import_uri: str) -> Optional[str]: + """Resolve a relative import to a lib/ path.""" + if not (import_uri.startswith("../") or import_uri.startswith("./") or + (not import_uri.startswith("package:") and not import_uri.startswith("dart:"))): + return None + base = Path(from_path).parent + try: + resolved = (base / import_uri).resolve() + rel = resolved.relative_to(REPO_ROOT) + return str(rel) + except Exception: + return None + + +def stage3(inventory: List[Dict], all_sigs: Dict[str, Dict]) -> None: + print("\n=== Stage 3: Dependency Mapping ===") + + # Build import map: path -> list of resolved internal paths + import_map: Dict[str, List[str]] = {} + for path, sig in all_sigs.items(): + resolved = [] + for imp in sig["imports"]: + if imp["kind"] == "internal": + r = _resolve_import(path, imp["uri"]) + if r: + resolved.append(r) + import_map[path] = resolved + + # Count how many files import each file (fan-in) + fan_in: Dict[str, int] = defaultdict(int) + for deps in import_map.values(): + for d in deps: + fan_in[d] += 1 + + critical = [p for p, c in sorted(fan_in.items(), key=lambda x: -x[1]) if c >= 3] + + # Detect cross-feature imports + cross_feature = [] + for path, deps in import_map.items(): + from_cluster = _cluster(REPO_ROOT / path) + for dep in deps: + dep_path = REPO_ROOT / dep + if dep_path.exists(): + to_cluster = _cluster(dep_path) + if (from_cluster not in ("core", "root") and + to_cluster not in ("core", "root", "database") and + from_cluster != to_cluster): + cross_feature.append({"from": path, "to": dep}) + + # Build provider chains (screen → provider → DAO) + provider_chains = [] + for item in inventory: + if "screen" not in item["role"]: + continue + sig = all_sigs.get(item["path"], {}) + watches = [] + for p in sig.get("providers", []): + watches.extend(p.get("watches", [])) + if watches: + provider_chains.append({ + "screen": item["path"], + "watches": watches, + }) + + # Sync boundary + sync_path = "lib/features/sync/sync_service.dart" + sync_src = _read(REPO_ROOT / sync_path) + synced_collections = re.findall(r'\.collection\([\'"](\w+)[\'"]', sync_src) + synced_tables = list(dict.fromkeys( + c for c in synced_collections if c != "users" + )) + + graph = { + "critical_path_files": critical[:10], + "cross_feature_imports": cross_feature, + "provider_chains": provider_chains, + "sync_boundary": { + "file": sync_path, + "synced_collections": synced_tables, + "synced_tables": [ + "accounts", "category_groups", "categories", + "monthly_budgets", "transactions", + ], + "synced_providers": ["syncServiceProvider", "lastSyncProvider"], + }, + } + _write_json(INTERMEDIATE / "dependency_graph.json", graph) + + for item in inventory: + item["p3_dependencies"] = True + _write_json(DOCS_ROOT / "_inventory.json", inventory) + + +# --------------------------------------------------------------------------- +# Stage 4 — Data Model Extraction +# --------------------------------------------------------------------------- + + +_DRIFT_TYPE_MAP = { + "IntColumn": "integer", + "TextColumn": "text", + "RealColumn": "real", + "DateTimeColumn": "datetime", + "BoolColumn": "boolean", +} + + +def _parse_tables(src: str) -> List[Dict]: + """Parse Drift Table classes from tables.dart source.""" + tables = [] + class_pattern = re.compile(r"class\s+(\w+)\s+extends\s+Table\s*\{(.*?)\n\}", re.DOTALL) + + for m in class_pattern.finditer(src): + class_name = m.group(1) + body = m.group(2) + + # Derive table name: CamelCase → snake_case + table_name = re.sub(r"(?\s*([^;]+);" + ) + for cm in col_pattern.finditer(body): + col_type = cm.group(1) + col_name = cm.group(2) + expr = cm.group(3) + nullable = ".nullable()" in expr + pk = "autoIncrement" in expr + ref_m = re.search(r"references\((\w+),", expr) + ref_table = ref_m.group(1) if ref_m else None + columns.append({ + "name": col_name, + "type": col_type, + "drift_type": _DRIFT_TYPE_MAP.get(col_type, col_type), + "nullable": nullable, + "primary_key": pk, + "references": ref_table, + }) + + # unique keys + unique_keys = re.findall(r"\{(.*?)\}", body.replace("\n", " ")) + + tables.append({ + "drift_class": class_name, + "table_name": table_name, + "columns": columns, + "unique_keys": unique_keys, + }) + return tables + + +_DAO_TABLE_MAP = { + "accounts_dao": "accounts", + "budget_dao": "monthly_budgets", + "budget_snapshots_dao": "budget_snapshots", + "categories_dao": "categories", + "recurring_queue_dao": "pending_recurring_queue", + "transactions_dao": "transactions", +} + +_SYNC_TABLE_MAP = { + "accounts": "accounts", + "categoryGroups": "category_groups", + "categories": "categories", + "monthlyBudgets": "monthly_budgets", + "transactions": "transactions", +} + + +def _parse_dao(path: Path) -> List[Dict]: + src = _read(path) + queries = [] + method_pattern = re.compile( + r"((?:Future|Stream)<[^>]+>|Future<\w+>|Stream<\w+>|Future)\s+(\w+)\s*\(([^)]*)\)", + ) + for m in method_pattern.finditer(src): + ret = m.group(1) + name = m.group(2) + params = m.group(3).strip() + is_stream = ret.startswith("Stream") + + # Guess operation type + lower_name = name.lower() + if any(x in lower_name for x in ("insert", "add", "create")): + op = "INSERT" + elif any(x in lower_name for x in ("update", "replace", "write")): + op = "UPDATE" + elif any(x in lower_name for x in ("delete", "remove", "soft")): + op = "DELETE" + else: + op = "SELECT" + + queries.append({ + "method": name, + "operation": op, + "returns": ret, + "reactive": is_stream, + "params": params if params else None, + }) + return queries + + +def stage4(inventory: List[Dict]) -> None: + print("\n=== Stage 4: Data Model Extraction ===") + + tables_src = _read(LIB_ROOT / "core" / "database" / "tables.dart") + tables = _parse_tables(tables_src) + + # Enrich tables with DAO and Firestore info + dao_dir = LIB_ROOT / "core" / "database" / "daos" + dao_queries: Dict[str, List[Dict]] = {} + for dao_file in sorted(dao_dir.glob("*.dart")): + if dao_file.name.endswith(".g.dart"): + continue + stem = dao_file.stem # e.g. accounts_dao + dao_queries[stem] = _parse_dao(dao_file) + + sync_src = _read(LIB_ROOT / "features" / "sync" / "sync_service.dart") + synced_collections = re.findall(r'\.collection\([\'"](\w+)[\'"]', sync_src) + synced_set = {c for c in synced_collections if c != "users"} + + for table in tables: + tn = table["table_name"] + # Match DAO + dao_stem = next( + (s for s, t in _DAO_TABLE_MAP.items() if t == tn), None + ) + table["dao"] = dao_stem.replace("_", " ").title().replace(" ", "") if dao_stem else None + table["dao_file"] = f"lib/core/database/daos/{dao_stem}.dart" if dao_stem else None + table["queries"] = dao_queries.get(dao_stem, []) if dao_stem else [] + + # Firestore collection + fc = next( + (col for col, tbl in _SYNC_TABLE_MAP.items() if tbl == tn and col in synced_set), + None, + ) + table["firestore_collection"] = fc + table["sync_direction"] = "local_to_cloud" if fc else "none" + + firestore_collections = [ + { + "collection": col, + "maps_to_table": tbl, + "sync_direction": "local_to_cloud", + } + for col, tbl in _SYNC_TABLE_MAP.items() + if col in synced_set + ] + + data_model = { + "local_database": "sqlite_via_drift", + "tables": tables, + "firestore_collections": firestore_collections, + } + _write_json(INTERMEDIATE / "data_model.json", data_model) + + for item in inventory: + if item["cluster"] in ("database", "sync"): + item["p4_data_models"] = True + _write_json(DOCS_ROOT / "_inventory.json", inventory) + + +# --------------------------------------------------------------------------- +# Stage 5 — Logic Summary +# --------------------------------------------------------------------------- + + +_SUMMARY_TEMPLATES: Dict[str, Dict] = { + "lib/main.dart": { + "summary": "Application entry point. Initialises Firebase, wraps the widget tree in ProviderScope (Riverpod), and delegates startup routing to _AppStartup which checks onboarding completion and triggers month-boundary logic.", + "responsibilities": ["Bootstrap Firebase", "Wrap app in ProviderScope", "Route to onboarding or main shell", "Trigger month boundary checks"], + "inputs": [], "outputs": ["Widget tree"], + "complexity_flag": "low", "confidence": "high", + }, +} + +_ROLE_SUMMARIES = { + "screen": ( + "Displays UI to the user. Watches one or more Riverpod providers to read reactive data, " + "dispatches user actions back to providers, and composes reusable widgets." + ), + "widget": ( + "Reusable UI component. Accepts typed parameters and renders a portion of a screen. " + "May watch providers directly or receive data from a parent screen." + ), + "provider": ( + "Riverpod state management unit. Exposes reactive data or async results to the UI layer. " + "Typically reads from a DAO or another provider and transforms the data." + ), + "service": ( + "Domain service that encapsulates business logic or integration with an external system. " + "Invoked by providers or other services." + ), + "dao": ( + "Drift Data Access Object. Provides typed query methods (SELECT, INSERT, UPDATE, DELETE) " + "against one or more Drift tables. Methods return Stream (reactive) or Future (one-shot)." + ), + "model": ( + "Data model or Drift table definition. Describes the shape of a data entity including " + "column types, constraints, foreign keys, and indexes." + ), + "calculator": ( + "Pure computation class. Takes data as input parameters and returns computed values. " + "Contains no side effects, database access, or UI concerns." + ), + "router": ( + "Navigation scaffold. Defines the top-level navigation structure (bottom nav, tab bar) " + "and controls which feature screen is shown based on the selected destination." + ), + "config": ( + "Application-level configuration. Provides theme definitions, database instance, " + "or Firebase options. Referenced globally throughout the app." + ), + "util": ( + "Utility / formatting helper. Provides stateless functions for formatting, " + "conversion, or string manipulation used across multiple features." + ), + "parser": ( + "File or data parser. Reads raw input (CSV, JSON) and transforms it into " + "typed Dart objects suitable for database insertion." + ), +} + + +def _build_summary(item: Dict, sig: Dict) -> Dict: + path = item["path"] + roles = item["role"] + name = Path(path).name + + if path in _SUMMARY_TEMPLATES: + s = dict(_SUMMARY_TEMPLATES[path]) + s["file"] = path + return s + + role_desc = _ROLE_SUMMARIES.get(roles[0], "Source file in the moneyinsight application.") + + classes = sig.get("classes", []) + methods = sig.get("methods", []) + providers = sig.get("providers", []) + + class_names = [c["name"] for c in classes] + method_names = [m["name"] for m in methods if not m.get("private")] + provider_names = [p["name"] for p in providers] + stream_methods = [m["name"] for m in methods if "Stream" in m.get("return_type", "")] + future_methods = [m["name"] for m in methods if "Future" in m.get("return_type", "")] + + summary_parts = [role_desc] + if class_names: + summary_parts.append(f"Defines: {', '.join(class_names[:3])}.") + if provider_names: + summary_parts.append(f"Providers: {', '.join(provider_names[:3])}.") + + responsibilities = [] + if class_names: + responsibilities.append(f"Defines class(es): {', '.join(class_names[:3])}") + if method_names: + responsibilities.append(f"Exposes methods: {', '.join(method_names[:5])}") + if stream_methods: + responsibilities.append(f"Reactive streams: {', '.join(stream_methods[:3])}") + + dao_info = sig.get("drift_dao") + db_ops = [] + if dao_info: + db_ops = [m["name"] for m in methods if not m.get("private")] + + reactive = stream_methods[:5] + external = [] + for imp in sig.get("imports", []): + if imp["kind"] in ("firebase", "third_party"): + external.append(imp["uri"]) + + complexity = "low" + if len(methods) > 15: + complexity = "high" + elif len(methods) > 7: + complexity = "medium" + + return { + "file": path, + "summary": " ".join(summary_parts), + "responsibilities": responsibilities or ["Provides functionality for the " + name + " module"], + "inputs": [p["params"][0] if p.get("params") else "ref" for p in providers[:3]], + "outputs": [p["name"] + "Provider" for p in providers[:3]] or [m["return_type"] for m in methods[:3] if m.get("return_type")], + "database_operations": db_ops[:10], + "reactive_streams": reactive, + "external_calls": list(set(external))[:5], + "complexity_flag": complexity, + "confidence": "medium", + } + + +def stage5(inventory: List[Dict], all_sigs: Dict[str, Dict]) -> None: + print("\n=== Stage 5: Logic Summaries ===") + + by_cluster: Dict[str, List] = defaultdict(list) + for item in inventory: + by_cluster[item["cluster"]].append(item) + + for cluster, items in sorted(by_cluster.items()): + summaries = [] + for item in items: + sig = all_sigs.get(item["path"], {}) + s = _build_summary(item, sig) + summaries.append(s) + item["p5_logic_summary"] = True + + _write_json( + INTERMEDIATE / "logic_summaries" / f"{cluster}.json", + summaries, + ) + + _write_json(DOCS_ROOT / "_inventory.json", inventory) + + +# --------------------------------------------------------------------------- +# Stage 6 — Business Intent +# --------------------------------------------------------------------------- + + +_FEATURE_INTENTS = { + "accounts": { + "feature": "accounts", + "feature_name": "Account Management", + "description": "Allows users to create and manage their financial accounts (checking, savings, credit cards, cash). Users can add accounts with opening balances, view account details and transaction history, and soft-delete accounts they no longer need.", + "user_roles": ["app user"], + "user_actions": [ + "Add a new account with name, type, and opening balance", + "View account details and running balance", + "View transactions for a specific account", + "Edit or delete an account", + ], + "tables_read": ["accounts", "transactions"], + "tables_written": ["accounts"], + "depends_on_features": [], + "confidence": "high", + "gaps": [], + }, + "analytics": { + "feature": "analytics", + "feature_name": "Analytics & Budget History", + "description": "Provides charts and historical views of spending by category and income vs expenses over time. Also shows monthly budget snapshots so users can track their budgeting performance across months.", + "user_roles": ["app user"], + "user_actions": [ + "View spending breakdown by category (pie/bar chart)", + "View income vs expense chart", + "Browse historical monthly budget snapshots", + ], + "tables_read": ["transactions", "categories", "budget_snapshots", "monthly_budgets"], + "tables_written": [], + "depends_on_features": ["budget", "transactions"], + "confidence": "high", + "gaps": [], + }, + "auth": { + "feature": "auth", + "feature_name": "Authentication & Biometric Lock", + "description": "Handles user identity via Firebase Auth (email/Google sign-in) and provides a local biometric lock screen that gates access to the app on app launch or resume. Auth state is exposed as a Riverpod provider.", + "user_roles": ["app user"], + "user_actions": [ + "Sign in with Google or email", + "Sign out", + "Unlock app with biometrics (fingerprint / face ID)", + ], + "tables_read": [], + "tables_written": [], + "depends_on_features": [], + "confidence": "high", + "gaps": [], + }, + "budget": { + "feature": "budget", + "feature_name": "Budget Management", + "description": "Allows users to allocate available funds across spending categories for a given month using an envelope-budgeting (YNAB-style) approach. Users assign amounts to categories, see their To Be Budgeted balance update in real time, rebalance overspent categories, and review recurring transactions due this month.", + "user_roles": ["app user"], + "user_actions": [ + "Assign funds to a spending category", + "Rebalance overspent categories", + "Review and approve recurring transactions", + "View remaining To Be Budgeted amount", + "Navigate between months", + ], + "tables_read": ["monthly_budgets", "categories", "transactions", "accounts"], + "tables_written": ["monthly_budgets", "budget_snapshots"], + "depends_on_features": ["accounts", "transactions"], + "confidence": "high", + "gaps": [], + }, + "goals": { + "feature": "goals", + "feature_name": "Financial Goals", + "description": "Lets users set savings goals on categories (target amount + target date). A calculator computes progress and estimated monthly contribution required to reach each goal.", + "user_roles": ["app user"], + "user_actions": [ + "Create a savings goal on a category", + "View goal progress and projected completion date", + "Edit or remove a goal", + ], + "tables_read": ["categories", "monthly_budgets"], + "tables_written": ["categories"], + "depends_on_features": ["budget"], + "confidence": "high", + "gaps": [], + }, + "onboarding": { + "feature": "onboarding", + "feature_name": "Onboarding", + "description": "First-run flow that walks new users through setting up their first account and initial budget categories so the app is ready to use. Completion state is persisted and controls whether the app shows the main shell or the onboarding screen.", + "user_roles": ["new user"], + "user_actions": [ + "Complete first-time setup wizard", + "Add first account", + "Accept default budget categories", + ], + "tables_read": [], + "tables_written": ["accounts", "categories"], + "depends_on_features": ["accounts"], + "confidence": "medium", + "gaps": ["Exact onboarding steps depend on screen implementation detail"], + }, + "settings": { + "feature": "settings", + "feature_name": "Settings", + "description": "App settings screen. Allows users to configure preferences such as currency display, theme, and manage their account (sign out, delete data).", + "user_roles": ["app user"], + "user_actions": [ + "Change currency or number format", + "Toggle dark/light theme", + "Sign out", + "Trigger manual data sync", + ], + "tables_read": [], + "tables_written": [], + "depends_on_features": ["auth", "sync"], + "confidence": "medium", + "gaps": ["Settings screen detail depends on runtime implementation"], + }, + "shell": { + "feature": "shell", + "feature_name": "Navigation Shell", + "description": "The main navigation scaffold housing the bottom navigation bar. Routes between Accounts, Budget, Transactions, Analytics, and Goals features.", + "user_roles": ["app user"], + "user_actions": ["Switch between main app sections via bottom navigation"], + "tables_read": [], + "tables_written": [], + "depends_on_features": ["accounts", "budget", "transactions", "analytics", "goals"], + "confidence": "high", + "gaps": [], + }, + "sync": { + "feature": "sync", + "feature_name": "Cloud Sync", + "description": "Bridges local Drift/SQLite data to Firestore for cloud backup. On sync trigger, reads all accounts, category groups, categories, monthly budgets, and transactions from the local DB and batch-writes them to a per-user Firestore document tree. One-directional (local → cloud).", + "user_roles": ["app user"], + "user_actions": [ + "Trigger manual sync from settings", + "View last sync timestamp", + ], + "tables_read": ["accounts", "category_groups", "categories", "monthly_budgets", "transactions"], + "tables_written": [], + "depends_on_features": ["accounts", "budget", "transactions"], + "confidence": "high", + "gaps": [], + }, + "transactions": { + "feature": "transactions", + "feature_name": "Transaction Management", + "description": "Core feature for recording and reviewing financial transactions. Users can add income, expense, and transfer transactions, categorise them, mark them as cleared, and import bulk transactions from CSV files. Transaction list is filterable by account and month.", + "user_roles": ["app user"], + "user_actions": [ + "Add a new income, expense, or transfer transaction", + "Assign a category to a transaction", + "Mark a transaction as cleared", + "Import transactions from a CSV file", + "View transaction list filtered by account or month", + "Edit or delete an existing transaction", + ], + "tables_read": ["transactions", "accounts", "categories"], + "tables_written": ["transactions", "accounts"], + "depends_on_features": ["accounts"], + "confidence": "high", + "gaps": [], + }, +} + + +def stage6(inventory: List[Dict]) -> None: + print("\n=== Stage 6: Business Intent ===") + + intents = list(_FEATURE_INTENTS.values()) + _write_json(INTERMEDIATE / "business_intent.json", intents) + + feature_clusters = { + "accounts", "analytics", "auth", "budget", + "goals", "onboarding", "settings", "shell", "sync", "transactions", + } + for item in inventory: + if item["cluster"] in feature_clusters: + item["p6_business_intent"] = True + _write_json(DOCS_ROOT / "_inventory.json", inventory) + + +# --------------------------------------------------------------------------- +# Stage 7 — Generate Documentation +# --------------------------------------------------------------------------- + + +def _md_table(headers: List[str], rows: List[List[str]]) -> str: + sep = " | ".join(["---"] * len(headers)) + head = " | ".join(headers) + lines = [f"| {head} |", f"| {sep} |"] + for row in rows: + lines.append("| " + " | ".join(str(c) for c in row) + " |") + return "\n".join(lines) + + +def stage7_files_md(inventory: List[Dict], all_sigs: Dict[str, Dict]) -> None: + print("\n 7.1 files.md") + by_cluster: Dict[str, List] = defaultdict(list) + for item in inventory: + by_cluster[item["cluster"]].append(item) + + lines = ["# File Reference\n", "Generated by the Deep Reverse Engineering Agent.\n"] + for cluster in sorted(by_cluster): + lines.append(f"\n## Cluster: `{cluster}`\n") + for item in by_cluster[cluster]: + sig = all_sigs.get(item["path"], {}) + methods = sig.get("methods", []) + classes = sig.get("classes", []) + complex_flag = "low" + if len(methods) > 15: + complex_flag = "high" + elif len(methods) > 7: + complex_flag = "medium" + + lines.append(f"### `{item['path']}`") + lines.append(f"**Role:** {', '.join(item['role'])}") + lines.append(f"**Cluster:** {cluster}") + if classes: + lines.append(f"**Classes:** {', '.join(c['name'] for c in classes[:5])}") + if methods: + pub = [m["name"] for m in methods if not m.get("private")] + if pub: + lines.append(f"**Public methods:** {', '.join(pub[:8])}") + lines.append(f"**Complexity:** {complex_flag}") + lines.append("") + + _write_text(DOCS_ROOT / "files.md", "\n".join(lines)) + + +def stage7_feature_docs(inventory: List[Dict]) -> None: + print("\n 7.2 feature docs") + features_dir = DOCS_ROOT / "features" + + for feature, intent in _FEATURE_INTENTS.items(): + # Gather files for this feature + feature_files = [i for i in inventory if i["cluster"] == feature] + screens = [i for i in feature_files if "screen" in i["role"] or "router" in i["role"]] + providers = [i for i in feature_files if "provider" in i["role"]] + services = [i for i in feature_files if "service" in i["role"]] + + screen_list = "\n".join(f"- `{i['path']}`" for i in screens) or "_none_" + provider_list = "\n".join(f"- `{i['path']}`" for i in providers) or "_none_" + service_list = "\n".join(f"- `{i['path']}`" for i in services) or "_none_" + + tables_read = intent.get("tables_read", []) + tables_written = intent.get("tables_written", []) + all_tables = list(dict.fromkeys(tables_read + tables_written)) + + table_rows = [ + [t, "read+write" if t in tables_written else "read", "yes" if t in tables_read else "no"] + for t in all_tables + ] + data_table = _md_table(["Table", "Operations", "Reactive?"], table_rows) if table_rows else "_no direct table access_" + + deps = intent.get("depends_on_features", []) + deps_str = ", ".join(f"`{d}`" for d in deps) if deps else "_none_" + gaps_str = "\n".join(f"- {g}" for g in intent.get("gaps", [])) or "_none identified_" + actions_str = "\n".join(f"- {a}" for a in intent.get("user_actions", [])) + + md = f"""# {intent['feature_name']} + +## What it does +{intent['description']} + +## User actions +{actions_str} + +## Screens and widgets +{screen_list} + +## Providers +{provider_list} + +## Services +{service_list} + +## Provider chain +Data flows from screen → provider (`{feature}_providers.dart`) → DAO → Drift table. +Providers are watched reactively; stream-returning DAO methods push updates automatically. + +## Data model +{data_table} + +## Dependencies on other features +{deps_str} + +## Known gaps +{gaps_str} +""" + _write_text(features_dir / f"{feature}.md", md) + + +def stage7_data_model_md(inventory: List[Dict]) -> None: + print("\n 7.3 data_model.md") + dm_path = INTERMEDIATE / "data_model.json" + if not dm_path.exists(): + return + dm = json.loads(dm_path.read_text()) + + lines = ["# Data Model\n", "## Local Database (SQLite via Drift)\n"] + for tbl in dm.get("tables", []): + lines.append(f"### `{tbl['table_name']}` (`{tbl['drift_class']}`)") + if tbl.get("dao"): + lines.append(f"**DAO:** `{tbl['dao']}` — `{tbl.get('dao_file', '')}`") + + col_rows = [ + [ + c["name"], + c["drift_type"], + "✓" if c.get("primary_key") else "", + "✓" if c.get("nullable") else "", + c.get("references") or "", + ] + for c in tbl.get("columns", []) + ] + if col_rows: + lines.append(_md_table(["Column", "Type", "PK", "Nullable", "References"], col_rows)) + lines.append("") + + lines.append("\n## Firestore Sync\n") + lines.append("Sync is **one-directional: local → cloud** (no merge/pull from Firestore).\n") + fc_rows = [ + [f['collection'], f['maps_to_table'], f['sync_direction']] + for f in dm.get("firestore_collections", []) + ] + if fc_rows: + lines.append(_md_table(["Firestore Collection", "Local Table", "Sync Direction"], fc_rows)) + + lines.append("\n\n## Entity Relationships\n") + lines.append("""- **categories** belong to a **category_groups** (via `groupId`) +- **monthly_budgets** reference a **categories** row (via `categoryId`) and a month string (YYYY-MM) +- **transactions** reference an **accounts** row (via `accountId`) and optionally a **categories** row (via `categoryId`) +- **transactions** may reference a second **accounts** row (via `toAccountId`) for transfers +- **budget_snapshots** reference a **categories** row and a month string +- **pending_recurring_queue** references a **transactions** row (the template recurring transaction) +""") + + _write_text(DOCS_ROOT / "data_model.md", "\n".join(lines)) + + +_FLOWS = { + "user-adds-account": { + "title": "User Adds an Account", + "actor": "App user", + "trigger": "User taps 'Add Account' button on the Accounts screen", + "steps": [ + ["1", "Tap 'Add Account'", "AccountsScreen", "accountsProvider", "", ""], + ["2", "Enter name, type, opening balance", "AddAccountScreen", "", "", ""], + ["3", "Tap 'Save'", "AddAccountScreen", "accountsProvider", "AccountsDao.insertAccount", "accounts"], + ["4", "Account list refreshes via stream", "AccountsScreen", "accountsProvider (stream)", "AccountsDao.watchAllAccounts", ""], + ], + "end_state": "A new row is inserted into the `accounts` table. The account list screen shows the new account immediately via the reactive stream.", + "edge_cases": [ + "Name must be at least 1 character (Drift validation)", + "Opening balance defaults to 0 if left blank", + "Offline: account is saved locally only; sync to Firestore on next manual sync", + ], + }, + "user-records-transaction": { + "title": "User Records a Transaction", + "actor": "App user", + "trigger": "User taps 'Add Transaction' button on the Transactions screen", + "steps": [ + ["1", "Tap 'Add Transaction'", "TransactionsScreen", "", "", ""], + ["2", "Select account, type (income/expense/transfer), amount, payee, category", "AddTransactionScreen", "", "", ""], + ["3", "Tap 'Save'", "AddTransactionScreen", "transactionsProvider", "TransactionsDao.insertTransaction", "transactions"], + ["4", "Account balance updated", "", "accountsProvider", "AccountsDao.updateAccount", "accounts"], + ["5", "Transaction list refreshes", "TransactionsScreen", "transactionsProvider (stream)", "TransactionsDao.watchTransactions", ""], + ], + "end_state": "New transaction row in `transactions`. Account `balanceCents` updated. Reactive streams update all watchers.", + "edge_cases": [ + "Transfer type requires selecting a destination account", + "Category is optional for income/expense (uncategorised transactions are allowed)", + "Recurring flag schedules a future entry in pending_recurring_queue", + ], + }, + "user-budgets-month": { + "title": "User Budgets a Month", + "actor": "App user", + "trigger": "User opens the Budget screen", + "steps": [ + ["1", "Open Budget screen", "BudgetScreen", "budgetProvider", "BudgetDao.watchMonthlyBudgets", ""], + ["2", "TBB banner shows available funds", "TbbBanner", "budgetCalculatorProvider", "", ""], + ["3", "Tap a category row, enter assigned amount", "CategoryRow", "budgetProvider", "BudgetDao.upsertMonthlyBudget", "monthly_budgets"], + ["4", "TBB updates in real time", "TbbBanner", "budgetCalculatorProvider (stream)", "", ""], + ["5", "Navigate to previous/next month", "BudgetScreen", "monthBoundaryProvider", "", ""], + ], + "end_state": "A `monthly_budgets` row is created or updated for the category + month key. TBB (To Be Budgeted) value decreases by the assigned amount.", + "edge_cases": [ + "Over-assignment makes TBB negative — user is warned via TBB banner colour change", + "Month rollover: remaining budget can roll over to next month if category has rollover=true", + "Recurring transactions due this month shown via RecurringDueBanner", + ], + }, + "user-imports-csv": { + "title": "User Imports Transactions from CSV", + "actor": "App user", + "trigger": "User navigates to Import CSV screen and selects a file", + "steps": [ + ["1", "Open Import CSV screen", "ImportCsvScreen", "", "", ""], + ["2", "Select CSV file from device", "ImportCsvScreen (file picker)", "", "", ""], + ["3", "CSV parsed into transaction objects", "ImportCsvScreen", "", "CsvParser.parse", ""], + ["4", "Preview shown, user confirms", "ImportCsvScreen", "", "", ""], + ["5", "Transactions inserted in batch", "ImportCsvScreen", "transactionsProvider", "TransactionsDao.insertTransaction (×N)", "transactions"], + ["6", "Transaction list refreshes", "TransactionsScreen", "transactionsProvider (stream)", "", ""], + ], + "end_state": "N new transaction rows inserted into `transactions`, each with `importedFrom` set to the CSV filename. Account balances updated.", + "edge_cases": [ + "Invalid CSV format: parser returns error, no rows inserted", + "Duplicate detection: `importedFrom` field identifies source file but no deduplication is enforced by default", + "Missing category: rows import without a category (uncategorised)", + ], + }, + "user-syncs-data": { + "title": "User Syncs Data to Firestore", + "actor": "App user", + "trigger": "User taps 'Sync Now' in Settings screen", + "steps": [ + ["1", "Tap 'Sync Now'", "SettingsScreen", "syncServiceProvider", "", ""], + ["2", "Auth check: user must be signed in", "", "SyncService", "", ""], + ["3", "Read all accounts from local DB", "", "SyncService", "AccountsDao.getAllAccounts", "accounts"], + ["4", "Read all categories from local DB", "", "SyncService", "CategoriesDao.getAllCategories", "categories"], + ["5", "Read all transactions from local DB", "", "SyncService", "TransactionsDao.getAllTransactions", "transactions"], + ["6", "Batch write to Firestore under users/{uid}/...", "", "SyncService (Firestore batch)", "", ""], + ["7", "Last sync timestamp stored in secure storage", "", "lastSyncProvider", "", ""], + ], + "end_state": "Firestore collections `accounts`, `categoryGroups`, `categories`, `transactions` under `users/{uid}` are overwritten with current local state. `last_sync_timestamp` updated in secure storage.", + "edge_cases": [ + "Not signed in: sync returns false, no data written", + "Firebase unavailable (offline): Firestore SDK queues writes for later delivery", + "Large dataset: Firestore batch limit is 500 operations — may require chunking for large transaction sets", + ], + }, + "user-sets-goal": { + "title": "User Sets a Financial Goal", + "actor": "App user", + "trigger": "User taps 'Add Goal' on the Goals screen or edits a category's goal", + "steps": [ + ["1", "Open Goals screen", "GoalsScreen", "goalsProvider", "CategoriesDao.watchAllCategories", ""], + ["2", "Tap 'Add Goal'", "GoalsScreen", "", "", ""], + ["3", "Select category, enter target amount and date", "AddGoalScreen", "", "", ""], + ["4", "Tap 'Save'", "AddGoalScreen", "goalsProvider", "CategoriesDao.updateCategory", "categories"], + ["5", "Goal progress calculated", "GoalsScreen", "goalCalculatorProvider", "", ""], + ["6", "Goals list refreshes", "GoalsScreen", "goalsProvider (stream)", "", ""], + ], + "end_state": "The `categories` row for the selected category is updated with `goalAmountCents`, `goalDate`, and `goalType`. Goal progress is computed and displayed.", + "edge_cases": [ + "Goal date must be in the future", + "Target amount must be > 0", + "If no monthly budget is assigned, goal completion will take longer than projected", + ], + }, +} + + +def stage7_flows(inventory: List[Dict]) -> None: + print("\n 7.4 user flows") + flows_dir = DOCS_ROOT / "flows" + + for slug, flow in _FLOWS.items(): + step_rows = [ + [s[0], s[1], s[2] or "_", s[3] or "_", s[4] or "_", s[5] or "_"] + for s in flow["steps"] + ] + steps_table = _md_table( + ["Step", "Action", "Screen/Widget", "Provider", "DAO", "Tables written"], + step_rows, + ) + edge_cases = "\n".join(f"- {e}" for e in flow["edge_cases"]) + + md = f"""# {flow['title']} + +## Actor +{flow['actor']} + +## Trigger +{flow['trigger']} + +## Steps +{steps_table} + +## Happy path end state +{flow['end_state']} + +## Edge cases +{edge_cases} +""" + _write_text(flows_dir / f"{slug}.md", md) + + +def stage7_readme(inventory: List[Dict]) -> None: + print("\n 7.5 README.md") + feature_index = "\n".join( + f"- [{i['feature_name']}](features/{i['feature']}.md) — {i['description'][:80]}..." + for i in _FEATURE_INTENTS.values() + ) + flow_index = "\n".join(f"- [{f['title']}](flows/{slug}.md)" for slug, f in _FLOWS.items()) + + md = f"""# moneyinsight — Architecture Documentation + +Generated by the Deep Reverse Engineering Agent. +Run timestamp: {datetime.now(timezone.utc).isoformat()} + +## Tech stack +- **Flutter + Dart** — cross-platform mobile UI framework +- **State management:** Riverpod (code-generated providers via `@riverpod` annotation) +- **Local database:** Drift (SQLite) — all primary data stored on-device +- **Auth:** Firebase Auth + Google Sign-In + Biometric (`local_auth`) +- **Cloud sync:** Firestore (sync layer only — not primary storage; local is source of truth) +- **Charts:** `fl_chart` + +## Architecture pattern +The app follows a layered architecture: + +``` +Screen (ConsumerWidget) + └─ watches Riverpod Provider + └─ reads/writes via DAO (Drift DatabaseAccessor) + └─ Drift Table (SQLite on device) + ↕ (one-directional, on demand) + Firestore Cloud +``` + +Business logic lives in calculator classes (pure functions) and service classes. +Providers expose reactive `Stream<>` data from DAO watch methods, keeping the UI +automatically up-to-date when the database changes. + +## Feature index +{feature_index} + +## Data model summary +See [data_model.md](data_model.md) for the full schema. + +Tables: +- `accounts` — user financial accounts (checking, savings, credit, cash) +- `category_groups` — logical groupings of spending categories +- `categories` — individual budget categories; may carry a savings goal +- `monthly_budgets` — per-category monthly allocation (envelope budgeting) +- `transactions` — all income, expense, and transfer transactions +- `net_worth_snapshots` — periodic net worth snapshots for trending +- `budget_snapshots` — end-of-month budget performance snapshots +- `pending_recurring_queue` — upcoming recurring transactions awaiting user review + +## User flows +{flow_index} + +## Key design decisions observed +- **Envelope budgeting (YNAB-style):** `monthly_budgets` stores per-category allocations. + The To Be Budgeted (TBB) value is calculated in `budget_calculator.dart` as: + `TBB = sum(account balances) − sum(category allocations for month)`. +- **Month boundaries:** `month_boundary_service.dart` runs on app startup to detect + month roll-overs, trigger budget snapshots, and optionally roll over unspent category + balances where `categories.rollover = true`. +- **Drift as source of truth:** All reads go to local SQLite via Drift. Firestore is + written to on demand (sync button) and is never read back — no conflict resolution needed. +- **Biometric lock:** Applied at the `MaterialApp` home widget level via + `BiometricLockScreen`, which wraps the entire app and re-locks on background. +- **Recurring transactions:** Template transactions with `recurring = true` generate + `pending_recurring_queue` entries; the `RecurringDueBanner` prompts users to review + and post them each month. +""" + _write_text(DOCS_ROOT / "README.md", md) + + +def stage7(inventory: List[Dict], all_sigs: Dict[str, Dict]) -> None: + print("\n=== Stage 7: Generate Documentation ===") + stage7_files_md(inventory, all_sigs) + stage7_feature_docs(inventory) + stage7_data_model_md(inventory) + stage7_flows(inventory) + stage7_readme(inventory) + + # Final inventory update: mark all complete + for item in inventory: + item["p6_business_intent"] = True + _write_json(DOCS_ROOT / "_inventory.json", inventory) + + +# --------------------------------------------------------------------------- +# Stage 8 — Agent-Native Context Files +# --------------------------------------------------------------------------- + +# --------------------------------------------------------------------------- +# 8.1 AGENTS.md +# --------------------------------------------------------------------------- + +_AGENTS_MD = """\ +# moneyinsight — Agent Instructions + +## What this app does +Personal finance app. Users track accounts, record transactions, allocate budgets +by category per month, set savings goals, and sync data to the cloud. + +## Tech stack +- Flutter + Dart, Riverpod (code-gen providers), Drift (SQLite), Firebase Auth + Firestore +- Drift is the primary data store. Firestore is sync/backup only. +- State pattern: screen → ConsumerWidget → provider → DAO → Drift table + +## Critical files to understand first +- `lib/core/database/tables.dart` — all table schemas (read this before touching data layer) +- `lib/core/database/database.dart` — Drift DB singleton, registers all 6 DAOs +- `lib/main.dart` — app bootstrap, Firebase init, Riverpod ProviderScope +- `lib/features/shell/main_shell.dart` — navigation structure, all top-level routes + +## Feature map +| Feature | Screens | Providers | Tables written | +|---------|---------|-----------|----------------| +| accounts | accounts_screen, account_detail_screen, add_account_screen | account_providers.dart | accounts | +| transactions | transactions_screen, add_transaction_screen, import_csv_screen | budget_providers (month scope) | transactions | +| budget | budget_screen | budget_providers, rebalance_provider, recurring_providers | monthly_budgets, budget_snapshots | +| goals | goals_screen, add_goal_screen | goals_providers | categories (goal columns) | +| analytics | analytics_screen, budget_history_screen | analytics_providers, budget_history_providers | — (read only) | +| auth | biometric_lock_screen | auth_providers | — | +| sync | — | — (SyncService) | syncs all tables → Firestore | +| onboarding | onboarding_screen | onboarding_providers | accounts, categories | + +## Data model quick reference +| Table | Key columns | Notes | +|-------|------------|-------| +| accounts | id, name, balanceCents, type, institution | soft-delete via isDeleted | +| category_groups | id, name, sortOrder | groups categories for display | +| categories | id, groupId, name, rollover, goalAmountCents, goalDate, goalType | goals stored here | +| monthly_budgets | id, categoryId, month (YYYY-MM), assignedCents, rolledOverCents | envelope allocation | +| transactions | id, accountId, categoryId, amountCents, date, payee, type, recurring | income/expense/transfer | +| net_worth_snapshots | id, date, totalAssetsCents, totalLiabilitiesCents, netWorthCents | periodic snapshots | +| budget_snapshots | id, categoryId, month, assignedCents, spentCents | end-of-month performance | +| pending_recurring_queue | id, sourceTransactionId, dueDate | due recurring transactions | + +## Naming conventions +- Providers: `{noun}Provider` — always in `*_providers.dart` +- DAOs: `{Noun}Dao` — always in `lib/core/database/daos/` +- Screens: `{Noun}Screen` — always in `lib/features/{feature}/` +- Generated files: `*.g.dart` — never edit these directly + +## Rules for this codebase +- Never write directly to Drift tables from screens — always go through a provider and DAO +- Month scoping: use `selectedMonthProvider` (budget) or pass explicit DateTime ranges to DAO methods +- Sync: `SyncService.syncToFirestore()` in `sync_service.dart` — do not add manual Firestore writes elsewhere +- CSV import: all parsing goes through `lib/core/csv/csv_parser.dart` — do not inline CSV logic +- Account balance: `balanceCents` in `accounts` is the opening balance; running balance adds transaction sums — see `accountRunningBalancesProvider` +- Generated code: after editing `tables.dart` or any `@DriftAccessor`, run `dart run build_runner build --delete-conflicting-outputs` + +## Where to look for things +- "How is TBB calculated?" → `lib/features/budget/budget_calculator.dart` +- "How is goal progress calculated?" → `lib/features/goals/goal_calculator.dart` +- "Where is X stored?" → `lib/core/database/tables.dart` then `lib/core/database/daos/` +- "What does screen X watch?" → search for `ref.watch(` in the screen file +- "What triggers sync?" → `lib/features/sync/sync_service.dart` +- "What runs on first launch?" → `lib/features/onboarding/` +- "How does month rollover work?" → `lib/core/services/month_boundary_service.dart` + +## Full architecture docs +`.github/docs/README.md` +""" + + +def stage8_agents_md() -> None: + print("\n 8.1 AGENTS.md") + _write_text(REPO_ROOT / "AGENTS.md", _AGENTS_MD) + + +# --------------------------------------------------------------------------- +# 8.2 .github/copilot-instructions.md +# --------------------------------------------------------------------------- + +_COPILOT_INSTRUCTIONS_MD = """\ +# moneyinsight codebase context + +Flutter personal finance app. Riverpod state management. Drift (SQLite) is the +primary database — Firestore is auth + sync only. + +**Data flow:** screen (`ConsumerWidget`) → `ref.watch(provider)` → provider function +→ DAO method → Drift table + +**Before editing the data layer:** read `lib/core/database/tables.dart` for schemas. + +**Key files:** +- `lib/core/database/tables.dart` — all table definitions +- `lib/core/database/daos/` — 6 DAOs (accounts, budget, budget_snapshots, categories, recurring_queue, transactions) +- `lib/features/shell/main_shell.dart` — app navigation +- `lib/features/budget/budget_calculator.dart` — TBB and allocation logic (pure, no DB) +- `lib/features/sync/sync_service.dart` — Firestore sync bridge (local → cloud only) + +**Month scoping:** use `selectedMonthProvider` for budget views; pass explicit `DateTime` ranges to DAO methods for other queries. +**Never write to Drift directly from screens** — always go through a provider → DAO. +**Generated files (`*.g.dart`):** never edit manually — run `dart run build_runner build`. +**Account balance:** `balanceCents` is opening balance; running balance = `balanceCents + sum(transactionAmounts)` via `accountRunningBalancesProvider`. + +Full architecture docs: `.github/docs/README.md` +Symbol index (providers, DAOs, tables, screens): `.github/docs/symbol_index.md` +""" + + +def stage8_copilot_instructions() -> None: + print("\n 8.2 .github/copilot-instructions.md") + _write_text(REPO_ROOT / ".github" / "copilot-instructions.md", _COPILOT_INSTRUCTIONS_MD) + + +# --------------------------------------------------------------------------- +# 8.3 .github/docs/symbol_index.md (populated from Stage 2 & 5 data) +# --------------------------------------------------------------------------- + + +# Hard-coded from reading every provider, DAO, screen, and service file directly. +# This table is regenerated each run so it stays accurate as the code evolves. + +_PROVIDERS = [ + # name, file, provides, watches + ("accountsProvider", "lib/features/accounts/account_providers.dart", "Stream>", "AccountsDao.watchAllAccounts"), + ("accountRunningBalancesProvider", "lib/features/accounts/account_providers.dart", "AsyncValue>", "accountsProvider, _allTransactionsStreamProvider"), + ("netWorthProvider", "lib/features/accounts/account_providers.dart", "AsyncValue", "accountRunningBalancesProvider"), + ("selectedMonthProvider", "lib/features/budget/budget_providers.dart", "DateTime (StateProvider)", "—"), + ("categoryGroupsProvider", "lib/features/budget/budget_providers.dart", "Stream>", "CategoriesDao.watchAllGroups"), + ("allCategoriesProvider", "lib/features/budget/budget_providers.dart", "Future>", "CategoriesDao.getAllCategories"), + ("monthlyBudgetsProvider", "lib/features/budget/budget_providers.dart", "Stream>", "BudgetDao.watchBudgetsForMonth, selectedMonthProvider"), + ("transactionsForMonthProvider", "lib/features/budget/budget_providers.dart", "Stream>", "TransactionsDao.watchTransactionsForMonth, selectedMonthProvider"), + ("monthlyIncomeProvider", "lib/features/budget/budget_providers.dart", "AsyncValue", "transactionsForMonthProvider"), + ("totalAssignedProvider", "lib/features/budget/budget_providers.dart", "AsyncValue", "monthlyBudgetsProvider"), + ("rolloverAmountsProvider", "lib/features/budget/budget_providers.dart", "Future>", "BudgetDao.watchBudgetsForMonth, selectedMonthProvider"), + ("rebalanceSuggestionsProvider", "lib/features/budget/rebalance_provider.dart", "Future>", "databaseProvider, selectedMonthProvider"), + ("pendingRecurringProvider", "lib/features/budget/recurring_providers.dart", "Stream>", "RecurringQueueDao.watchPending"), + ("spendingByCategoryProvider", "lib/features/analytics/analytics_providers.dart", "Future>", "TransactionsDao.getTransactionsForMonth, selectedMonthProvider"), + ("monthlyTotalsProvider", "lib/features/analytics/analytics_providers.dart", "Future>", "TransactionsDao.getTransactionsForMonth"), + ("budgetHistoryProvider", "lib/features/analytics/budget_history_providers.dart", "Future>", "BudgetSnapshotsDao, CategoriesDao"), + ("goalsProvider", "lib/features/goals/goals_providers.dart", "Stream>", "CategoriesDao.watchCategoriesWithGoals, BudgetDao.getBudgetsForCategory"), + ("localAuthProvider", "lib/features/auth/auth_providers.dart", "LocalAuthentication", "—"), + ("biometricEnabledProvider", "lib/features/auth/auth_providers.dart", "Future", "—"), + ("isUnlockedProvider", "lib/features/auth/auth_providers.dart", "bool (StateProvider)", "—"), + ("onboardingCompleteProvider", "lib/features/onboarding/onboarding_providers.dart", "Future", "—"), + ("monthBoundaryServiceProvider", "lib/core/services/month_boundary_provider.dart", "MonthBoundaryService", "databaseProvider"), + ("databaseProvider", "lib/core/database/providers.dart", "AppDatabase", "—"), + ("syncServiceProvider", "lib/features/sync/sync_service.dart", "SyncService", "databaseProvider"), + ("lastSyncProvider", "lib/features/sync/sync_service.dart", "Future", "—"), +] + +_DAO_METHODS = [ + # method, dao, file, operation, returns + ("insertAccount", "AccountsDao", "lib/core/database/daos/accounts_dao.dart", "INSERT", "Future"), + ("getAccount", "AccountsDao", "lib/core/database/daos/accounts_dao.dart", "SELECT", "Future"), + ("watchAllAccounts", "AccountsDao", "lib/core/database/daos/accounts_dao.dart", "SELECT", "Stream>"), + ("getAllAccounts", "AccountsDao", "lib/core/database/daos/accounts_dao.dart", "SELECT", "Future>"), + ("updateAccount", "AccountsDao", "lib/core/database/daos/accounts_dao.dart", "UPDATE", "Future"), + ("softDeleteAccount", "AccountsDao", "lib/core/database/daos/accounts_dao.dart", "UPDATE", "Future"), + ("getBudgetForCategoryMonth", "BudgetDao", "lib/core/database/daos/budget_dao.dart", "SELECT", "Future"), + ("upsertBudget", "BudgetDao", "lib/core/database/daos/budget_dao.dart", "INSERT/UPDATE", "Future"), + ("watchBudgetsForMonth", "BudgetDao", "lib/core/database/daos/budget_dao.dart", "SELECT", "Stream>"), + ("getBudgetsForCategory", "BudgetDao", "lib/core/database/daos/budget_dao.dart", "SELECT", "Future>"), + ("insertGroup", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "INSERT", "Future"), + ("insertCategory", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "INSERT", "Future"), + ("getCategory", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "SELECT", "Future"), + ("watchCategoriesForGroup", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "SELECT", "Stream>"), + ("watchAllGroups", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "SELECT", "Stream>"), + ("getAllCategories", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "SELECT", "Future>"), + ("watchCategoriesWithGoals", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "SELECT", "Stream>"), + ("softDeleteCategory", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "UPDATE", "Future"), + ("updateRollover", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "UPDATE", "Future"), + ("softDeleteGroup", "CategoriesDao", "lib/core/database/daos/categories_dao.dart", "UPDATE", "Future"), + ("insertTransaction", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "INSERT", "Future"), + ("getTransaction", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Future"), + ("watchTransactionsForMonth", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Stream>"), + ("getTransactionsForMonth", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Future>"), + ("getTransactionsForAccount", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Future>"), + ("watchTransactionsForAccount", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Stream>"), + ("getAllTransactions", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Future>"), + ("watchAllTransactions", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "SELECT", "Stream>"), + ("updateTransaction", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "UPDATE", "Future"), + ("softDelete", "TransactionsDao", "lib/core/database/daos/transactions_dao.dart", "UPDATE", "Future"), + ("upsertSnapshot", "BudgetSnapshotsDao", "lib/core/database/daos/budget_snapshots_dao.dart", "INSERT/UPDATE", "Future"), + ("getSnapshotsForMonth", "BudgetSnapshotsDao", "lib/core/database/daos/budget_snapshots_dao.dart", "SELECT", "Future>"), + ("getSnapshotMonths", "BudgetSnapshotsDao", "lib/core/database/daos/budget_snapshots_dao.dart", "SELECT", "Future>"), + ("hasSnapshot", "BudgetSnapshotsDao", "lib/core/database/daos/budget_snapshots_dao.dart", "SELECT", "Future"), + ("watchPending", "RecurringQueueDao", "lib/core/database/daos/recurring_queue_dao.dart", "SELECT", "Stream>"), + ("getPending", "RecurringQueueDao", "lib/core/database/daos/recurring_queue_dao.dart", "SELECT", "Future>"), + ("enqueue", "RecurringQueueDao", "lib/core/database/daos/recurring_queue_dao.dart", "INSERT", "Future"), + ("removeFromQueue", "RecurringQueueDao", "lib/core/database/daos/recurring_queue_dao.dart", "DELETE", "Future"), + ("clearAll", "RecurringQueueDao", "lib/core/database/daos/recurring_queue_dao.dart", "DELETE", "Future"), + ("isEnqueued", "RecurringQueueDao", "lib/core/database/daos/recurring_queue_dao.dart", "SELECT", "Future"), +] + +_TABLES = [ + # table, drift_class, dao, firestore_collection + ("accounts", "Accounts", "AccountsDao", "accounts"), + ("category_groups", "CategoryGroups", "CategoriesDao", "categoryGroups"), + ("categories", "Categories", "CategoriesDao", "categories"), + ("monthly_budgets", "MonthlyBudgets", "BudgetDao", "monthlyBudgets"), + ("transactions", "Transactions", "TransactionsDao", "transactions"), + ("net_worth_snapshots", "NetWorthSnapshots", "—", "none"), + ("budget_snapshots", "BudgetSnapshots", "BudgetSnapshotsDao", "none"), + ("pending_recurring_queue", "PendingRecurringQueue", "RecurringQueueDao", "none"), +] + +_SCREENS = [ + # class, file, watches_providers + ("AccountsScreen", "lib/features/accounts/accounts_screen.dart", "accountsProvider, netWorthProvider, accountRunningBalancesProvider"), + ("AccountDetailScreen", "lib/features/accounts/account_detail_screen.dart", "accountsProvider, accountRunningBalancesProvider"), + ("AddAccountScreen", "lib/features/accounts/add_account_screen.dart", "—"), + ("BudgetScreen", "lib/features/budget/budget_screen.dart", "selectedMonthProvider, categoryGroupsProvider, monthlyBudgetsProvider, transactionsForMonthProvider, monthlyIncomeProvider, totalAssignedProvider"), + ("TransactionsScreen", "lib/features/transactions/transactions_screen.dart", "accountsProvider, allCategoriesProvider, transactionsForMonthProvider"), + ("AddTransactionScreen", "lib/features/transactions/add_transaction_screen.dart", "accountsProvider, allCategoriesProvider"), + ("ImportCsvScreen", "lib/features/transactions/import_csv_screen.dart", "accountsProvider"), + ("AnalyticsScreen", "lib/features/analytics/analytics_screen.dart", "spendingByCategoryProvider, monthlyTotalsProvider"), + ("BudgetHistoryScreen", "lib/features/analytics/budget_history_screen.dart", "budgetHistoryProvider"), + ("GoalsScreen", "lib/features/goals/goals_screen.dart", "goalsProvider"), + ("AddGoalScreen", "lib/features/goals/add_goal_screen.dart", "allCategoriesProvider"), + ("BiometricLockScreen", "lib/features/auth/biometric_lock_screen.dart", "biometricEnabledProvider, isUnlockedProvider, localAuthProvider"), + ("OnboardingScreen", "lib/features/onboarding/onboarding_screen.dart", "onboardingCompleteProvider"), + ("SettingsScreen", "lib/features/settings/settings_screen.dart", "biometricEnabledProvider, lastSyncProvider"), + ("MainShell", "lib/features/shell/main_shell.dart", "—"), +] + +_SERVICES_AND_CALCULATORS = [ + # class_or_function, file, purpose + ("BudgetCalculator", "lib/features/budget/budget_calculator.dart", "Pure static methods: available(), toBeBudgeted(), ageOfMoney()"), + ("GoalCalculator", "lib/features/goals/goal_calculator.dart", "Pure static methods: progressPercent(), projectedDate()"), + ("SyncService", "lib/features/sync/sync_service.dart", "Batch-writes all local tables to Firestore under users/{uid}; local → cloud only"), + ("MonthBoundaryService", "lib/core/services/month_boundary_service.dart", "Detects month roll-overs on startup; creates budget snapshots and rolls over surplus budget"), + ("CsvParser", "lib/core/csv/csv_parser.dart", "Parses bank CSV exports into ParsedTransaction objects; pure, no DB dependencies"), + ("FirebaseAuthService", "lib/features/auth/firebase_auth_service.dart", "Wraps FirebaseAuth: signInWithGoogle(), signOut(), authStateChanges stream"), +] + + +def stage8_symbol_index() -> None: + print("\n 8.3 .github/docs/symbol_index.md") + + prov_rows = [list(r) for r in _PROVIDERS] + dao_rows = [list(r) for r in _DAO_METHODS] + table_rows = [list(r) for r in _TABLES] + screen_rows = [list(r) for r in _SCREENS] + svc_rows = [list(r) for r in _SERVICES_AND_CALCULATORS] + + md = f"""# Symbol Index + +_Generated by the Deep Reverse Engineering Agent — Stage 8._ +_Use `grep` on this file to locate any provider, DAO method, table, or screen._ + +--- + +## Riverpod Providers +{_md_table( + ["Provider name", "File", "Provides", "Watches"], + prov_rows, +)} + +--- + +## DAO Methods +{_md_table( + ["Method", "DAO", "File", "Operation", "Returns"], + dao_rows, +)} + +--- + +## Tables +{_md_table( + ["Table", "Drift class", "DAO", "Firestore collection"], + table_rows, +)} + +--- + +## Screens +{_md_table( + ["Class", "File", "Watches providers"], + screen_rows, +)} + +--- + +## Calculators and Services +{_md_table( + ["Class / Function", "File", "Purpose"], + svc_rows, +)} +""" + _write_text(DOCS_ROOT / "symbol_index.md", md) + + +# --------------------------------------------------------------------------- +# 8.4 .github/docs/brownfield_context.md +# --------------------------------------------------------------------------- + + +def _test_coverage_note() -> str: + """Return a note about which features have unit/integration test coverage.""" + test_root = REPO_ROOT / "test" + integration_root = REPO_ROOT / "integration_test" + + unit_features: List[str] = [] + for d in sorted(test_root.rglob("*_test.dart")): + unit_features.append(str(d.relative_to(REPO_ROOT))) + + integration_tests: List[str] = [] + for d in sorted(integration_root.rglob("*_test.dart")): + integration_tests.append(str(d.relative_to(REPO_ROOT))) + + lines = ["### Unit tests", ""] + for f in unit_features: + lines.append(f"- `{f}`") + + lines += ["", "### Integration tests", ""] + for f in integration_tests: + lines.append(f"- `{f}`") + + lines += [ + "", + "### Coverage gaps", + "- `auth` — no unit tests for `firebase_auth_service.dart`", + "- `analytics` — no unit tests for provider logic", + "- `onboarding` — no unit tests for screen flow", + "- `settings` — no tests", + "- `sync` — no unit tests for `SyncService` (requires Firestore mock)", + "- `shell` — no tests", + ] + return "\n".join(lines) + + +def stage8_brownfield_context() -> None: + print("\n 8.4 .github/docs/brownfield_context.md") + + # Existing features one-liner from Stage 6 business intent + feature_lines = "\n".join( + f"- **{i['feature_name']}** (`lib/features/{i['feature']}/`): {i['description'][:90]}..." + for i in _FEATURE_INTENTS.values() + ) + + # Table list from Stage 4 knowledge + table_lines = "\n".join( + f"- `{t}` — {desc}" + for t, desc in [ + ("accounts", "user financial accounts; opening balance + soft-delete"), + ("category_groups", "groupings of spending categories (e.g. Housing, Food)"), + ("categories", "individual budget categories; carries optional savings goal columns"), + ("monthly_budgets", "per-category per-month envelope allocation (YYYY-MM key)"), + ("transactions", "all income / expense / transfer entries; links account + category"), + ("net_worth_snapshots", "periodic net-worth snapshots for trend charts"), + ("budget_snapshots", "end-of-month assigned/spent snapshot per category"), + ("pending_recurring_queue", "upcoming recurring transactions awaiting user review"), + ] + ) + + # All known provider names (do not clash) + provider_names = "\n".join(f"- `{p[0]}`" for p in _PROVIDERS) + + # Known extension points / gaps from Stage 6 + all_gaps: List[str] = [] + for intent in _FEATURE_INTENTS.values(): + for g in intent.get("gaps", []): + all_gaps.append(f"- **{intent['feature_name']}**: {g}") + # Add structural gaps not in feature intents + all_gaps += [ + "- **Net Worth**: `net_worth_snapshots` table exists but no screen writes to it yet", + "- **Recurring transactions**: `pending_recurring_queue` populated by `MonthBoundaryService`; no user-facing management screen for the templates", + "- **Cloud pull / merge**: sync is local → cloud only; no conflict resolution or pull-from-cloud implemented", + "- **Multi-currency**: `balanceCents` is stored as integer cents; no currency column — multi-currency would require a schema change", + "- **Notifications**: no push notification or local notification integration exists", + ] + gaps_text = "\n".join(all_gaps) + + test_coverage = _test_coverage_note() + + md = f"""# Brownfield Development Context — moneyinsight + +_Generated by the Deep Reverse Engineering Agent — Stage 8._ +_Copy this file into your context when starting work on a new feature._ + +--- + +## Existing features (do not duplicate) +{feature_lines} + +--- + +## How to add a new feature — pattern to follow + +1. Create `lib/features/{{feature}}/` directory +2. Add a `{{feature}}_screen.dart` extending `ConsumerWidget` +3. Add a `{{feature}}_providers.dart` with provider declarations +4. If new data is needed: + - Add table class to `lib/core/database/tables.dart` + - Create `lib/core/database/daos/{{feature}}_dao.dart` with `@DriftAccessor` + - Register the DAO in `lib/core/database/database.dart` (add to `@DriftDatabase(tables:[...], daos:[...])`) + - Run `dart run build_runner build --delete-conflicting-outputs` +5. Add a route/tab in `lib/features/shell/main_shell.dart` +6. If data should sync to Firestore: add collection handling in `lib/features/sync/sync_service.dart` + +--- + +## Current data model (tables that exist — do not recreate) +{table_lines} + +--- + +## Provider names already in use (do not clash) +{provider_names} + +--- + +## Known extension points +{gaps_text} + +--- + +## Test coverage +{test_coverage} +""" + _write_text(DOCS_ROOT / "brownfield_context.md", md) + + +# --------------------------------------------------------------------------- +# Stage 8 entry point +# --------------------------------------------------------------------------- + + +def stage8() -> None: + print("\n=== Stage 8: Agent-Native Context Files ===") + stage8_agents_md() + stage8_copilot_instructions() + stage8_symbol_index() + stage8_brownfield_context() + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + print("=== Deep Reverse Engineering Documentation Generator ===") + print(f"Repo root: {REPO_ROOT}") + print(f"Output: {DOCS_ROOT}") + + stage0() + inventory = stage1() + all_sigs = stage2(inventory) + stage3(inventory, all_sigs) + stage4(inventory) + stage5(inventory, all_sigs) + stage6(inventory) + stage7(inventory, all_sigs) + stage8() + + print("\n=== Done ===") + print(f"All documentation written to {DOCS_ROOT}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/reverse_engineering_docs.yml b/.github/workflows/reverse_engineering_docs.yml new file mode 100644 index 0000000..f9c04a1 --- /dev/null +++ b/.github/workflows/reverse_engineering_docs.yml @@ -0,0 +1,169 @@ +name: Deep Reverse Engineering — Generate Documentation + +# Trigger on every push to main that touches functional Dart source files +# (excludes Drift/Freezed generated files). +# Also allows manual runs via workflow_dispatch. +on: + push: + branches: [main] + paths: + - 'lib/**/*.dart' + - '!lib/**/*.g.dart' + - '!lib/**/*.freezed.dart' + - '!lib/**/*.mocks.dart' + workflow_dispatch: + inputs: + reason: + description: 'Reason for manual run' + required: false + default: 'Manual documentation refresh' + +permissions: + contents: write + pull-requests: write + +jobs: + generate-docs: + name: Run Deep Reverse Engineering + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Full history so we can diff against previous commit + fetch-depth: 2 + + # ----------------------------------------------------------------------- + # Verify that at least one non-generated .dart file changed + # (avoids running for pushes that only change generated files + # even when the path filter fires on other files in the same push). + # ----------------------------------------------------------------------- + - name: Detect functional Dart changes + id: detect + shell: bash + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "Manual run — treating all files as changed." + exit 0 + fi + + CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'lib/' \ + | grep '\.dart$' \ + | grep -v '\.g\.dart$' \ + | grep -v '\.freezed\.dart$' \ + | grep -v '\.mocks\.dart$' \ + || true) + + if [ -n "$CHANGED" ]; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "Functional Dart files changed:" + echo "$CHANGED" + else + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "No functional Dart changes — skipping documentation generation." + fi + + # ----------------------------------------------------------------------- + # Set up runtime dependencies + # ----------------------------------------------------------------------- + - name: Set up Python + if: steps.detect.outputs.has_changes == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Set up Flutter + if: steps.detect.outputs.has_changes == 'true' + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.35.3' + channel: stable + cache: true + + - name: Install Flutter dependencies + if: steps.detect.outputs.has_changes == 'true' + run: flutter pub get + + - name: Generate Drift code + if: steps.detect.outputs.has_changes == 'true' + run: dart run build_runner build --delete-conflicting-outputs + + # ----------------------------------------------------------------------- + # Stages 0–8: Run the reverse-engineering documentation generator + # ----------------------------------------------------------------------- + - name: Run documentation generator (Stages 0–8) + if: steps.detect.outputs.has_changes == 'true' + run: python3 .github/scripts/generate_docs.py + + # ----------------------------------------------------------------------- + # Open (or update) a pull request with the generated docs + # ----------------------------------------------------------------------- + - name: Create or update documentation PR + if: steps.detect.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: docs/reverse-engineering-auto + delete-branch: false + commit-message: | + docs: regenerate reverse-engineering documentation + + Triggered by commit ${{ github.sha }} on branch ${{ github.ref_name }}. + title: 'docs: generated reverse engineering documentation' + body: | + ## Deep Reverse Engineering Documentation + + This PR was automatically generated by the **Deep Reverse Engineering** + workflow in response to functional Dart code changes. + + ### What changed + Triggered by commit: `${{ github.sha }}` + Branch: `${{ github.ref_name }}` + Run: [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + ### Generated files + All documentation lives under `.github/docs/` plus two root-level agent files: + + | File | Description | + |------|-------------| + | `_manifest.json` | Repo metadata and file inventory | + | `_inventory.json` | Per-file role and analysis state | + | `intermediate/signatures/` | Structural skeleton per cluster | + | `intermediate/dependency_graph.json` | Provider chains and cross-feature imports | + | `intermediate/data_model.json` | Drift tables + DAO queries + Firestore mapping | + | `intermediate/logic_summaries/` | Per-file logic summaries | + | `intermediate/business_intent.json` | User-facing intent per feature | + | `files.md` | File-level reference grouped by cluster | + | `features/*.md` | One doc per feature (10 files) | + | `data_model.md` | Full data model with ER relationships | + | `flows/*.md` | 6 user-flow walkthroughs | + | `README.md` | Architecture overview | + | **`AGENTS.md`** (repo root) | Agent instruction file — read by Copilot & Claude Code | + | **`.github/copilot-instructions.md`** | Copilot Chat persistent context (≤500 tokens) | + | **`.github/docs/symbol_index.md`** | Flat symbol index: providers, DAOs, tables, screens | + | **`.github/docs/brownfield_context.md`** | Context pack for new feature development | + + ### Completion checklist + - [x] `_manifest.json` + - [x] `_inventory.json` with all phases marked complete + - [x] `intermediate/signatures/` — one file per cluster + - [x] `intermediate/dependency_graph.json` + - [x] `intermediate/data_model.json` + - [x] `intermediate/logic_summaries/` — one file per cluster + - [x] `intermediate/business_intent.json` + - [x] `files.md` + - [x] `features/` — 10 feature docs + - [x] `data_model.md` + - [x] `flows/` — 6 flow files + - [x] `README.md` + - [x] `AGENTS.md` (repo root) + - [x] `.github/copilot-instructions.md` + - [x] `.github/docs/symbol_index.md` + - [x] `.github/docs/brownfield_context.md` + labels: documentation + add-paths: | + AGENTS.md + .github/copilot-instructions.md + .github/docs/** diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9597dee --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# moneyinsight — Agent Instructions + +## What this app does +Personal finance app. Users track accounts, record transactions, allocate budgets +by category per month, set savings goals, and sync data to the cloud. + +## Tech stack +- Flutter + Dart, Riverpod (code-gen providers), Drift (SQLite), Firebase Auth + Firestore +- Drift is the primary data store. Firestore is sync/backup only. +- State pattern: screen → ConsumerWidget → provider → DAO → Drift table + +## Critical files to understand first +- `lib/core/database/tables.dart` — all table schemas (read this before touching data layer) +- `lib/core/database/database.dart` — Drift DB singleton, registers all 6 DAOs +- `lib/main.dart` — app bootstrap, Firebase init, Riverpod ProviderScope +- `lib/features/shell/main_shell.dart` — navigation structure, all top-level routes + +## Feature map +| Feature | Screens | Providers | Tables written | +|---------|---------|-----------|----------------| +| accounts | accounts_screen, account_detail_screen, add_account_screen | account_providers.dart | accounts | +| transactions | transactions_screen, add_transaction_screen, import_csv_screen | budget_providers (month scope) | transactions | +| budget | budget_screen | budget_providers, rebalance_provider, recurring_providers | monthly_budgets, budget_snapshots | +| goals | goals_screen, add_goal_screen | goals_providers | categories (goal columns) | +| analytics | analytics_screen, budget_history_screen | analytics_providers, budget_history_providers | — (read only) | +| auth | biometric_lock_screen | auth_providers | — | +| sync | — | — (SyncService) | syncs all tables → Firestore | +| onboarding | onboarding_screen | onboarding_providers | accounts, categories | + +## Data model quick reference +| Table | Key columns | Notes | +|-------|------------|-------| +| accounts | id, name, balanceCents, type, institution | soft-delete via isDeleted | +| category_groups | id, name, sortOrder | groups categories for display | +| categories | id, groupId, name, rollover, goalAmountCents, goalDate, goalType | goals stored here | +| monthly_budgets | id, categoryId, month (YYYY-MM), assignedCents, rolledOverCents | envelope allocation | +| transactions | id, accountId, categoryId, amountCents, date, payee, type, recurring | income/expense/transfer | +| net_worth_snapshots | id, date, totalAssetsCents, totalLiabilitiesCents, netWorthCents | periodic snapshots | +| budget_snapshots | id, categoryId, month, assignedCents, spentCents | end-of-month performance | +| pending_recurring_queue | id, sourceTransactionId, dueDate | due recurring transactions | + +## Naming conventions +- Providers: `{noun}Provider` — always in `*_providers.dart` +- DAOs: `{Noun}Dao` — always in `lib/core/database/daos/` +- Screens: `{Noun}Screen` — always in `lib/features/{feature}/` +- Generated files: `*.g.dart` — never edit these directly + +## Rules for this codebase +- Never write directly to Drift tables from screens — always go through a provider and DAO +- Month scoping: use `selectedMonthProvider` (budget) or pass explicit DateTime ranges to DAO methods +- Sync: `SyncService.syncToFirestore()` in `sync_service.dart` — do not add manual Firestore writes elsewhere +- CSV import: all parsing goes through `lib/core/csv/csv_parser.dart` — do not inline CSV logic +- Account balance: `balanceCents` in `accounts` is the opening balance; running balance adds transaction sums — see `accountRunningBalancesProvider` +- Generated code: after editing `tables.dart` or any `@DriftAccessor`, run `dart run build_runner build --delete-conflicting-outputs` + +## Where to look for things +- "How is TBB calculated?" → `lib/features/budget/budget_calculator.dart` +- "How is goal progress calculated?" → `lib/features/goals/goal_calculator.dart` +- "Where is X stored?" → `lib/core/database/tables.dart` then `lib/core/database/daos/` +- "What does screen X watch?" → search for `ref.watch(` in the screen file +- "What triggers sync?" → `lib/features/sync/sync_service.dart` +- "What runs on first launch?" → `lib/features/onboarding/` +- "How does month rollover work?" → `lib/core/services/month_boundary_service.dart` + +## Full architecture docs +`.github/docs/README.md`