Withdrawal Source Allocation Probe

Verifies that voluntary withdrawal streams honor their configured funding source. Read-only diagnostic — does not run the full projection engine.

Overall: ALL SCENARIOS PASS — source allocation is honored end-to-end.

Direct allocator (allocateBaselineWithdrawal)

ScenariofromIrafromRothExpected (IRA / Roth)Result
Source = IRA Only$500,000$0$500,000 / $0PASS
Source = Roth Only$0$500,000$0 / $500,000PASS
Source = Smart (engine decides — IRA-first default)$500,000$0$500,000 / $0PASS
Bug-repro (now fixed): source=smart + useSmartLogic=false → must be IRA-first$500,000$0$500,000 / $0PASS
Bug report row: IRA=$676K, voluntary=$500K, source=IRA Only$500,000$0$500,000 / $0PASS

Through router (resolveStreamWithdrawal, splitMode='none')

ScenariofromIrafromRothExpected (IRA / Roth)Result
Source = IRA Only$500,000$0$500,000 / $0PASS
Source = Roth Only$0$500,000$0 / $500,000PASS
Source = Smart (engine decides — IRA-first default)$500,000$0$500,000 / $0PASS
Bug-repro (now fixed): source=smart + useSmartLogic=false → must be IRA-first$500,000$0$500,000 / $0PASS
Bug report row: IRA=$676K, voluntary=$500K, source=IRA Only$500,000$0$500,000 / $0PASS

2026-06-04 fix (Option C) — applied

Both the normalizer and the allocator have been hardened. Legacy streams without an explicit source now default to 'ira'(never 'smart' or 'roth'). The allocator's'smart' branch is now unconditionally IRA-first;useSmartLogic no longer participates in source selection. Roth-first behavior only occurs when source === 'roth'.

Live engine diagnostics (still available)

To capture the 6-stage stream/source/router/allocator trace described in the bug report, open your browser console and run:

window.__WITHDRAWAL_SOURCE_DEBUG__ = true;

Then re-run the failing projection (age 78, $500K voluntary). The console will print, in order:

  • [WSD][1-raw] — raw stream config from the scenario
  • [WSD][2-normalized] — config after normalization
  • [WSD][3-router-in] — inputs to resolveStreamWithdrawal()
  • [WSD][4-allocator-in] — inputs to allocateBaselineWithdrawal()
  • [WSD][5-allocator-out] — fromIra / fromRoth / shortfall
  • [WSD][6-final-row] — withdrawalIra / withdrawalRoth on the committed row

Filter widening (if needed):

window.__WITHDRAWAL_SOURCE_DEBUG_ALL_YEARS__ = true;  // all ages
window.__WITHDRAWAL_SOURCE_DEBUG_VERBOSE__   = true;  // every stream/year

The diagnostics are observability-only — they do not alter allocator behavior. They remain available after the 2026-06-04 fix so any future regression in the source-routing pipeline can be traced end-to-end.

About this probe

Scenarios 1–3 exercise the canonical paths an advisor would expect from the UI: explicit IRA, explicit Roth, and Smart (default). Scenario 4 reproduces the field-reported drift case where saved data carried source=smart and useSmartLogic=falsefrom a legacy save path. The 2026-06-04 Option C fix hardens both the normalizer (legacy/missing source → IRA, never Roth) and the allocator (smart branch is unconditionally IRA-first; useSmartLogic no longer participates in source selection). Scenario 5 mirrors the exact age-78 row from the bug report. All five scenarios must pass.

If any row shows FAIL, do not use the engine output in production — re-run after redeploying.