RetirePath is a vanilla JavaScript SPA for US-Australia cross-border retirement planning. No build step, no package manager, no framework — just static files served directly to the browser.
- Live deployment: GitHub Pages
- Local dev:
python3 -m http.server 8000ornpx serve .— ES6 modules require an HTTP server; openingindex.htmldirectly viafile://will not work. - External deps: Chart.js 4.4.1 (CDN), chartjs-plugin-annotation (CDN), Google Fonts (CDN)
The codebase uses native ES6 modules (import/export) with no build tooling. app.js is the single entry point loaded by index.html as <script type="module">. CDN libraries (Chart.js, annotation plugin) are loaded as plain <script> tags before the module so they are available on window.
assets/js/
app.js ← Entry point. Wires all modules, runs projection loop.
appState.js ← Singleton state store (pub/sub, localStorage)
projectionEngine.js ← Deterministic year-by-year retirement simulation
chartManager.js ← Chart.js wrapper for all charts
uiManager.js ← Input binding, KPI display, table rendering, modals
debugLogger.js ← Development logging utility
assets/
BaseAsset.js ← Root of hierarchy; id, name, country, currency, getDisplayLabel()
RealEstate.js ← extends BaseAsset; applyAppreciation(), applyMortgageReduction(), getEquity(), isSoldThisYear(), hasActiveMortgage()
Land.js ← extends BaseAsset; undeveloped land stub (not yet in state)
BaseAccount.js ← extends BaseAsset; adds balance, growthRate, ownerId, type; applyGrowth()
RetirementAccount.js ← extends BaseAccount; base class with getContribution(), applyContributions(), canWithdraw(),
getUSAccountTreatment(), getAUAccountTreatment() stubs
accounts/
Account401k.js ← extends RetirementAccount; US 401(k); enforces country=US/currency=USD
RothAccount.js ← extends RetirementAccount; Roth IRA; tracks contributions corpus; enforces US/USD
TraditionalIRAAccount.js ← extends RetirementAccount; Traditional IRA; enforces US/USD
SuperAccount.js ← extends RetirementAccount; AU Super; enforces country=AUS/currency=AUD;
holds CONTRIBUTIONS_TAX_RATE, EARNINGS_TAX_RATE, WITHDRAWAL_TAX_UNDER_60 constants
BrokerageAccount.js ← extends BaseAccount; applyContributions(bal,basis), calculateWithdrawal()
AssetFactory.js ← wrapRetirementAccount() dispatches by type to correct subclass;
wrapBrokerageAccount(), wrapAsset() — creates class instances from POJOs
tax/
BaseTaxModule.js ← Abstract base class; shared _inflateBrackets/_applyBrackets utilities
USTaxModuleBase.js ← Shared US logic: calcIncomeTax, calcCapitalGainsTax, accountTreatment, calcSocialSecurity
USTaxModule2024.js ← US 2024 bracket data (IRS Rev. Proc. 2023-34)
USTaxModule2025.js ← US 2025 bracket data (IRS Rev. Proc. 2024-40)
AustraliaTaxModuleBase.js ← Shared AU logic: calcIncomeTax, calcCapitalGainsTax, accountTreatment, calcBrokerageAUGain
AustraliaTaxModule2024.js ← AU FY2024-25 brackets (Stage 3 tax cuts)
AustraliaTaxModule2025.js ← AU FY2025-26 brackets (30% rate extended to $135k)
TaxEngine.js ← Registry keyed by 'COUNTRY_YEAR'; get(countryCode, year) with best-year fallback; singleton taxEngine
ui/
formatters.js ← fmtShort(), makeFmt() — pure number formatting
modalHelpers.js ← setField(), getField(), closeModal() — pure DOM
toastManager.js ← showToast() — stateless notification helper
| File | Role |
|---|---|
appState.js |
Singleton appState. Pub/sub state store with localStorage persistence. All state mutations go through its methods. |
projectionEngine.js |
Deterministic year-by-year simulation. Receives appState and taxEngine via constructor. |
tax/TaxEngine.js |
Singleton taxEngine. Registry keyed by 'COUNTRY_YEAR'. get(countryCode, year) resolves the highest registered module whose year is ≤ the requested year, enabling forward projection via bracket inflation. |
uiManager.js |
Receives appState via constructor. Binds inputs, renders projection table, manages modals. Imports helpers from ui/. |
chartManager.js |
Imports appState singleton. Owns all Chart.js instances. |
app.js |
Module entry point. Imports all modules, constructs chartManager, instantiates UIManager, subscribes to state changes. |
User Input → UIManager → appState.set() → observer notifies App
↓ (300ms debounce)
ProjectionEngine.run()
↓
ChartManager.updateAll() + UIManager.updateKPIs()
+ UIManager.renderProjectionTable()
State changes are debounced 300ms before triggering a projection run.
JavaScript (ES6 modules):
- Native
import/export— no CommonJS, no bundler - ES6 classes; singleton pattern for
appState(exported fromappState.js) andtaxEngine(exported fromtax/TaxEngine.js) camelCasefor methods and propertiesUPPER_SNAKE_CASEfor constants (tax brackets, rates)_underscoreprefix for private/internal methods and properties- JSDoc comments on public methods
- Visual section separators:
// ──────────────────
HTML:
- BEM-inspired classes:
app-header,sidebar-section,form-group,kpi-card data-panel,data-currency,data-close-modalattributes for behavior hooks- Semantic elements:
<header>,<nav>,<aside>,<main>,<section>
CSS:
- CSS Custom Properties for all colors/spacing:
--bg-primary,--accent-blue,--text-secondary - Dark theme (financial/professional aesthetic)
- Flexbox + CSS Grid for layout
FILE COPYRIGHT:
- Use the Apache 2 license text below and add language specific copyright headers to all source code files (HTML, css, javascript)
Copyright (c) 2026 Terry Packer.
This file is part of Terry Packer's Work.
See www.terrypacker.com for further info.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Adding a new tax year:
Create a new file in assets/js/tax/ named {Country}TaxModule{YEAR}.js that extends the country's base module (e.g. USTaxModuleBase). In the constructor call super(year) and set the year-specific data properties (_brackets_mfj, _ltcg_mfj, _stdDeduction_mfj, _ficaWageBase for US; _brackets, _medicareLevy, _medicareLevySurcharge for AU). If the new year requires different calculation logic, override the relevant method. Import and register the new module in tax/TaxEngine.js.
Adding a new tax jurisdiction:
Create a {Country}TaxModuleBase.js extending BaseTaxModule with all shared country logic. Add year-specific subclasses following the same pattern as US/AU. Import all year modules in tax/TaxEngine.js and register them in the constructor.
Adding a new account type:
- Create a subclass of
RetirementAccountinassets/js/assets/accounts/. Enforcetype,country, andcurrencyin the constructor. OverridegetUSAccountTreatment()andgetAUAccountTreatment()with the type-specific rules. - Register it in
AssetFactory.wrapRetirementAccount()with a newcasein the switch. - Add the type to
AppStatedefault accounts and CRUD helpers if needed. - Add the
<option>to the account type<select>inindex.html. The country/currency selectors are auto-locked byUIManager._updateAccountTypeConstraints()— add an entry there if the new type has locked country/currency. ProjectionEnginecallsacc.getUSAccountTreatment()/acc.getAUAccountTreatment()automatically — no engine changes needed for typical account types.
State mutations:
Always go through AppState methods — never mutate _state directly from outside the class.
No async: The entire calculation pipeline is synchronous. No Promises, no async/await in the core engine.
- No build tooling — do not introduce webpack, Vite, Rollup, or similar. Keep it deployable as static files.
- No frameworks — do not introduce React, Vue, etc.
- No npm/package.json — all dependencies remain CDN-loaded.
- HTTP server required for local dev — ES6 modules are blocked by browsers on
file://origins. - Tests use Node.js only — no npm, no build tools. Node.js is needed only for the test runner.
- Deterministic only — the projection engine is intentionally deterministic (no Monte Carlo). Do not add stochastic modeling without discussion.
- Tax modules are country+year-specific — each module represents one jurisdiction and one base year. The engine selects the best available module (highest year ≤ projection year) and inflates brackets forward from there. US modules carry 2024 and 2025 MFJ rates; AU modules carry FY2024-25 and FY2025-26 rates.
Tests live in tests/ and use the Node.js built-in node:test runner — no npm, no external dependencies. Test files use the .mjs extension to enable ES6 modules without a package.json.
make setupThis installs or repairs Node.js via nvm (if present) or Homebrew. The required Node version is pinned in .nvmrc (currently 22).
make testOr directly:
node --test tests/projectionEngine.test.mjs| File | Purpose |
|---|---|
tests/projectionEngine.test.mjs |
Year-by-year projection correctness |
tests/helpers/mockState.mjs |
Wraps a JSON scenario into the interface ProjectionEngine expects |
tests/scenarios/*.json |
Deterministic scenario configs (load one, verify outputs) |
To add a new scenario: create a .json file in tests/scenarios/ mirroring the appState structure, then import and run it in a test file.
Per the embedded methodology docs:
- No RMDs (Required Minimum Distributions)
- No state income taxes
- Simplified Social Security taxation (85% inclusion rule)
- No FBAR/FATCA modeling
- No estate/inheritance planning
- Australian Super as US-citizen trust complexity is simplified
Do not attempt to fix these silently — they are acknowledged limitations, not bugs.
Embedded help pages live in assets/help/:
inputs.htm— field-by-field input guidemethodology.htm— projection algorithm explanationtax-guide.htm— US-AU tax treatment referenceprojections.htm— projections tab reference
Update these when changing calculation behavior or adding new fields.