Skip to content

$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) or superpowers:executing-plans to 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)”

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.

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.

#LoanRateReturnTax RateYearsNotes
1$100,0005.5%7%40%10Baseline
2$100,0005.5%4%40%10Low return (near break-even)
3$50,0006%7%43%10Smaller loan
4$200,0005%9%35%10Higher income
5$100,0005.5%0%40%10Zero return (worst case)

The fixture fields must be derived from what LevPro actually outputs — do not assume field names before studying the VB6 source.


ActionFilePurpose
Createtools/snapshot-generator/Python package root
Createtools/snapshot-generator/pyproject.tomlPython project config
Createtools/snapshot-generator/src/interest_only.pyVB6 math port
Createtools/snapshot-generator/tests/fixtures/interest-only-ca.jsonLevPro golden fixtures
Createtools/snapshot-generator/tests/test_interest_only.pypytest golden-fixture tests
Createtools/snapshot-generator/generate_snapshot.pyCLI: generates snapshot JSON
Createapps/sd-app/static/data/snapshot-ca-en.jsonPre-calculated snapshot (generated, committed)
Modifyapps/sd-app/src/routes/[country]/[lang]/int-only/+page.svelteSnapshot renderer
Createapps/sd-app/src/routes/[country]/[lang]/int-only/+page.tsStatic data loader

Files:

  • Create: tools/snapshot-generator/pyproject.toml

  • Create: tools/snapshot-generator/src/__init__.py

  • Step 1.1: Create tools directory and Python package

Terminal window
mkdir -p /home/ta/projects/monorepo/tools/snapshot-generator/src
mkdir -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
Terminal window
cd /home/ta/projects/monorepo/tools/snapshot-generator
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
  • Step 1.4: Verify pytest runs (no tests yet = pass)
Terminal window
pytest

Expected: no tests ran

  • Step 1.5: Commit
Terminal window
git add tools/snapshot-generator/
git commit -m "feat(sd-app): add Python snapshot generator skeleton"

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
Terminal window
git add tools/snapshot-generator/tests/fixtures/
git commit -m "feat(sd-app): add LevPro golden fixture template (values TBD)"

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 json
from pathlib import Path
import pytest
from 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
Terminal window
cd /home/ta/projects/monorepo/tools/snapshot-generator
source .venv/bin/activate
pytest -v

Expected: ImportError: cannot import name 'calculate_interest_only'

  • Step 3.3: Commit failing tests
Terminal window
git add tools/snapshot-generator/tests/test_interest_only.py
git 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.

tools/snapshot-generator/src/interest_only.py
#
# 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
Terminal window
pytest -v 2>&1

First 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

Terminal window
git add tools/snapshot-generator/src/interest_only.py \
tools/snapshot-generator/tests/fixtures/interest-only-ca.json
git commit -m "feat(sd-app): port LevPro interest-only to Python — 5 core fixtures pass"

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:

CategoryScenarios
Return sweep1%, 2%, 3%, 5%, 6%, 8%, 10%, 12% (at baseline loan/rate/tax)
Interest rate sweep4%, 6%, 7%, 8%
Tax rate extremes25%, 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
Terminal window
pytest -v

Expected: all pass. Any failure = translation bug; fix before proceeding.

  • Step 5.4: Commit
Terminal window
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.


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
Terminal window
git add tools/snapshot-generator/profiles/
git commit -m "feat(sd-app): add canonical investor profiles for SD Snapshot (ca-en)"

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 writes
a 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 argparse
import json
from pathlib import Path
from datetime import date
from 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
Terminal window
cd /home/ta/projects/monorepo/tools/snapshot-generator
source .venv/bin/activate
python generate_snapshot.py --country ca --lang en

Expected: ✓ 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
Terminal window
git add tools/snapshot-generator/generate_snapshot.py \
apps/sd-app/static/data/snapshot-ca-en.json
git commit -m "feat(sd-app): add snapshot generator CLI; commit initial ca-en snapshot"

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

+page.ts
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
Terminal window
cd /home/ta/projects/monorepo/apps/sd-app
pnpm check

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

5173/ca/en/int-only
pnpm dev

Check:

  • 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

Terminal window
pnpm build

Expected: succeeds, no TypeScript errors

  • Step 9.4: Commit
Terminal window
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"

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."
Terminal window
chmod +x tools/update-snapshots.sh
  • Step 10.2: Document in CLAUDE.md

Add to apps/sd-app/CLAUDE.md under Commands:

Terminal window
# Regenerate snapshot data (run from monorepo root):
./tools/update-snapshots.sh
# Then review and commit any changed snapshot JSON files
  • Step 10.3: Commit
Terminal window
git add tools/update-snapshots.sh
git commit -m "feat(sd-app): add weekly snapshot update utility"

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:

DecisionChoiceRationale
Math enginePython onlyVB6 port in Python; TypeScript never does math
Browser roleRenderer onlyReads pre-calculated JSON; no computation
Phase 1 interactivityNone (demo-teaser)Full interactive calculator is Phase 2
Snapshot updateWeekly utility scriptManual trigger; automatic cron in Phase 2

DateChange
2026-03-20Initial version — replaces incorrect TypeScript math engine plan
2026-03-20Architectural correction: TypeScript math engine plan abandoned; Python-generates-JSON architecture adopted; Phase 1 scope narrowed to SD Snapshot (demo-teaser) only