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:
PortfolioManager.cashproperty — Detects when thecashfield is empty and infers it fromcash_cny/aud_cashPortfolioManager.holdingsproperty — Detects when theholdingsfield is empty and infers it fromndq_shares/gold_grams_ensure_v2_inplace()— Converts v1 data in-place to v2 at the entry point ofwith_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_memoryintest_web_api.pyto persistently usecash_cnyinstead of thecashdict, masking the actual coverage of schema validation. - Schema Slippage: The newly added
PortfolioDataPydantic model requiresschema_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
| Location | Deleted Code |
|---|---|
core/portfolio_manager.py cash property | 8 lines inferring from cash_cny/aud_cash |
core/portfolio_manager.py holdings property | 25 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.pyitself — it is a data-layer tool, retained for historical records and fork users.
Timeline
| Date | Event |
|---|---|
| 2026-05-06 | B7 refactoring introduces v2 data model, retains v1 fallback compatibility |
| 2026-05-06 | scripts/migrate_portfolio_to_holdings.py written, implements idempotent v1→v2 migration |
| 2026-05-10 | Production 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-10 | New 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:
# 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 doctorThe 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 emptypass; 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.