Search by

bherila / us-tax-advantaged-params

Historical and current U.S. tax-advantaged account parameters. Retirement accounts are covered today: contribution limits, phase-outs, shared-limit allocation, and Roth-conversion tax effects.

Maintainers

Package info

github.com/bherila/us-tax-advantaged-params

Language:HTML

pkg:composer/bherila/us-tax-advantaged-params

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 9

v0.3.0 2026-08-31 17:40 UTC

README

CI License: MIT

us-tax-advantaged-params is a dependency-free calculation engine for historical and current U.S. tax-advantaged account parameters. It calculates account-level and household-level contribution capacity, IRA phase-outs, shared statutory limits, federal income effects, and Roth-conversion taxability for retirement accounts, and contribution capacity for health savings accounts under IRC §223.

The repository contains two native implementations with the same behavior:

  • TypeScript for npm, exported as USTaxAdvantagedParams.
  • PHP 8.5+ for Packagist, in the USTaxAdvantagedParams namespace.

Annual legal parameters are maintained once in data/retirement-parameters.json and data/hsa-parameters.json, and generated into each single-file runtime. Shared conformance vectors and a full-output parity check keep the TypeScript and PHP engines synchronized.

Tax-software scope, not tax advice. This package calculates statutory parameters from caller-supplied facts. It does not determine whether a plan document permits a contribution, perform ERISA nondiscrimination testing, calculate self-employment tax, replace Form 8606, provide an actuarial valuation, or prepare a tax return. Review material results against the governing plan document and current primary authority.

Supported tax years

The encoded range is 1975 through 2026. The package does not extrapolate a future year. Calling a year outside the range throws UnsupportedTaxYearError in TypeScript or UnsupportedTaxYearException in PHP.

The 1975 starting point corresponds to the first generally available IRA contribution year. Some early employer-plan years cannot be reduced to a universal modern dollar ceiling from tax year alone. In those cases the engine returns an explicit indeterminate status and diagnostic rather than inventing a value.

USTaxAdvantagedParams.supportedTaxYears();
// { minimum: 1975, maximum: 2026 }

Health savings accounts have their own range, 2004 through 2026, because IRC §223 was added by the Medicare Prescription Drug, Improvement, and Modernization Act of 2003 effective for taxable years beginning after 2003. A year before 2004 returns an unavailable HSA result rather than an extrapolated one.

USTaxAdvantagedParams.supportedHsaTaxYears();
// { minimum: 2004, maximum: 2026 }

Flexible spending arrangements have their own range, 1987 through 2026. It starts at 1987 because the Tax Reform Act of 1986 §1163 added the §129(a)(2)(A) dependent care exclusion limitation for taxable years beginning after December 31, 1986; before that §129(a) carried no dollar cap. The §125(i) health FSA limit starts later, at 2013, because the Affordable Care Act §9005 added it for plan years beginning after December 31, 2012. A year between the two returns dependent care figures and a null healthFsa.

USTaxAdvantagedParams.supportedFsaTaxYears();
// { minimum: 1987, maximum: 2026 }
USTaxAdvantagedParams.fsaParametersForYear(2012)?.healthFsa;
// null

Installation

npm

npm install us-tax-advantaged-params

The npm package provides ESM, CommonJS, and TypeScript declarations and supports Node.js 20 or later.

// ESM
import USTaxAdvantagedParams from "us-tax-advantaged-params";

// CommonJS — the class is the module's default export
const USTaxAdvantagedParams = require("us-tax-advantaged-params").default;

Composer / Packagist

composer require bherila/us-tax-advantaged-params

The PHP package requires PHP 8.5 or later and loads the native single-file implementation through Composer.

TypeScript builder example

import USTaxAdvantagedParams, {
  AccountType,
  ConversionType,
  FilingStatus,
} from "us-tax-advantaged-params";

const result = USTaxAdvantagedParams.forTaxYear(2026)
  .filingStatus(FilingStatus.MARRIED_FILING_JOINTLY)
  .taxpayer("taxpayer", (person) => {
    person
      .bornIn(1963)
      .iraCompensation(180_000)
      .w2Compensation(180_000)
      .rothIraMagi(240_000)
      .traditionalIraDeductionMagi(240_000)
      .coveredByEmployerPlan(true)
      .priorYearFicaWages("employer-a", 180_000)
      .aggregateTraditionalSepSimpleIraBasis(20_000)
      .yearEndTraditionalSepSimpleIraValue(80_000);
  })
  .spouse("spouse", (person) => {
    person
      .bornIn(1970)
      .iraCompensation(0)
      .rothIraMagi(240_000)
      .traditionalIraDeductionMagi(240_000)
      .coveredByEmployerPlan(false);
  })
  .account(
    "taxpayer-401k",
    "taxpayer",
    AccountType.TRADITIONAL_401K,
    (account) => {
      account
        .employer("employer-a")
        .annualAdditionsGroup("employer-a")
        .planCompensation(180_000)
        .permitsRothContributions()
        .permitsRothCatchUp()
        .permitsAfterTaxContributions()
        .expectedEmployerContribution(9_000)
        .priority(10);
    },
  )
  .account("taxpayer-roth-ira", "taxpayer", AccountType.ROTH_IRA, (account) => {
    account.priority(20);
  })
  .account("spouse-traditional-ira", "spouse", AccountType.TRADITIONAL_IRA, (account) => {
    account.priority(30);
  })
  .conversion(
    "ira-conversion",
    "taxpayer",
    ConversionType.IRA_TO_ROTH_IRA,
    10_000,
  )
  .calculate();

console.log(result.accounts[0].maximumAnnualContributionBasedOnInputs);
console.log(result.totals.federalAgiReduction);
console.log(result.conversions[0].taxableAmount);

A built scenario can be inspected and calculated repeatedly:

const scenario = USTaxAdvantagedParams.forTaxYear(2026)
  .filingStatus("MFJ")
  .taxpayer("taxpayer", (person) => person.bornIn(1980).w2Compensation(200_000))
  .build();

const input = scenario.toInput();
const result = scenario.calculate();

PHP builder example

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use USTaxAdvantagedParams\AccountType;
use USTaxAdvantagedParams\FilingStatus;
use USTaxAdvantagedParams\PersonBuilder;
use USTaxAdvantagedParams\AccountBuilder;
use USTaxAdvantagedParams\USTaxAdvantagedParams as TaxAdvantagedParams;

$result = TaxAdvantagedParams::forTaxYear(2026)
    ->filingStatus(FilingStatus::MARRIED_FILING_JOINTLY)
    ->taxpayer('taxpayer', static function (PersonBuilder $person): void {
        $person
            ->bornIn(1963)
            ->iraCompensation(180_000)
            ->w2Compensation(180_000)
            ->rothIraMagi(240_000)
            ->traditionalIraDeductionMagi(240_000)
            ->coveredByEmployerPlan(true)
            ->priorYearFicaWages('employer-a', 180_000);
    })
    ->spouse('spouse', static function (PersonBuilder $person): void {
        $person
            ->bornIn(1970)
            ->iraCompensation(0)
            ->rothIraMagi(240_000)
            ->traditionalIraDeductionMagi(240_000)
            ->coveredByEmployerPlan(false);
    })
    ->account(
        'taxpayer-401k',
        'taxpayer',
        AccountType::TRADITIONAL_401K,
        static function (AccountBuilder $account): void {
            $account
                ->employer('employer-a')
                ->annualAdditionsGroup('employer-a')
                ->planCompensation(180_000)
                ->permitsRothContributions()
                ->permitsRothCatchUp()
                ->permitsAfterTaxContributions()
                ->expectedEmployerContribution(9_000)
                ->priority(10);
        },
    )
    ->account('spouse-ira', 'spouse', AccountType::TRADITIONAL_IRA)
    ->calculate();

