diff --git a/.ai-context/ARCHITECTURE.md b/.ai-context/ARCHITECTURE.md index 2ab2f2b..8c1627c 100644 --- a/.ai-context/ARCHITECTURE.md +++ b/.ai-context/ARCHITECTURE.md @@ -5,6 +5,8 @@ - `src/bankbuddy/` contains banking domain code, the `bankbuddy` CLI, shared runtime helpers, SQLite access, migrations, importers, reports, and storage layout code. +- `src/bankbuddy/bb/` contains the side-by-side v2 source namespace for new + `BB_` schema DAOs, records, and the `bb` CLI. - `src/bankbuddy/tax/` contains the TaxBuddy document-readiness slice. - `src/bankbuddy/migrations/` contains ordered SQL migrations. - `tests/` contains pytest coverage and `tests/validate.sh`. @@ -84,8 +86,10 @@ future work unless a focused issue says otherwise. ## Financial Intelligence V2 Direction -The accepted v2 direction keeps the product and CLI name as `bankbuddy` while -moving the long-term architecture toward `Document`, `Entity`, `Observation`, +The accepted v2 direction keeps the product name as BankBuddy while introducing +the short `bb` CLI and `bankbuddy.bb` source namespace for new v2 work. Existing +`bankbuddy` and `taxbuddy` command surfaces stay intact during the transition. +The long-term architecture moves toward `Document`, `Entity`, `Observation`, and `Relationship` foundations. New schema proposals use `BB_`-prefixed singular table names and prefer normalized relational tables over JSON blobs. diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index 6697f71..4ee2e96 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -9,6 +9,7 @@ basectl setup bankbuddy basectl check bankbuddy basectl doctor bankbuddy basectl test bankbuddy +basectl run bankbuddy bb -- status basectl run bankbuddy bankbuddy -- status basectl run bankbuddy taxbuddy -- status basectl activate bankbuddy @@ -26,6 +27,19 @@ into `uv run` through `runner: uv`. `basectl activate bankbuddy` enters a project shell and sources `.base/activate.sh`, which defaults `BANKBUDDY_ENV=dev` when unset. +## bb CLI + +Run the v2 financial intelligence CLI with: + +```bash +bb --help +bb status +``` + +`bb` is the side-by-side command surface for new `BB_` schema work. It should +grow new document/entity/observation workflows while the legacy command +surfaces remain available. + ## BankBuddy CLI Run the banking CLI with: diff --git a/.ai-context/STATUS.md b/.ai-context/STATUS.md index bc6992f..e42dd93 100644 --- a/.ai-context/STATUS.md +++ b/.ai-context/STATUS.md @@ -14,6 +14,8 @@ section is `Unreleased`. - Local data homes for `prod`, `dev`, and named environments. - Banking CLI commands for banks, accounts, statement refs, imports, transactions, categories, reports, exports, storage migration, and status. +- Side-by-side `bb` CLI for v2 financial intelligence foundation status and + `BB_` schema visibility. - Supported banking imports for Bank of America PDF/CSV, Apple Card PDF, ICICI `.xls`, and HDFC `.xls`. - Statement inventory and statement coverage auditing. @@ -26,6 +28,8 @@ section is `Unreleased`. while moving the target model toward documents, entities, observations, and relationships. Active design docs are the vision, architecture, and open questions documents. +- Additive `bankbuddy.bb` source namespace and `bb` console script for v2 work, + leaving existing `bankbuddy` and `taxbuddy` commands in place. - Prospective relicensing from MIT to AGPL-3.0-or-later. - Canonical data-home layout with `database/`, `bank/`, and `tax/` directories. - First TaxBuddy CLI slice and `tax_documents` metadata index. diff --git a/.ai-context/WORKFLOWS.md b/.ai-context/WORKFLOWS.md index 9b73b1b..163296c 100644 --- a/.ai-context/WORKFLOWS.md +++ b/.ai-context/WORKFLOWS.md @@ -33,6 +33,7 @@ The Base dogfood validation path is: ```bash basectl setup bankbuddy basectl test bankbuddy +basectl run bankbuddy bb -- status basectl run bankbuddy bankbuddy -- status basectl run bankbuddy taxbuddy -- status ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 492c106..1d8eec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,10 @@ and versions are tracked in the repo-root `VERSION` file. - Added v2 financial document storage roots, document object metadata, generated human-readable document view metadata, path-resolution helpers, - and read-only managed-file guardrails under the `financial/` namespace. + and read-only managed-file guardrails under the `bankbuddy.bb` namespace. +- Added the side-by-side `bb` console command and `bankbuddy.bb` source + namespace for financial intelligence v2 work, while leaving existing + `bankbuddy` and `taxbuddy` commands intact. - Added additive financial intelligence v2 foundation tables, seed type dictionaries, indexes, and DAO-based read/write helpers without dropping existing banking or tax tables. diff --git a/README.md b/README.md index 26a3f87..0a9b333 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ basectl test bankbuddy Run uv-backed project commands through Base when you want one-command dispatch: ```bash +basectl run bankbuddy bb -- status basectl run bankbuddy bankbuddy -- status basectl run bankbuddy taxbuddy -- status ``` @@ -42,6 +43,7 @@ Activate the project shell once, then run the CLI directly: ```bash basectl activate bankbuddy +bb status bankbuddy --help bankbuddy status bankbuddy init @@ -137,6 +139,11 @@ taxbuddy docs list --type 1099-INT taxbuddy docs show 1 ``` +`bb` is the side-by-side v2 CLI for the financial intelligence foundation. It +uses the additive `BB_` schema and `bankbuddy.bb` source namespace while the +existing `bankbuddy` and `taxbuddy` commands remain available during the +transition. + ## Environments BankBuddy keeps separate local data homes for named environments. Outside an @@ -147,6 +154,7 @@ which maps to `~/BankBuddy-dev`. Use `status` to confirm the active environment and database: ```bash +bb status bankbuddy status ``` diff --git a/docs/financial_intelligence_architecture.md b/docs/financial_intelligence_architecture.md index 121df8c..ccc3f94 100644 --- a/docs/financial_intelligence_architecture.md +++ b/docs/financial_intelligence_architecture.md @@ -11,7 +11,10 @@ BankBuddy should evolve into the broader personal financial intelligence platform described in the product vision, while remaining in the existing -BankBuddy repository and keeping the `bankbuddy` CLI name. +BankBuddy repository and keeping BankBuddy as the product name. New v2 work +should start behind the shorter `bb` CLI and the `bankbuddy.bb` source +namespace so the existing `bankbuddy` and `taxbuddy` commands can keep working +while the new model matures. The right target architecture is not a banking app with more tables. It is a local-first evidence graph: @@ -34,7 +37,12 @@ need to be discarded just to make room for the new architecture. The following decisions define the current v2 direction: -- Keep the product and CLI name as `bankbuddy`. +- Keep the product name as BankBuddy. +- Introduce `bb` as the side-by-side v2 CLI for financial intelligence + workflows. Keep the existing `bankbuddy` and `taxbuddy` commands intact until + the v2 command surface is stable enough for cleanup. +- Put new v2 source files under `bankbuddy.bb` so new DAOs, domain records, and + CLI commands do not collide with legacy banking modules. - Use `Document`, `Entity`, `Observation`, and `Relationship` as the conceptual foundation, while allowing details to evolve during implementation. - Introduce the v2 foundation schema additively beside the existing legacy @@ -942,6 +950,19 @@ This service is new and should be a first-class workflow, not an afterthought. ## 8. Proposed Package and Module Structure +Near-term v2 code should live in a side-by-side namespace: + +```text +src/bankbuddy/ + bb/ + cli.py + dao.py + records.py +``` + +This namespace is intentionally small at first. It gives v2 work a clean home +without forcing a broad package refactor before the model is proven. + Target structure: ```text @@ -1035,49 +1056,51 @@ This can be reached gradually. Do not move everything in one PR. ## 9. Proposed CLI Evolution -The CLI should remain `bankbuddy` until the platform shape is stable. Separate -`taxbuddy` can remain temporarily, but the long-term command model should be -one platform CLI with domain groups. +The v2 command surface should start as `bb` while the legacy `bankbuddy` and +`taxbuddy` CLIs remain available. This lets the new document/entity/observation +model evolve without breaking the working banking and tax-document commands. +After the v2 surface is stable, cleanup can decide whether `bb` becomes the +only command or whether `bankbuddy` is kept as an alias. Target command families: ```text -bankbuddy status -bankbuddy init - -bankbuddy documents import FILE -bankbuddy documents inbox --dry-run -bankbuddy documents list -bankbuddy documents show DOCUMENT_ID - -bankbuddy inspect documents --year YEAR -bankbuddy inspect gaps --year YEAR -bankbuddy inspect accounts --years 2024,2025 -bankbuddy inspect review - -bankbuddy review observations -bankbuddy review accept OBSERVATION_ID -bankbuddy review reject OBSERVATION_ID - -bankbuddy entities list -bankbuddy entities show ENTITY_ID - -bankbuddy accounts list -bankbuddy tx list -bankbuddy report spending -bankbuddy report net-worth - -bankbuddy tax sources -bankbuddy tax gaps --year YEAR -bankbuddy tax summary --year YEAR +bb status +bb init + +bb documents import FILE +bb documents inbox --dry-run +bb documents list +bb documents show DOCUMENT_ID + +bb inspect documents --year YEAR +bb inspect gaps --year YEAR +bb inspect accounts --years 2024,2025 +bb inspect review + +bb review observations +bb review accept OBSERVATION_ID +bb review reject OBSERVATION_ID + +bb entities list +bb entities show ENTITY_ID + +bb accounts list +bb tx list +bb report spending +bb report net-worth + +bb tax sources +bb tax gaps --year YEAR +bb tax summary --year YEAR ``` Potential later commands, only after the infer design is clarified: ```text -bankbuddy infer document DOCUMENT_ID -bankbuddy infer year YEAR -bankbuddy infer compare --years 2024,2025 +bb infer document DOCUMENT_ID +bb infer year YEAR +bb infer compare --years 2024,2025 ``` Existing `bankbuddy import`, `bankbuddy tx`, `bankbuddy report`, and @@ -1194,8 +1217,8 @@ Validation: Build: -- `bankbuddy documents import`; -- `bankbuddy documents inbox --dry-run`; +- `bb documents import`; +- `bb documents inbox --dry-run`; - document listing/show commands; - generic duplicate detection; - managed document archive paths; @@ -1334,7 +1357,9 @@ CLI output, and defer durable text indexing until encryption is designed. Resolved decisions: -1. Keep the user-facing product and CLI name as `BankBuddy` / `bankbuddy`. +1. Keep the user-facing product name as BankBuddy, introduce `bb` as the + side-by-side v2 CLI, and keep `bankbuddy` / `taxbuddy` intact during the + transition. 2. Use `Document/Entity/Observation/Relationship` as the foundation, with details refined during implementation. 3. Introduce the v2 foundation schema additively beside existing legacy tables. diff --git a/pyproject.toml b/pyproject.toml index 59d1f55..a44848f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ ] [project.scripts] +bb = "bankbuddy.bb.cli:main" bankbuddy = "bankbuddy.cli:main" taxbuddy = "bankbuddy.tax.cli:main" diff --git a/src/bankbuddy/financial/__init__.py b/src/bankbuddy/bb/__init__.py similarity index 100% rename from src/bankbuddy/financial/__init__.py rename to src/bankbuddy/bb/__init__.py diff --git a/src/bankbuddy/bb/cli.py b/src/bankbuddy/bb/cli.py new file mode 100644 index 0000000..309cd2a --- /dev/null +++ b/src/bankbuddy/bb/cli.py @@ -0,0 +1,158 @@ +"""Command line interface for BankBuddy v2 workflows.""" + +from __future__ import annotations + +import sqlite3 + +import click + +from bankbuddy import __version__ +from bankbuddy.paths import resolve_app_paths +from bankbuddy.runtime import CliRuntime +from bankbuddy.runtime import RuntimeConfigError +from bankbuddy.runtime import create_runtime + + +BB_FOUNDATION_TABLES = { + "BB_CURRENCY", + "BB_DOCUMENT", + "BB_DOCUMENT_OBJECT", + "BB_DOCUMENT_VIEW", + "BB_ENTITY", + "BB_ENTITY_ATTRIBUTE", + "BB_ENTITY_ATTRIBUTE_TYPE", + "BB_EXTRACTION_RUN", + "BB_HOUSEHOLD", + "BB_HOUSEHOLD_MEMBER", + "BB_IMPORT_ATTEMPT", + "BB_JURISDICTION", + "BB_OBSERVATION", + "BB_OBSERVATION_EVIDENCE", + "BB_OBSERVATION_TYPE", + "BB_PARSER", + "BB_PERSON", + "BB_RELATIONSHIP", + "BB_RELATIONSHIP_TYPE", + "BB_STORAGE_ROOT", +} + +LEGACY_TABLES = { + "accounts", + "banks", + "import_attempts", + "import_files", + "tax_documents", + "transactions", +} + + +@click.group( + name="bb", + context_settings={"help_option_names": ["-h", "--help"]}, +) +@click.version_option(__version__, prog_name="bb") +@click.option( + "-v", + "--debug", + is_flag=True, + help="Enable DEBUG logging on the user-facing stream.", +) +@click.option("--environment", help="Set the BankBuddy environment for this command.") +@click.option( + "--config", + "config_path", + type=click.Path(dir_okay=False), + help="Load an additional config file.", +) +@click.option("--keep-temp", is_flag=True, help="Preserve this run's temp directory.") +@click.option( + "--log-file", + type=click.Path(dir_okay=False), + help="Override the persistent log file.", +) +@click.pass_context +def main( + ctx: click.Context, + debug: bool, + environment: str | None, + config_path: str | None, + keep_temp: bool, + log_file: str | None, +) -> None: + """BankBuddy v2 financial intelligence workflows.""" + + try: + runtime = create_runtime( + debug=debug, + environment=environment, + config_path=config_path, + keep_temp=keep_temp, + log_file=log_file, + cli_name="bb", + ) + except (OSError, RuntimeConfigError) as exc: + raise click.ClickException(str(exc)) from exc + + ctx.obj = runtime + ctx.call_on_close(runtime.cleanup) + + +@main.command() +@click.pass_context +def status(ctx: click.Context) -> None: + """Show the local BankBuddy v2 app state.""" + + runtime = runtime_from_context(ctx) + paths = resolve_app_paths(environment=runtime.environment) + table_names = _database_table_names(paths.database) + initialized = "yes" if paths.database.exists() else "no" + bb_table_count = len([name for name in table_names if name.startswith("BB_")]) + has_v2_foundation = BB_FOUNDATION_TABLES.issubset(table_names) + has_legacy_tables = bool(LEGACY_TABLES.intersection(table_names)) + + runtime.log.debug( + "bb_status environment=%s data_home=%s database=%s initialized=%s " + "bb_table_count=%s v2_foundation=%s legacy_tables=%s", + paths.environment, + paths.root, + paths.database, + initialized, + bb_table_count, + has_v2_foundation, + has_legacy_tables, + ) + + click.echo("CLI: bb") + click.echo(f"Environment: {paths.environment}") + click.echo(f"Storage layout: {paths.layout}") + click.echo(f"Data home: {paths.root}") + click.echo(f"Database: {paths.database}") + click.echo(f"Initialized: {initialized}") + click.echo(f"V2 foundation: {_yes_no(has_v2_foundation)}") + click.echo(f"BB tables: {bb_table_count}") + click.echo(f"Legacy tables: {'present' if has_legacy_tables else 'absent'}") + + +def _database_table_names(database_path) -> set[str]: + if not database_path.exists(): + return set() + with sqlite3.connect(database_path) as conn: + return { + str(row[0]) + for row in conn.execute( + "select name from sqlite_master where type = 'table'" + ).fetchall() + } + + +def _yes_no(value: bool) -> str: + return "yes" if value else "no" + + +def runtime_from_context(ctx: click.Context) -> CliRuntime: + """Return the root bb runtime context.""" + + runtime = ctx.find_root().obj + if not isinstance(runtime, CliRuntime): + raise click.ClickException("bb runtime context is not active.") + return runtime diff --git a/src/bankbuddy/financial/dao.py b/src/bankbuddy/bb/dao.py similarity index 99% rename from src/bankbuddy/financial/dao.py rename to src/bankbuddy/bb/dao.py index ce31a20..e3dfe18 100644 --- a/src/bankbuddy/financial/dao.py +++ b/src/bankbuddy/bb/dao.py @@ -4,7 +4,7 @@ import sqlite3 -from bankbuddy.financial.records import ( +from bankbuddy.bb.records import ( DocumentCreate, DocumentRecord, EntityAttributeCreate, diff --git a/src/bankbuddy/financial/records.py b/src/bankbuddy/bb/records.py similarity index 100% rename from src/bankbuddy/financial/records.py rename to src/bankbuddy/bb/records.py diff --git a/src/bankbuddy/financial/storage.py b/src/bankbuddy/bb/storage.py similarity index 99% rename from src/bankbuddy/financial/storage.py rename to src/bankbuddy/bb/storage.py index 435f141..dc33b25 100644 --- a/src/bankbuddy/financial/storage.py +++ b/src/bankbuddy/bb/storage.py @@ -5,7 +5,7 @@ from pathlib import Path, PurePosixPath import sqlite3 -from bankbuddy.financial.records import ( +from bankbuddy.bb.records import ( DocumentObjectCreate, DocumentObjectRecord, DocumentViewCreate, diff --git a/tests/test_bb_cli.py b/tests/test_bb_cli.py new file mode 100644 index 0000000..0b6da1b --- /dev/null +++ b/tests/test_bb_cli.py @@ -0,0 +1,45 @@ +from click.testing import CliRunner + +from bankbuddy.bb.cli import main +from bankbuddy.database import initialize_database +from bankbuddy.paths import resolve_app_paths + + +def test_bb_cli_version() -> None: + result = CliRunner().invoke(main, ["--version"]) + + assert result.exit_code == 0 + assert "bb, version 0.1.0" in result.output + + +def test_bb_status_reports_environment_data_home_and_v2_schema(tmp_path) -> None: + initialize_database(resolve_app_paths(tmp_path)) + + result = CliRunner().invoke( + main, + ["status"], + env={ + "BANKBUDDY_HOME": str(tmp_path), + "BANKBUDDY_ENV": "dev", + }, + ) + + assert result.exit_code == 0 + assert "CLI: bb" in result.output + assert "Environment: dev" in result.output + assert f"Data home: {tmp_path}" in result.output + assert f"Database: {tmp_path / 'database' / 'bankbuddy.sqlite3'}" in result.output + assert "Initialized: yes" in result.output + assert "V2 foundation: yes" in result.output + assert "BB tables: 20" in result.output + assert "Legacy tables: present" in result.output + + +def test_bb_status_reports_missing_v2_schema(tmp_path) -> None: + result = CliRunner().invoke(main, ["status"], env={"BANKBUDDY_HOME": str(tmp_path)}) + + assert result.exit_code == 0 + assert "CLI: bb" in result.output + assert "Initialized: no" in result.output + assert "V2 foundation: no" in result.output + assert "BB tables: 0" in result.output diff --git a/tests/test_financial_foundation.py b/tests/test_bb_foundation.py similarity index 96% rename from tests/test_financial_foundation.py rename to tests/test_bb_foundation.py index d77a595..75c62f7 100644 --- a/tests/test_financial_foundation.py +++ b/tests/test_bb_foundation.py @@ -1,6 +1,6 @@ from bankbuddy.database import connect_database, initialize_database -from bankbuddy.financial.dao import FinancialIntelligenceDAO -from bankbuddy.financial.records import ( +from bankbuddy.bb.dao import FinancialIntelligenceDAO +from bankbuddy.bb.records import ( DocumentCreate, EntityAttributeCreate, EntityCreate, @@ -10,7 +10,7 @@ from bankbuddy.paths import resolve_app_paths -def test_financial_foundation_schema_is_additive_and_seeded(tmp_path) -> None: +def test_bb_foundation_schema_is_additive_and_seeded(tmp_path) -> None: paths = resolve_app_paths(tmp_path) initialize_database(paths) diff --git a/tests/test_financial_storage.py b/tests/test_bb_storage.py similarity index 91% rename from tests/test_financial_storage.py rename to tests/test_bb_storage.py index 23b0470..edd32e6 100644 --- a/tests/test_financial_storage.py +++ b/tests/test_bb_storage.py @@ -4,12 +4,12 @@ from bankbuddy.database import connect_database from bankbuddy.database import initialize_database -from bankbuddy.financial.dao import FinancialIntelligenceDAO -from bankbuddy.financial.records import DocumentCreate +from bankbuddy.bb.dao import FinancialIntelligenceDAO +from bankbuddy.bb.records import DocumentCreate from bankbuddy.paths import resolve_app_paths -def test_financial_storage_schema_seeds_roots(tmp_path) -> None: +def test_bb_storage_schema_seeds_roots(tmp_path) -> None: paths = resolve_app_paths(tmp_path / "home") initialize_database(paths) @@ -50,7 +50,7 @@ def test_financial_storage_schema_seeds_roots(tmp_path) -> None: } -def test_financial_storage_paths_are_separate_from_bank_and_tax_dirs(tmp_path) -> None: +def test_bb_storage_paths_are_separate_from_bank_and_tax_dirs(tmp_path) -> None: ( _, _, @@ -232,7 +232,7 @@ def test_storage_keys_reject_absolute_and_parent_traversal_paths(tmp_path) -> No _, _, ) = _storage_api() - from bankbuddy.financial.storage import validate_storage_key + from bankbuddy.bb.storage import validate_storage_key paths = resolve_app_paths(tmp_path / "home") @@ -266,7 +266,7 @@ def test_storage_keys_reject_absolute_and_parent_traversal_paths(tmp_path) -> No def test_protect_managed_path_sets_read_only_modes(tmp_path) -> None: - from bankbuddy.financial.storage import protect_managed_path + from bankbuddy.bb.storage import protect_managed_path managed_file = tmp_path / "financial" / "canonical" / "object.pdf" managed_file.parent.mkdir(parents=True) @@ -287,13 +287,13 @@ def test_protect_managed_path_sets_read_only_modes(tmp_path) -> None: def _storage_api(): try: - from bankbuddy.financial.records import DocumentObjectCreate - from bankbuddy.financial.records import DocumentViewCreate - from bankbuddy.financial.storage import FinancialStorageDAO - from bankbuddy.financial.storage import FinancialStoragePathError - from bankbuddy.financial.storage import ensure_financial_storage_dirs - from bankbuddy.financial.storage import object_key_for_hash - from bankbuddy.financial.storage import resolve_storage_path + from bankbuddy.bb.records import DocumentObjectCreate + from bankbuddy.bb.records import DocumentViewCreate + from bankbuddy.bb.storage import FinancialStorageDAO + from bankbuddy.bb.storage import FinancialStoragePathError + from bankbuddy.bb.storage import ensure_financial_storage_dirs + from bankbuddy.bb.storage import object_key_for_hash + from bankbuddy.bb.storage import resolve_storage_path except ImportError as exc: pytest.fail(f"Financial storage API is not available: {exc}") diff --git a/tests/test_packaging.py b/tests/test_packaging.py index d2fd70b..ab843ef 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -1,13 +1,14 @@ import tomllib -def test_project_installs_bankbuddy_and_taxbuddy_console_commands() -> None: +def test_project_installs_bankbuddy_taxbuddy_and_bb_console_commands() -> None: with open("pyproject.toml", "rb") as pyproject_file: pyproject = tomllib.load(pyproject_file) scripts = pyproject["project"]["scripts"] assert scripts == { + "bb": "bankbuddy.bb.cli:main", "bankbuddy": "bankbuddy.cli:main", "taxbuddy": "bankbuddy.tax.cli:main", }