Skip to content

Cursor Task: Normalize Snapshots to $500/Month

Section titled “Cursor Task: Normalize Snapshots to $500/Month”

The SD App snapshot JSONs currently use test fixture loan amounts (interest-only: $100,000; term loan: $37,543). These were chosen to validate math correctness, not for user-facing presentation.

The user-facing standard has been set: $500/month of gross (before-tax) investable cash flow. All snapshot outputs should reflect this normalization.

  • Interest-only: loan_amount such that annual gross interest = $6,000 → $66,667 at 9%
  • Term loan: loan_amount such that annual payment = $6,000 → $38,506 at 9% for 10 years; loan_payment = $6,000

This is a pure Python task — update two snapshot generator files, regenerate two JSON files. No API changes, no SvelteKit changes.


  1. packages/sd-math/tools/generate_snapshot.py — interest-only generator
  2. packages/sd-math/tools/generate_snapshot_term_loan.py — term loan generator
  3. apps/sd-app/static/snapshots/int-only-ca.json — current output (for reference)
  4. apps/sd-app/static/snapshots/term-loan-ca.json — current output (for reference)
  5. packages/sd-math/src/sd_math/models/inputs.py — LoanProfile, TermLoanProfile definitions
  6. packages/sd-math/src/sd_math/strategies/interest_only.py — to understand at_annual_investment derivation
  7. packages/sd-math/src/sd_math/strategies/term_loan.py — to understand loan_payment handling

  • Do NOT change sd-math library code — only the generator scripts and the output JSON files
  • Do NOT change the API (packages/sd-api/)
  • Do NOT change SvelteKit files (apps/sd-app/)
  • Do NOT change test fixtures (packages/sd-math/tests/fixtures/) — those are for math validation, not UX normalization
  • Field names in the output JSONs must remain identical to current — only the values change
  • Run the generators using the project’s Python environment (same as sd-math tests): cd packages/sd-math && python tools/generate_snapshot.py etc.

1. packages/sd-math/tools/generate_snapshot.py

Section titled “1. packages/sd-math/tools/generate_snapshot.py”

Change the LOAN definition:

# BEFORE
LOAN = LoanProfile(
loan_amount=100_000.0,
...
)
# AFTER
LOAN = LoanProfile(
loan_amount=66_667.0, # $500/month gross: $66,667 × 9% = $6,000/year
...
)

Everything else in the file stays the same. The generator will produce new output values automatically.

2. packages/sd-math/tools/generate_snapshot_term_loan.py

Section titled “2. packages/sd-math/tools/generate_snapshot_term_loan.py”

Change the TERM_LOAN definition:

# BEFORE
TERM_LOAN = TermLoanProfile(
loan_amount=37_543.0,
loan_payment=5_850.0,
loan_interest_rate=0.09,
n_loan_periods=10,
)
# AFTER
TERM_LOAN = TermLoanProfile(
loan_amount=38_506.0, # $500/month gross: annual payment = $6,000
loan_payment=6_000.0, # $500/month × 12 = $6,000/year
loan_interest_rate=0.09,
n_loan_periods=10,
)

Everything else in the file stays the same.


  1. Both generator files updated with new loan amounts
  2. Both JSON snapshots regenerated:
    • apps/sd-app/static/snapshots/int-only-ca.json
    • apps/sd-app/static/snapshots/term-loan-ca.json

After regenerating, confirm these values in the new JSON files:

int-only-ca.json — middle-income profile:

  • inputs.loan_amount = 66667
  • analysis_summary.at_annual_investment ≈ 3900 (within ±50) (66,667 × 9% = $6,000 gross, minus 35% tax benefit = $3,900 net)
  • analysis_summary.bt_annual_interest = 6000 (exactly)

term-loan-ca.json — middle-income profile:

  • inputs.loan_amount = 38506
  • inputs.loan_payment = 6000
  • analysis_details.annual_rows[0].interest_part_of_pmt ≈ 3465 (Year 1: 38,506 × 9%)
  • analysis_details.annual_rows[0].loan_reduction ≈ 2535 (Year 1: 6,000 - 3,465)

Paste the output of both generator runs here:

# Run 1 — Interest-only
cd packages/sd-math && python tools/generate_snapshot.py
# [paste output]
# Run 2 — Term loan
cd packages/sd-math && python tools/generate_snapshot_term_loan.py
# [paste output]