var_dump($result['totals']);

The PHP result is an associative-array equivalent of the TypeScript result. Enum values serialize to the same snake-case strings.

Direct unified interface

Builders are optional. Both engines accept the same language-neutral scenario shape, which is useful for services, fixtures, database records, and cross-runtime integrations.

const result = USTaxAdvantagedParams.calculate({
  taxYear: 2026,
  filingStatus: "HOH",
  persons: [
    {
      id: "taxpayer",
      role: "taxpayer",
      birthYear: 1975,
      compensation: { iraCompensation: 140_000, w2Compensation: 140_000 },
      magi: { rothIra: 158_000, traditionalIraDeduction: 158_000 },
      coveredByEmployerRetirementPlan: true,
    },
  ],
  accounts: [
    {
      id: "401k",
      ownerId: "taxpayer",
      type: "traditional_401k",
      employerId: "employer-a",
      planRules: {
        planCompensation: 140_000,
        annualAdditionsGroupId: "employer-a",
        expectedEmployerContribution: 7_000,
      },
    },
    { id: "roth-ira", ownerId: "taxpayer", type: "roth_ira", priority: 20 },
  ],
});

Filing-status aliases include S, SINGLE, MFJ, MFS, HOH, QSS, and QW. The alias M is accepted as MFJ but emits an ambiguity diagnostic. Canonical values are preferred in persisted data.

Input rejection

The unified interface is where stale, mistyped, and cross-runtime data arrives, so an input it cannot honour is rejected rather than coerced. Both engines throw the same error code and the same message for the same bad input.

Code Raised for
INVALID_TAX_YEAR A taxYear that is not an integer
INVALID_FILING_STATUS A missing filingStatus, a non-string, or an unrecognized alias
PERSON_REQUIRED persons missing, not a list, or empty
INVALID_ACCOUNTS / INVALID_CONVERSIONS accounts or conversions present but not a list
INVALID_PERSON / INVALID_ACCOUNT / INVALID_CONVERSION An entry of persons, accounts, or conversions that is not an object
PERSON_ID_REQUIRED / ACCOUNT_ID_REQUIRED / CONVERSION_ID_REQUIRED An id that is missing, blank, or not a string
ACCOUNT_OWNER_REQUIRED / CONVERSION_OWNER_REQUIRED An ownerId that is missing, blank, or not a string
UNKNOWN_ACCOUNT_OWNER / UNKNOWN_CONVERSION_OWNER An ownerId that names no supplied person
INVALID_ACCOUNT_TYPE / INVALID_CONVERSION_TYPE A type that is not a string, or an unrecognized one
INVALID_INPUT_OBJECT A structured field — planRules, existingContributions, compensation, magi, priorYearFicaWagesByEmployer, hsa, hsaCoverage, special403bCatchUp, section457SpecialCatchUp — holding something other than an object
INVALID_CONTRIBUTION_PREFERENCE A contributionPreference outside account_type, pretax_first, roth_first (a valid preference that a pension-linked emergency savings account cannot honour is reported as a diagnostic, not rejected — see Pension-linked emergency savings accounts)
INVALID_EMPLOYER_CONTRIBUTION_TAX_TREATMENT An employerContributionTaxTreatment outside pretax, roth
INVALID_SIMPLE_EMPLOYER_CONTRIBUTION_METHOD A simpleEmployerContributionMethod outside match_3_percent, nonelective_2_percent, custom
INVALID_MONEY / INVALID_RATE A negative or non-finite amount, or a rate outside 0 through 1
INVALID_BOOLEAN A flag field holding something other than true or false

Enum-valued fields in particular are checked rather than compared loosely: a stale or camel-cased value such as "rothFirst" would otherwise fall through to a different branch and return a plausible but wrong allocation. Structured fields are checked for the same reason — a scalar where an object belongs used to be ignored in silence, taking every rule it carried with it. Flag fields must be actual booleans, because JavaScript and PHP disagree about the truthiness of "0" and of an empty array.

Two shapes are deliberately not rejected. A missing accounts or conversions key, and an explicit null in its place, both mean an empty list. And a JSON object whose keys are exactly "0", "1", … is accepted wherever a list is expected, because json_decode cannot tell it apart from a JSON array, so neither engine may.

Account coverage

Family Account types
Individual retirement arrangements Traditional IRA, Roth IRA, rollover IRA, payroll-deduction IRA, deemed traditional/Roth IRA, inherited traditional/Roth IRA
Small-employer arrangements SEP IRA, Roth SEP IRA, SIMPLE IRA, Roth SIMPLE IRA, grandfathered SARSEP
Qualified elective plans Traditional/Roth 401(k), Solo/Roth Solo 401(k), SIMPLE/Roth SIMPLE 401(k), starter 401(k), pension-linked emergency savings account (PLESA)
Tax-sheltered annuities Traditional/Roth 403(b), safe-harbor deferral-only 403(b) — see the §403(b)(2) note below for tax years 1987-2001
Deferred compensation Governmental/Roth governmental 457(b), nongovernmental eligible 457(b), 457(f), governmental 457(b)-hosted PLESA
Federal plan Traditional and Roth TSP
Employer-only defined-contribution plans 401(a), profit-sharing, money-purchase, Keogh, ESOP
Pension arrangements Defined-benefit and cash-balance plans
Health accounts Health savings account (HSA), health flexible spending arrangement (health FSA)
Dependent care Dependent care assistance program (dependent care FSA)

Defined-benefit and cash-balance contributions are deliberately returned as indeterminate; their funding requires the plan formula, census, assets, actuarial assumptions, and funding rules. The §415(b)(1)(A) limitation on the annual benefit is a different thing — a flat statutory ceiling published in the same annual notice as the defined-contribution figures, requiring no actuary — so it is reported alongside that indeterminate contribution status:

const result = USTaxAdvantagedParams.calculate({
  taxYear: 2026,
  filingStatus: "S",
  persons: [{ id: "t", birthYear: 1970 }],
  accounts: [{ id: "db", ownerId: "t", type: "defined_benefit_plan", employerId: "e" }],
});
result.accounts[0].status;                             // "indeterminate"
result.accounts[0].statutoryMaximumAnnualContribution; // null
result.accounts[0].definedBenefit?.annualBenefitLimit; // 290000
Rule Treatment
§415(b)(1)(A) annual benefit Reported on definedBenefit.annualBenefitLimit for both defined-benefit and cash-balance accounts, with an info diagnostic stating it
§415(b)(2) and §415(b)(5) adjustments Not applied. The published figure assumes a straight life annuity beginning between ages 62 and 65; adjusting it for another benefit form, another starting age, or fewer than ten years of participation or service is participant-specific
Years with no transcribed figure null. The encoded figures are those transcribed from the notices committed under evidence/retirement-limits/, which cover 2009, 2010, and 2013 onward. A year outside that set reports null rather than a carried-forward or extrapolated amount
Contribution and funding Still indeterminate; a benefit ceiling is not a contribution ceiling, and nothing here computes a funding requirement

