Skip to content

ADR-004: v1 Portfolio Fallback Officially Retired ​

Status: Accepted
Date: 2026-05-10
Author: Dev-Core Sprint 2


Context ​

During the B7 refactoring on 2026-05-06, openInvest migrated portfolio.md from "flat fields" (v1) to "cash dict + holdings list" (v2). To ensure zero-downtime upgrade during the refactoring, read-time fallbacks were retained in three places:

  1. PortfolioManager.cash property — Detects when the cash field is empty and infers it from cash_cny/aud_cash
  2. PortfolioManager.holdings property — Detects when the holdings field is empty and infers it from ndq_shares/gold_grams
  3. _ensure_v2_inplace() — Converts v1 data in-place to v2 at the entry point of with_portfolio_tx()

These fallbacks were necessary during the migration period. However, after the migration script (scripts/migrate_portfolio_to_holdings.py) has been running stably, retaining them introduced the following issues:

  • Code Noise: The read path has dual branches (v2 fresh → v1 fallback), making it impossible for readers to intuitively determine the current production data format.
  • Testing Difficulties: Test fixtures could use the v1 format to bypass schema validation, causing _seed_memory in test_web_api.py to persistently use cash_cny instead of the cash dict, masking the actual coverage of schema validation.
  • Schema Slippage: The newly added PortfolioData Pydantic model requires schema_version=2, but the fallback allowed v1 data to bypass write-time validation.

Decision ​

On 2026-05-10, after confirming that all production instances have completed the v1→v2 migration (schema_version=2), all v1 fallbacks were formally deleted:

Deleted Content ​

LocationDeleted Code
core/portfolio_manager.py cash property8 lines inferring from cash_cny/aud_cash
core/portfolio_manager.py holdings property25 lines inferring from ndq_shares/gold_grams
core/portfolio_manager.py _ensure_v2_inplace()v1→v2 in-place conversion logic; function retained as a pass shell (call sites left untouched for now)

Retained Content ​

  • Logic in with_portfolio_tx() that clears old v1 field names (cash_cny, aud_cash, etc.) upon exit — prevents dirty fields from being committed by manually corrupted files during a transaction commit.
  • The migration script scripts/migrate_portfolio_to_holdings.py itself — it is a data-layer tool, retained for historical records and fork users.

Timeline ​

DateEvent
2026-05-06B7 refactoring introduces v2 data model, retains v1 fallback compatibility
2026-05-06scripts/migrate_portfolio_to_holdings.py written, implements idempotent v1→v2 migration
2026-05-10Production data confirmed as v2, v1 fallback formally deleted (this ADR)
2026-05-10_seed_memory fixture in tests/test_web_api.py upgraded to v2 format
2026-05-10New tests/test_portfolio_manager.py added; the 502-line PM code finally has independent tests

Affected Users ​

Fork users still using the v1 format portfolio.md (where the schema_version field is absent or set to 1):

Upgrade steps:

bash
# 1. Backup the current portfolio.md
cp memory/portfolio.md memory/portfolio.md.bak

# 2. Run the migration script (idempotent, can be executed multiple times)
python -m scripts.migrate_portfolio_to_holdings

# 3. Verify the result
python -m scripts.skill doctor

The migration script will:

  • Automatically back up the original file as portfolio.md.bak.<timestamp>
  • Convert cash_cny → cash.CNY, aud_cash → cash.AUD
  • Convert ndq_shares → holdings[NDQ.AX].units, gold_grams → holdings[GC=F].units
  • Set schema_version: 2
  • Perform an atomic write after validation (automatically rollback on failure)

Next Steps ​

  • The _ensure_v2_inplace() function is currently left as an empty pass; it will be completely removed along with its call sites in the next cleanup sprint.
  • The v1 field cleanup logic in with_portfolio_tx() upon exit can be removed together after confirming there are no remaining users.