us-tax-advantaged-params 0.1.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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/SOURCES.md +68 -0
- package/data/retirement-parameters.json +5239 -0
- package/dist/cjs/USTaxAdvantagedParams.js +7930 -0
- package/dist/cjs/USTaxAdvantagedParams.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/USTaxAdvantagedParams.js +7918 -0
- package/dist/esm/USTaxAdvantagedParams.js.map +1 -0
- package/dist/types/USTaxAdvantagedParams.d.cts +482 -0
- package/dist/types/USTaxAdvantagedParams.d.ts +482 -0
- package/package.json +89 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. The project follows Semantic Versioning.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Renamed the package and its public symbol.** The scope is broadening from federal retirement accounts to U.S. tax-advantaged accounts generally, so the identities were changed before the package acquired consumers:
|
|
10
|
+
- npm: `usa-retirement-account-parameters` → `us-tax-advantaged-params`
|
|
11
|
+
- Composer: `bherila/usa-retirement-account-parameters` → `bherila/us-tax-advantaged-params`
|
|
12
|
+
- TypeScript class / default export and PHP class and namespace: `USARetirementAccountParameters` → `USTaxAdvantagedParams`
|
|
13
|
+
- Engine, test, and built artifact filenames follow the symbol (`src/USTaxAdvantagedParams.ts`, `php/src/USTaxAdvantagedParams.php`, `dist/{esm,cjs}/USTaxAdvantagedParams.js`, `dist/types/USTaxAdvantagedParams.d.{ts,cts}`).
|
|
14
|
+
- The GitHub repository moved to `bherila/us-tax-advantaged-params`; GitHub redirects the previous path.
|
|
15
|
+
|
|
16
|
+
This is a breaking rename with no compatibility alias. Calculation behavior, the encoded 1975-2026 parameter range, the result shape, and the conformance vectors are unchanged.
|
|
17
|
+
|
|
18
|
+
- `data/retirement-parameters.json` keeps its filename and its `./data/retirement-parameters.json` subpath export; only its `package` field was updated to the new npm name. Renaming the data file is a separate module-layout decision.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `npm pack --dry-run` no longer fails through `prepack`. npm exported `npm_config_dry_run=true` into the nested `npm pack` that `check:types` runs, so no tarball was written and `attw` failed with `ENOENT`. The inner pack now passes `--no-dry-run`.
|
|
23
|
+
|
|
24
|
+
## [0.1.0] - 2026-08-28
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Designated Roth governmental 457(b) contributions are now gated on their statutory 2011 start date (Small Business Jobs Act of 2010, IRC §402A(e)(1)); earlier years return `unavailable` instead of a determinate Roth deferral.
|
|
29
|
+
- The SIMPLE 401(k) 3% matching contribution is now computed on §401(a)(17)-capped compensation; the SIMPLE IRA match correctly remains exempt from the compensation cap.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Composer autoloading switched from `files` to lazy `classmap` (the engine no longer defines namespace-level constants; `PACKAGE_NAME` and `ENGINE_VERSION` are class constants).
|
|
34
|
+
- The PHP requirement was lowered from 8.5 to 8.2, with PHP 8.2-8.5 tested in CI.
|
|
35
|
+
- The npm package now ships a CommonJS-flavored `.d.cts` declaration for `require` consumers and exports `./data/retirement-parameters.json` and `./package.json` through the exports map.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Error-expectation conformance vectors (`expectError`) exercised by both native suites and the cross-language parity check, plus new vectors covering MFS phase-outs, modern spousal IRAs, ordinary age-50 catch-up, age-64 reversion, and effective-year boundaries (1975, 2010, 2020, 2023).
|
|
40
|
+
- `@arethetypeswrong/cli` package-resolution gate, tag-triggered npm publish workflow with provenance, and Dependabot configuration.
|
|
41
|
+
- Native single-file TypeScript and PHP retirement-account calculation engines.
|
|
42
|
+
- Builder and direct-input interfaces for taxpayers, spouses, accounts, existing contributions, plan rules, and Roth conversions.
|
|
43
|
+
- Historical parameter coverage from 1975 through 2026 without future-year extrapolation.
|
|
44
|
+
- Traditional/Roth IRA sharing, IRA deduction and Roth phase-outs, spousal IRA compensation, historical restrictions, and nondeductible IRA treatment.
|
|
45
|
+
- Qualified-plan §402(g), §414(v), and §415(c) shared-pool allocation.
|
|
46
|
+
- Governmental and nongovernmental 457(b), 403(b) 15-year catch-up, 457(b) special catch-up, SIMPLE, SEP, SARSEP, TSP, employer-only plans, and explicit actuarial indeterminacy for pensions.
|
|
47
|
+
- Enhanced age-60-to-63 catch-up and high-wage Roth catch-up classification.
|
|
48
|
+
- IRA-to-Roth, qualified-plan-to-Roth-IRA, and in-plan Roth conversion calculations with aggregate pro-rata IRA basis.
|
|
49
|
+
- Canonical annual data, generation tooling, native tests, shared conformance vectors, and complete cross-runtime parity checking.
|
|
50
|
+
- ESM, CommonJS, TypeScript declarations, Composer manifest, CI, release validation, documentation, and MIT license.
|
|
51
|
+
|
|
52
|
+
#### Pre-release corrections
|
|
53
|
+
|
|
54
|
+
- Apply the applicable §401(a)(17) recognized-compensation ceiling before multiplying compensation by employer nonelective, employer matching, and common-law SEP contribution rates.
|
|
55
|
+
- For self-employed percentage contributions, compare the reduced-rate net-earnings amount with recognized compensation multiplied by the unreduced plan rate before applying other annual limits.
|
|
56
|
+
- Preserve actual-compensation treatment for employee elective deferrals rather than imposing the §401(a)(17) ceiling as an additional §402(g) cutoff.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Herila
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# us-tax-advantaged-params
|
|
2
|
+
|
|
3
|
+
[](https://github.com/bherila/us-tax-advantaged-params/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
`us-tax-advantaged-params` is a dependency-free calculation engine for historical and current U.S. tax-advantaged account parameters. Retirement accounts are the coverage today: it calculates account-level and household-level contribution capacity, IRA phase-outs, shared statutory limits, federal income effects, and Roth-conversion taxability.
|
|
7
|
+
|
|
8
|
+
The repository contains two native implementations with the same behavior:
|
|
9
|
+
|
|
10
|
+
- **TypeScript** for npm, exported as `USTaxAdvantagedParams`.
|
|
11
|
+
- **PHP 8.2+** for Packagist, in the `USTaxAdvantagedParams` namespace.
|
|
12
|
+
|
|
13
|
+
Annual legal parameters are maintained once in `data/retirement-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.
|
|
14
|
+
|
|
15
|
+
> **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.
|
|
16
|
+
|
|
17
|
+
## Supported tax years
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
USTaxAdvantagedParams.supportedTaxYears();
|
|
25
|
+
// { minimum: 1975, maximum: 2026 }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### npm
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install us-tax-advantaged-params
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The npm package provides ESM, CommonJS, and TypeScript declarations and supports Node.js 20 or later.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
// ESM
|
|
40
|
+
import USTaxAdvantagedParams from "us-tax-advantaged-params";
|
|
41
|
+
|
|
42
|
+
// CommonJS — the class is the module's default export
|
|
43
|
+
const USTaxAdvantagedParams = require("us-tax-advantaged-params").default;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Composer / Packagist
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
composer require bherila/us-tax-advantaged-params
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The PHP package requires PHP 8.2 or later and loads the native single-file implementation through Composer.
|
|
53
|
+
|
|
54
|
+
## TypeScript builder example
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import USTaxAdvantagedParams, {
|
|
58
|
+
AccountType,
|
|
59
|
+
ConversionType,
|
|
60
|
+
FilingStatus,
|
|
61
|
+
} from "us-tax-advantaged-params";
|
|
62
|
+
|
|
63
|
+
const result = USTaxAdvantagedParams.forTaxYear(2026)
|
|
64
|
+
.filingStatus(FilingStatus.MARRIED_FILING_JOINTLY)
|
|
65
|
+
.taxpayer("taxpayer", (person) => {
|
|
66
|
+
person
|
|
67
|
+
.bornIn(1963)
|
|
68
|
+
.iraCompensation(180_000)
|
|
69
|
+
.w2Compensation(180_000)
|
|
70
|
+
.rothIraMagi(240_000)
|
|
71
|
+
.traditionalIraDeductionMagi(240_000)
|
|
72
|
+
.coveredByEmployerPlan(true)
|
|
73
|
+
.priorYearFicaWages("employer-a", 180_000)
|
|
74
|
+
.aggregateTraditionalSepSimpleIraBasis(20_000)
|
|
75
|
+
.yearEndTraditionalSepSimpleIraValue(80_000);
|
|
76
|
+
})
|
|
77
|
+
.spouse("spouse", (person) => {
|
|
78
|
+
person
|
|
79
|
+
.bornIn(1970)
|
|
80
|
+
.iraCompensation(0)
|
|
81
|
+
.rothIraMagi(240_000)
|
|
82
|
+
.traditionalIraDeductionMagi(240_000)
|
|
83
|
+
.coveredByEmployerPlan(false);
|
|
84
|
+
})
|
|
85
|
+
.account(
|
|
86
|
+
"taxpayer-401k",
|
|
87
|
+
"taxpayer",
|
|
88
|
+
AccountType.TRADITIONAL_401K,
|
|
89
|
+
(account) => {
|
|
90
|
+
account
|
|
91
|
+
.employer("employer-a")
|
|
92
|
+
.annualAdditionsGroup("employer-a")
|
|
93
|
+
.planCompensation(180_000)
|
|
94
|
+
.permitsRothContributions()
|
|
95
|
+
.permitsRothCatchUp()
|
|
96
|
+
.permitsAfterTaxContributions()
|
|
97
|
+
.expectedEmployerContribution(9_000)
|
|
98
|
+
.priority(10);
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
.account("taxpayer-roth-ira", "taxpayer", AccountType.ROTH_IRA, (account) => {
|
|
102
|
+
account.priority(20);
|
|
103
|
+
})
|
|
104
|
+
.account("spouse-traditional-ira", "spouse", AccountType.TRADITIONAL_IRA, (account) => {
|
|
105
|
+
account.priority(30);
|
|
106
|
+
})
|
|
107
|
+
.conversion(
|
|
108
|
+
"ira-conversion",
|
|
109
|
+
"taxpayer",
|
|
110
|
+
ConversionType.IRA_TO_ROTH_IRA,
|
|
111
|
+
10_000,
|
|
112
|
+
)
|
|
113
|
+
.calculate();
|
|
114
|
+
|
|
115
|
+
console.log(result.accounts[0].maximumAnnualContributionBasedOnInputs);
|
|
116
|
+
console.log(result.totals.federalAgiReduction);
|
|
117
|
+
console.log(result.conversions[0].taxableAmount);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A built scenario can be inspected and calculated repeatedly:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const scenario = USTaxAdvantagedParams.forTaxYear(2026)
|
|
124
|
+
.filingStatus("MFJ")
|
|
125
|
+
.taxpayer("taxpayer", (person) => person.bornIn(1980).w2Compensation(200_000))
|
|
126
|
+
.build();
|
|
127
|
+
|
|
128
|
+
const input = scenario.toInput();
|
|
129
|
+
const result = scenario.calculate();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## PHP builder example
|
|
133
|
+
|
|
134
|
+
```php
|
|
135
|
+
<?php
|
|
136
|
+
|
|
137
|
+
declare(strict_types=1);
|
|
138
|
+
|
|
139
|
+
require_once __DIR__ . '/vendor/autoload.php';
|
|
140
|
+
|
|
141
|
+
use USTaxAdvantagedParams\AccountType;
|
|
142
|
+
use USTaxAdvantagedParams\FilingStatus;
|
|
143
|
+
use USTaxAdvantagedParams\PersonBuilder;
|
|
144
|
+
use USTaxAdvantagedParams\RetirementAccountBuilder;
|
|
145
|
+
use USTaxAdvantagedParams\USTaxAdvantagedParams as TaxAdvantagedParams;
|
|
146
|
+
|
|
147
|
+
$result = TaxAdvantagedParams::forTaxYear(2026)
|
|
148
|
+
->filingStatus(FilingStatus::MARRIED_FILING_JOINTLY)
|
|
149
|
+
->taxpayer('taxpayer', static function (PersonBuilder $person): void {
|
|
150
|
+
$person
|
|
151
|
+
->bornIn(1963)
|
|
152
|
+
->iraCompensation(180_000)
|
|
153
|
+
->w2Compensation(180_000)
|
|
154
|
+
->rothIraMagi(240_000)
|
|
155
|
+
->traditionalIraDeductionMagi(240_000)
|
|
156
|
+
->coveredByEmployerPlan(true)
|
|
157
|
+
->priorYearFicaWages('employer-a', 180_000);
|
|
158
|
+
})
|
|
159
|
+
->spouse('spouse', static function (PersonBuilder $person): void {
|
|
160
|
+
$person
|
|
161
|
+
->bornIn(1970)
|
|
162
|
+
->iraCompensation(0)
|
|
163
|
+
->rothIraMagi(240_000)
|
|
164
|
+
->traditionalIraDeductionMagi(240_000)
|
|
165
|
+
->coveredByEmployerPlan(false);
|
|
166
|
+
})
|
|
167
|
+
->account(
|
|
168
|
+
'taxpayer-401k',
|
|
169
|
+
'taxpayer',
|
|
170
|
+
AccountType::TRADITIONAL_401K,
|
|
171
|
+
static function (RetirementAccountBuilder $account): void {
|
|
172
|
+
$account
|
|
173
|
+
->employer('employer-a')
|
|
174
|
+
->annualAdditionsGroup('employer-a')
|
|
175
|
+
->planCompensation(180_000)
|
|
176
|
+
->permitsRothContributions()
|
|
177
|
+
->permitsRothCatchUp()
|
|
178
|
+
->permitsAfterTaxContributions()
|
|
179
|
+
->expectedEmployerContribution(9_000)
|
|
180
|
+
->priority(10);
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
->account('spouse-ira', 'spouse', AccountType::TRADITIONAL_IRA)
|
|
184
|
+
->calculate();
|
|
185
|
+
|
|
186
|
+
var_dump($result['totals']);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The PHP result is an associative-array equivalent of the TypeScript result. Enum values serialize to the same snake-case strings.
|
|
190
|
+
|
|
191
|
+
## Direct unified interface
|
|
192
|
+
|
|
193
|
+
Builders are optional. Both engines accept the same language-neutral scenario shape, which is useful for services, fixtures, database records, and cross-runtime integrations.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const result = USTaxAdvantagedParams.calculate({
|
|
197
|
+
taxYear: 2026,
|
|
198
|
+
filingStatus: "HOH",
|
|
199
|
+
persons: [
|
|
200
|
+
{
|
|
201
|
+
id: "taxpayer",
|
|
202
|
+
role: "taxpayer",
|
|
203
|
+
birthYear: 1975,
|
|
204
|
+
compensation: { iraCompensation: 140_000, w2Compensation: 140_000 },
|
|
205
|
+
magi: { rothIra: 158_000, traditionalIraDeduction: 158_000 },
|
|
206
|
+
coveredByEmployerRetirementPlan: true,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
accounts: [
|
|
210
|
+
{
|
|
211
|
+
id: "401k",
|
|
212
|
+
ownerId: "taxpayer",
|
|
213
|
+
type: "traditional_401k",
|
|
214
|
+
employerId: "employer-a",
|
|
215
|
+
planRules: {
|
|
216
|
+
planCompensation: 140_000,
|
|
217
|
+
annualAdditionsGroupId: "employer-a",
|
|
218
|
+
expectedEmployerContribution: 7_000,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{ id: "roth-ira", ownerId: "taxpayer", type: "roth_ira", priority: 20 },
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
228
|
+
## Account coverage
|
|
229
|
+
|
|
230
|
+
| Family | Account types |
|
|
231
|
+
|---|---|
|
|
232
|
+
| Individual retirement arrangements | Traditional IRA, Roth IRA, rollover IRA, payroll-deduction IRA, deemed traditional/Roth IRA, inherited traditional/Roth IRA |
|
|
233
|
+
| Small-employer arrangements | SEP IRA, Roth SEP IRA, SIMPLE IRA, Roth SIMPLE IRA, grandfathered SARSEP |
|
|
234
|
+
| Qualified elective plans | Traditional/Roth 401(k), Solo/Roth Solo 401(k), SIMPLE/Roth SIMPLE 401(k), starter 401(k) |
|
|
235
|
+
| Tax-sheltered annuities | Traditional/Roth 403(b), safe-harbor deferral-only 403(b) |
|
|
236
|
+
| Deferred compensation | Governmental/Roth governmental 457(b), nongovernmental eligible 457(b), 457(f) |
|
|
237
|
+
| Federal plan | Traditional and Roth TSP |
|
|
238
|
+
| Employer-only defined-contribution plans | 401(a), profit-sharing, money-purchase, Keogh, ESOP |
|
|
239
|
+
| Pension arrangements | Defined-benefit and cash-balance plans |
|
|
240
|
+
|
|
241
|
+
Defined-benefit and cash-balance contributions are deliberately returned as `indeterminate`; their funding requires the plan formula, census, assets, actuarial assumptions, and funding rules.
|
|
242
|
+
|
|
243
|
+
## Result semantics
|
|
244
|
+
|
|
245
|
+
| Field | Meaning |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `statutoryMaximumAnnualContribution` | Overall monetary legal ceiling when determinable from encoded law and supplied facts |
|
|
248
|
+
| `maximumAnnualContributionBasedOnInputs` | Maximum supported by law and supplied plan capabilities/formulas |
|
|
249
|
+
| `maximumAdditionalContributionBasedOnInputs` | Remaining supported amount after existing contributions |
|
|
250
|
+
| `existingAnnualContribution` | Existing contribution components supplied by the caller |
|
|
251
|
+
| `planTermDependentCapacity` | Potential space that cannot be allocated without additional plan/employer facts |
|
|
252
|
+
| `contributionComponents` | Pretax, Roth, after-tax, employer, IRA, and catch-up components |
|
|
253
|
+
| `federalTaxEffects` | Federal AGI, taxable-income, W-2 box 1, nondeductible, after-tax/Roth, and conversion effects |
|
|
254
|
+
| `sharedLimits` | Audit trail showing each statutory pool used by the account |
|
|
255
|
+
| `diagnostics` | Assumptions, warnings, unavailable rules, and legal references |
|
|
256
|
+
|
|
257
|
+
`maximumAnnualContributionBasedOnInputs` is a mechanical result, not a contribution recommendation.
|
|
258
|
+
|
|
259
|
+
## Shared-limit allocation
|
|
260
|
+
|
|
261
|
+
Accounts are allocated in ascending `priority` and then input order. This makes overlapping limits deterministic.
|
|
262
|
+
|
|
263
|
+
The engine tracks, among other pools:
|
|
264
|
+
|
|
265
|
+
- Traditional and Roth IRA contributions per owner.
|
|
266
|
+
- Joint-return compensation available for spousal IRAs.
|
|
267
|
+
- The owner-level §402(g) elective-deferral limit across applicable 401(k), 403(b), TSP, SARSEP, and SIMPLE sources.
|
|
268
|
+
- The owner-level §414(v) age-based catch-up pool.
|
|
269
|
+
- A separate §457(b) limit.
|
|
270
|
+
- §415(c) annual additions per participant and controlled-employer group.
|
|
271
|
+
- The owner-level 403(b) 15-years-of-service catch-up pool.
|
|
272
|
+
- The 457(b) last-three-years special catch-up.
|
|
273
|
+
|
|
274
|
+
Use the same `annualAdditionsGroupId` for plans that share one §415(c) controlled-employer limit. Unrelated employers should normally use different group IDs.
|
|
275
|
+
|
|
276
|
+
## Recognized compensation under §401(a)(17)
|
|
277
|
+
|
|
278
|
+
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:
|
|
279
|
+
|
|
280
|
+
- Employer nonelective formulas.
|
|
281
|
+
- Employer matching formulas whose matchable compensation is expressed as a fraction of compensation.
|
|
282
|
+
- Common-law employee SEP formulas.
|
|
283
|
+
- The plan-rate side of self-employed SEP and qualified-plan formulas.
|
|
284
|
+
|
|
285
|
+
For a self-employed owner, the maximum percentage contribution is the lesser of:
|
|
286
|
+
|
|
287
|
+
1. net earnings after the deductible half of self-employment tax multiplied by the reduced self-employed rate; and
|
|
288
|
+
2. recognized compensation multiplied by the unreduced plan contribution rate.
|
|
289
|
+
|
|
290
|
+
The result remains subject to §415(c), plan-document limits, and existing annual additions.
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
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.
|
|
297
|
+
|
|
298
|
+
## IRA phase-outs and spousal IRAs
|
|
299
|
+
|
|
300
|
+
The package models:
|
|
301
|
+
|
|
302
|
+
- The combined traditional/Roth IRA annual contribution limit.
|
|
303
|
+
- Age-50 IRA catch-up amounts.
|
|
304
|
+
- Roth IRA MAGI phase-outs.
|
|
305
|
+
- Traditional IRA active-participant deduction phase-outs.
|
|
306
|
+
- The separate phase-out for a noncovered spouse married to a covered participant.
|
|
307
|
+
- Married-filing-separately rules, including whether spouses lived together during the year.
|
|
308
|
+
- MFJ spousal-IRA compensation sharing.
|
|
309
|
+
- Historical one-earner spousal limits.
|
|
310
|
+
- The pre-2020 traditional-IRA age-70½ contribution restriction.
|
|
311
|
+
- Nondeductible traditional IRA capacity when a deduction is unavailable.
|
|
312
|
+
- IRS worksheet-style phase-out rounding and the positive reduced minimum.
|
|
313
|
+
|
|
314
|
+
Supply the MAGI value applicable to each calculation. The engine does not derive tax-return MAGI from raw income items.
|
|
315
|
+
|
|
316
|
+
## Catch-up contributions and birth data
|
|
317
|
+
|
|
318
|
+
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.
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
Supported catch-up logic includes:
|
|
323
|
+
|
|
324
|
+
- Ordinary age-50 catch-up.
|
|
325
|
+
- Enhanced age-60-to-63 catch-up beginning in 2025.
|
|
326
|
+
- 403(b) 15-years-of-service catch-up, including annual and lifetime residuals.
|
|
327
|
+
- Governmental 457(b) age catch-up.
|
|
328
|
+
- The 457(b) special last-three-years catch-up, selecting the larger applicable method rather than combining incompatible methods.
|
|
329
|
+
- High-wage Roth catch-up classification using prior-year FICA wages for the sponsoring employer when applicable.
|
|
330
|
+
|
|
331
|
+
## Roth conversions and in-plan Roth rollovers
|
|
332
|
+
|
|
333
|
+
Conversions are separate from contributions and do not consume the annual IRA or elective-deferral limit.
|
|
334
|
+
|
|
335
|
+
Supported conversion categories are:
|
|
336
|
+
|
|
337
|
+
- Traditional/SEP/SIMPLE IRA to Roth IRA.
|
|
338
|
+
- Qualified plan to Roth IRA.
|
|
339
|
+
- In-plan Roth rollover.
|
|
340
|
+
|
|
341
|
+
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.
|
|
342
|
+
|
|
343
|
+
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.
|
|
344
|
+
|
|
345
|
+
## Calculation status and diagnostics
|
|
346
|
+
|
|
347
|
+
Possible statuses are:
|
|
348
|
+
|
|
349
|
+
- `determinate`
|
|
350
|
+
- `determinate_with_assumptions`
|
|
351
|
+
- `indeterminate`
|
|
352
|
+
- `unavailable`
|
|
353
|
+
- `ineligible`
|
|
354
|
+
|
|
355
|
+
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.
|
|
356
|
+
|
|
357
|
+
## Native TypeScript/PHP parity
|
|
358
|
+
|
|
359
|
+
The DRY boundary is the statutory data and behavioral specification, not a cross-language runtime dependency:
|
|
360
|
+
|
|
361
|
+
```text
|
|
362
|
+
data/retirement-parameters.json
|
|
363
|
+
│
|
|
364
|
+
├── generated TypeScript parameter block
|
|
365
|
+
├── generated PHP parameter block
|
|
366
|
+
└── shared conformance vectors
|
|
367
|
+
│
|
|
368
|
+
└── complete serialized-output parity test
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
This gives npm consumers an idiomatic TypeScript package and Packagist consumers an idiomatic PHP package without duplicating annual parameter maintenance.
|
|
372
|
+
|
|
373
|
+
## Development
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
npm ci
|
|
377
|
+
npm run validate:data
|
|
378
|
+
npm run generate:check
|
|
379
|
+
npm run typecheck
|
|
380
|
+
npm run test:ts
|
|
381
|
+
npm run test:php
|
|
382
|
+
npm run test:parity
|
|
383
|
+
npm run verify
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
After changing `data/retirement-parameters.json`:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
npm run generate
|
|
390
|
+
npm run verify
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`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.
|
|
394
|
+
|
|
395
|
+
See [DESIGN.md](DESIGN.md), [SOURCES.md](SOURCES.md), and [CONTRIBUTING.md](CONTRIBUTING.md) before changing legal parameters or calculation semantics.
|
|
396
|
+
|
|
397
|
+
## Deliberate exclusions
|
|
398
|
+
|
|
399
|
+
The package does not calculate:
|
|
400
|
+
|
|
401
|
+
- State income-tax treatment.
|
|
402
|
+
- The retirement savings contributions credit.
|
|
403
|
+
- Required minimum distributions or distribution penalties.
|
|
404
|
+
- Plan eligibility, vesting, loans, or distributions generally.
|
|
405
|
+
- ADP, ACP, coverage, top-heavy, or other nondiscrimination testing.
|
|
406
|
+
- Employer controlled-group ownership from raw entity records.
|
|
407
|
+
- Full payroll, self-employment tax, or tax-return MAGI.
|
|
408
|
+
- Defined-benefit or cash-balance actuarial funding.
|
|
409
|
+
- Investment returns, retirement sufficiency, or withdrawal planning.
|
|
410
|
+
|
|
411
|
+
## License
|
|
412
|
+
|
|
413
|
+
MIT. See [LICENSE](LICENSE).
|
package/SOURCES.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Primary Authorities and Data Provenance
|
|
2
|
+
|
|
3
|
+
## Source policy
|
|
4
|
+
|
|
5
|
+
Retirement parameters are maintained from primary federal sources whenever available. The canonical machine-readable source list is embedded in `data/retirement-parameters.json` and exposed at runtime through `USTaxAdvantagedParams.sourceMetadata()`.
|
|
6
|
+
|
|
7
|
+
Source metadata documents provenance; it is not fetched at runtime. Calculations are deterministic and network-free.
|
|
8
|
+
|
|
9
|
+
## Core annual-limit sources
|
|
10
|
+
|
|
11
|
+
- **IRS Notice 2025-67 / Internal Revenue Bulletin 2025-49** — 2026 retirement-plan and IRA cost-of-living adjustments, including §402(g), IRA, SIMPLE, catch-up, §415(c), §401(a)(17), SEP eligibility, and phase-out figures.
|
|
12
|
+
`https://www.irs.gov/pub/irs-irbs/irb25-49.pdf`
|
|
13
|
+
|
|
14
|
+
- **IRS Notice 2024-80 / Internal Revenue Bulletin 2024-47** — 2025 retirement-plan and IRA cost-of-living adjustments.
|
|
15
|
+
`https://www.irs.gov/pub/irs-irbs/irb24-47.pdf`
|
|
16
|
+
|
|
17
|
+
- **IRS Cash or Deferred Arrangements manual** — historical elective-deferral and related qualified-plan limitation tables and mechanics.
|
|
18
|
+
`https://www.irs.gov/pub/irs-tege/codas.pdf`
|
|
19
|
+
|
|
20
|
+
- **IRS SEP/SARSEP Audit Techniques** — SEP/SARSEP historical and operational limits.
|
|
21
|
+
`https://www.irs.gov/pub/irs-tege/epche1303.pdf`
|
|
22
|
+
|
|
23
|
+
- **IRS Statistics of Income Bulletin describing early IRA law** — historical 1981-and-earlier and 1982 IRA limits.
|
|
24
|
+
`https://www.irs.gov/pub/irs-soi/83rpsumbul.pdf`
|
|
25
|
+
|
|
26
|
+
- **U.S. Department of Labor 401(k) history** — contextual history of 401(k) plans and their development.
|
|
27
|
+
`https://www.dol.gov/agencies/ebsa/about-ebsa/our-activities/resource-center/faqs/401k-plans`
|
|
28
|
+
|
|
29
|
+
## Section 401(a)(17) employer-formula authorities
|
|
30
|
+
|
|
31
|
+
- **IRS Notice 2001-56** states that §401(a)(17) limits annual compensation used to determine allocations under a defined-contribution plan and discusses the effective date of the EGTRRA compensation-limit increase.
|
|
32
|
+
`https://www.irs.gov/pub/irs-drop/n-01-56.pdf`
|
|
33
|
+
|
|
34
|
+
- **IRS Employee Plans News, Fall 2009, “When Limits Collide, Which One Wins?”** distinguishes §401(a)(17), §415(c), and §402(g). It describes the compensation cap for plan contributions while explaining why the cap should not be applied as an extra cutoff preventing an employee from completing the annual elective-deferral amount.
|
|
35
|
+
`https://www.irs.gov/pub/irs-tege/fall09.pdf`
|
|
36
|
+
|
|
37
|
+
- **IRS Publication 535 (2001), self-employed qualified-plan worksheet** demonstrates that self-employed contribution calculations compare the reduced-rate net-earnings amount with the annual compensation ceiling multiplied by the unreduced plan rate.
|
|
38
|
+
`https://www.irs.gov/pub/irs-prior/p535--2001.pdf`
|
|
39
|
+
|
|
40
|
+
- **IRS SARSEP Fix-it Guide** describes SEP/SARSEP total contribution limits as the lesser of the annual dollar limit and the applicable percentage of compensation, with compensation considered subject to the annual limit.
|
|
41
|
+
`https://www.irs.gov/retirement-plans/sarsep-fix-it-guide-total-contributions-employee-elective-deferrals-and-nonelective-employer-contributions-exceeded-the-maximum-legal-limits`
|
|
42
|
+
|
|
43
|
+
## IRA phase-outs and rounding
|
|
44
|
+
|
|
45
|
+
Annual IRA contribution and phase-out values are encoded from the applicable IRS annual adjustment notices and IRA publications. The calculation follows the IRS reduced-contribution worksheet pattern: determine the reduction fraction, subtract it from the otherwise available contribution, round the result upward to the specified increment, apply the positive reduced minimum when applicable, and subtract other IRA contributions sharing the annual limit.
|
|
46
|
+
|
|
47
|
+
## Historical confidence policy
|
|
48
|
+
|
|
49
|
+
Historical tax law is not uniform across the full 1975–2026 range. Early salary-reduction arrangements, legacy 403(b) exclusion allowances, plan-document-specific limits, and actuarial pension funding cannot always be represented by one universal amount. The engines return diagnostics and an indeterminate result where the data does not support a precise answer.
|
|
50
|
+
|
|
51
|
+
A historical row must not be added solely from a secondary summary when an IRS bulletin, publication, notice, regulation, statute, or Department of Labor source is reasonably available.
|
|
52
|
+
|
|
53
|
+
## Annual update checklist
|
|
54
|
+
|
|
55
|
+
For each new tax year:
|
|
56
|
+
|
|
57
|
+
1. Obtain the final IRS cost-of-living adjustment notice or revenue procedure.
|
|
58
|
+
2. Update all relevant limit, catch-up, compensation, SEP/SIMPLE, 457(b), starter-plan, and IRA phase-out fields.
|
|
59
|
+
3. Add the primary source to the canonical source list.
|
|
60
|
+
4. Run `npm run generate`.
|
|
61
|
+
5. Add conformance vectors for changed rules and boundary values.
|
|
62
|
+
6. Run `npm run verify` under supported Node and PHP versions.
|
|
63
|
+
7. Review the generated diff in both runtime files; only generated data blocks should change for a data-only annual update.
|
|
64
|
+
8. Record the update in `CHANGELOG.md` and update the package version as appropriate.
|
|
65
|
+
|
|
66
|
+
## Legal-change policy
|
|
67
|
+
|
|
68
|
+
A statutory or regulatory change that affects algorithmic behavior requires more than a data-row update. It should include a primary authority, a design note covering aggregation/effective date/transition relief/plan dependencies, native tests in both languages, shared conformance vectors, full-output parity, and migration notes when serialized contracts change.
|