Pension-linked emergency savings accounts (IRC §402A(e))

SECURE 2.0 §127 added §402A(e), effective for plan years beginning after December 31, 2023. §402A(e)(1)(A)(i) treats a PLESA "for purposes of this title as a designated Roth account", so its contributions are always Roth.

That is a characteristic of the account rather than an election, so it precedes the caller's: planRules.contributionPreference, permitsRothContributions and permitsRothCatchUp are disregarded on a PLESA — with an INFO PENSION_LINKED_EMERGENCY_SAVINGS_CONTRIBUTIONS_ARE_ALWAYS_ROTH saying so — rather than honoured into a pre-tax contribution the statute leaves no capacity for. No accepted input produces a pre-tax contribution to a PLESA. On every other account type, including an ordinary designated Roth 401(k) or 403(b), those fields keep their ordinary effect: §402A(b)(1) offers the designated Roth election in addition to pre-tax deferrals, so there the split is a plan and participant choice.

§402A(f)(1) names three plans that may host one, and which limits apply turns on the host, so the third is a distinct account type:

Host Account type Deferral limit §415(c)
§401(a) trust — §402A(f)(1)(A) pension_linked_emergency_savings §402(g) Yes
§403(b) plan — §402A(f)(1)(B) pension_linked_emergency_savings §402(g) Yes
Governmental §457(b) — §402A(f)(1)(C) governmental_457b_pension_linked_emergency_savings §457(e)(15) No

For the first two, model the account inside the plan with the same annualAdditionsGroupId as the plan's other accounts. The third shares no pool with them: §402(g)(3) enumerates elective deferrals exhaustively and lists no §457(b) deferral, so its contributions run against the §457(e)(15) applicable dollar amount through §457(b)(2)(A); and §415(a)(1)–(2) enumerates the plans the annual-additions limit reaches without naming §457(b), so it joins no annual-additions group at all. One person may hold a PLESA on more than one host in the same year, and the ceilings then stand side by side rather than as one — §402A(e)(3)(A) caps "the portion of the account balance", so each account has its own.

The §402A(e)(3)(A)(i) figure is a cap on a balance, not an annual allowance. The statute bars a contribution "to the extent such contribution would cause the portion of the account balance attributable to participant contributions to exceed" the lesser of that figure and an amount the plan sponsor sets. That portion of the balance carries across years, and §402A(e)(7) — which requires the plan to permit withdrawal at least monthly — moves it back down. So the balance must be supplied; the year alone does not determine what is left:

const result = USTaxAdvantagedParams.calculate({
  taxYear: 2026,
  filingStatus: "S",
  persons: [{ id: "t", compensation: { w2Compensation: 90000 } }],
  accounts: [{
    id: "plesa",
    ownerId: "t",
    type: "pension_linked_emergency_savings",
    employerId: "e",
    planRules: { pensionLinkedEmergencySavingsParticipantContributionBalance: 1000 },
  }],
});
result.accounts[0].statutoryMaximumAnnualContribution; // 1600 — 2600 less the 1000 balance
result.accounts[0].contributionComponents.employeeRothDeferral; // 1600

Because the cap is on a balance, a year's gross contributions may exceed it. A participant who contributed $600, withdrew $400 under §402A(e)(7), and so holds $200 attributable to participant contributions has $2,400 of room left and may reach $3,000 for the year — the Department of Labor's PLESA guidance is explicit that a plan may not impose a separate annual PLESA contribution limit, precisely so the account can be replenished.

Rule Treatment
§402A(e)(3)(A)(i) dollar figure parameters.pensionLinkedEmergencySavingsBalanceCap402A. $2,500 for 2024 and 2025, $2,600 for 2026
§402A(e)(3)(A)(ii) plan sponsor amount Supplied as planRules.planDocumentEmployeeDeferralLimit; it lowers the contributable amount but not the reported statutory maximum
Participant-contribution balance Required. Supplied as planRules.pensionLinkedEmergencySavingsParticipantContributionBalance: the portion of the balance attributable to participant contributions immediately before the proposed allocation — including amounts contributed earlier in the same year that are still in the account, net of withdrawals under the plan's accounting, excluding earnings. Pass 0 for a new account. Omitted — or supplied as an explicit null, which states the absence of the fact in exactly the same way — the account is indeterminate with PENSION_LINKED_EMERGENCY_SAVINGS_PRIOR_BALANCE_REQUIRED (whose "prior" means immediately prior to the allocation, not an opening or prior-year figure) rather than defaulted to an empty account. This differs from the optional §402A(e)(3)(A)(ii) sponsor amount above, where a null means the sponsor set none
§402(g) and §415(c) On a §401(a)- or §403(b)-hosted account, base deferrals are consumed like any other elective deferral and annual addition, in the owner's and the employer group's shared pools; a §414(v) catch-up draws the owner's catch-up pool and, per §414(v)(3)(A)(i), not the annual-additions group. A governmental §457(b)-hosted account consumes neither: it draws the owner's §457(e)(15) pool and joins no annual-additions group, whatever annualAdditionsGroupId the caller supplies
§457(b)(2)(B) includible compensation Applies to a §457(b)-hosted account, capped at 100 percent of includible compensation like any other deferral under that plan
§457(b)(3) last-three-years catch-up Available on a §457(b)-hosted account, within the balance cap, on the same reasoning as §414(v): it raises "the ceiling set forth in paragraph (2)", a limit on deferrals under the plan, while §402A(e)(3)(A) gates the account balance. §457(e)(18) gives the participant the greater of it and the §414(v) catch-up, never their sum. Reported as special457RothCatchUp, since a PLESA contribution is Roth whatever limitation supplied its capacity. Known limitation: that greater-of rule is enforced within a single account, not across a participant's §457(b) accounts — two accounts can each take a catch-up under a different heading and stack past what §457(e)(18) allows, and the §457(b)(3) ceiling does not subtract catch-up amounts already supplied as existing contributions. Both predate the PLESA work and are tracked in issue #49
§402A(e)(3)(A) room An account-local pool, reported in sharedLimits as plesa402Ae3:{accountId}, seeded with the supplied balance and drawn by base deferrals and catch-up alike
Age-based catch-up Available, within the balance cap. §402A(e)(3)(A) gates a balance, while §414(v) relieves a plan- or employee-level deferral limit — 26 CFR §1.414(v)-1(b)(1)(i) lists them and none is account-level — so the two compose and both bind. Once the host's §402(g) pool is spent, remaining room may be filled from the §414(v) catch-up; a catch-up is outside §415(c) under §414(v)(3)(A)(i). As elsewhere in this package, capacity follows from age rather than a plan-document election, under the standing assumption that the plan permits and so characterises it. A birth year is required only where a catch-up could reach unfilled room — not where the host's base capacity already covers it, and not on a §457(b) host where the §457(b)(3) catch-up exceeds the largest age-based catch-up the year offers at any age, since §414(v)(6)(C) then removes the age-based one whatever the participant's age. An equal §457(b)(3) amount is not enough: §414(v)(6)(C) speaks of a higher limitation, so the age still decides which route applies. Where the route is unresolved, no catch-up is allocated under either heading — §457(e)(18) chooses between pools that are reported separately, so a figure known to be reachable one way or the other is still not attributable to either
Employer contributions Never allocated here. §402A(e)(6)(A) directs any match earned on PLESA contributions to the participant's other account under the plan, and §402A(e)(8)(B) bars transfers in
2023 and earlier unavailable, on every host. Pub. L. 117-328 §127(g) applies §127 to plan years beginning after December 31, 2023

