$MART DEBT App — Phase 1: SD Snapshot (Interest-Only Demo-Teaser)
Section titled “$MART DEBT App — Phase 1: SD Snapshot (Interest-Only Demo-Teaser)”For agentic workers: REQUIRED: Use
superpowers:subagent-driven-development(if subagents available) orsuperpowers:executing-plansto implement this plan. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Build the SD Snapshot — a static SvelteKit PWA that renders pre-calculated interest-only projections for canonical investor profiles. Creates the aha moment for advisors and investors; drives visitors to the full SD App.
Architecture: Python owns all math. SvelteKit owns the UI. JSON is the contract.
- Python generator (in
tools/snapshot-generator/): ports VB6 interest-only math → produces pre-calculated JSON - JSON snapshot file (in
apps/sd-app/static/data/): shipped with PWA; updated weekly by generator - SvelteKit renderer (
/ca/en/int-only/): reads JSON, no math, pure presentation + CTA - Weekly update utility: Python script (re)generates JSON on a schedule
TypeScript never does any math. The browser client is a renderer only.
Phase 1 scope:
- Canada only, non-QC tax treatment
- Interest-only projections (not term loans)
- 10-year horizon (fixed for snapshot)
- 3–5 canonical investor profiles
- No user-adjustable inputs (demo-teaser, not interactive calculator)
- English only
⚠️ Architectural Correction — 2026-03-20
Section titled “⚠️ Architectural Correction — 2026-03-20”The prior Phase 1 plan (now superseded) incorrectly proposed porting VB6 math to TypeScript for in-browser calculation. That approach is abandoned entirely.
Correct architecture:
- Python backend is the authoritative math engine (all phases)
- Phase 1 uses Python to pre-calculate a fixed JSON snapshot
- Phase 2+ (full SD App) calls the Python backend live via API
- TypeScript/SvelteKit: UI and presentation only — never math
Pre-Conditions (Two Human Actions — Both Blocking)
Section titled “Pre-Conditions (Two Human Actions — Both Blocking)”Pre-Condition A: VB6 Source Study
Section titled “Pre-Condition A: VB6 Source Study”Human action: Open the LevPro VB6 project. Identify and share the interest-only calculation function(s) — entry point, inputs, outputs, and the annual calculation loop.
What the Python implementer needs before writing a single line:
- The interest-only calculation entry point (VB6 function name, parameters)
- How total return is split between dividends and capital gains — and how each is taxed
- How capital gains are handled: annually realised vs. deferred to end of holding period
- ACB tracking: how is the adjusted cost base updated each year?
- How the loan is repaid at end (after-tax dollars — exact VB6 mechanism)
- The unleveraged comparison baseline (same cash flows, no loan)
- Whether “Better Than” return is computed in LevPro or must be added
- Any rounding or truncation on intermediate values
Do not implement the Python math engine until this is understood.
Pre-Condition B: LevPro Fixture Capture
Section titled “Pre-Condition B: LevPro Fixture Capture”Run LevPro for these 5 canonical scenarios. Record every output field LevPro displays (annual rows AND terminal summary). Store in tools/snapshot-generator/tests/fixtures/interest-only-ca.json.
| # | Loan | Rate | Return | Tax Rate | Years | Notes |
|---|---|---|---|---|---|---|
| 1 | $100,000 | 5.5% | 7% | 40% | 10 | Baseline |
| 2 | $100,000 | 5.5% | 4% | 40% | 10 | Low return (near break-even) |
| 3 | $50,000 | 6% | 7% | 43% | 10 | Smaller loan |
| 4 | $200,000 | 5% | 9% | 35% | 10 | Higher income |
| 5 | $100,000 | 5.5% | 0% | 40% | 10 | Zero return (worst case) |
The fixture fields must be derived from what LevPro actually outputs — do not assume field names before studying the VB6 source.
File Map
Section titled “File Map”| Action | File | Purpose |
|---|---|---|
| Create | tools/snapshot-generator/ | Python package root |
| Create | tools/snapshot-generator/pyproject.toml | Python project config |
| Create | tools/snapshot-generator/src/interest_only.py | VB6 math port |
| Create | tools/snapshot-generator/tests/fixtures/interest-only-ca.json | LevPro golden fixtures |
| Create | tools/snapshot-generator/tests/test_interest_only.py | pytest golden-fixture tests |
| Create | tools/snapshot-generator/generate_snapshot.py | CLI: generates snapshot JSON |
| Create | apps/sd-app/static/data/snapshot-ca-en.json | Pre-calculated snapshot (generated, committed) |
| Modify | apps/sd-app/src/routes/[country]/[lang]/int-only/+page.svelte | Snapshot renderer |
| Create | apps/sd-app/src/routes/[country]/[lang]/int-only/+page.ts | Static data loader |
Chunk 1: Python Math Foundation
Section titled “Chunk 1: Python Math Foundation”Task 1: Set up Python environment
Section titled “Task 1: Set up Python environment”Files:
-
Create:
tools/snapshot-generator/pyproject.toml -
Create:
tools/snapshot-generator/src/__init__.py -
Step 1.1: Create tools directory and Python package
mkdir -p /home/ta/projects/monorepo/tools/snapshot-generator/srcmkdir -p /home/ta/projects/monorepo/tools/snapshot-generator/tests/fixtures- Step 1.2: Create
tools/snapshot-generator/pyproject.toml
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"
[project]name = "sd-snapshot-generator"version = "0.1.0"requires-python = ">=3.11"dependencies = []
[project.optional-dependencies]dev = ["pytest>=8.0", "ruff>=0.4"]
[tool.ruff]line-length = 100
[tool.pytest.ini_options]testpaths = ["tests"]- Step 1.3: Create virtual environment and install
cd /home/ta/projects/monorepo/tools/snapshot-generatorpython3 -m venv .venvsource .venv/bin/activatepip install -e ".[dev]"- Step 1.4: Verify pytest runs (no tests yet = pass)
pytestExpected: no tests ran
- Step 1.5: Commit
git add tools/snapshot-generator/git commit -m "feat(sd-app): add Python snapshot generator skeleton"Task 2: Create golden fixture file
Section titled “Task 2: Create golden fixture file”Files:
- Create:
tools/snapshot-generator/tests/fixtures/interest-only-ca.json
Prerequisite: Pre-Condition B — LevPro outputs must be captured before this file has real values. Create the schema first with PLACEHOLDER values; replace after running LevPro.
- Step 2.1: Create fixture template
Create tools/snapshot-generator/tests/fixtures/interest-only-ca.json:
{ "_note": "Golden fixtures from LevPro VB6. Replace PLACEHOLDER with actual LevPro outputs.", "_tolerance": "Match to ~5 significant digits (0.01%)", "fixtures": [ { "id": "io-ca-001", "description": "Baseline: $100k, 5.5% interest, 7% return, 40% tax, 10 years", "inputs": { "loan_amount": 100000, "interest_rate": 5.5, "expected_return": 7.0, "marginal_tax_rate": 40, "holding_period": 10 }, "expected": "PLACEHOLDER — replace with every output field LevPro displays" }, { "id": "io-ca-002", "description": "Low return: $100k, 5.5% interest, 4% return, 40% tax, 10 years", "inputs": { "loan_amount": 100000, "interest_rate": 5.5, "expected_return": 4.0, "marginal_tax_rate": 40, "holding_period": 10 }, "expected": "PLACEHOLDER" }, { "id": "io-ca-003", "description": "Smaller loan: $50k, 6% interest, 7% return, 43% tax, 10 years", "inputs": { "loan_amount": 50000, "interest_rate": 6.0, "expected_return": 7.0, "marginal_tax_rate": 43, "holding_period": 10 }, "expected": "PLACEHOLDER" }, { "id": "io-ca-004", "description": "Higher income: $200k, 5% interest, 9% return, 35% tax, 10 years", "inputs": { "loan_amount": 200000, "interest_rate": 5.0, "expected_return": 9.0, "marginal_tax_rate": 35, "holding_period": 10 }, "expected": "PLACEHOLDER" }, { "id": "io-ca-005", "description": "Worst case: $100k, 5.5% interest, 0% return, 40% tax, 10 years", "inputs": { "loan_amount": 100000, "interest_rate": 5.5, "expected_return": 0.0, "marginal_tax_rate": 40, "holding_period": 10 }, "expected": "PLACEHOLDER" } ]}STOP HERE until LevPro outputs are captured. Replace every PLACEHOLDER with real LevPro output fields and values. Commit. Then continue to Task 3.
- Step 2.2: Commit fixture template
git add tools/snapshot-generator/tests/fixtures/git commit -m "feat(sd-app): add LevPro golden fixture template (values TBD)"Chunk 2: Python Math Engine
Section titled “Chunk 2: Python Math Engine”Task 3: Failing pytest tests
Section titled “Task 3: Failing pytest tests”Files:
- Create:
tools/snapshot-generator/tests/test_interest_only.py
Prerequisite: Pre-Condition A (VB6 source study) AND Pre-Condition B (fixtures filled in).
- Step 3.1: Create test file
Create tools/snapshot-generator/tests/test_interest_only.py:
"""Golden-fixture tests for the interest-only calculator.
Every assertion must match LevPro output to ~5 significant digits (0.01%)."""import jsonfrom pathlib import Pathimport pytestfrom src.interest_only import calculate_interest_only
FIXTURES_PATH = Path(__file__).parent / "fixtures" / "interest-only-ca.json"TOLERANCE = 0.0001 # 0.01% relative error
def approx_equal(actual: float, expected: float, label: str) -> None: if expected == 0: assert abs(actual) < 0.01, f"{label}: expected ~0, got {actual}" return rel_err = abs((actual - expected) / expected) assert rel_err <= TOLERANCE, ( f"{label}: relative error {rel_err:.4%} exceeds {TOLERANCE:.4%}. " f"actual={actual}, expected={expected}" )
with open(FIXTURES_PATH) as f: fixture_data = json.load(f)
@pytest.mark.parametrize("fixture", fixture_data["fixtures"], ids=[f["id"] for f in fixture_data["fixtures"]])def test_golden_fixtures(fixture): inputs = fixture["inputs"] expected = fixture["expected"] result = calculate_interest_only(**inputs)
for field, expected_value in expected.items(): approx_equal(result[field], expected_value, f"{fixture['id']}.{field}")- Step 3.2: Run tests — verify FAIL with ImportError
cd /home/ta/projects/monorepo/tools/snapshot-generatorsource .venv/bin/activatepytest -vExpected: ImportError: cannot import name 'calculate_interest_only'
- Step 3.3: Commit failing tests
git add tools/snapshot-generator/tests/test_interest_only.pygit commit -m "test(sd-app): add failing golden-fixture pytest (TDD red)"Task 4: Port VB6 interest-only math to Python
Section titled “Task 4: Port VB6 interest-only math to Python”Files:
- Create:
tools/snapshot-generator/src/interest_only.py
Prerequisite: Pre-Condition A complete. The Python is a faithful translation of VB6 — not a reimplementation from financial theory.
- Step 4.1: Document the VB6 algorithm as comments before writing Python
Open tools/snapshot-generator/src/interest_only.py and write the algorithm as pseudocode comments first — one block per VB6 function or logical section. Cite the VB6 function name for each section.
## Port of LevPro VB6 interest-only calculation.# VB6 source function(s): [fill in from VB6 source]## ANNUAL LOOP (years 1..n):# [document each step from VB6 here]## TERMINAL VALUE:# [document from VB6]## UNLEVERAGED COMPARISON:# [document from VB6]## BETTER THAN RETURN:# [document if in LevPro, or note it must be added via binary search]- Step 4.2: Implement
calculate_interest_only()translating VB6 line by line
def calculate_interest_only( loan_amount: float, interest_rate: float, # annual %, e.g. 5.5 expected_return: float, # annual %, e.g. 7.0 marginal_tax_rate: float, # %, e.g. 40 holding_period: int, # years capital_gains_inclusion: float = 0.5, # Canada non-QC default) -> dict: """ Port of LevPro VB6 interest-only projection. Returns dict of all output fields matching LevPro output exactly. """ # [implementation derived from VB6 source] ...- Step 4.3: Run tests
pytest -v 2>&1First run: some assertions will fail — each failure identifies a translation error. Fix against VB6 source only. Do not adjust to match intuition about the math.
- Step 4.4: Iterate until all 5 core fixtures pass
Common VB6 translation pitfalls:
-
VB6 integer division (
\) vs float division (/) — use//where VB6 uses\ -
Order of operations: does VB6 apply tax deduction before or after computing next year’s value?
-
Whether dividends are taxed on gross or net investment value
-
Whether ACB is updated annually or only at sale
-
Step 4.5: Commit passing core port
git add tools/snapshot-generator/src/interest_only.py \ tools/snapshot-generator/tests/fixtures/interest-only-ca.jsongit commit -m "feat(sd-app): port LevPro interest-only to Python — 5 core fixtures pass"Task 5: Extended validation
Section titled “Task 5: Extended validation”Purpose: Eliminate doubt across the full input space before generating the snapshot.
- Step 5.1: Capture 15–20 additional LevPro scenarios
Run LevPro for the following, add to an extended fixture file:
| Category | Scenarios |
|---|---|
| Return sweep | 1%, 2%, 3%, 5%, 6%, 8%, 10%, 12% (at baseline loan/rate/tax) |
| Interest rate sweep | 4%, 6%, 7%, 8% |
| Tax rate extremes | 25%, 50% |
| Loan size extremes | $25,000, $500,000 |
Store in tools/snapshot-generator/tests/fixtures/interest-only-ca-extended.json using the same schema.
- Step 5.2: Add extended fixture suite to test file
# In test_interest_only.py, add:with open(Path(__file__).parent / "fixtures" / "interest-only-ca-extended.json") as f: extended_data = json.load(f)
@pytest.mark.parametrize("fixture", extended_data["fixtures"], ids=[f["id"] for f in extended_data["fixtures"]])def test_extended_fixtures(fixture): # same body as test_golden_fixtures ...- Step 5.3: All extended fixtures must pass
pytest -vExpected: all pass. Any failure = translation bug; fix before proceeding.
- Step 5.4: Commit
git add tools/snapshot-generator/git commit -m "test(sd-app): extended LevPro validation — 20+ scenarios pass"Gate: Do not proceed to Task 6 until all extended fixtures pass. The snapshot and all future phases depend on this math being correct.
Chunk 3: Snapshot Generator
Section titled “Chunk 3: Snapshot Generator”Task 6: Define canonical investor profiles and JSON schema
Section titled “Task 6: Define canonical investor profiles and JSON schema”Purpose: Define the 3–5 profiles the snapshot will display. These represent the target market sweet spot (financial advisors showing clients the leverage case).
- Step 6.1: Define profiles
Create tools/snapshot-generator/profiles/ca-en.json:
{ "_note": "Canonical investor profiles for the SD Snapshot. Reviewed and approved before generation.", "country": "ca", "holding_period": 10, "profiles": [ { "id": "conservative", "label": "Conservative Investor", "loan_amount": 50000, "interest_rate": 6.0, "expected_return": 5.0, "marginal_tax_rate": 40 }, { "id": "moderate", "label": "Moderate Investor", "loan_amount": 100000, "interest_rate": 5.5, "expected_return": 7.0, "marginal_tax_rate": 43 }, { "id": "growth", "label": "Growth Investor", "loan_amount": 200000, "interest_rate": 5.5, "expected_return": 9.0, "marginal_tax_rate": 46 } ]}Profiles are reviewed and adjusted by Talbot before the generator runs — these are marketing-critical inputs.
- Step 6.2: Commit profiles
git add tools/snapshot-generator/profiles/git commit -m "feat(sd-app): add canonical investor profiles for SD Snapshot (ca-en)"Task 7: Build snapshot generator CLI
Section titled “Task 7: Build snapshot generator CLI”Files:
-
Create:
tools/snapshot-generator/generate_snapshot.py -
Step 7.1: Implement generator
Create tools/snapshot-generator/generate_snapshot.py:
#!/usr/bin/env python3"""SD Snapshot Generator
Runs the interest-only math for all canonical profiles and writesa pre-calculated JSON snapshot file for the SvelteKit renderer.
Usage: python generate_snapshot.py --country ca --lang en --out ../../apps/sd-app/static/data/"""import argparseimport jsonfrom pathlib import Pathfrom datetime import datefrom src.interest_only import calculate_interest_only
def main(): parser = argparse.ArgumentParser(description="Generate SD Snapshot JSON") parser.add_argument("--country", default="ca") parser.add_argument("--lang", default="en") parser.add_argument("--out", default="../../apps/sd-app/static/data/") args = parser.parse_args()
profiles_path = Path(__file__).parent / "profiles" / f"{args.country}-{args.lang}.json" with open(profiles_path) as f: config = json.load(f)
snapshots = [] for profile in config["profiles"]: result = calculate_interest_only( loan_amount=profile["loan_amount"], interest_rate=profile["interest_rate"], expected_return=profile["expected_return"], marginal_tax_rate=profile["marginal_tax_rate"], holding_period=config["holding_period"], ) snapshots.append({ "profile": profile, "result": result, })
output = { "_generated": str(date.today()), "_note": "Pre-calculated by sd-snapshot-generator. Do not edit manually.", "country": args.country, "holding_period": config["holding_period"], "snapshots": snapshots, }
out_dir = Path(args.out) out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / f"snapshot-{args.country}-{args.lang}.json" with open(out_path, "w") as f: json.dump(output, f, indent=2)
print(f"✓ Generated {len(snapshots)} profile snapshots → {out_path}")
if __name__ == "__main__": main()- Step 7.2: Run generator and verify JSON output
cd /home/ta/projects/monorepo/tools/snapshot-generatorsource .venv/bin/activatepython generate_snapshot.py --country ca --lang enExpected: ✓ Generated 3 profile snapshots → ../../apps/sd-app/static/data/snapshot-ca-en.json
- Step 7.3: Inspect output JSON — verify values are sensible
Check that snapshot-ca-en.json contains calculated results (not zeroes, not errors) matching LevPro outputs for the profile inputs.
- Step 7.4: Commit generator and generated snapshot
git add tools/snapshot-generator/generate_snapshot.py \ apps/sd-app/static/data/snapshot-ca-en.jsongit commit -m "feat(sd-app): add snapshot generator CLI; commit initial ca-en snapshot"Chunk 4: SvelteKit Renderer
Section titled “Chunk 4: SvelteKit Renderer”Task 8: Load snapshot data in SvelteKit route
Section titled “Task 8: Load snapshot data in SvelteKit route”Files:
-
Modify:
apps/sd-app/src/routes/[country]/[lang]/int-only/+page.ts -
Step 8.1: Import snapshot JSON in the page load function
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => { const { country, lang } = params;
// Load pre-calculated snapshot — no math in the browser const res = await fetch(`/data/snapshot-${country}-${lang}.json`); const snapshot = await res.json();
return { snapshot, country, lang };};
export const prerender = true;- Step 8.2: TypeScript check
cd /home/ta/projects/monorepo/apps/sd-apppnpm checkTask 9: Build snapshot renderer page
Section titled “Task 9: Build snapshot renderer page”Files:
- Modify:
apps/sd-app/src/routes/[country]/[lang]/int-only/+page.svelte
No math in this file. All values come from
data.snapshot. The renderer displays, formats, and presents — nothing more.
- Step 9.1: Replace “coming soon” with snapshot renderer
Key UI elements (exact design to be determined with frontend-design skill):
-
Hero: “Better Than” return — the single most compelling number, displayed large
-
Profile cards: 3 scenarios (conservative / moderate / growth) side by side
- Each card shows: loan amount, net leveraged value, unleveraged comparison, net advantage (green/red)
-
Annual net cost: one-line summary (“Costs $X/year after tax deduction”)
-
CTA: “Get your numbers in the full SD App →” — prominent, above the fold on mobile
-
Responsive, mobile-first (390px baseline), WCAG AA
-
No inputs — this is a display-only snapshot
-
Step 9.2: Verify dev server
pnpm devCheck:
-
All 3 profiles display correctly
-
“Better Than” return shown prominently
-
CTA visible without scrolling on 390px viewport
-
No horizontal overflow on mobile
-
Numbers match what LevPro produces for these inputs
-
Step 9.3: Build check
pnpm buildExpected: succeeds, no TypeScript errors
- Step 9.4: Commit
git add apps/sd-app/src/routes/ apps/sd-app/static/data/git commit -m "feat(sd-app): Phase 1 SD Snapshot renderer — pre-calculated profiles, CTA to full app"Chunk 5: Update Utility
Section titled “Chunk 5: Update Utility”Task 10: Weekly snapshot update script
Section titled “Task 10: Weekly snapshot update script”Purpose: Regenerate the snapshot JSON on a schedule (profiles or rates change, or future phases add historical data).
- Step 10.1: Add update script to
tools/
Create tools/update-snapshots.sh:
#!/bin/bash# Update all SD Snapshots (run weekly or when profiles change)set -e
cd "$(dirname "$0")/snapshot-generator"source .venv/bin/activate
echo "Generating ca/en snapshot..."python generate_snapshot.py --country ca --lang en
echo "Done. Commit updated snapshot files if values changed."chmod +x tools/update-snapshots.sh- Step 10.2: Document in CLAUDE.md
Add to apps/sd-app/CLAUDE.md under Commands:
# Regenerate snapshot data (run from monorepo root):./tools/update-snapshots.sh# Then review and commit any changed snapshot JSON files- Step 10.3: Commit
git add tools/update-snapshots.shgit commit -m "feat(sd-app): add weekly snapshot update utility"Summary
Section titled “Summary”Phase 1 Deliverable: SD Snapshot at /ca/en/int-only that:
- Displays pre-calculated interest-only projections for 3 canonical Canadian investor profiles
- Shows “Better Than” return prominently (the aha moment for advisors and investors)
- Colors net advantage green/red (honest downside disclosure)
- Drives visitors to the full SD App via clear CTA
- Runs fully offline after first load (static JSON, no API calls)
- Math validated against LevPro golden fixtures to 0.01% tolerance in Python
What this unlocks:
- Phase 2: Full SD App — live Python API, user-adjustable inputs, full annual schedule
- Phase 3 (historical): “If I had used $MART DEBT” — historical market data analysis
- Embeddable Snapshot component for sdc.com blog posts and landing pages
Locked decisions:
| Decision | Choice | Rationale |
|---|---|---|
| Math engine | Python only | VB6 port in Python; TypeScript never does math |
| Browser role | Renderer only | Reads pre-calculated JSON; no computation |
| Phase 1 interactivity | None (demo-teaser) | Full interactive calculator is Phase 2 |
| Snapshot update | Weekly utility script | Manual trigger; automatic cron in Phase 2 |
Revision History
Section titled “Revision History”| Date | Change |
|---|---|
| 2026-03-20 | Initial version — replaces incorrect TypeScript math engine plan |
| 2026-03-20 | Architectural correction: TypeScript math engine plan abandoned; Python-generates-JSON architecture adopted; Phase 1 scope narrowed to SD Snapshot (demo-teaser) only |