@robotixai/calculator-engine 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,OAAO,EACR,MAAM,SAAS,CAAC;AAwIjB,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAqrB/C"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,OAAO,EACR,MAAM,SAAS,CAAC;AAyIjB,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAusB/C"}
package/dist/advanced.js CHANGED
@@ -19,6 +19,7 @@
19
19
  import { CadenceMultiplier } from './defaults';
20
20
  import { calculateTax } from './tax';
21
21
  import { calculateWithdrawal, } from './withdrawal';
22
+ import { getLogger } from './logger';
22
23
  // =============================================================================
23
24
  // Helper Functions
24
25
  // =============================================================================
@@ -140,6 +141,12 @@ const INCOME_CATEGORIES = new Set([
140
141
  export function runAdvancedProjection(scenario, overrideReturns) {
141
142
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
142
143
  const { current_age, retirement_age, end_age, inflation_pct, inflation_enabled, financial_items, liquidity_events, enable_taxes, effective_tax_rate_pct, tax_jurisdiction, tax_config, black_swan_enabled, black_swan_age, black_swan_loss_pct, desired_estate, } = scenario;
144
+ const log = getLogger();
145
+ log.info('Starting advanced projection', {
146
+ currentAge: current_age,
147
+ retirementAge: retirement_age,
148
+ itemCount: financial_items.length,
149
+ });
143
150
  const items = financial_items.filter((item) => item.enabled);
144
151
  // -------------------------------------------------------------------------
145
152
  // Initialize state
@@ -489,7 +496,9 @@ export function runAdvancedProjection(scenario, overrideReturns) {
489
496
  continue;
490
497
  // Cap at available cash
491
498
  if (contrib > Math.max(0, cashBalance)) {
492
- shortfallContributions += contrib - Math.max(0, cashBalance);
499
+ const shortfall = contrib - Math.max(0, cashBalance);
500
+ log.warn('Contribution shortfall', { age, shortfall, requested: contrib, available: Math.max(0, cashBalance) });
501
+ shortfallContributions += shortfall;
493
502
  contrib = Math.max(0, cashBalance);
494
503
  }
495
504
  cashBalance -= contrib;
@@ -552,6 +561,9 @@ export function runAdvancedProjection(scenario, overrideReturns) {
552
561
  // 8. INSOLVENCY CHECK
553
562
  // =====================================================================
554
563
  const insolvency = cashBalance < 0;
564
+ if (insolvency) {
565
+ log.warn('Insolvency detected', { age, cashBalance });
566
+ }
555
567
  if (insolvency && firstShortfallAge === null) {
556
568
  firstShortfallAge = age;
557
569
  }
@@ -722,6 +734,11 @@ export function runAdvancedProjection(scenario, overrideReturns) {
722
734
  ? Math.min(200, (totalActual / totalDesired) * 100)
723
735
  : 100;
724
736
  }
737
+ const insolvencyCount = timeline.filter((r) => r.insolvency).length;
738
+ log.info('Advanced projection complete', {
739
+ terminalReal,
740
+ insolvencyCount,
741
+ });
725
742
  const metrics = {
726
743
  terminal_nominal: terminalNominal,
727
744
  terminal_real: terminalReal,
@@ -1 +1 @@
1
- {"version":3,"file":"backtest.d.ts","sourceRoot":"","sources":["../src/backtest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AA6KjD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACrE,cAAc,CAoEhB"}
1
+ {"version":3,"file":"backtest.d.ts","sourceRoot":"","sources":["../src/backtest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AA8KjD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACrE,cAAc,CAyEhB"}
package/dist/backtest.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getLogger } from './logger';
1
2
  // ---------------------------------------------------------------------------
2
3
  // Historical Backtest — Shiller Data (real total stock returns, 1871-2024)
3
4
  // ---------------------------------------------------------------------------
@@ -177,6 +178,7 @@ const SHILLER_DATA = [
177
178
  * @returns Array of BacktestPeriod results and the overall success rate (0-100).
178
179
  */
179
180
  export function runHistoricalBacktest(scenario, projectionFn) {
181
+ const log = getLogger();
180
182
  const span = scenario.end_age - scenario.current_age;
181
183
  // Guard: span must be at least 1
182
184
  if (span < 1) {
@@ -199,6 +201,7 @@ export function runHistoricalBacktest(scenario, projectionFn) {
199
201
  for (const entry of SHILLER_DATA) {
200
202
  returnsByYear.set(entry.year, entry.realStockReturn);
201
203
  }
204
+ log.info('Starting backtest', { spanYears: span, windowCount });
202
205
  let survivedCount = 0;
203
206
  for (let i = 0; i < windowCount; i++) {
204
207
  const startYear = firstYear + i;
@@ -231,5 +234,6 @@ export function runHistoricalBacktest(scenario, projectionFn) {
231
234
  const successRate = periods.length > 0
232
235
  ? (survivedCount / periods.length) * 100
233
236
  : 0;
237
+ log.info('Backtest complete', { successRate, periodsAnalyzed: periods.length });
234
238
  return { periods, successRate };
235
239
  }
@@ -1,4 +1,10 @@
1
- import type { Cadence, Scenario } from './types';
1
+ import type { Cadence, CurrencyCode, Scenario } from './types';
2
2
  export declare const CadenceMultiplier: Record<Cadence, number>;
3
+ export interface CurrencyInfo {
4
+ code: CurrencyCode;
5
+ symbol: string;
6
+ decimals: number;
7
+ }
8
+ export declare const CURRENCY_MAP: Record<CurrencyCode, CurrencyInfo>;
3
9
  export declare const DEFAULT_SCENARIO: Scenario;
4
10
  //# sourceMappingURL=defaults.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMjD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAKrD,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,QAqF9B,CAAC"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM/D,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAKrD,CAAC;AAMF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY,CAiB3D,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,QAqF9B,CAAC"}
package/dist/defaults.js CHANGED
@@ -10,6 +10,24 @@ export const CadenceMultiplier = {
10
10
  'Bi-Weekly': 26,
11
11
  Weekly: 52,
12
12
  };
13
+ export const CURRENCY_MAP = {
14
+ USD: { code: 'USD', symbol: '$', decimals: 2 },
15
+ EUR: { code: 'EUR', symbol: '€', decimals: 2 },
16
+ GBP: { code: 'GBP', symbol: '£', decimals: 2 },
17
+ CHF: { code: 'CHF', symbol: 'CHF', decimals: 2 },
18
+ HKD: { code: 'HKD', symbol: 'HK$', decimals: 2 },
19
+ SGD: { code: 'SGD', symbol: 'S$', decimals: 2 },
20
+ AED: { code: 'AED', symbol: 'د.إ', decimals: 2 },
21
+ JPY: { code: 'JPY', symbol: '¥', decimals: 0 },
22
+ CAD: { code: 'CAD', symbol: 'C$', decimals: 2 },
23
+ AUD: { code: 'AUD', symbol: 'A$', decimals: 2 },
24
+ NZD: { code: 'NZD', symbol: 'NZ$', decimals: 2 },
25
+ ZAR: { code: 'ZAR', symbol: 'R', decimals: 2 },
26
+ INR: { code: 'INR', symbol: '₹', decimals: 2 },
27
+ BRL: { code: 'BRL', symbol: 'R$', decimals: 2 },
28
+ MXN: { code: 'MXN', symbol: 'Mex$', decimals: 2 },
29
+ KYD: { code: 'KYD', symbol: 'CI$', decimals: 2 },
30
+ };
13
31
  // ---------------------------------------------------------------------------
14
32
  // Default Scenario
15
33
  // ---------------------------------------------------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"heatmap.d.ts","sourceRoot":"","sources":["../src/heatmap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMjD,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EACnD,OAAO,CAAC,EAAE,cAAc,GACvB,WAAW,EAAE,CAoDf"}
1
+ {"version":3,"file":"heatmap.d.ts","sourceRoot":"","sources":["../src/heatmap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOjD,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EACnD,OAAO,CAAC,EAAE,cAAc,GACvB,WAAW,EAAE,CA0Df"}
package/dist/heatmap.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getLogger } from './logger';
1
2
  /**
2
3
  * Deep-clone a scenario.
3
4
  */
@@ -34,6 +35,8 @@ export function generateHeatmap(scenario, projectionFn, options) {
34
35
  const effectiveSteps = Math.max(steps, 2);
35
36
  const ageStep = (ageMax - ageMin) / (effectiveSteps - 1);
36
37
  const spendStep = (spendMax - spendMin) / (effectiveSteps - 1);
38
+ const log = getLogger();
39
+ log.info('Generating heatmap', { rows: effectiveSteps, cols: effectiveSteps });
37
40
  const cells = [];
38
41
  const desiredEstate = (_k = scenario.desired_estate) !== null && _k !== void 0 ? _k : 0;
39
42
  for (let ai = 0; ai < effectiveSteps; ai++) {
@@ -59,5 +62,7 @@ export function generateHeatmap(scenario, projectionFn, options) {
59
62
  });
60
63
  }
61
64
  }
65
+ const viableCells = cells.filter((c) => c.viable).length;
66
+ log.info('Heatmap complete', { viableCells, totalCells: cells.length });
62
67
  return cells;
63
68
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { Cadence, CurrencyCode, ContribStep, ProfitStep, RaiseStep, YieldStep, IncomeStep, LoanDraw, LumpRepayment, SpendingPhase, TaxConfig, IncomeSource, Asset, LiquidityEvent, FinancialItemCategory, FinancialItem, Scenario, TimelineRow, FanChartRow, Metrics, } from './types';
2
- export { CadenceMultiplier, DEFAULT_SCENARIO } from './defaults';
2
+ export { CadenceMultiplier, CURRENCY_MAP, DEFAULT_SCENARIO, type CurrencyInfo } from './defaults';
3
3
  export { runProjection } from './projection';
4
4
  export { runAdvancedProjection } from './advanced';
5
5
  export { runMonteCarloSimulation, type MCOptions, type MCResult, type ProjectionFn, } from './monte-carlo';
@@ -8,4 +8,5 @@ export { runHistoricalBacktest, type BacktestPeriod, type BacktestResult, } from
8
8
  export { findEarliestRetirementAge, type OptimizerResult, type OptimizerOutput, type OptimizerOptions, } from './optimizer';
9
9
  export { generateHeatmap, type HeatmapCell, type HeatmapOptions, } from './heatmap';
10
10
  export { blendPortfolio, calculateEstateValue, type BlendedPortfolio, } from './portfolio';
11
+ export { getLogger, setLogLevel, setLogger, type Logger, type LogLevel } from './logger';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,YAAY,EACV,OAAO,EACP,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,QAAQ,EACR,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,KAAK,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGjE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGnD,OAAO,EACL,uBAAuB,EACvB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,sBAAsB,EACtB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,YAAY,EACV,OAAO,EACP,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,QAAQ,EACR,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,KAAK,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlG,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGnD,OAAO,EACL,uBAAuB,EACvB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,sBAAsB,EACtB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // Engine — barrel export
3
3
  // ---------------------------------------------------------------------------
4
- export { CadenceMultiplier, DEFAULT_SCENARIO } from './defaults';
4
+ export { CadenceMultiplier, CURRENCY_MAP, DEFAULT_SCENARIO } from './defaults';
5
5
  // Deterministic projection (basic & advanced)
6
6
  export { runProjection } from './projection';
7
7
  export { runAdvancedProjection } from './advanced';
@@ -17,3 +17,5 @@ export { findEarliestRetirementAge, } from './optimizer';
17
17
  export { generateHeatmap, } from './heatmap';
18
18
  // Portfolio blending & estate value
19
19
  export { blendPortfolio, calculateEstateValue, } from './portfolio';
20
+ // Logger utilities
21
+ export { getLogger, setLogLevel, setLogger } from './logger';
@@ -0,0 +1,14 @@
1
+ export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
2
+ export interface Logger {
3
+ error(message: string, data?: Record<string, unknown>): void;
4
+ warn(message: string, data?: Record<string, unknown>): void;
5
+ info(message: string, data?: Record<string, unknown>): void;
6
+ debug(message: string, data?: Record<string, unknown>): void;
7
+ }
8
+ /** Get the active logger instance */
9
+ export declare function getLogger(): Logger;
10
+ /** Set the minimum log level (default: 'warn') */
11
+ export declare function setLogLevel(level: LogLevel): void;
12
+ /** Provide a custom logger implementation (e.g., for server-side or testing) */
13
+ export declare function setLogger(logger: Logger | null): void;
14
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9D;AAyCD,qCAAqC;AACrC,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAErD"}
package/dist/logger.js ADDED
@@ -0,0 +1,56 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Logger — structured logging with configurable levels
3
+ //
4
+ // Consumers can configure via setLogLevel() or provide a custom logger.
5
+ // Defaults to silent in production, 'warn' otherwise.
6
+ // ---------------------------------------------------------------------------
7
+ const LOG_PRIORITY = {
8
+ silent: 0,
9
+ error: 1,
10
+ warn: 2,
11
+ info: 3,
12
+ debug: 4,
13
+ };
14
+ let currentLevel = 'warn';
15
+ let customLogger = null;
16
+ function shouldLog(level) {
17
+ return LOG_PRIORITY[level] <= LOG_PRIORITY[currentLevel];
18
+ }
19
+ function formatMessage(level, message, data) {
20
+ const timestamp = new Date().toISOString();
21
+ const prefix = `[calculator-engine] ${timestamp} ${level.toUpperCase()}:`;
22
+ if (data && Object.keys(data).length > 0) {
23
+ return `${prefix} ${message} ${JSON.stringify(data)}`;
24
+ }
25
+ return `${prefix} ${message}`;
26
+ }
27
+ const defaultLogger = {
28
+ error(message, data) {
29
+ if (shouldLog('error'))
30
+ console.error(formatMessage('error', message, data));
31
+ },
32
+ warn(message, data) {
33
+ if (shouldLog('warn'))
34
+ console.warn(formatMessage('warn', message, data));
35
+ },
36
+ info(message, data) {
37
+ if (shouldLog('info'))
38
+ console.info(formatMessage('info', message, data));
39
+ },
40
+ debug(message, data) {
41
+ if (shouldLog('debug'))
42
+ console.debug(formatMessage('debug', message, data));
43
+ },
44
+ };
45
+ /** Get the active logger instance */
46
+ export function getLogger() {
47
+ return customLogger !== null && customLogger !== void 0 ? customLogger : defaultLogger;
48
+ }
49
+ /** Set the minimum log level (default: 'warn') */
50
+ export function setLogLevel(level) {
51
+ currentLevel = level;
52
+ }
53
+ /** Provide a custom logger implementation (e.g., for server-side or testing) */
54
+ export function setLogger(logger) {
55
+ customLogger = logger;
56
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"monte-carlo.d.ts","sourceRoot":"","sources":["../src/monte-carlo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3E,MAAM,MAAM,YAAY,GAAG,CACzB,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,KACvB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAMD,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,GAAE,MAAW;IAI7B,uEAAuE;IACvE,IAAI,IAAI,MAAM;IAQd,yEAAyE;IACzE,QAAQ,IAAI,MAAM;CAUnB;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,GAAG,QAAQ,GACpC,MAAM,CAuBR;AAMD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E;AAMD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,GAAE,SAAc,GACtB,QAAQ,CAoIV"}
1
+ {"version":3,"file":"monte-carlo.d.ts","sourceRoot":"","sources":["../src/monte-carlo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAO3E,MAAM,MAAM,YAAY,GAAG,CACzB,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,KACvB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAMD,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,GAAE,MAAW;IAI7B,uEAAuE;IACvE,IAAI,IAAI,MAAM;IAQd,yEAAyE;IACzE,QAAQ,IAAI,MAAM;CAUnB;AAMD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,GAAG,QAAQ,GACpC,MAAM,CAuBR;AAMD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E;AAMD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,GAAE,SAAc,GACtB,QAAQ,CA+IV"}
@@ -6,6 +6,7 @@
6
6
  * a caller-provided projection function, keeping MC fully decoupled from the
7
7
  * projection engine.
8
8
  */
9
+ import { getLogger } from './logger';
9
10
  // ---------------------------------------------------------------------------
10
11
  // SeededRNG — Deterministic PRNG (mulberry32)
11
12
  // ---------------------------------------------------------------------------
@@ -88,6 +89,8 @@ export function runMonteCarloSimulation(scenario, projectionFn, options = {}) {
88
89
  if (runs < 100 || runs > 10000) {
89
90
  throw new Error(`mc_runs must be 0 (disabled) or between 100 and 10000. Got: ${runs}`);
90
91
  }
92
+ const log = getLogger();
93
+ log.info('Starting Monte Carlo', { runs, seed, distribution: scenario.return_distribution });
91
94
  const rng = new SeededRNG(seed);
92
95
  const startTime = Date.now();
93
96
  const numYears = scenario.end_age - scenario.current_age;
@@ -108,6 +111,7 @@ export function runMonteCarloSimulation(scenario, projectionFn, options = {}) {
108
111
  const elapsed = Date.now() - startTime;
109
112
  if (elapsed > budgetMs) {
110
113
  truncated = true;
114
+ log.warn('Monte Carlo truncated due to budget', { runsCompleted: run, elapsed, budgetMs });
111
115
  break;
112
116
  }
113
117
  }
@@ -165,6 +169,12 @@ export function runMonteCarloSimulation(scenario, projectionFn, options = {}) {
165
169
  // Compute probability of no shortfall
166
170
  // -----------------------------------------------------------------------
167
171
  const probabilityNoShortfall = runsCompleted > 0 ? (noShortfallCount / runsCompleted) * 100 : 0;
172
+ log.info('Monte Carlo complete', {
173
+ runsCompleted,
174
+ successProbability: probabilityNoShortfall,
175
+ medianTerminal: p50Terminal,
176
+ truncated,
177
+ });
168
178
  return {
169
179
  probability_no_shortfall: probabilityNoShortfall,
170
180
  median_terminal: p50Terminal,
@@ -1 +1 @@
1
- {"version":3,"file":"optimizer.d.ts","sourceRoot":"","sources":["../src/optimizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMjD,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA4GD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EACnD,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,wBAAwB,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,CAkDjB"}
1
+ {"version":3,"file":"optimizer.d.ts","sourceRoot":"","sources":["../src/optimizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOjD,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA4GD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EACnD,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,wBAAwB,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,CAiEjB"}
package/dist/optimizer.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getLogger } from './logger';
1
2
  /**
2
3
  * Deep-clone a scenario.
3
4
  */
@@ -102,11 +103,16 @@ function findMinContribution(scenario, retirementAge, projectionFn, mcFn, mcThre
102
103
  */
103
104
  export function findEarliestRetirementAge(scenario, projectionFn, mcFn, options) {
104
105
  var _a;
106
+ const log = getLogger();
105
107
  const mcThreshold = (_a = options === null || options === void 0 ? void 0 : options.mcThreshold) !== null && _a !== void 0 ? _a : 90;
106
108
  const results = [];
107
109
  let earliestViableAge = null;
108
110
  const startAge = scenario.current_age + 1;
109
111
  const endAge = scenario.end_age - 1;
112
+ log.info('Starting optimizer', {
113
+ searchRange: [startAge, endAge],
114
+ mcEnabled: mcFn != null,
115
+ });
110
116
  // Budget guard: track wall-clock time (50s limit per CONTRACT-005)
111
117
  const startTime = Date.now();
112
118
  const BUDGET_MS = 50000;
@@ -116,6 +122,12 @@ export function findEarliestRetirementAge(scenario, projectionFn, mcFn, options)
116
122
  break;
117
123
  }
118
124
  const { result, viable } = isViable(scenario, age, projectionFn, mcFn, mcThreshold);
125
+ log.debug('Optimizer candidate', {
126
+ age,
127
+ terminalReal: result.terminalReal,
128
+ survived: result.survived,
129
+ viable,
130
+ });
119
131
  results.push(result);
120
132
  if (viable && earliestViableAge === null) {
121
133
  earliestViableAge = age;
@@ -126,6 +138,7 @@ export function findEarliestRetirementAge(scenario, projectionFn, mcFn, options)
126
138
  if (earliestViableAge !== null) {
127
139
  minContribution = findMinContribution(scenario, earliestViableAge, projectionFn, mcFn, mcThreshold);
128
140
  }
141
+ log.info('Optimizer complete', { earliestViableAge, minContribution });
129
142
  return {
130
143
  results,
131
144
  earliestViableAge,
@@ -1 +1 @@
1
- {"version":3,"file":"portfolio.d.ts","sourceRoot":"","sources":["../src/portfolio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAMpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAsDhE;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,aAAa,EAAE,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAcR"}
1
+ {"version":3,"file":"portfolio.d.ts","sourceRoot":"","sources":["../src/portfolio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAOpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,gBAAgB,CA6DhE;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,aAAa,EAAE,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAcR"}
package/dist/portfolio.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getLogger } from './logger';
1
2
  /**
2
3
  * Computes weighted-average return, fee, perf-fee, and liquid percentage
3
4
  * across a set of basic-mode assets.
@@ -44,6 +45,12 @@ export function blendPortfolio(assets) {
44
45
  .filter((a) => a.is_liquid)
45
46
  .reduce((sum, a) => sum + a.current_value, 0);
46
47
  const liquidPct = (liquidTotal / totalValue) * 100;
48
+ const log = getLogger();
49
+ log.debug('Portfolio blended', {
50
+ assetCount: enabled.length,
51
+ blendedReturn,
52
+ liquidPct,
53
+ });
47
54
  return {
48
55
  totalValue,
49
56
  blendedReturn,
@@ -1 +1 @@
1
- {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,SAAS,CAAC;AAoE5E,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CA2a/C"}
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,SAAS,CAAC;AAqE5E,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CA+b/C"}
@@ -11,6 +11,7 @@
11
11
  import { CadenceMultiplier } from './defaults';
12
12
  import { calculateTax, getRMDAmount, calculateRothConversion } from './tax';
13
13
  import { calculateWithdrawal, NEAR_ZERO_THRESHOLD, } from './withdrawal';
14
+ import { getLogger } from './logger';
14
15
  // =============================================================================
15
16
  // Helpers
16
17
  // =============================================================================
@@ -57,6 +58,13 @@ export function runProjection(scenario, overrideReturns) {
57
58
  const { current_age, retirement_age, end_age, current_balance, contrib_amount, contrib_cadence, contrib_increase_pct, nominal_return_pct, inflation_pct, inflation_enabled, fee_pct, perf_fee_pct, enable_taxes, effective_tax_rate_pct, tax_jurisdiction, tax_config, tax_deferred_pct, planning_mode, partner_current_age, partner_income_sources, income_sources, liquidity_events,
58
59
  // assets — not used in basic-mode projection (estate_pct is advanced-mode only)
59
60
  black_swan_enabled, black_swan_age, black_swan_loss_pct, spending_phases, withdrawal_strategy, } = scenario;
61
+ const log = getLogger();
62
+ log.info('Starting projection', {
63
+ currentAge: current_age,
64
+ retirementAge: retirement_age,
65
+ endAge: end_age,
66
+ detailMode: scenario.detail_mode,
67
+ });
60
68
  const timeline = [];
61
69
  // Running state
62
70
  let prevEndBalance = current_balance;
@@ -271,6 +279,7 @@ export function runProjection(scenario, overrideReturns) {
271
279
  if (black_swan_enabled && age === black_swan_age) {
272
280
  // Override growth with the loss
273
281
  growth = -(startBalance * (black_swan_loss_pct / 100));
282
+ log.warn('Black swan event triggered', { age, lossPct: black_swan_loss_pct });
274
283
  }
275
284
  else {
276
285
  // Mid-year cash flow assumption:
@@ -286,6 +295,7 @@ export function runProjection(scenario, overrideReturns) {
286
295
  // Track shortfall before flooring
287
296
  if (endBalance < 0 && age >= retirement_age && firstShortfallAge === null) {
288
297
  firstShortfallAge = age;
298
+ log.warn('First shortfall detected', { age, endBalance });
289
299
  }
290
300
  // Near-zero depletion threshold (edge case: asymptotic drain)
291
301
  if (endBalance >= 0 &&
@@ -293,6 +303,7 @@ export function runProjection(scenario, overrideReturns) {
293
303
  age >= retirement_age &&
294
304
  firstShortfallAge === null) {
295
305
  firstShortfallAge = age;
306
+ log.warn('Near-zero depletion shortfall', { age, endBalance });
296
307
  }
297
308
  // Floor at 0 in basic mode
298
309
  endBalance = Math.max(endBalance, 0);
@@ -330,6 +341,7 @@ export function runProjection(scenario, overrideReturns) {
330
341
  shortfall_contributions: 0,
331
342
  shortfall_withdrawals: shortfallWithdrawals,
332
343
  };
344
+ log.debug('Year end', { age, end_balance: endBalance });
333
345
  timeline.push(row);
334
346
  // Update running state for next year
335
347
  prevEndBalance = endBalance;
@@ -378,5 +390,11 @@ export function runProjection(scenario, overrideReturns) {
378
390
  total_taxes: totalTaxes,
379
391
  estate_value: estateValue,
380
392
  };
393
+ log.info('Projection complete', {
394
+ terminalReal,
395
+ terminalNominal,
396
+ shortfallAge: firstShortfallAge,
397
+ years: timeline.length,
398
+ });
381
399
  return { timeline, metrics };
382
400
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sensitivity.d.ts","sourceRoot":"","sources":["../src/sensitivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMjD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAkCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAClD,iBAAiB,EAAE,CAgErB"}
1
+ {"version":3,"file":"sensitivity.d.ts","sourceRoot":"","sources":["../src/sensitivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOjD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAkCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAClD,iBAAiB,EAAE,CA6ErB"}
@@ -1,3 +1,4 @@
1
+ import { getLogger } from './logger';
1
2
  const PARAMETERS = [
2
3
  { name: 'nominal_return_pct', label: 'Return +/-1%', delta: 1, deltaIsPct: false },
3
4
  { name: 'inflation_pct', label: 'Inflation +/-0.5%', delta: 0.5, deltaIsPct: false },
@@ -36,6 +37,9 @@ function clamp(value, min, max) {
36
37
  * - withdrawal_pct delta is skipped when withdrawal_strategy is Age-Banded
37
38
  */
38
39
  export function runSensitivityAnalysis(scenario, projectionFn) {
40
+ var _a;
41
+ const log = getLogger();
42
+ log.info('Starting sensitivity analysis', { parameterCount: PARAMETERS.length });
39
43
  const factors = [];
40
44
  for (const param of PARAMETERS) {
41
45
  // Skip withdrawal_pct when strategy is Age-Banded (not applicable)
@@ -76,6 +80,13 @@ export function runSensitivityAnalysis(scenario, projectionFn) {
76
80
  const highScenario = cloneScenario(scenario);
77
81
  highScenario[param.name] = highValue;
78
82
  const highResult = projectionFn(highScenario);
83
+ const spread = Math.abs(highResult.metrics.terminal_real - lowResult.metrics.terminal_real);
84
+ log.debug('Sensitivity parameter result', {
85
+ name: param.name,
86
+ lowTerminal: lowResult.metrics.terminal_real,
87
+ highTerminal: highResult.metrics.terminal_real,
88
+ spread,
89
+ });
79
90
  factors.push({
80
91
  name: param.name,
81
92
  label: param.label,
@@ -83,10 +94,11 @@ export function runSensitivityAnalysis(scenario, projectionFn) {
83
94
  highValue,
84
95
  lowTerminal: lowResult.metrics.terminal_real,
85
96
  highTerminal: highResult.metrics.terminal_real,
86
- spread: Math.abs(highResult.metrics.terminal_real - lowResult.metrics.terminal_real),
97
+ spread,
87
98
  });
88
99
  }
89
100
  // Sort by spread descending (largest impact first)
90
101
  factors.sort((a, b) => b.spread - a.spread);
102
+ log.info('Sensitivity complete', { topFactor: (_a = factors[0]) === null || _a === void 0 ? void 0 : _a.name });
91
103
  return factors;
92
104
  }
package/dist/tax.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tax.d.ts","sourceRoot":"","sources":["../src/tax.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AA4JzC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,GACnB,MAAM,CAiBR;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,SAAS,EAAE,MAAM,GAChB,MAAM,CAUR;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAChB,MAAM,CAMR"}
1
+ {"version":3,"file":"tax.d.ts","sourceRoot":"","sources":["../src/tax.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AA6JzC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,GACnB,MAAM,CA4BR;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,SAAS,EAAE,MAAM,GAChB,MAAM,CAUR;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAChB,MAAM,CAMR"}
package/dist/tax.js CHANGED
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Also includes RMD (Required Minimum Distribution) and Roth conversion logic.
8
8
  */
9
+ import { getLogger } from './logger';
9
10
  const US_STANDARD_DEDUCTION_SINGLE = 15000;
10
11
  const US_STANDARD_DEDUCTION_MFJ = 30000;
11
12
  const US_BRACKETS_2025_SINGLE = [
@@ -136,17 +137,25 @@ function calculateUKTax(grossIncome) {
136
137
  export function calculateTax(taxableIncome, config, jurisdiction) {
137
138
  if (taxableIncome <= 0)
138
139
  return 0;
140
+ let taxAmount;
139
141
  switch (jurisdiction) {
140
142
  case 'Cayman Islands':
141
- return 0;
143
+ taxAmount = 0;
144
+ break;
142
145
  case 'US':
143
- return calculateUSTax(taxableIncome, config.filing_status);
146
+ taxAmount = calculateUSTax(taxableIncome, config.filing_status);
147
+ break;
144
148
  case 'UK':
145
- return calculateUKTax(taxableIncome);
149
+ taxAmount = calculateUKTax(taxableIncome);
150
+ break;
146
151
  case 'Custom':
147
152
  default:
148
- return taxableIncome * (config.flat_rate_pct / 100);
153
+ taxAmount = taxableIncome * (config.flat_rate_pct / 100);
154
+ break;
149
155
  }
156
+ const log = getLogger();
157
+ log.debug('Tax calculated', { jurisdiction, taxableIncome, taxAmount });
158
+ return taxAmount;
150
159
  }
151
160
  // =============================================================================
152
161
  // RMD (Required Minimum Distributions)
@@ -1 +1 @@
1
- {"version":3,"file":"withdrawal.d.ts","sourceRoot":"","sources":["../src/withdrawal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAMvD,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAMvC,MAAM,WAAW,OAAO;IACtB,sEAAsE;IACtE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,8EAA8E;IAC9E,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAExB,4EAA4E;IAC5E,cAAc,EAAE,wBAAwB,CAAC;IAEzC,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,8BAA8B,EAAE,MAAM,CAAC;CACxC;AAMD,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,wBAAwB,GAC/B,MAAM,CA2BR;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,6BAA6B,GACpC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAkF1C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,yBAAyB,GAChC,MAAM,CA6BR;AAMD,0FAA0F;AAC1F,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,QAAQ,EAAE,QAAQ,CAAC;IAEnB,gCAAgC;IAChC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;KACzB,CAAC;CACH;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAC/B,gBAAgB,CA0FlB"}
1
+ {"version":3,"file":"withdrawal.d.ts","sourceRoot":"","sources":["../src/withdrawal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAOvD,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAMvC,MAAM,WAAW,OAAO;IACtB,sEAAsE;IACtE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD,MAAM,WAAW,wBAAwB;IACvC,gBAAgB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IAEjB,8EAA8E;IAC9E,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAExB,4EAA4E;IAC5E,cAAc,EAAE,wBAAwB,CAAC;IAEzC,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,8BAA8B,EAAE,MAAM,CAAC;CACxC;AAMD,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,wBAAwB,GAC/B,MAAM,CA2BR;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,6BAA6B,GACpC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAkF1C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,yBAAyB,GAChC,MAAM,CA2BR;AAMD,0FAA0F;AAC1F,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,QAAQ,EAAE,QAAQ,CAAC;IAEnB,gCAAgC;IAChC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;KACzB,CAAC;CACH;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAC/B,gBAAgB,CA6FlB"}
@@ -11,6 +11,7 @@
11
11
  * - GK oscillation: bounded by floor/ceiling — no special handling needed
12
12
  * - RMD override: caller responsibility (if RMD > withdrawal, caller uses RMD)
13
13
  */
14
+ import { getLogger } from './logger';
14
15
  // ---------------------------------------------------------------------------
15
16
  // Constants
16
17
  // ---------------------------------------------------------------------------
@@ -150,8 +151,8 @@ export function calculateAgeBandedWithdrawal(params) {
150
151
  const phase = spendingPhases.find((p) => age >= p.start_age && age <= p.end_age);
151
152
  if (!phase) {
152
153
  // Gap in spending phases — no withdrawal for this year
153
- console.warn(`[withdrawal] Age-Banded: no spending phase covers age ${age}. Withdrawal defaults to $0. ` +
154
- `Check for gaps in spending phase definitions.`);
154
+ const log = getLogger();
155
+ log.warn('Age-Banded gap: no spending phase covers age', { age });
155
156
  return 0;
156
157
  }
157
158
  let withdrawal;
@@ -230,6 +231,8 @@ export function calculateWithdrawal(scenario, state) {
230
231
  throw new Error(`Unknown withdrawal strategy: ${_exhaustive}`);
231
232
  }
232
233
  }
234
+ const log = getLogger();
235
+ log.debug('Withdrawal calculated', { strategy: withdrawal_strategy, amount: withdrawal });
233
236
  // Near-zero threshold: if balance after withdrawal would be below $100, treat as depleted
234
237
  const balanceAfterWithdrawal = availableBalance - withdrawal;
235
238
  const effectivelyDepleted = balanceAfterWithdrawal >= 0 && balanceAfterWithdrawal < NEAR_ZERO_THRESHOLD;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robotixai/calculator-engine",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Financial retirement projection engine with Monte Carlo simulation, multi-jurisdiction tax, and withdrawal strategies",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",