The 2024 figure comes from the Code rather than from a notice: Notice 2023-75 does not state one, and the flush text of §402A(e)(3)(A) adjusts the $2,500 only "[i]n the case of contributions made in taxable years beginning after December 31, 2024", leaving the first effective year on the unadjusted statutory amount.

Health savings accounts (IRC §223)

HSA contribution capacity is calculated from caller-supplied coverage facts. Whether a person is an eligible individual under §223(c)(1) — including Medicare entitlement under §223(b)(7) — is an input, not something the engine infers.

Rule Treatment
§223(b)(2) monthly limitation The limit is the sum of the monthly amounts divided by 12, so partial-year eligibility prorates by month of coverage
§223(b)(3) age-55 additional amount Per spouse and not shareable; each spouse's catch-up must be contributed to that spouse's own HSA
§223(b)(5) family coverage Spouses share a single family limit, divided equally or as agreed. Only the family-months portion is divided; self-only months stay with the individual
§223(b)(5)(B)(ii) agreed division An agreed division must exhaust the limitation. Shares that total more or less than 1 are both reported as errors and return indeterminate (see below)
§223(b)(5)(A) If either spouse has family coverage, both are treated as having family coverage for those months — whether or not that spouse owns an HSA (see below)
§223(b)(8) last-month rule Eligible on December 1 allows the full annual amount, creating a 13-month testing period obligation
Testing-period failure The attributable amount is included in income in the following year and carries a 10% additional tax, unless failure is by death or disability
Pre-2007 years §223(b)(2) capped the monthly limitation at 1/12 of the lesser of the plan's annual deductible and the dollar amount, until the Tax Relief and Health Care Act of 2006 §303 removed it
§106(d) employer contributions Excluded from income rather than deducted, reducing W-2 box 1 and FICA wages and reducing the §223(b)(4)(B) deduction
§223(b)(4)(A) Archer MSA reduction The aggregate amount paid for the year to that individual's Archer MSAs reduces the whole subsection (b) limitation — the §223(b)(3) increase included — but not below zero
§223(b)(5)(B)(i) Archer MSA reduction For a married individual to whom §223(b)(5) applies, both spouses' aggregate reduces the single family limitation before §223(b)(5)(B)(ii) divides it, and never touches the §223(b)(3) amount
§223(b)(4)(C) qualified HSA funding distribution The amount contributed under §408(d)(9) reduces that individual's own subsection (b) limitation — the §223(b)(3) increase included — but not below zero. It is never withdrawn by the flush text and never taken before the §223(b)(5)(B)(ii) division

The testing period spans two tax years, so a caller who has not yet resolved it receives an explicit obligation in the result rather than an assumed outcome.

Spousal coverage: persons[].hsaCoverage

§223(b)(5)(A) turns on whether either spouse has family coverage, not on whether either spouse owns a health savings account. A spouse with family HDHP coverage and no HSA of their own still changes the other spouse's limitation, so that coverage is stated on the person:

const result = USTaxAdvantagedParams.forTaxYear(2026)
  .filingStatus(FilingStatus.MARRIED_FILING_JOINTLY)
  .taxpayer("taxpayer", (person) => person.bornIn(1985))
  // The spouse has family HDHP coverage but no HSA of their own.
  .spouse("spouse", (person) => person.bornIn(1986).hsaCoverage("family"))
  .account("taxpayer-hsa", "taxpayer", AccountType.HSA, (account) => {
    account.hsaCoverage("self_only");
  })
  .calculate();

persons[].hsaCoverage takes the same coverage fields as planRules.hsa (coverageTier, eligibleMonths, monthlyCoverage, hdhpAnnualDeductible). Where a person owns an HSA, planRules.hsa already carries these facts; supplying both is allowed but they must be identical, and a contradiction returns HSA_PERSON_AND_ACCOUNT_COVERAGE_FACTS_CONFLICT.

Supplying the key at all declares the fact known. An empty object — { id: "s", hsaCoverage: {} }, or .noHsaCoverage() on the builder — records that the spouse held no high deductible health plan coverage in any month.

Archer MSA contributions: persons[].archerMsaContributions

§223(b)(4)(A) reduces the §223(b) limitation by "the aggregate amount paid for such taxable year to Archer MSAs of such individual", and §223(b)(5)(B)(i) reduces the single family limitation by "the aggregate amount paid to Archer MSAs of such spouses". Both take an amount paid, not a §220 limitation, so the amount is a caller-supplied fact in the same way eligible-individual status is, and no part of §220 is modelled or checked against it.

const result = USTaxAdvantagedParams.forTaxYear(2026)
  .filingStatus(FilingStatus.SINGLE)
  .taxpayer("taxpayer", (person) => person.bornIn(1986).archerMsaContributions(1200))
  .account("taxpayer-hsa", "taxpayer", AccountType.HSA, (account) => {
    account.hsaCoverage("self_only");
  })
  .calculate();
// 4400 - 1200 = 3200

Because §223(b)(5)(B)(i) reads the couple's aggregate, the amount belongs to the person rather than to an account: a spouse who owns no HSA can still carry one, and it still reduces the limitation the other spouse divides.

The ordering is not cosmetic. The §223(b)(4) flush text says "Subparagraph (A) shall not apply with respect to any individual to whom paragraph (5) applies", so a married individual with family coverage is reduced under §223(b)(5)(B)(i) — before the equal division, not after. Two spouses with a 2026 family limitation of 8750 and 3000 of aggregate Archer contributions get (8750 − 3000) ÷ 2 = 2875 each, not 4375 − 3000 = 1375 each, which would subtract the aggregate twice. §223(b)(5)(B) also operates "without regard to any additional contribution amount under paragraph (3)", so a married individual's age-55 amount survives a reduction that would have consumed it under §223(b)(4)(A).

Each account's hsa detail reports archerMsaContributionsApplied, archerMsaReductionPrecedesFamilyDivision, and archerMsaLimitReduction, and an HSA_ARCHER_MSA_CONTRIBUTIONS_REDUCE_LIMIT diagnostic names the paragraph that applied.

