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.
| Scenario | fromIra | fromRoth | Expected (IRA / Roth) | Result |
|---|---|---|---|---|
| Source = IRA Only | $500,000 | $0 | $500,000 / $0 | PASS |
| Source = Roth Only | $0 | $500,000 | $0 / $500,000 | PASS |
| Source = Smart (engine decides — IRA-first default) | $500,000 | $0 | $500,000 / $0 | PASS |
| Bug-repro (now fixed): source=smart + useSmartLogic=false → must be IRA-first | $500,000 | $0 | $500,000 / $0 | PASS |
| Bug report row: IRA=$676K, voluntary=$500K, source=IRA Only | $500,000 | $0 | $500,000 / $0 | PASS |
| Scenario | fromIra | fromRoth | Expected (IRA / Roth) | Result |
|---|---|---|---|---|
| Source = IRA Only | $500,000 | $0 | $500,000 / $0 | PASS |
| Source = Roth Only | $0 | $500,000 | $0 / $500,000 | PASS |
| Source = Smart (engine decides — IRA-first default) | $500,000 | $0 | $500,000 / $0 | PASS |
| Bug-repro (now fixed): source=smart + useSmartLogic=false → must be IRA-first | $500,000 | $0 | $500,000 / $0 | PASS |
| Bug report row: IRA=$676K, voluntary=$500K, source=IRA Only | $500,000 | $0 | $500,000 / $0 | PASS |
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'.
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 rowFilter 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.
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.