us-tax-advantaged-params 0.1.0 → 0.2.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 CHANGED
@@ -4,8 +4,30 @@ All notable changes to this project will be documented in this file. The project
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.0] - 2026-08-30
8
+
7
9
  ### Changed
8
10
 
11
+ - **Renamed the remaining retirement-scoped public symbols.** The package models only retirement accounts today, so these names were accurate, but they become wrong the moment a non-retirement account type lands, and renaming them after adoption would be a breaking change for real consumers. Done now, while the consumer count is zero:
12
+
13
+ | Before | After |
14
+ | --- | --- |
15
+ | `RetirementAccountInput` | `AccountInput` |
16
+ | `RetirementScenarioInput` | `ScenarioInput` |
17
+ | `RetirementScenarioResult` | `ScenarioResult` |
18
+ | `RetirementAccountBuilder` | `AccountBuilder` |
19
+ | `RetirementScenario` | `Scenario` |
20
+ | `RetirementScenarioBuilder` | `ScenarioBuilder` |
21
+ | `RetirementParameterError` (TS) | `ParameterError` |
22
+ | `RetirementParameterException` (PHP) | `ParameterException` |
23
+ | `calculateRetirementScenario` (TS) | `calculateScenario` |
24
+
25
+ This makes the surface internally consistent: the prefix was already absent from `PersonBuilder`, `RothConversionBuilder`, `AccountType`, and `UnsupportedTaxYearError`/`Exception`, so the retirement-prefixed names were the minority, not the convention. `UnsupportedTaxYearError extends ParameterError` (and the PHP equivalent) is unchanged.
26
+
27
+ Genuinely retirement-specific *data* fields keep their names, because they describe retirement plans and will still be correct alongside other account types: `coveredByEmployerRetirementPlan` (the §219(g) active-participant test) and `selfEmployedRetirementDeduction`.
28
+
29
+ Breaking, with no compatibility aliases. Calculation behavior, the encoded 1975-2026 parameter range, the result shape, and the conformance vectors are unchanged — the 46-vector cross-language parity suite passes untouched.
30
+
9
31
  - **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
32
  - npm: `usa-retirement-account-parameters` → `us-tax-advantaged-params`
11
33
  - Composer: `bherila/usa-retirement-account-parameters` → `bherila/us-tax-advantaged-params`
@@ -23,6 +45,8 @@ All notable changes to this project will be documented in this file. The project
23
45
 
24
46
  ## [0.1.0] - 2026-08-28
25
47
 
48
+ > **The `0.1.0` published to npm on 2026-08-30 does not match this entry.** It was built from a tree that already carried the package/symbol rename listed under 0.2.0 above, so its public API is `USTaxAdvantagedParams` with the `Retirement*`-prefixed member names — a combination no tagged release describes. It exists only because the rename was published before a release entry was cut. Use 0.2.0; `0.1.0` is superseded and should be treated as a bootstrap artifact.
49
+
26
50
  ### Fixed
27
51
 
28
52
  - 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.
package/README.md CHANGED
@@ -141,7 +141,7 @@ require_once __DIR__ . '/vendor/autoload.php';
141
141
  use USTaxAdvantagedParams\AccountType;
142
142
  use USTaxAdvantagedParams\FilingStatus;
143
143
  use USTaxAdvantagedParams\PersonBuilder;
144
- use USTaxAdvantagedParams\RetirementAccountBuilder;
144
+ use USTaxAdvantagedParams\AccountBuilder;
145
145
  use USTaxAdvantagedParams\USTaxAdvantagedParams as TaxAdvantagedParams;
146
146
 
147
147
  $result = TaxAdvantagedParams::forTaxYear(2026)
@@ -168,7 +168,7 @@ $result = TaxAdvantagedParams::forTaxYear(2026)
168
168
  'taxpayer-401k',
169
169
  'taxpayer',
170
170
  AccountType::TRADITIONAL_401K,