Qualified HSA funding distributions: persons[].qualifiedHsaFundingDistributions

§223(b)(4)(C) reduces the §223(b) limitation by "the aggregate amount contributed to health savings accounts of such individual for such taxable year under section 408(d)(9)" — a once-in-a-lifetime IRA-to-HSA rollover. Like the Archer amount it is a fact about the person, taken as supplied: the §408(d)(9)(C) once-per-lifetime limitation and the separate §408(d)(9)(D) testing period are not modelled, and the amount is not checked against the IRA it came from.

const result = USTaxAdvantagedParams.forTaxYear(2026)
  .filingStatus(FilingStatus.SINGLE)
  .taxpayer("taxpayer", (person) => person.bornIn(1986).qualifiedHsaFundingDistributions(1500))
  .account("taxpayer-hsa", "taxpayer", AccountType.HSA, (account) => {
    account.hsaCoverage("self_only");
  })
  .calculate();
// 4400 - 1500 = 2900

It behaves the opposite way to the Archer reduction for a married couple, and the flush text is why. "Subparagraph (A) shall not apply with respect to any individual to whom paragraph (5) applies" names subparagraph (A) alone, and §223(b)(5)(B)(i) reduces the family limitation only by the Archer amount, so nothing routes (C) through paragraph (5). It stays an amount of "such individual" reducing the limitation applying to that individual under subsection (b) — which for a married spouse is the share left by the §223(b)(5)(B)(ii) division, after the division rather than before it, plus their own §223(b)(3) amount.

Two spouses with the 2026 family limitation of 8750 divide it to 4375 each. A $2,000 rollover by one spouse leaves 2375 and 4375; the same $2,000 paid to an Archer MSA instead leaves 3375 and 3375, because that reduction comes off the 8750 first. And since (C) is not governed by §223(b)(5)(B)'s "without regard to any additional contribution amount under paragraph (3)", it reaches a married individual's age-55 amount where the Archer reduction cannot.

When both reductions apply, §223(b)(4) reduces by "the sum of" them but not below zero. The fall is attributed in subparagraph order, so archerMsaLimitReduction is reported in full and qualifiedHsaFundingLimitReduction reports only what was left for (C) to reach. Each account's hsa detail reports qualifiedHsaFundingDistributionsApplied and qualifiedHsaFundingLimitReduction, and an HSA_QUALIFIED_HSA_FUNDING_DISTRIBUTION_REDUCES_LIMIT diagnostic states which ordering applied.

Agreed divisions must exhaust the family limitation

planRules.hsa.familyLimitShare records a §223(b)(5)(B)(ii) agreement to divide the single family limitation other than equally. Supply it on every spouse who owns an HSA, and make the shares total exactly 1 — the statute divides the limitation "unless they agree on a different division", and an allocation that leaves part of it belonging to neither spouse is not a division.

Both failures are ERROR diagnostics and both return indeterminate:

Shares Diagnostic
Total above 1 HSA_FAMILY_LIMIT_SHARES_EXCEED_ONE — the couple would claim more than one family limitation
Total below 1 HSA_FAMILY_LIMIT_SHARES_BELOW_ONE — capacity the couple is entitled to would be silently forfeited
Supplied on one spouse but not the other HSA_FAMILY_LIMIT_SHARE_REQUIRED_FOR_BOTH_SPOUSES

They are diagnosed rather than rejected, because a share is a caller-supplied fact and this package reports on facts rather than overriding them — but at ERROR severity, because unlike an unusual eligibility fact this one is arithmetically impossible, and a determinate number computed from it would assert a ceiling the statute does not produce. Two spouses at 0.3 each for 2026 would otherwise return a confident 2625 apiece against an 8750 limitation, forfeiting 3500 with no signal at all.

The constraint is on the sum, not on either share: 1 and 0 is a valid division that gives one spouse the whole limitation. And where only one spouse owns an HSA, the shares that can be supplied cover one spouse, so a share below 1 there is a complete division whose remainder the other spouse has no account to use, and no error applies.

Because §223(b)(5)(A) can only ever raise a self-only month to a family month, the spouse's coverage is required exactly when it could change the answer. On a married return, an HSA owner with at least one self-only month and no stated spousal coverage returns indeterminate with HSA_SPOUSE_COVERAGE_FACTS_REQUIRED rather than a number the input cannot support. An owner whose months are all family months is unaffected, since family is already the higher tier.

Encoded HSA parameters are verified against the Revenue Procedure that published them — see evidence/hsa-limits/.

Health flexible spending arrangements (IRC §125(i))

The §125(i) ceiling on salary reduction contributions is calculated from caller-supplied plan facts. Plan design is not inferred: this engine cannot read a plan document, so whether the plan offers a carryover or a grace period, whether employer flex credits could be elected as cash, and the arrangement's Rev. Rul. 2004-45 purpose are all inputs.

Rule Treatment
§125(i)(1) salary-reduction limit The indexed dollar limitation, applied per employee per employer
Years before 2013 §125(i) did not exist, so there was no statutory ceiling at all — only whatever the plan document imposed. The result is indeterminate with a null limit, not a fabricated one and not unavailable: the account existed, the limit did not
Notice 2013-71 carryover The carried amount is the lesser of the prior year's unused amount and that year's cap. The rest is forfeited
Carryover does not reduce the limit Notice 2013-71: the carryover "does not count against or otherwise affect" the §125(i) limit, so it sits on top of the receiving year's ceiling
Carryover or grace period, never both Notice 2013-71 forbids the combination. Asserting both describes a plan that cannot exist, so the result is indeterminate with an ERROR
Neither offered The whole unused amount is forfeited under the use-or-lose rule, and the forfeiture is reported rather than dropped
Employer flex credits Outside §125(i), which reaches salary reduction contributions alone — unless the employee could have elected them as cash or another taxable benefit, in which case Notice 2012-40 treats them as salary reduction contributions and they consume the limit
Election above the limit An ERROR, never silent truncation. Notice 2012-40 holds that a plan permitting a higher election is not a §125 cafeteria plan at all, so truncating would report a smaller consequence than the statute produces
Per employee per employer Notice 2012-40: two unrelated employers carry two full limits; arrangements sharing an employerId share one, which is how §125(g)(4) controlled-group aggregation is expressed
Spouses Each spouse carries a full limit, even in the same plan of the same employer. This is the deliberate contrast with §129, which is per return
§125(a) exclusion, not a deduction A salary reduction never enters gross income, so it reduces W-2 box 1 and FICA wages and contributes nothing to federalAgiReduction

The carryover cap belongs to the year the money came from

Notice 2013-71 created the carryover at a fixed $500 and Notice 2020-33 raised it to 20 percent of the §125(i) limit "for that plan year". Both phrase it as the maximum unused amount from a plan year carried to the immediately following one, so carryoverLimitForPriorYear is the figure that governs an amount arriving this year, and carryoverLimitForThisYear is what may leave at the end of it. Reading the cap off the receiving year is the natural mistake and gives a different number in every year the limit moved.

Plan year versus tax year

