A dual-tax calculation path treated local-currency amounts as US dollars when evaluating US citizenship tax. Large numerical amounts in currencies such as yen and dong therefore crossed dollar-denominated tax thresholds incorrectly. The withdrawal PR reported incorrect results for 12 of 30 countries in a published comparison.
The response was to withdraw the article, chart and downloadable data, correct the calculation, and add tests using realistic exchange rates across both scalar and vectorized calculation paths.
Repair timeline
| Event | PR | Merge time (UTC) |
|---|---|---|
| Withdraw the article and data | #1089 | August 13, 22:55:18 |
| Correct conversion at each tax-policy currency boundary | #1090 | August 14, 02:38:36 |
| Freeze and propagate exchange-rate context through execution and replay | #1091 | August 14, 02:59:56 |
The core fix merged 3 hours 43 minutes after the withdrawal; the follow-up merged 21 minutes 20 seconds later. These are merge times. Thiago reports that republication followed shortly after the fix; the exported records do not independently timestamp republication.
What changed
The core fix converts monetary inputs into each policy’s currency before evaluating that policy and converts results back to a common currency before comparing foreign-tax credits. It adds explicit coverage of monetary fields, same-currency behavior, and scalar/vector results at realistic exchange rates.
The follow-up carries a frozen exchange-rate context from enqueue through execution, result artifacts and replay. It also covers cross-currency relocation and missing-rate behavior.
The withdrawal record also documents an earlier unsuccessful repair, PR #1087: its tests exercised an already-correct scalar path, leaving the vectorized path used by simulations unfixed. The subsequent regression checks explicitly exercise both paths.
Original PR descriptions
PR #1089: Unpublish the FIRE-multiple article pending a currency fix
Takes
/blog/fire-number-multiple-by-countryoffline, with its CSV, JSON, and chart. Merge before the engine fix — the numbers are wrong for 12 of 30 countries while it is live.The defect
The dual-tax overlay computes the US citizenship-country tax on the local-currency amount without converting to USD. Holding real income fixed at $20,000:
Country Currency FX per USD US citizenship tax US USD 1.0 0.0% Portugal EUR 0.9 0.0% UK GBP 0.8 0.0% Thailand THB 35.8 17.4% Japan JPY 155 22.3% Chile CLP 950 23.6% Vietnam VND 25,000 23.8% EUR and GBP are near parity, so those households stay under the US standard deduction and correctly pay nothing — right by luck, not by logic. High-FX currencies land in the top US brackets, and 23.8% is exactly the 20% LTCG rate plus 3.8% NIIT.
Exposed: Chile, Costa Rica, Hungary, Japan, Uruguay, Thailand, Czechia, South Africa, Mexico, Brazil, and marginally Malaysia and the UAE. Correcting it moves them down the ranking, so the published ordering was wrong — “Thailand needs a larger multiple than the US” does not survive.
The Netherlands mechanism (EUR) and the US $0 are unaffected and still reconcile by hand.
How it is taken down
The manifest is renamed, not deleted, so republishing is a rename once the numbers are correct.
glob("*.yaml")no longer matches it, so the route, sitemap, and RSS drop it and the URL returns the existingnoindex404. The article source, generator, and grid data stay in the repo. The dataset and chart are withdrawn rather than left downloadable under CC BY.Two process failures, recorded in the log
My earlier Vietnam diagnosis was wrong. Decomposing the vectorized result shows
residency_taxis 25,000,000 VND — 5%, correct, for both citizenships. Vietnam never charged 20%; that was the US side landing in a component labelled for investment income. A component label is not a component attribution.Spec #1087 shipped a fix that fixes nothing.
spec status: passed, no functional code changed — one comment string, a version bump, and tests — and it bumpedassumptions_versionto2026-08-13.v1declaring citizenship-independence. Its tests pass because they exercisecompute_dual_tax, the scalar path, which returns 5% for both citizenships and always did. The simulation uses the vectorized path. So the manifest now carries a false receipt, and any spec touching the tax engine must pin the vectorized path.259 blog/marketing tests pass with the article unpublished.
🤖 Generated with Claude Code
PR #1090: Convert dual-tax inputs at each policy currency boundary
Summary
Fixes the dual-tax currency defect in both production paths.
- converts every monetary scalar, vector, and nested breakdown field from the declared input currency into each policy currency before evaluation
- converts residency and citizenship results back into the shared source currency before foreign-tax-credit comparisons
- keeps same-currency cases bit-identical
- adds exhaustive field-classification guards and real-FX scalar/vector regression oracles, including the high-FX countries that exposed the bug
- records the functional model change as assumptions version
2026-08-14.v1Why this matters
The previous vectorized path treated local-currency amounts as USD on the US-citizenship side. High-FX currencies therefore pushed ordinary dollar-scale income into the top US capital-gains and NIIT brackets. EUR and GBP appeared correct only because their exchange rates are near one.
This PR supplies the core policy-boundary conversion and the strongest tax-oracle coverage. #1091 is stacked on it and adds frozen run-level FX provenance, explicit execution/replay plumbing, and cross-currency relocation coverage.
🤖 Generated with Claude Code
PR #1091: Freeze and propagate dual-tax FX context
Summary
Built on #1090 and intentionally preserves its exhaustive policy conversion and real-FX tax oracles. This PR adds the execution guarantees needed for reproducible live runs.
- declares the scenario/source currency explicitly at scalar and vector tax boundaries
- freezes a provider-backed
RateTableat enqueue, persists its receipt, and reuses it in worker execution, artifacts, sweeps, Roth exploration, grids, and deterministic replay- fails queued cross-currency runs closed when provider rates are unavailable; direct legacy calls retain a visible approximate fallback
- routes cross-currency relocation years through the same conversion boundary even when no US-citizenship overlay applies
- records the execution/replay semantic change as assumptions version
2026-08-14.v2Relationship to #1090
#1090 provides the core exhaustive conversion, same-currency identity behavior, and broad scalar/vector policy oracles. This PR adds frozen rate provenance and closes the surrounding execution, replay, and relocation gaps. The combined tree keeps both sets of protections.
Validation
make test— 5,181 passed, 8 skippedmake lint— Alembic preflight, Ruff, mypy, Grafana validation, frontend typecheck/ESLint, and Reddit app typecheck/build all passed- post-rebase publication/assumptions sanity slice — 8 passed, 2 skipped
Source record
PR metadata, merge timestamps and repair spec. The source descriptions are reproduced above; the historical test suite was not rerun for publication.