Then paste the analysis_summary block from each new JSON file (middle-income profile only) so the values can be spot-checked.


# Run 1 — Interest-only
cd packages/sd-math && python tools/generate_snapshot.py
✓ Snapshot written to /home/ta/projects/monorepo/apps/sd-app/static/snapshots/int-only-ca.json
Profiles: ['middle-income', 'high-income']
middle-income: 5 summary scenarios, 10 annual rows
high-income: 5 summary scenarios, 10 annual rows
# Run 2 — Term loan
cd packages/sd-math && python tools/generate_snapshot_term_loan.py
✓ Snapshot written to /home/ta/projects/monorepo/apps/sd-app/static/snapshots/term-loan-ca.json
Profiles: ['middle-income', 'high-income']
middle-income: 5 summary scenarios, 10 annual rows
high-income: 5 summary scenarios, 10 annual rows

Verification — int-only-ca.json (middle-income)

Section titled “Verification — int-only-ca.json (middle-income)”
CheckExpectedActualPass?
inputs.loan_amount6666766667
analysis_summary.at_annual_investment≈ 3900 (±50)3900
analysis_summary.bt_annual_interest60006000

analysis_summary block:

{
"at_annual_investment": 3900,
"bt_annual_interest": 6000,
"scenarios": [
{ "expected_return": 0.0, "no_lev_bt_balance": 39000, "net_lev_bt_bal": 0, "net_increase": -39000, "pct_increase": -1.0, "is_better_than": false },
{ "expected_return": 0.03, "no_lev_bt_balance": 44391, "net_lev_bt_bal": 19413, "net_increase": -24979, "pct_increase": -0.5627, "is_better_than": false },
{ "expected_return": 0.0666, "no_lev_bt_balance": 52184, "net_lev_bt_bal": 52184, "net_increase": 0, "pct_increase": 0.0, "is_better_than": true },
{ "expected_return": 0.07, "no_lev_bt_balance": 52972, "net_lev_bt_bal": 55759, "net_increase": 2787, "pct_increase": 0.0526, "is_better_than": false },
{ "expected_return": 0.1, "no_lev_bt_balance": 60636, "net_lev_bt_bal": 92733, "net_increase": 32097, "pct_increase": 0.5293, "is_better_than": false }
]
}

Verification — term-loan-ca.json (middle-income)

Section titled “Verification — term-loan-ca.json (middle-income)”
CheckExpectedActualPass?
inputs.loan_amount3850638506
inputs.loan_payment60006000
annual_rows[0].interest_part_of_pmt≈ 3465 (38,506 × 9%)3466
annual_rows[0].loan_reduction≈ 2535 (6,000 − 3,465)2534

analysis_summary block:

{
"scenarios": [
{ "expected_return": 0.0, "no_lev_bt_balance": 52477, "net_lev_bt_bal": 38506, "net_increase": -13971, "pct_increase": -0.2662, "is_better_than": false },
{ "expected_return": 0.03, "no_lev_bt_balance": 59430, "net_lev_bt_bal": 50975, "net_increase": -8456, "pct_increase": -0.1423, "is_better_than": false },
{ "expected_return": 0.0617, "no_lev_bt_balance": 67989, "net_lev_bt_bal": 67989, "net_increase": 0, "pct_increase": 0.0, "is_better_than": true },
{ "expected_return": 0.07, "no_lev_bt_balance": 70464, "net_lev_bt_bal": 73224, "net_increase": 2760, "pct_increase": 0.0392, "is_better_than": false },
{ "expected_return": 0.1, "no_lev_bt_balance": 80291, "net_lev_bt_bal": 95278, "net_increase": 14988, "pct_increase": 0.1867, "is_better_than": false }
]
}
  • packages/sd-math/tools/generate_snapshot.pyloan_amount 100,000 → 66,667
  • packages/sd-math/tools/generate_snapshot_term_loan.pyloan_amount 37,543 → 38,506; loan_payment 5,850 → 6,000
  • apps/sd-app/static/snapshots/int-only-ca.json — regenerated
  • apps/sd-app/static/snapshots/term-loan-ca.json — regenerated

No library code, API code, SvelteKit files, or test fixtures were modified.