Notice 2012-40 §III holds that "taxable year" in §125(i) means the plan year of the cafeteria plan, and prorates a short plan year by its months. Every annual Revenue Procedure nonetheless publishes the figure "for taxable years beginning in" the year, and this package is keyed by tax year throughout, so the two agree exactly for a calendar-year plan — which is the ordinary case and the default here.

For a non-calendar plan year the governing figure depends on the plan year start date, which the engine does not hold. Supplying planYearIsCalendarYear: false therefore returns indeterminate with an ERROR rather than quietly applying the calendar-year figure. Key the scenario to the tax year in which the plan year begins if you want that year's number.

COVID-era relief is disclosed, not modelled

§214 of the Consolidated Appropriations Act, 2021 (Notice 2021-15) let a plan carry over all unused amounts from plan years ending in 2020 and 2021, and let a dependent care program carry over at all, which it otherwise may not. Adopting it was entirely a plan option. The engine applies the ordinary cap and attaches HEALTH_FSA_SECTION_214_RELIEF_NOT_MODELLED whenever a carryover out of 2020 or 2021 is computed, so a plan that adopted the relief is visibly under-reported rather than silently so.

A bare FSA is rejected

health_fsa, healthcare_fsa, medical_fsa, and health_flexible_spending_arrangement all resolve. FSA alone does not: it names a health FSA and a dependent care FSA equally well, and the two carry different limits and different household aggregation, so it raises INVALID_ACCOUNT_TYPE with a message naming both spellings rather than silently picking one.

Encoded §125 and §129 parameters are verified against the documents that published them — see evidence/fsa-limits/.

Dependent care assistance (IRC §129)

§129(a)(2)(A) is a per-return amount, which is the single most important difference from §125(i). Two spouses filing jointly do not get one each.