171
- static function (RetirementAccountBuilder $account): void {
171
+ static function (AccountBuilder $account): void {
172
172
  $account
173
173
  ->employer('employer-a')
174
174
  ->annualAdditionsGroup('employer-a')
@@ -13,10 +13,10 @@
13
13
  * determined defined-benefit/cash-balance contributions.
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.USTaxAdvantagedParams = exports.RetirementScenarioBuilder = exports.RetirementScenario = exports.RothConversionBuilder = exports.RetirementAccountBuilder = exports.PersonBuilder = exports.UnsupportedTaxYearError = exports.RetirementParameterError = exports.DiagnosticSeverity = exports.CalculationStatus = exports.ConversionType = exports.AccountType = exports.FilingStatus = exports.ENGINE_VERSION = exports.PACKAGE_NAME = void 0;
17
- exports.calculateRetirementScenario = calculateRetirementScenario;
16
+ exports.USTaxAdvantagedParams = exports.ScenarioBuilder = exports.Scenario = exports.RothConversionBuilder = exports.AccountBuilder = exports.PersonBuilder = exports.UnsupportedTaxYearError = exports.ParameterError = exports.DiagnosticSeverity = exports.CalculationStatus = exports.ConversionType = exports.AccountType = exports.FilingStatus = exports.ENGINE_VERSION = exports.PACKAGE_NAME = void 0;
17
+ exports.calculateScenario = calculateScenario;
18
18
  exports.PACKAGE_NAME = "us-tax-advantaged-params";
19
- exports.ENGINE_VERSION = "0.1.0";
19
+ exports.ENGINE_VERSION = "0.2.0";
20
20
  var FilingStatus;
21
21
  (function (FilingStatus) {
22
22
  FilingStatus["SINGLE"] = "single";
@@ -5325,16 +5325,16 @@ const RAW_PARAMETERS = {
5325
5325
  }
5326
5326
  };
5327
5327
  /* </generated-parameters> */
5328
- class RetirementParameterError extends Error {
5328
+ class ParameterError extends Error {
5329
5329
  code;
5330
5330
  constructor(code, message) {
5331
5331
  super(message);
5332
- this.name = "RetirementParameterError";
5332
+ this.name = "ParameterError";
5333
5333
  this.code = code;
5334
5334
  }
5335
5335
  }
5336
- exports.RetirementParameterError = RetirementParameterError;
5337
- class UnsupportedTaxYearError extends RetirementParameterError {
5336
+ exports.ParameterError = ParameterError;
5337
+ class UnsupportedTaxYearError extends ParameterError {
5338
5338
  constructor(year, minimum, maximum) {
5339
5339
  super("UNSUPPORTED_TAX_YEAR", `Tax year ${year} is not supported. Supported years are ${minimum}-${maximum}; future years are never extrapolated.`);
5340
5340
  this.name = "UnsupportedTaxYearError";
@@ -5597,7 +5597,7 @@ function parseFilingStatus(value, diagnostics) {
5597
5597
  const token = normalizeToken(String(value));
5598
5598
  const parsed = FILING_STATUS_ALIASES[token];
5599
5599
  if (!parsed) {
5600
- throw new RetirementParameterError("INVALID_FILING_STATUS", `Unsupported filing status: ${value}`);
5600
+ throw new ParameterError("INVALID_FILING_STATUS", `Unsupported filing status: ${value}`);
5601
5601
  }
5602
5602
  if (token === "M") {
5603
5603
  diagnostics?.push(diagnostic("AMBIGUOUS_M_ALIAS_ASSUMED_MFJ", DiagnosticSeverity.WARNING, 'Filing-status alias "M" was interpreted as married filing jointly. Use MFJ or MFS to be explicit.', "filingStatus"));
@@ -5610,7 +5610,7 @@ function parseAccountType(value) {
5610
5610
  }
5611
5611
  const parsed = ACCOUNT_TYPE_ALIASES[normalizeToken(String(value))];
5612
5612
  if (!parsed) {
5613
- throw new RetirementParameterError("INVALID_ACCOUNT_TYPE", `Unsupported retirement account type: ${value}`);
5613
+ throw new ParameterError("INVALID_ACCOUNT_TYPE", `Unsupported retirement account type: ${value}`);
5614
5614
  }
5615
5615
  return parsed;
5616
5616
  }
@@ -5620,7 +5620,7 @@ function parseConversionType(value) {
5620
5620
  }
5621
5621
  const parsed = CONVERSION_TYPE_ALIASES[normalizeToken(String(value))];
5622
5622
  if (!parsed) {
5623
- throw new RetirementParameterError("INVALID_CONVERSION_TYPE", `Unsupported Roth conversion type: ${value}`);
5623
+ throw new ParameterError("INVALID_CONVERSION_TYPE", `Unsupported Roth conversion type: ${value}`);
5624
5624
  }
5625
5625
  return parsed;
5626
5626
  }
@@ -5631,7 +5631,7 @@ function money(value, path, defaultValue = 0) {
5631
5631
  if (value === undefined || value === null)
5632
5632
  return defaultValue;
5633
5633
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
5634
- throw new RetirementParameterError("INVALID_MONEY", `${path} must be a finite, nonnegative number.`);
5634
+ throw new ParameterError("INVALID_MONEY", `${path} must be a finite, nonnegative number.`);
5635
5635
  }
5636
5636
  return roundMoney(value);
5637
5637
  }
@@ -5639,7 +5639,7 @@ function rate(value, path, defaultValue = 0) {
5639
5639
  if (value === undefined || value === null)
5640
5640
  return defaultValue;
5641
5641
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
5642
- throw new RetirementParameterError("INVALID_RATE", `${path} must be a number from 0 through 1.`);
5642
+ throw new ParameterError("INVALID_RATE", `${path} must be a number from 0 through 1.`);
5643
5643
  }
5644
5644
  return value;
5645
5645
  }
@@ -5781,7 +5781,7 @@ function mergeTaxEffects(target, source) {
5781
5781
  }
5782
5782
  function getParametersForYear(year) {
5783
5783
  if (!Number.isInteger(year)) {
5784
- throw new RetirementParameterError("INVALID_TAX_YEAR", "taxYear must be an integer.");
5784
+ throw new ParameterError("INVALID_TAX_YEAR", "taxYear must be an integer.");
5785
5785
  }
5786
5786
  const { minimum, maximum } = RAW_PARAMETERS.supportedTaxYears;
5787
5787
  if (year < minimum || year > maximum || !RAW_PARAMETERS.years[String(year)]) {
@@ -5794,18 +5794,18 @@ function deepClone(value) {
5794
5794
  }
5795
5795
  function normalizePersons(persons) {
5796
5796
  if (!Array.isArray(persons) || persons.length === 0) {
5797
- throw new RetirementParameterError("PERSON_REQUIRED", "At least one person is required.");
5797
+ throw new ParameterError("PERSON_REQUIRED", "At least one person is required.");
5798
5798
  }
5799
5799
  const result = new Map();
5800
5800
  for (const [index, input] of persons.entries()) {
5801
5801
  if (!input.id?.trim()) {
5802
- throw new RetirementParameterError("PERSON_ID_REQUIRED", `persons[${index}].id is required.`);
5802
+ throw new ParameterError("PERSON_ID_REQUIRED", `persons[${index}].id is required.`);
5803
5803
  }
5804
5804
  if (result.has(input.id)) {
5805
- throw new RetirementParameterError("DUPLICATE_PERSON_ID", `Duplicate person ID: ${input.id}`);
5805
+ throw new ParameterError("DUPLICATE_PERSON_ID", `Duplicate person ID: ${input.id}`);
5806
5806
  }
5807
5807
  if (input.birthYear !== undefined && (!Number.isInteger(input.birthYear) || input.birthYear < 1800 || input.birthYear > 3000)) {
5808
- throw new RetirementParameterError("INVALID_BIRTH_YEAR", `persons[${index}].birthYear is invalid.`);
5808
+ throw new ParameterError("INVALID_BIRTH_YEAR", `persons[${index}].birthYear is invalid.`);
5809
5809
  }
5810
5810
  if (input.birthDate !== undefined)
5811
5811
  validateIsoDate(input.birthDate, `persons[${index}].birthDate`);
@@ -5823,7 +5823,7 @@ function normalizePersons(persons) {
5823
5823
  }
5824
5824
  const role = input.role ?? (index === 0 ? "taxpayer" : index === 1 ? "spouse" : "other");
5825
5825
  if (role !== "taxpayer" && role !== "spouse" && role !== "other") {
5826
- throw new RetirementParameterError("INVALID_PERSON_ROLE", `persons[${index}].role must be taxpayer, spouse, or other.`);
5826
+ throw new ParameterError("INVALID_PERSON_ROLE", `persons[${index}].role must be taxpayer, spouse, or other.`);
5827
5827
  }
5828
5828
  result.set(input.id, {
5829
5829
  ...input,
@@ -5845,7 +5845,7 @@ function normalizePersons(persons) {
5845
5845
  for (const role of ["taxpayer", "spouse"]) {
5846
5846
  const matching = [...result.values()].filter((person) => person.role === role);
5847
5847
  if (matching.length > 1) {
5848
- throw new RetirementParameterError("DUPLICATE_PERSON_ROLE", `Only one person may have the ${role} role; found ${matching.map((person) => person.id).join(", ")}.`);
5848
+ throw new ParameterError("DUPLICATE_PERSON_ROLE", `Only one person may have the ${role} role; found ${matching.map((person) => person.id).join(", ")}.`);
5849
5849
  }
5850
5850
  }
5851
5851
  return result;
@@ -5854,14 +5854,14 @@ function normalizeAccounts(accounts, persons) {
5854
5854
  const ids = new Set();
5855
5855
  return accounts.map((input, index) => {
5856
5856
  if (!input.id?.trim()) {
5857
- throw new RetirementParameterError("ACCOUNT_ID_REQUIRED", `accounts[${index}].id is required.`);
5857
+ throw new ParameterError("ACCOUNT_ID_REQUIRED", `accounts[${index}].id is required.`);
5858
5858
  }
5859
5859
  if (ids.has(input.id)) {
5860
- throw new RetirementParameterError("DUPLICATE_ACCOUNT_ID", `Duplicate account ID: ${input.id}`);
5860
+ throw new ParameterError("DUPLICATE_ACCOUNT_ID", `Duplicate account ID: ${input.id}`);
5861
5861
  }
5862
5862
  ids.add(input.id);
5863
5863
  if (!persons.has(input.ownerId)) {
5864
- throw new RetirementParameterError("UNKNOWN_ACCOUNT_OWNER", `Account ${input.id} references unknown owner ${input.ownerId}.`);
5864
+ throw new ParameterError("UNKNOWN_ACCOUNT_OWNER", `Account ${input.id} references unknown owner ${input.ownerId}.`);
5865
5865
  }
5866
5866
  const planRules = input.planRules ?? {};
5867
5867
  validatePlanRules(planRules, `accounts[${index}].planRules`);
@@ -5890,7 +5890,7 @@ function validatePlanRules(rules, path) {
5890
5890
  if (rules.special403bCatchUp) {
5891
5891
  const special = rules.special403bCatchUp;
5892
5892
  if (!Number.isFinite(special.yearsOfService) || special.yearsOfService < 0) {
5893
- throw new RetirementParameterError("INVALID_YEARS_OF_SERVICE", `${path}.special403bCatchUp.yearsOfService is invalid.`);
5893
+ throw new ParameterError("INVALID_YEARS_OF_SERVICE", `${path}.special403bCatchUp.yearsOfService is invalid.`);
5894
5894
  }
5895
5895
  money(special.priorElectiveDeferrals, `${path}.special403bCatchUp.priorElectiveDeferrals`);
5896
5896
  money(special.priorSpecialCatchUpUsed, `${path}.special403bCatchUp.priorSpecialCatchUpUsed`);
@@ -5901,12 +5901,12 @@ function validatePlanRules(rules, path) {
5901
5901
  }
5902
5902
  function validateIsoDate(value, path) {
5903
5903
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
5904
- throw new RetirementParameterError("INVALID_DATE", `${path} must use YYYY-MM-DD.`);
5904
+ throw new ParameterError("INVALID_DATE", `${path} must use YYYY-MM-DD.`);
5905
5905
  }
5906
5906
  const [year, month, day] = value.split("-").map(Number);
5907
5907
  const date = new Date(Date.UTC(year, month - 1, day));
5908
5908
  if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
5909
- throw new RetirementParameterError("INVALID_DATE", `${path} is not a valid calendar date.`);
5909
+ throw new ParameterError("INVALID_DATE", `${path} is not a valid calendar date.`);
5910
5910
  }
5911
5911
  }
5912
5912
  function ageAtEndOfTaxYear(person, taxYear) {
@@ -7244,17 +7244,17 @@ function normalizeConversions(conversions, persons, accountsById) {
7244
7244
  const ids = new Set();
7245
7245
  return conversions.map((input, index) => {
7246
7246
  if (!input.id?.trim()) {
7247
- throw new RetirementParameterError("CONVERSION_ID_REQUIRED", `conversions[${index}].id is required.`);
7247
+ throw new ParameterError("CONVERSION_ID_REQUIRED", `conversions[${index}].id is required.`);
7248
7248
  }
7249
7249
  if (ids.has(input.id)) {
7250
- throw new RetirementParameterError("DUPLICATE_CONVERSION_ID", `Duplicate conversion ID: ${input.id}`);
7250
+ throw new ParameterError("DUPLICATE_CONVERSION_ID", `Duplicate conversion ID: ${input.id}`);
7251
7251
  }
7252
7252
  ids.add(input.id);
7253
7253
  if (!persons.has(input.ownerId)) {
7254
- throw new RetirementParameterError("UNKNOWN_CONVERSION_OWNER", `Conversion ${input.id} references unknown owner ${input.ownerId}.`);
7254
+ throw new ParameterError("UNKNOWN_CONVERSION_OWNER", `Conversion ${input.id} references unknown owner ${input.ownerId}.`);
7255
7255
  }
7256
7256
  if (input.sourceAccountId && !accountsById.has(input.sourceAccountId)) {
7257
- throw new RetirementParameterError("UNKNOWN_CONVERSION_SOURCE_ACCOUNT", `Conversion ${input.id} references unknown source account ${input.sourceAccountId}.`);
7257
+ throw new ParameterError("UNKNOWN_CONVERSION_SOURCE_ACCOUNT", `Conversion ${input.id} references unknown source account ${input.sourceAccountId}.`);
7258
7258
  }
7259
7259
  return {
7260
7260
  ...input,
@@ -7471,7 +7471,7 @@ function calculateIraConversionGroup(context, ownerId, conversions, accountResul
7471
7471
  };
7472
7472
  });
7473
7473
  }
7474
- function calculateRetirementScenario(input) {
7474
+ function calculateScenario(input) {
7475
7475
  const scenarioDiagnostics = [];
7476
7476
  const taxYear = input.taxYear;
7477
7477
  const parameters = getParametersForYear(taxYear);
@@ -7669,7 +7669,7 @@ class PersonBuilder {
7669
7669
  }
7670
7670
  }
7671
7671
  exports.PersonBuilder = PersonBuilder;
7672
- class RetirementAccountBuilder {
7672
+ class AccountBuilder {
7673
7673
  value;
7674
7674
  constructor(id, ownerId, type) {
7675
7675
  this.value = {
@@ -7794,7 +7794,7 @@ class RetirementAccountBuilder {
7794
7794
  return deepClone(this.value);
7795
7795
  }
7796
7796
  }
7797
- exports.RetirementAccountBuilder = RetirementAccountBuilder;
7797
+ exports.AccountBuilder = AccountBuilder;
7798
7798
  class RothConversionBuilder {
7799
7799
  value;
7800
7800
  constructor(id, ownerId, type, amount) {
@@ -7825,23 +7825,23 @@ class RothConversionBuilder {
7825
7825
  }
7826
7826
  }
7827
7827
  exports.RothConversionBuilder = RothConversionBuilder;
7828
- class RetirementScenario {
7828
+ class Scenario {
7829
7829
  input;
7830
7830
  constructor(input) {
7831
7831
  this.input = input;
7832
7832
  }
7833
7833
  calculate() {
7834
- return calculateRetirementScenario(deepClone(this.input));
7834
+ return calculateScenario(deepClone(this.input));
7835
7835
  }
7836
7836
  toInput() {
7837
7837
  return deepClone(this.input);
7838
7838
  }
7839
7839
  }
7840
- exports.RetirementScenario = RetirementScenario;
7841
- class RetirementScenarioBuilder {
7840
+ exports.Scenario = Scenario;
7841
+ class ScenarioBuilder {
7842
7842
  value;
7843
7843
  static forTaxYear(taxYear) {
7844
- return new RetirementScenarioBuilder(taxYear);
7844
+ return new ScenarioBuilder(taxYear);
7845
7845
  }
7846
7846
  constructor(taxYear) {
7847
7847
  this.value = {
@@ -7871,11 +7871,11 @@ class RetirementScenarioBuilder {
7871
7871
  return this.addPerson(builder);
7872
7872
  }
7873
7873
  addAccount(account) {
7874
- this.value.accounts.push(account instanceof RetirementAccountBuilder ? account.build() : deepClone(account));
7874
+ this.value.accounts.push(account instanceof AccountBuilder ? account.build() : deepClone(account));
7875
7875
  return this;
7876
7876
  }
7877
7877
  account(id, ownerId, type, configure) {
7878
- const builder = new RetirementAccountBuilder(id, ownerId, type);
7878
+ const builder = new AccountBuilder(id, ownerId, type);
7879
7879
  configure?.(builder);
7880
7880
  return this.addAccount(builder);
7881
7881
  }
@@ -7889,7 +7889,7 @@ class RetirementScenarioBuilder {
7889
7889
  return this.addConversion(builder);
7890
7890
  }
7891
7891
  build() {
7892
- return new RetirementScenario(deepClone(this.value));
7892
+ return new Scenario(deepClone(this.value));
7893
7893
  }
7894
7894
  calculate() {
7895
7895
  return this.build().calculate();
@@ -7898,13 +7898,13 @@ class RetirementScenarioBuilder {
7898
7898
  return deepClone(this.value);
7899
7899
  }
7900
7900
  }
7901
- exports.RetirementScenarioBuilder = RetirementScenarioBuilder;
7901
+ exports.ScenarioBuilder = ScenarioBuilder;
7902
7902
  class USTaxAdvantagedParams {
7903
7903
  static forTaxYear(taxYear) {
7904
- return RetirementScenarioBuilder.forTaxYear(taxYear);
7904
+ return ScenarioBuilder.forTaxYear(taxYear);
7905
7905
  }
7906
7906
  static calculate(input) {
7907
- return calculateRetirementScenario(input);
7907
+ return calculateScenario(input);
7908
7908
  }
7909
7909
  static parametersForYear(taxYear) {
7910
7910
  return getParametersForYear(taxYear);