Rule Treatment
§129(a)(2)(A) exclusion Not inflation-adjusted, so it appears in no Revenue Procedure and is cited to the Code. Each year is encoded as its own row, so the 2021 increase and its reversion are both data rather than a rule
Married filing separately The statutory parenthetical amount. Separate returns mean each spouse carries their own halved amount rather than dividing one
2021 only ARPA §9632 substituted "$10,500 (half such dollar amount" for taxable years beginning after 2020 and before 2022 — enacted in March 2021, so Rev. Proc. 2020-45 could not carry it
2026 onward Pub. L. 119-21 §70404 struck $5,000 ($2,500 and inserted $7,500 ($3,750 for taxable years beginning after December 31, 2025. A fixed-dollar substitution: the amount changed, the absence of indexing did not
Household sharing Spouses filing jointly draw on one pool, reported through sharedLimits so the constraint is visible. Assistance above it is includibleInIncome under §129(a)(2)(B), not silently dropped
§129(b)(1) earned income Applied whenever the caller supplies the figures: the employee's earned income, or for a married employee the lesser of theirs and their spouse's. Absent, the ceiling is the §129(a)(2)(A) amount alone and a WARNING says the limitation was not applied
Years before 1987 §129 existed from 1982 but carried no dollar ceiling until the Tax Reform Act of 1986 §1163. Those years are indeterminate with a null limit; 1981 and earlier, when §129 did not exist at all, are unavailable with a zero
§129(a)(1) exclusion Reduces W-2 box 1 and FICA wages and contributes nothing to federalAgiReduction, exactly as the §125 and §106(d) exclusions do

Why the earned income limitation is here at all

The package's boundary is that it does not derive income, not that it ignores supplied facts. §129(b)(1) is a hard statutory ceiling, so leaving it out entirely would over-report the exclusion for exactly the taxpayers it was written for. Both figures are caller-supplied, like every other fact here.

§129(b)(2) deeming is not modelled. For a spouse who is a student or incapable of self-care, §129(b)(2) applies the §21(d)(2) monthly schedule. That schedule is not encoded, because no primary source for it is committed to this package's evidence corpus and an unattested figure is never encoded. Asserting isStudentOrIncapableOfSelfCare on the person records that the dependentCareEarnedIncome supplied for them is the deemed amount, and emits a diagnostic saying the schedule is not applied for you.

The §129(b)(1) facts live on the person, not the program. The limitation is one figure for the return — the employee's own earned income, or for a married employee the lesser of theirs and their spouse's — so dependentCareEarnedIncome is a PersonInput field. While it sat on each account's plan rules, two dependent care programs on one return could state it differently and the engine had to report the contradiction as an error; putting it on the person removes the possibility instead of diagnosing it.

The §125 / §223 interaction: diagnose, do not enforce

A general-purpose health FSA and an HSA cannot both be right. The engine says so and returns the §223 figures the inputs imply, unchanged.

Health FSA purpose Effect on the HSA in the same scenario
general_purpose ERROR HEALTH_FSA_DISQUALIFIES_HSA_ELIGIBILITY citing §223(c)(1)(A)(ii) and Rev. Rul. 2004-45. Every §223(b) figure is unchanged — the limitation, the prorated amount, the components, the totals
general_purpose held by the spouse ERROR SPOUSE_HEALTH_FSA_DISQUALIFIES_HSA_ELIGIBILITY. Rev. Rul. 2004-45 says the result is the same where the arrangement is sponsored by the spouse's employer, because it can reimburse this individual's expenses. Figures again unchanged
limited_purpose or post_deductible No conflict. An INFO records that the arrangement was treated as HSA-compatible
absent ERROR HEALTH_FSA_PURPOSE_REQUIRED_FOR_HSA_INTERACTION, and the §223 limitation is indeterminate

The last row is the one that differs, and deliberately. With a stated general_purpose the conflict is known, and reporting the caller's own figures is the whole point: eligible-individual status is caller-supplied everywhere in this engine, so someone who ended the arrangement mid-year and supplied the correct eligible months must still get the answer their facts imply. With the purpose unstated nothing about §223 is known — the two classifications give opposite answers — so a confident number would be the defect rather than the diagnostic.

Two consequences worth stating:

  • A carryover of general-purpose funds disqualifies the whole receiving plan year. Notice 2013-71 makes the carried amount available for expenses incurred during the entire plan year it is carried to, so it is general-purpose coverage for that year and not merely until it is spent.
  • A grace period extends the disqualification into the following plan year. Notice 2005-86: coverage during the grace period blocks eligibility until the first day of the month after it ends, even at a zero balance. Those months fall outside the year being calculated, so it is reported as INFO rather than folded into the month list.

The account's reported status still becomes indeterminate when an ERROR is attached — that is the engine's uniform rule, not an enforcement of §223. What "diagnose, do not enforce" means here is that no number moves.

A dependent care FSA never raises this: §129 assistance reimburses dependent care rather than §213(d) medical expenses, so it is not coverage §223(c)(1)(A)(ii) reaches.

Multiple employers

Statutory pools are keyed to match the statute rather than to the taxpayer uniformly:

  • §402(g)(1) elective deferrals aggregate per person across every employer.
  • §415(c) annual additions apply per employer, so unrelated employers carry independent limits. Set annualAdditionsGroupId on the plan rules to aggregate plans of a controlled or affiliated service group under §414(b)/(c)/(m)/(o) and §415(h).
  • §414(v)(7)(A) Roth catch-up classification tests prior-year FICA wages from the sponsoring employer, supplied through priorYearFicaWages(employerId, amount).

Whether two employers are a single employer for §415 is a legal determination about ownership, so it is a caller-supplied fact rather than something inferred from the inputs.

Result semantics

Field Meaning
statutoryMaximumAnnualContribution Overall monetary legal ceiling when determinable from encoded law and supplied facts. Restrictions the plan document imposes are not folded in — they lower maximumAnnualContributionBasedOnInputs instead — so a §457(b) plan writing a deferral limit below the §457(e)(15) amount, or a PLESA sponsor setting a §402A(e)(3)(A)(ii) amount below the published figure, lowers what may be contributed without lowering this field
maximumAnnualContributionBasedOnInputs Maximum supported by law and supplied plan capabilities/formulas
maximumAdditionalContributionBasedOnInputs Remaining supported amount after existing contributions
existingAnnualContribution Existing contribution components supplied by the caller
excessContribution Supplied amount above the account's determinable statutory ceiling; null when that ceiling is indeterminate
planTermDependentCapacity Potential space that cannot be allocated without additional plan/employer facts
contributionComponents Pretax, Roth, after-tax, employer, IRA, and catch-up components. The statutory source of a catch-up and its tax treatment are independent, so both are recorded: a §457(b)(3) last-three-years catch-up is special457CatchUp when pre-tax and special457RothCatchUp when made to a designated Roth account — including any PLESA, where §402A(e)(1)(A)(i) makes Roth the only possibility. Both seed the same §457(b)(3) pool when handed back as an existing contribution
federalTaxEffects Federal AGI, taxable-income, W-2 box 1, nondeductible, after-tax/Roth, and conversion effects
sharedLimits Audit trail showing each statutory pool used by the account
diagnostics Assumptions, warnings, unavailable rules, and legal references

maximumAnnualContributionBasedOnInputs is a mechanical result, not a contribution recommendation.

Shared-limit allocation

Accounts are allocated in ascending priority and then input order. This makes overlapping limits deterministic.

The engine tracks, among other pools:

  • Traditional and Roth IRA contributions per owner.
  • Joint-return compensation available for spousal IRAs.
  • The owner-level §402(g) elective-deferral limit across applicable 401(k), 403(b), TSP, SARSEP, and SIMPLE sources.
  • The owner-level §414(v) age-based catch-up pool.
  • A separate §457(b) limit, drawn on by every §457(b) account including a §402A(f)(1)(C)-hosted PLESA.
  • §415(c) annual additions per participant and controlled-employer group.
  • The owner-level 403(b) 15-years-of-service catch-up pool.
  • The 457(b) last-three-years special catch-up.

Use the same annualAdditionsGroupId for plans that share one §415(c) controlled-employer limit. Unrelated employers should normally use different group IDs.

Recognized compensation under §401(a)(17)

When a caller supplies an employer contribution rate, the engine first limits plan compensation to the applicable annual recognized-compensation ceiling and then applies the rate. This applies to:

  • Employer nonelective formulas.
  • Employer matching formulas whose matchable compensation is expressed as a fraction of compensation.
  • Common-law employee SEP formulas.
  • The plan-rate side of self-employed SEP and qualified-plan formulas.

For a self-employed owner, the maximum percentage contribution is the lesser of:

  1. net earnings after the deductible half of self-employment tax multiplied by the reduced self-employed rate; and
  2. recognized compensation multiplied by the unreduced plan contribution rate.

The result remains subject to §415(c), plan-document limits, and existing annual additions.

The compensation ceiling is not imposed as an extra dollar cap that prematurely stops an employee’s otherwise valid §402(g) elective deferral. Employee deferrals remain subject to actual compensation, §402(g), catch-up rules, shared pools, and plan terms.

SIMPLE formulas preserve their distinct treatment: the ordinary 3% matching method is based on compensation and deferrals, while the 2% nonelective method and applicable additional nonelective contribution use recognized compensation.

Supplying expectedEmployerContribution bypasses formula inference because it represents a known caller-provided employer amount. The amount is still constrained by applicable annual-additions and plan-document ceilings.

IRA phase-outs and spousal IRAs

The package models:

  • The combined traditional/Roth IRA annual contribution limit.
  • Age-50 IRA catch-up amounts.
  • Roth IRA MAGI phase-outs.
  • Traditional IRA active-participant deduction phase-outs.
  • The separate phase-out for a noncovered spouse married to a covered participant.
  • Married-filing-separately rules, including whether spouses lived together during the year.
  • MFJ spousal-IRA compensation sharing.
  • Historical one-earner spousal limits.
  • The pre-2020 traditional-IRA age-70½ contribution restriction.
  • Nondeductible traditional IRA capacity when a deduction is unavailable.
  • IRS worksheet-style phase-out rounding and the positive reduced minimum.

Supply the MAGI value applicable to each calculation. The engine does not derive tax-return MAGI from raw income items.

Catch-up contributions and birth data

Age is generally determined at the end of the tax year. bornIn(year) is sufficient for ordinary age-50 and age-60-to-63 catch-up rules; bornOn(YYYY-MM-DD) is preferred for legacy age-70½ edge cases.

There is no general pre-1960/post-1960 retirement-account contribution-limit split. The 1960 boundary is primarily associated with Social Security full retirement age, not these contribution limits.

Supported catch-up logic includes:

  • Ordinary age-50 catch-up.
  • Enhanced age-60-to-63 catch-up beginning in 2025.
  • 403(b) 15-years-of-service catch-up, including annual and lifetime residuals.
  • Governmental 457(b) age catch-up.
  • The 457(b) special last-three-years catch-up, selecting the larger applicable method rather than combining incompatible methods.
  • High-wage Roth catch-up classification using prior-year FICA wages for the sponsoring employer when applicable.

Roth conversions and in-plan Roth rollovers

Conversions are separate from contributions and do not consume the annual IRA or elective-deferral limit.

Supported conversion categories are:

  • Traditional/SEP/SIMPLE IRA to Roth IRA.
  • Qualified plan to Roth IRA.
  • In-plan Roth rollover.

For IRA conversions, the engine can allocate aggregate traditional/SEP/SIMPLE IRA basis using Form 8606-style pro-rata treatment. Supply aggregate basis, year-end aggregate IRA value, and other current-year distributions when relevant. Multiple same-year conversion inputs share basis without penny over-allocation.

The package reports gross converted amount, taxable amount, nontaxable basis, AGI increase, and diagnostics. It does not calculate withholding, estimated-tax penalties, five-year holding periods, early-distribution recapture, state tax, or full plan distribution eligibility.

Calculation status and diagnostics

Possible statuses are:

  • determinate
  • determinate_with_assumptions
  • indeterminate
  • unavailable
  • ineligible

Pre-2002 403(b): the §403(b)(2) exclusion allowance

A 403(b) account for a tax year 1987 through 2001 returns indeterminate with PRE_2002_403B_EXCLUSION_ALLOWANCE_NOT_APPLIED, and both its statutory maximum and its input-supported maximum are null.

Before EGTRRA, §403(b)(2) capped the excludable amount at the exclusion allowance, and IRS Publication 571 (2001) computes the maximum amount contributable as the least of that allowance, the §415(c) annual-additions limit, and the §402(g) elective-deferral limit. The allowance is 20% of includible compensation for the most recent year of service, multiplied by years of service, reduced by amounts previously excludable — a lifetime aggregate over the participant's service with that employer, which no input supplies. With one of the three unknown, the least of them cannot be identified, so reporting the lesser of §415(c) and §402(g) would state a ceiling the omitted term can only lower. The package does not model the allowance; it declines to answer, exactly as SOURCES.md says it does.

The window closes at 2001 because EGTRRA (Pub. L. 107-16) §632(a)(2)(B) struck §403(b)(2) and §632(a)(3)(E) struck the §415(c)(4) alternative elections, both applying "to years beginning after December 31, 2001" (§632(a)(4)). 2002 onward is answerable from §415 and §402(g) alone. The window opens at 1987 only because 1986 and earlier already return indeterminate with HISTORICAL_415C_LIMIT_INDETERMINATE, there being no encoded §415(c) limit at all. Plans other than 403(b) are untouched: a 2001 401(k) is still determinate_with_assumptions.

Do not discard diagnostics. They are part of the calculation contract. A non-error status may still contain warnings about missing plan terms, historical uncertainty, employer aggregation, Roth catch-up classification, or caller assumptions.

Native TypeScript/PHP parity

The DRY boundary is the statutory data and behavioral specification, not a cross-language runtime dependency:

data/retirement-parameters.json
           │
           ├── generated TypeScript parameter block
           ├── generated PHP parameter block
           └── shared conformance vectors
                         │
                         ├── complete serialized-output parity test
                         └── seeded randomized differential test

This gives npm consumers an idiomatic TypeScript package and Packagist consumers an idiomatic PHP package without duplicating annual parameter maintenance.

npm run test:parity compares complete serialized output for every conformance vector. That set is fixed, so npm run test:fuzz compares the two engines on randomized scenarios instead — varying tax year across the supported range, account types, HSA coverage shapes and monthly patterns, existing contributions, conversions, filing statuses, and deliberately malformed inputs — and diffs the full output including thrown error codes and messages. It is deterministic: every run prints its seed, and --seed=<n> replays a failure exactly.

npm run test:fuzz                            # 5,000 scenarios, random seed
node scripts/fuzz-parity.mjs --seed=1234      # replay
node scripts/fuzz-parity.mjs --cases=50000    # deeper sweep

It runs in npm run verify and in CI because it is cheap — 10,000 scenarios take under three seconds, since the PHP side is batched into one process. Nine of the input-validation divergences fixed in this package were found by it rather than by the vectors.

Development

npm ci
npm run validate:data
npm run generate:check
npm run typecheck
npm run test:ts
npm run test:php
npm run test:parity
npm run test:fuzz
npm run verify

After changing data/retirement-parameters.json:

npm run generate
npm run verify

npm run generate:check fails if either native embedded data block differs from canonical JSON. npm run test:parity compares the complete TypeScript and PHP result for every shared vector, not merely selected assertions.

See DESIGN.md, SOURCES.md, and CONTRIBUTING.md before changing legal parameters or calculation semantics.

Deliberate exclusions

The package does not calculate:

  • State income-tax treatment.
  • HRAs of every kind — standard, ICHRA, EBHRA, QSEHRA, suspended, retiree-only — even where they interact with §223 exactly as a health FSA does. Health FSAs under §125(i), including the carryover, are modelled.
  • Archer MSAs themselves. The §220 limitation is not calculated, so an amount supplied as persons[].archerMsaContributions is taken as stated and never tested against it. The HSA §223(b)(4)(A) and §223(b)(5)(B)(i) reductions are applied, because both take an amount paid rather than an Archer limitation.
  • Cafeteria plan qualification and nondiscrimination testing under §125(b)–(d), the §414(b)/(c)/(m) controlled-group determination that §125(g)(4) applies to the health FSA limit, the Notice 2012-40 proration of a short plan year, and the uniform-coverage and run-out-period mechanics.
  • The §214 relief of the Consolidated Appropriations Act, 2021. It is entirely a plan option; a carryover computed out of 2020 or 2021 carries a diagnostic saying so.
  • Adoption assistance under §137, commuter benefits under §132(f), and educational assistance under §127.
  • The §21 dependent care credit, and the §21(c) interaction whereby §129 exclusions reduce that credit's expense base. The §129 exclusion is calculated; the credit is not.
  • The §21(d)(2) deemed-earned-income schedule that §129(b)(2) applies to a student or incapacitated spouse. The §129(b)(1) limitation itself is applied, from the earned income supplied on planRules.dependentCareFsa.
  • Whether a dependent care program meets the §129(d) written-plan and nondiscrimination requirements, the §129(c) denial for amounts paid to a related individual, and whether the individuals cared for qualify.
  • The §408(d)(9)(C) once-per-lifetime limitation on a qualified HSA funding distribution and the separate §408(d)(9)(D) testing period. The §223(b)(4)(C) reduction itself is applied, from the amount supplied as persons[].qualifiedHsaFundingDistributions, which is taken as stated.
  • The retirement savings contributions credit.
  • Required minimum distributions or distribution penalties.
  • Plan eligibility, vesting, loans, or distributions generally.
  • ADP, ACP, coverage, top-heavy, or other nondiscrimination testing.
  • Employer controlled-group ownership from raw entity records.
  • Full payroll, self-employment tax, or tax-return MAGI.
  • The pre-2002 §403(b)(2) maximum exclusion allowance and the §415(c)(4) alternative elections. Both are diagnosed and the affected years return indeterminate; neither is computed.
  • Defined-benefit or cash-balance actuarial funding, and the participant-specific §415(b)(2) and §415(b)(5) adjustments to the annual benefit limit. The flat §415(b)(1)(A) figure itself is reported.
  • Everything about a pension-linked emergency savings account except its §402A(e)(3)(A) contribution ceiling and the pools that ceiling feeds: the §402A(e)(2) eligibility test, which turns on §414(q) highly-compensated-employee status and the plan's own age and service terms; the §402A(e)(4) automatic contribution arrangement; the §402A(e)(5) participant disclosures; the §402A(e)(7) withdrawal right and the §402A(e)(8) treatment on termination; and the §402A(e)(12) anti-abuse procedures. All three §402A(f)(1) hosts are modelled, the governmental §457(b) one as its own account type. §402A(e)(9), which orders excess deferrals distributed under §402(g)(2)(A) out of the emergency account first, is not implemented at all — no excess-deferral ordering is — and its reach is in any case unsettled for a §457(b)-hosted account: it speaks of "any pension-linked emergency savings account of the participant", while a §457(b) deferral is not among the elective deferrals §402(g)(3) enumerates and so can produce no §402(g)(2)(A) excess of its own. No regulation or notice addresses the cross-plan case.
  • Investment returns, retirement sufficiency, or withdrawal planning.

License

MIT. See LICENSE.