@markcolabs/mcp 0.2.0 → 0.3.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/LICENSE +21 -0
- package/LICENSE-API.md +111 -0
- package/README.md +128 -205
- package/dist/engines/data/irs2026.d.ts +110 -3
- package/dist/engines/data/irs2026.d.ts.map +1 -1
- package/dist/engines/data/irs2026.js +86 -3
- package/dist/engines/data/irs2026.js.map +1 -1
- package/dist/engines/data/rmd2026.d.ts +59 -0
- package/dist/engines/data/rmd2026.d.ts.map +1 -0
- package/dist/engines/data/rmd2026.js +75 -0
- package/dist/engines/data/rmd2026.js.map +1 -0
- package/dist/engines/data/stateTax2026.d.ts +114 -0
- package/dist/engines/data/stateTax2026.d.ts.map +1 -0
- package/dist/engines/data/stateTax2026.js +348 -0
- package/dist/engines/data/stateTax2026.js.map +1 -0
- package/dist/engines/hsa.d.ts +110 -0
- package/dist/engines/hsa.d.ts.map +1 -0
- package/dist/engines/hsa.js +83 -0
- package/dist/engines/hsa.js.map +1 -0
- package/dist/engines/ira.d.ts +115 -0
- package/dist/engines/ira.d.ts.map +1 -0
- package/dist/engines/ira.js +127 -0
- package/dist/engines/ira.js.map +1 -0
- package/dist/engines/paycheck.d.ts +51 -16
- package/dist/engines/paycheck.d.ts.map +1 -1
- package/dist/engines/paycheck.js +53 -24
- package/dist/engines/paycheck.js.map +1 -1
- package/dist/engines/rmd.d.ts +107 -0
- package/dist/engines/rmd.d.ts.map +1 -0
- package/dist/engines/rmd.js +109 -0
- package/dist/engines/rmd.js.map +1 -0
- package/dist/engines/rothConversion.d.ts +124 -0
- package/dist/engines/rothConversion.d.ts.map +1 -0
- package/dist/engines/rothConversion.js +145 -0
- package/dist/engines/rothConversion.js.map +1 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -6
- package/dist/index.js.map +1 -1
- package/dist/shared/bounds.d.ts +50 -0
- package/dist/shared/bounds.d.ts.map +1 -1
- package/dist/shared/bounds.js +85 -0
- package/dist/shared/bounds.js.map +1 -1
- package/dist/tools/hsa.d.ts +58 -0
- package/dist/tools/hsa.d.ts.map +1 -0
- package/dist/tools/hsa.js +129 -0
- package/dist/tools/hsa.js.map +1 -0
- package/dist/tools/ira.d.ts +55 -0
- package/dist/tools/ira.d.ts.map +1 -0
- package/dist/tools/ira.js +117 -0
- package/dist/tools/ira.js.map +1 -0
- package/dist/tools/paycheck.d.ts +14 -7
- package/dist/tools/paycheck.d.ts.map +1 -1
- package/dist/tools/paycheck.js +24 -11
- package/dist/tools/paycheck.js.map +1 -1
- package/dist/tools/rmd.d.ts +60 -0
- package/dist/tools/rmd.d.ts.map +1 -0
- package/dist/tools/rmd.js +130 -0
- package/dist/tools/rmd.js.map +1 -0
- package/dist/tools/rothConversion.d.ts +66 -0
- package/dist/tools/rothConversion.d.ts.map +1 -0
- package/dist/tools/rothConversion.js +141 -0
- package/dist/tools/rothConversion.js.map +1 -0
- package/package.json +18 -4
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IRA contribution-limit engine — lifted from
|
|
3
|
+
* site/src/utils/ira-limits.js (functions `getContributionLimit`,
|
|
4
|
+
* `isCatchUpEligible`, `getRothContributionLimit`).
|
|
5
|
+
*
|
|
6
|
+
* Per ADR-0039 § 5: pure synchronous function, ENGINE_VERSION constant,
|
|
7
|
+
* parity test against the site source as the gate.
|
|
8
|
+
*
|
|
9
|
+
* Scope (S141 Wave 1A Item #2): MAGI + age + filing status → eligible Roth /
|
|
10
|
+
* Traditional contribution + phase-out reduction. Catch-up eligibility for
|
|
11
|
+
* age ≥ 50. Does NOT compute future-value projection or tax-deduction
|
|
12
|
+
* reinvestment side-account math — those stay in the calculator UI / a future
|
|
13
|
+
* tool. This is a contribution-LIMIT tool, not a projection tool.
|
|
14
|
+
*
|
|
15
|
+
* Math reference (Roth, per IRC §408A(c)(3)):
|
|
16
|
+
* baseCap = age ≥ 50 ? standard + catchUp : standard
|
|
17
|
+
* if MAGI ≤ phaseOut.start → eligible = baseCap
|
|
18
|
+
* if MAGI ≥ phaseOut.end → eligible = 0
|
|
19
|
+
* else:
|
|
20
|
+
* reductionRatio = (MAGI − phaseOut.start) / (phaseOut.end − phaseOut.start)
|
|
21
|
+
* reduced = baseCap × (1 − reductionRatio)
|
|
22
|
+
* eligible = max(round_to_$10(reduced), reduced > 0 ? $200 : 0)
|
|
23
|
+
*
|
|
24
|
+
* Math reference (Traditional, per IRC §219(b) + §219(g)):
|
|
25
|
+
* eligible = baseCap (Traditional CONTRIBUTION is not MAGI-limited;
|
|
26
|
+
* only the DEDUCTION is, via §219(g) phase-out.
|
|
27
|
+
* The contributionLimit tool reports the
|
|
28
|
+
* contribution; the deduction phase-out is
|
|
29
|
+
* surfaced as a separate field for clients that
|
|
30
|
+
* need it.)
|
|
31
|
+
*
|
|
32
|
+
* Source: IRS Notice 2025-67 (2026 retirement plan limits) +
|
|
33
|
+
* SECURE 2.0 Act §107 (catch-up indexing).
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* SemVer of the IRA engine. Per ADR-0039 § 5 (reaffirmed in ADR-0041 Position #4a):
|
|
37
|
+
* major bump = math change; minor = additive output; patch = numerical correction.
|
|
38
|
+
* NOT bumped for cosmetic refactors.
|
|
39
|
+
* - 1.0.0: initial v1 lift from ira-limits.js (S141 Wave 1A).
|
|
40
|
+
*/
|
|
41
|
+
export declare const ENGINE_VERSION = "1.0.0";
|
|
42
|
+
/** Filing status (camelCase, matches the MCP package's federalTax convention). */
|
|
43
|
+
export type IraFilingStatus = "single" | "marriedFilingJointly" | "marriedFilingSeparately" | "headOfHousehold";
|
|
44
|
+
/** IRA type — drives Roth vs Traditional phase-out semantics. */
|
|
45
|
+
export type IraType = "traditional" | "roth";
|
|
46
|
+
/** Inputs to the IRA contribution-limit engine. */
|
|
47
|
+
export interface IraContributionLimitInput {
|
|
48
|
+
/** Current age (used for catch-up eligibility). */
|
|
49
|
+
age: number;
|
|
50
|
+
/** Federal filing status. */
|
|
51
|
+
filingStatus: IraFilingStatus;
|
|
52
|
+
/**
|
|
53
|
+
* Modified Adjusted Gross Income (MAGI), USD. Drives Roth phase-out and
|
|
54
|
+
* is reported alongside the Traditional deduction-phase-out signal.
|
|
55
|
+
*/
|
|
56
|
+
magi: number;
|
|
57
|
+
/** Whether to compute the limit for Traditional or Roth. */
|
|
58
|
+
type: IraType;
|
|
59
|
+
}
|
|
60
|
+
/** Result payload returned by the engine. */
|
|
61
|
+
export interface IraContributionLimitResult {
|
|
62
|
+
/** User's max eligible contribution after phase-out, USD. */
|
|
63
|
+
eligibleContribution: number;
|
|
64
|
+
/** Base IRA limit (before catch-up), USD. Same for Traditional and Roth. */
|
|
65
|
+
traditionalCap: number;
|
|
66
|
+
/** Base Roth IRA limit (before catch-up), USD. Same number as traditionalCap. */
|
|
67
|
+
rothCap: number;
|
|
68
|
+
/** Catch-up contribution if age ≥ 50, else 0, USD. */
|
|
69
|
+
catchUp: number;
|
|
70
|
+
/**
|
|
71
|
+
* Reduction applied due to MAGI phase-out (Roth only, USD).
|
|
72
|
+
* For Traditional this is always 0 because contributionLimit is not
|
|
73
|
+
* MAGI-gated (the DEDUCTION is, surfaced separately).
|
|
74
|
+
*/
|
|
75
|
+
phaseOutReduction: number;
|
|
76
|
+
/**
|
|
77
|
+
* Whether the user is in the catch-up window (age ≥ catchUpAge).
|
|
78
|
+
* Set independent of `type` so clients can advertise the catch-up regardless
|
|
79
|
+
* of which IRA type they're querying.
|
|
80
|
+
*/
|
|
81
|
+
catchUpEligible: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Traditional IRA deduction phase-out signal (informational; does not
|
|
84
|
+
* affect `eligibleContribution`). Useful for clients that want to advise
|
|
85
|
+
* users their Traditional contribution may be non-deductible at high MAGI.
|
|
86
|
+
* - "fullyDeductible" — MAGI ≤ traditionalPhaseOut.start
|
|
87
|
+
* - "partiallyDeductible" — MAGI within phase-out range
|
|
88
|
+
* - "notDeductible" — MAGI ≥ traditionalPhaseOut.end
|
|
89
|
+
*
|
|
90
|
+
* Assumes the contributor (or spouse) IS covered by a workplace plan; if
|
|
91
|
+
* neither is covered, full deduction applies regardless of MAGI.
|
|
92
|
+
*/
|
|
93
|
+
traditionalDeductionStatus: "fullyDeductible" | "partiallyDeductible" | "notDeductible";
|
|
94
|
+
/** Provenance metadata. */
|
|
95
|
+
meta: {
|
|
96
|
+
/** IRS tax year these limits apply to. */
|
|
97
|
+
tableYear: number;
|
|
98
|
+
/** Source authority (IRS notice / IRC citation). */
|
|
99
|
+
source: string;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Compute IRA contribution limit + phase-out reduction for the given inputs.
|
|
104
|
+
*
|
|
105
|
+
* 1-to-1 logic port of site/src/utils/ira-limits.js `getContributionLimit` +
|
|
106
|
+
* `isCatchUpEligible` + `getRothContributionLimit` (the contribution-limit
|
|
107
|
+
* portion; future-value projection is out of scope per S141 Wave 1A).
|
|
108
|
+
*
|
|
109
|
+
* Filing-status key mapping vs. site (which uses 'single' / 'mfj' / 'mfs'):
|
|
110
|
+
* the MCP camelCase keys ('single' / 'marriedFilingJointly' /
|
|
111
|
+
* 'marriedFilingSeparately' / 'headOfHousehold') resolve to the same numeric
|
|
112
|
+
* phase-out thresholds — see ROTH_PHASE_OUT_2026 in data/irs2026.ts.
|
|
113
|
+
*/
|
|
114
|
+
export declare function contributionLimit(input: IraContributionLimitInput): IraContributionLimitResult;
|
|
115
|
+
//# sourceMappingURL=ira.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ira.d.ts","sourceRoot":"","sources":["../../src/engines/ira.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAQH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,kFAAkF;AAClF,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,sBAAsB,GACtB,yBAAyB,GACzB,iBAAiB,CAAC;AAEtB,iEAAiE;AACjE,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,MAAM,CAAC;AAE7C,mDAAmD;AACnD,MAAM,WAAW,yBAAyB;IACxC,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,YAAY,EAAE,eAAe,CAAC;IAC9B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6CAA6C;AAC7C,MAAM,WAAW,0BAA0B;IACzC,6DAA6D;IAC7D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;OAUG;IACH,0BAA0B,EACtB,iBAAiB,GACjB,qBAAqB,GACrB,eAAe,CAAC;IACpB,2BAA2B;IAC3B,IAAI,EAAE;QACJ,0CAA0C;QAC1C,SAAS,EAAE,MAAM,CAAC;QAClB,oDAAoD;QACpD,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,yBAAyB,GAC/B,0BAA0B,CAgE5B"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IRA contribution-limit engine — lifted from
|
|
3
|
+
* site/src/utils/ira-limits.js (functions `getContributionLimit`,
|
|
4
|
+
* `isCatchUpEligible`, `getRothContributionLimit`).
|
|
5
|
+
*
|
|
6
|
+
* Per ADR-0039 § 5: pure synchronous function, ENGINE_VERSION constant,
|
|
7
|
+
* parity test against the site source as the gate.
|
|
8
|
+
*
|
|
9
|
+
* Scope (S141 Wave 1A Item #2): MAGI + age + filing status → eligible Roth /
|
|
10
|
+
* Traditional contribution + phase-out reduction. Catch-up eligibility for
|
|
11
|
+
* age ≥ 50. Does NOT compute future-value projection or tax-deduction
|
|
12
|
+
* reinvestment side-account math — those stay in the calculator UI / a future
|
|
13
|
+
* tool. This is a contribution-LIMIT tool, not a projection tool.
|
|
14
|
+
*
|
|
15
|
+
* Math reference (Roth, per IRC §408A(c)(3)):
|
|
16
|
+
* baseCap = age ≥ 50 ? standard + catchUp : standard
|
|
17
|
+
* if MAGI ≤ phaseOut.start → eligible = baseCap
|
|
18
|
+
* if MAGI ≥ phaseOut.end → eligible = 0
|
|
19
|
+
* else:
|
|
20
|
+
* reductionRatio = (MAGI − phaseOut.start) / (phaseOut.end − phaseOut.start)
|
|
21
|
+
* reduced = baseCap × (1 − reductionRatio)
|
|
22
|
+
* eligible = max(round_to_$10(reduced), reduced > 0 ? $200 : 0)
|
|
23
|
+
*
|
|
24
|
+
* Math reference (Traditional, per IRC §219(b) + §219(g)):
|
|
25
|
+
* eligible = baseCap (Traditional CONTRIBUTION is not MAGI-limited;
|
|
26
|
+
* only the DEDUCTION is, via §219(g) phase-out.
|
|
27
|
+
* The contributionLimit tool reports the
|
|
28
|
+
* contribution; the deduction phase-out is
|
|
29
|
+
* surfaced as a separate field for clients that
|
|
30
|
+
* need it.)
|
|
31
|
+
*
|
|
32
|
+
* Source: IRS Notice 2025-67 (2026 retirement plan limits) +
|
|
33
|
+
* SECURE 2.0 Act §107 (catch-up indexing).
|
|
34
|
+
*/
|
|
35
|
+
import { LIMITS_IRA_2026, ROTH_PHASE_OUT_2026, TRADITIONAL_PHASE_OUT_2026, } from "./data/irs2026.js";
|
|
36
|
+
/**
|
|
37
|
+
* SemVer of the IRA engine. Per ADR-0039 § 5 (reaffirmed in ADR-0041 Position #4a):
|
|
38
|
+
* major bump = math change; minor = additive output; patch = numerical correction.
|
|
39
|
+
* NOT bumped for cosmetic refactors.
|
|
40
|
+
* - 1.0.0: initial v1 lift from ira-limits.js (S141 Wave 1A).
|
|
41
|
+
*/
|
|
42
|
+
export const ENGINE_VERSION = "1.0.0";
|
|
43
|
+
/**
|
|
44
|
+
* IRS rounding rule for phase-out reductions: round to nearest $10, minimum
|
|
45
|
+
* $200 if any contribution is allowed. Per IRC §408A(c)(3)(B)(ii).
|
|
46
|
+
*/
|
|
47
|
+
function roundPhaseOutReduction(reduced) {
|
|
48
|
+
if (reduced <= 0)
|
|
49
|
+
return 0;
|
|
50
|
+
const rounded = Math.round(reduced / 10) * 10;
|
|
51
|
+
return Math.max(rounded, 200);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compute IRA contribution limit + phase-out reduction for the given inputs.
|
|
55
|
+
*
|
|
56
|
+
* 1-to-1 logic port of site/src/utils/ira-limits.js `getContributionLimit` +
|
|
57
|
+
* `isCatchUpEligible` + `getRothContributionLimit` (the contribution-limit
|
|
58
|
+
* portion; future-value projection is out of scope per S141 Wave 1A).
|
|
59
|
+
*
|
|
60
|
+
* Filing-status key mapping vs. site (which uses 'single' / 'mfj' / 'mfs'):
|
|
61
|
+
* the MCP camelCase keys ('single' / 'marriedFilingJointly' /
|
|
62
|
+
* 'marriedFilingSeparately' / 'headOfHousehold') resolve to the same numeric
|
|
63
|
+
* phase-out thresholds — see ROTH_PHASE_OUT_2026 in data/irs2026.ts.
|
|
64
|
+
*/
|
|
65
|
+
export function contributionLimit(input) {
|
|
66
|
+
const { age, filingStatus, magi, type } = input;
|
|
67
|
+
// Base cap (Traditional and Roth share the same base contribution limit).
|
|
68
|
+
const baseCap = LIMITS_IRA_2026.standard;
|
|
69
|
+
const catchUpEligible = age >= LIMITS_IRA_2026.catchUpAge;
|
|
70
|
+
const catchUp = catchUpEligible ? LIMITS_IRA_2026.catchUp : 0;
|
|
71
|
+
const fullCap = baseCap + catchUp;
|
|
72
|
+
// --- Roth phase-out (eligibility-gated) ---
|
|
73
|
+
// The Roth contribution itself is phased out at high MAGI. Traditional
|
|
74
|
+
// contribution is NOT (only its deductibility is — surfaced separately).
|
|
75
|
+
let eligibleContribution;
|
|
76
|
+
let phaseOutReduction;
|
|
77
|
+
if (type === "roth") {
|
|
78
|
+
const phaseOut = ROTH_PHASE_OUT_2026[filingStatus];
|
|
79
|
+
if (magi <= phaseOut.start) {
|
|
80
|
+
eligibleContribution = fullCap;
|
|
81
|
+
phaseOutReduction = 0;
|
|
82
|
+
}
|
|
83
|
+
else if (magi >= phaseOut.end) {
|
|
84
|
+
eligibleContribution = 0;
|
|
85
|
+
phaseOutReduction = fullCap;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const range = phaseOut.end - phaseOut.start;
|
|
89
|
+
const overStart = magi - phaseOut.start;
|
|
90
|
+
const reductionRatio = overStart / range;
|
|
91
|
+
const reducedRaw = fullCap * (1 - reductionRatio);
|
|
92
|
+
eligibleContribution = roundPhaseOutReduction(reducedRaw);
|
|
93
|
+
phaseOutReduction = fullCap - eligibleContribution;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Traditional: contribution is never MAGI-phased-out, only deduction is.
|
|
98
|
+
eligibleContribution = fullCap;
|
|
99
|
+
phaseOutReduction = 0;
|
|
100
|
+
}
|
|
101
|
+
// --- Traditional deduction-phase-out signal (informational, always reported) ---
|
|
102
|
+
const tradPhaseOut = TRADITIONAL_PHASE_OUT_2026[filingStatus];
|
|
103
|
+
let traditionalDeductionStatus;
|
|
104
|
+
if (magi <= tradPhaseOut.start) {
|
|
105
|
+
traditionalDeductionStatus = "fullyDeductible";
|
|
106
|
+
}
|
|
107
|
+
else if (magi >= tradPhaseOut.end) {
|
|
108
|
+
traditionalDeductionStatus = "notDeductible";
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
traditionalDeductionStatus = "partiallyDeductible";
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
eligibleContribution,
|
|
115
|
+
traditionalCap: baseCap,
|
|
116
|
+
rothCap: baseCap,
|
|
117
|
+
catchUp,
|
|
118
|
+
phaseOutReduction,
|
|
119
|
+
catchUpEligible,
|
|
120
|
+
traditionalDeductionStatus,
|
|
121
|
+
meta: {
|
|
122
|
+
tableYear: 2026,
|
|
123
|
+
source: "IRS Notice 2025-67",
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=ira.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ira.js","sourceRoot":"","sources":["../../src/engines/ira.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,25 +6,37 @@
|
|
|
6
6
|
* Per ADR-0039 § 5: pure synchronous functions, ENGINE_VERSION constant,
|
|
7
7
|
* parity test against the site source as the gate.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* tables loaded async via fetch(), SDI, NYC local tax, hourly-mode handling,
|
|
11
|
-
* and YTD wage tracking. None of those fit a synchronous, self-contained
|
|
12
|
-
* MCP tool. The v0.2.0 paycheck engine therefore covers:
|
|
13
|
-
* - Federal income-tax withholding (Percentage Method, IRS Pub 15-T 2026)
|
|
14
|
-
* - FICA (SS 6.2% + Medicare 1.45% + Additional Medicare 0.9%)
|
|
15
|
-
* - State estimate: $0 for no-tax states, otherwise a flat 5% estimate
|
|
16
|
-
* (clearly disclaimed as an approximation pending a future stateful tool).
|
|
17
|
-
* - Pre-tax + post-tax deductions on an annualized basis
|
|
9
|
+
* ## v0.3.0 changes (S141 Item #5)
|
|
18
10
|
*
|
|
19
|
-
*
|
|
11
|
+
* Two YMYL-grade corrections vs v1.0.0:
|
|
20
12
|
*
|
|
21
|
-
*
|
|
13
|
+
* 1. **MCP-AUDIT-011 — State-aware tax estimate.** The v1.0.0 flat 5% state
|
|
14
|
+
* estimate is replaced with a state-aware effective-rate lookup using
|
|
15
|
+
* marginal-bracket simplification (see `engines/data/stateTax2026.ts`).
|
|
16
|
+
* Rates sourced from the Tax Foundation 2026 State Individual Income Tax
|
|
17
|
+
* Rates report. No-tax states still return $0.
|
|
18
|
+
*
|
|
19
|
+
* 2. **MCP-AUDIT-012 — Additional Medicare base.** The 0.9% Additional
|
|
20
|
+
* Medicare tax now correctly uses the FICA wages base
|
|
21
|
+
* (gross − pre-tax cafeteria-plan deductions: 401k/HSA/dep-care/FSA),
|
|
22
|
+
* not raw gross. Matches the IRS § 3101(b)(2) rule.
|
|
23
|
+
*
|
|
24
|
+
* Engine version bumped 1.0.0 → 1.1.0 (additive: new `state` semantics +
|
|
25
|
+
* new result fields). Math output differs in two ways: state tax becomes
|
|
26
|
+
* state-correct, and Additional Medicare correctly excludes pre-tax
|
|
27
|
+
* deductions. Per ADR-0039 § 5 / ADR-0041 D4a, a math fix is a patch bump
|
|
28
|
+
* if not breaking the public surface; we're using a MINOR bump because we
|
|
29
|
+
* add new result fields (`stateTaxSource`, `stateTaxNotes`,
|
|
30
|
+
* `stateEffectiveRate`).
|
|
31
|
+
*
|
|
32
|
+
* Math reference (annualized) — v0.3.0:
|
|
22
33
|
* federalTaxableIncome = max(0, grossAnnual − preTaxAnnual − stdDeduction)
|
|
23
34
|
* federalTax = bracketsLookup(federalTaxableIncome, brackets[filingStatus])
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
35
|
+
* ficaWages = max(0, grossAnnual − preTaxAnnual) // pre-tax deductions reduce FICA base
|
|
36
|
+
* ssTax = min(ficaWages, ssWageBase) * 0.062
|
|
37
|
+
* medicare = ficaWages * 0.0145
|
|
38
|
+
* addlMed = max(0, ficaWages − addlMedThreshold[filingStatus]) * 0.009
|
|
39
|
+
* stateTax = noTaxState ? 0 : federalTaxableIncome * stateEffectiveRate(state, federalTaxableIncome)
|
|
28
40
|
* netAnnual = grossAnnual − preTaxAnnual − federalTax − ficaTax − stateTax − postTaxAnnual
|
|
29
41
|
* per-paycheck values = annual / payPeriodsPerYear
|
|
30
42
|
*/
|
|
@@ -36,8 +48,13 @@ import { type FilingStatus, type PayFrequencyLabel } from "./data/federalTax.js"
|
|
|
36
48
|
* - 0.2.0 (v0.1.0 publish): initial port (S136), shipped under out-of-policy package-surface version.
|
|
37
49
|
* - 1.0.0 (v0.2.0 publish): one-time reset to align with ADR-0039 § 5 per ADR-0041 D4a.
|
|
38
50
|
* Math unchanged.
|
|
51
|
+
* - 1.1.0 (v0.3.0 publish, S141 Item #5): state-aware tax (MCP-AUDIT-011) +
|
|
52
|
+
* Additional Medicare on FICA wages (MCP-AUDIT-012). Additive output fields:
|
|
53
|
+
* stateTaxSource, stateTaxNotes, stateEffectiveRate. Per-state math differs
|
|
54
|
+
* vs 1.0.0 for non-no-tax states; Additional Medicare differs for high
|
|
55
|
+
* earners with pre-tax deductions.
|
|
39
56
|
*/
|
|
40
|
-
export declare const ENGINE_VERSION = "1.
|
|
57
|
+
export declare const ENGINE_VERSION = "1.1.0";
|
|
41
58
|
/** Inputs to the paycheck net-pay engine. */
|
|
42
59
|
export interface PaycheckNetPayInput {
|
|
43
60
|
/** Gross annual salary, USD. */
|
|
@@ -79,6 +96,24 @@ export interface PaycheckNetPayResult {
|
|
|
79
96
|
netPayAnnual: number;
|
|
80
97
|
/** Whether the state has no income tax. */
|
|
81
98
|
noStateIncomeTax: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* v1.1.0 additive — citable source for the state-tax effective rate used.
|
|
101
|
+
* Same Tax Foundation 2026 report regardless of state; included for caller
|
|
102
|
+
* provenance.
|
|
103
|
+
*/
|
|
104
|
+
stateTaxSource: string;
|
|
105
|
+
/**
|
|
106
|
+
* v1.1.0 additive — state-specific caveats (e.g., "Excludes NYC local tax",
|
|
107
|
+
* "Local municipal EIT NOT included"). Empty string when no caveats apply
|
|
108
|
+
* (no-tax states, flat-rate states with no local-tax overlay, etc.).
|
|
109
|
+
*/
|
|
110
|
+
stateTaxNotes: string;
|
|
111
|
+
/**
|
|
112
|
+
* v1.1.0 additive — the effective rate (decimal, 0.093 = 9.3%) the engine
|
|
113
|
+
* applied to taxable income for the state-tax estimate. 0 for no-tax states
|
|
114
|
+
* and for state codes not modeled in our table.
|
|
115
|
+
*/
|
|
116
|
+
stateEffectiveRate: number;
|
|
82
117
|
}
|
|
83
118
|
/**
|
|
84
119
|
* Compute net pay (and the annualized + per-paycheck breakdown).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paycheck.d.ts","sourceRoot":"","sources":["../../src/engines/paycheck.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"paycheck.d.ts","sourceRoot":"","sources":["../../src/engines/paycheck.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAQL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAEvB,MAAM,sBAAsB,CAAC;AAO9B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB;IACrB,YAAY,EAAE,iBAAiB,CAAC;IAChC,kEAAkE;IAClE,mBAAmB,EAAE,YAAY,CAAC;IAClC,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,sBAAsB,EAAE,MAAM,CAAC;IAC/B,wFAAwF;IACxF,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACnC,mCAAmC;IACnC,gBAAgB,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,gBAAgB,EAAE,MAAM,CAAC;IACzB,4BAA4B;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAmBD;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,oBAAoB,CA+EvE"}
|
package/dist/engines/paycheck.js
CHANGED
|
@@ -6,29 +6,42 @@
|
|
|
6
6
|
* Per ADR-0039 § 5: pure synchronous functions, ENGINE_VERSION constant,
|
|
7
7
|
* parity test against the site source as the gate.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* tables loaded async via fetch(), SDI, NYC local tax, hourly-mode handling,
|
|
11
|
-
* and YTD wage tracking. None of those fit a synchronous, self-contained
|
|
12
|
-
* MCP tool. The v0.2.0 paycheck engine therefore covers:
|
|
13
|
-
* - Federal income-tax withholding (Percentage Method, IRS Pub 15-T 2026)
|
|
14
|
-
* - FICA (SS 6.2% + Medicare 1.45% + Additional Medicare 0.9%)
|
|
15
|
-
* - State estimate: $0 for no-tax states, otherwise a flat 5% estimate
|
|
16
|
-
* (clearly disclaimed as an approximation pending a future stateful tool).
|
|
17
|
-
* - Pre-tax + post-tax deductions on an annualized basis
|
|
9
|
+
* ## v0.3.0 changes (S141 Item #5)
|
|
18
10
|
*
|
|
19
|
-
*
|
|
11
|
+
* Two YMYL-grade corrections vs v1.0.0:
|
|
20
12
|
*
|
|
21
|
-
*
|
|
13
|
+
* 1. **MCP-AUDIT-011 — State-aware tax estimate.** The v1.0.0 flat 5% state
|
|
14
|
+
* estimate is replaced with a state-aware effective-rate lookup using
|
|
15
|
+
* marginal-bracket simplification (see `engines/data/stateTax2026.ts`).
|
|
16
|
+
* Rates sourced from the Tax Foundation 2026 State Individual Income Tax
|
|
17
|
+
* Rates report. No-tax states still return $0.
|
|
18
|
+
*
|
|
19
|
+
* 2. **MCP-AUDIT-012 — Additional Medicare base.** The 0.9% Additional
|
|
20
|
+
* Medicare tax now correctly uses the FICA wages base
|
|
21
|
+
* (gross − pre-tax cafeteria-plan deductions: 401k/HSA/dep-care/FSA),
|
|
22
|
+
* not raw gross. Matches the IRS § 3101(b)(2) rule.
|
|
23
|
+
*
|
|
24
|
+
* Engine version bumped 1.0.0 → 1.1.0 (additive: new `state` semantics +
|
|
25
|
+
* new result fields). Math output differs in two ways: state tax becomes
|
|
26
|
+
* state-correct, and Additional Medicare correctly excludes pre-tax
|
|
27
|
+
* deductions. Per ADR-0039 § 5 / ADR-0041 D4a, a math fix is a patch bump
|
|
28
|
+
* if not breaking the public surface; we're using a MINOR bump because we
|
|
29
|
+
* add new result fields (`stateTaxSource`, `stateTaxNotes`,
|
|
30
|
+
* `stateEffectiveRate`).
|
|
31
|
+
*
|
|
32
|
+
* Math reference (annualized) — v0.3.0:
|
|
22
33
|
* federalTaxableIncome = max(0, grossAnnual − preTaxAnnual − stdDeduction)
|
|
23
34
|
* federalTax = bracketsLookup(federalTaxableIncome, brackets[filingStatus])
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
35
|
+
* ficaWages = max(0, grossAnnual − preTaxAnnual) // pre-tax deductions reduce FICA base
|
|
36
|
+
* ssTax = min(ficaWages, ssWageBase) * 0.062
|
|
37
|
+
* medicare = ficaWages * 0.0145
|
|
38
|
+
* addlMed = max(0, ficaWages − addlMedThreshold[filingStatus]) * 0.009
|
|
39
|
+
* stateTax = noTaxState ? 0 : federalTaxableIncome * stateEffectiveRate(state, federalTaxableIncome)
|
|
28
40
|
* netAnnual = grossAnnual − preTaxAnnual − federalTax − ficaTax − stateTax − postTaxAnnual
|
|
29
41
|
* per-paycheck values = annual / payPeriodsPerYear
|
|
30
42
|
*/
|
|
31
43
|
import { FEDERAL_TAX_BRACKETS_2026, STANDARD_DEDUCTIONS_2026, ADDITIONAL_MEDICARE_THRESHOLDS_2026, FICA_RATES, SS_WAGE_BASE, PAY_PERIODS_PER_YEAR, NO_TAX_STATES, } from "./data/federalTax.js";
|
|
44
|
+
import { getStateEffectiveRate, getStateEntry, STATE_TAX_2026_META, } from "./data/stateTax2026.js";
|
|
32
45
|
/**
|
|
33
46
|
* SemVer of the paycheck engine. Per ADR-0039 § 5 (reaffirmed in ADR-0041 Position #4a):
|
|
34
47
|
* major bump = math change; minor = additive output; patch = numerical correction.
|
|
@@ -36,8 +49,13 @@ import { FEDERAL_TAX_BRACKETS_2026, STANDARD_DEDUCTIONS_2026, ADDITIONAL_MEDICAR
|
|
|
36
49
|
* - 0.2.0 (v0.1.0 publish): initial port (S136), shipped under out-of-policy package-surface version.
|
|
37
50
|
* - 1.0.0 (v0.2.0 publish): one-time reset to align with ADR-0039 § 5 per ADR-0041 D4a.
|
|
38
51
|
* Math unchanged.
|
|
52
|
+
* - 1.1.0 (v0.3.0 publish, S141 Item #5): state-aware tax (MCP-AUDIT-011) +
|
|
53
|
+
* Additional Medicare on FICA wages (MCP-AUDIT-012). Additive output fields:
|
|
54
|
+
* stateTaxSource, stateTaxNotes, stateEffectiveRate. Per-state math differs
|
|
55
|
+
* vs 1.0.0 for non-no-tax states; Additional Medicare differs for high
|
|
56
|
+
* earners with pre-tax deductions.
|
|
39
57
|
*/
|
|
40
|
-
export const ENGINE_VERSION = "1.
|
|
58
|
+
export const ENGINE_VERSION = "1.1.0";
|
|
41
59
|
const ROUND = (v) => Math.round(v * 100) / 100;
|
|
42
60
|
/**
|
|
43
61
|
* Progressive bracket lookup — verbatim port of site `calculateBracketTax`.
|
|
@@ -72,23 +90,31 @@ export function netPay(input) {
|
|
|
72
90
|
const stdDeduction = STANDARD_DEDUCTIONS_2026[federalFilingStatus];
|
|
73
91
|
const federalTaxable = Math.max(0, adjustedAnnual - stdDeduction);
|
|
74
92
|
const federalTaxAnnual = bracketTax(federalTaxable, FEDERAL_TAX_BRACKETS_2026[federalFilingStatus]);
|
|
75
|
-
// FICA on gross
|
|
76
|
-
// apply pre-tax deductions to FICA since the
|
|
77
|
-
//
|
|
93
|
+
// FICA on gross less pre-tax cafeteria-plan deductions; we conservatively
|
|
94
|
+
// apply all pre-tax deductions to FICA since the input doesn't split 401k
|
|
95
|
+
// vs cafeteria-plan deductions — matches the more lenient employee view.
|
|
78
96
|
const ficaWages = adjustedAnnual;
|
|
79
97
|
const ssWageBase = SS_WAGE_BASE[2026] ?? SS_WAGE_BASE[2025];
|
|
80
98
|
const ssTaxAnnual = Math.min(ficaWages, ssWageBase) * FICA_RATES.socialSecurityRate;
|
|
81
99
|
const medicareAnnual = ficaWages * FICA_RATES.medicareRate;
|
|
82
100
|
const addlMedThreshold = ADDITIONAL_MEDICARE_THRESHOLDS_2026[federalFilingStatus];
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
// MCP-AUDIT-012 fix (v1.1.0): Additional Medicare uses FICA wages base,
|
|
102
|
+
// not raw gross. Matches IRS § 3101(b)(2) / Form 8959 rule. Pre-tax
|
|
103
|
+
// health/HSA/dep-care deductions DO reduce the Additional Medicare base.
|
|
104
|
+
const addlMedAnnual = ficaWages > addlMedThreshold
|
|
105
|
+
? (ficaWages - addlMedThreshold) * FICA_RATES.additionalMedicareRate
|
|
85
106
|
: 0;
|
|
86
107
|
const ficaTaxAnnual = ssTaxAnnual + medicareAnnual + addlMedAnnual;
|
|
87
|
-
// State tax: no-tax states explicit zero; everyone else
|
|
88
|
-
//
|
|
108
|
+
// State tax: no-tax states explicit zero; everyone else uses state-aware
|
|
109
|
+
// effective-rate lookup (MCP-AUDIT-011 fix, v1.1.0). See stateTax2026.ts.
|
|
89
110
|
const stateUpper = state.trim().toUpperCase();
|
|
90
111
|
const noStateIncomeTax = NO_TAX_STATES.includes(stateUpper);
|
|
91
|
-
const
|
|
112
|
+
const stateEffectiveRate = noStateIncomeTax
|
|
113
|
+
? 0
|
|
114
|
+
: getStateEffectiveRate(stateUpper, federalTaxable);
|
|
115
|
+
const stateTaxAnnual = federalTaxable * stateEffectiveRate;
|
|
116
|
+
const stateEntry = noStateIncomeTax ? undefined : getStateEntry(stateUpper);
|
|
117
|
+
const stateTaxNotes = stateEntry?.notes ?? "";
|
|
92
118
|
// Net annual + per-paycheck.
|
|
93
119
|
const netPayAnnual = grossAnnualSalary -
|
|
94
120
|
preTaxDeductionsAnnual -
|
|
@@ -108,6 +134,9 @@ export function netPay(input) {
|
|
|
108
134
|
stateTaxAnnual: ROUND(stateTaxAnnual),
|
|
109
135
|
netPayAnnual: ROUND(netPayAnnual),
|
|
110
136
|
noStateIncomeTax,
|
|
137
|
+
stateTaxSource: STATE_TAX_2026_META.source,
|
|
138
|
+
stateTaxNotes,
|
|
139
|
+
stateEffectiveRate,
|
|
111
140
|
};
|
|
112
141
|
}
|
|
113
142
|
//# sourceMappingURL=paycheck.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paycheck.js","sourceRoot":"","sources":["../../src/engines/paycheck.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"paycheck.js","sourceRoot":"","sources":["../../src/engines/paycheck.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RMD (Required Minimum Distribution) engine — lifted from
|
|
3
|
+
* site/src/utils/rmd-engine.js (functions `calculateRMD`,
|
|
4
|
+
* `getDistributionPeriod`, `getRMDStartAge`).
|
|
5
|
+
*
|
|
6
|
+
* Per ADR-0039 § 5: pure synchronous function, ENGINE_VERSION constant,
|
|
7
|
+
* parity test against the site source as the gate.
|
|
8
|
+
*
|
|
9
|
+
* Scope (S141 Wave 1B Item #3): SINGLE-YEAR RMD distribution-amount
|
|
10
|
+
* calculation only. The site calculator's multi-year projection
|
|
11
|
+
* (`generateRMDProjection`) and pre-RMD age-projection path are OUT of
|
|
12
|
+
* scope for v1 — callers needing year-by-year projections should call the
|
|
13
|
+
* tool repeatedly with the projected balance for each year.
|
|
14
|
+
*
|
|
15
|
+
* Table coverage: ONLY the Uniform Lifetime Table (Pub. 590-B Table III,
|
|
16
|
+
* post-2022 version) is implemented, mirroring the site source which uses
|
|
17
|
+
* `UNIFORM_LIFETIME_TABLE` exclusively. The Joint Life Expectancy Table
|
|
18
|
+
* (Table II, applied when spouse is sole beneficiary AND 10+ years younger)
|
|
19
|
+
* is NOT in the site engine and is therefore NOT in this tool — adding it
|
|
20
|
+
* here would create a parity gap with the site calculator. The `tableUsed`
|
|
21
|
+
* field is always `"uniform-lifetime"` for v1; the union type leaves room
|
|
22
|
+
* for `"joint-life-expectancy"` in a future minor bump when the site adds it.
|
|
23
|
+
*
|
|
24
|
+
* Math reference (per IRC §401(a)(9) + Pub. 590-B):
|
|
25
|
+
* distributionPeriod = UNIFORM_LIFETIME_TABLE[ownerAge]
|
|
26
|
+
* rmdAmount = accountBalance / distributionPeriod
|
|
27
|
+
*
|
|
28
|
+
* Missed-RMD penalty (SECURE 2.0 §302, effective 2023+):
|
|
29
|
+
* penaltyIfMissed = rmdAmount × 0.25 (default 25% excise tax)
|
|
30
|
+
* reduced to 10% if corrected within IRS correction window (typically 2 years)
|
|
31
|
+
* (down from 50% pre-SECURE-2.0)
|
|
32
|
+
*
|
|
33
|
+
* Source: IRS Publication 590-B (Uniform Lifetime Table III, post-2022) +
|
|
34
|
+
* SECURE 2.0 Act §107 (RMD age tiers) + §302 (25% excise tax).
|
|
35
|
+
* Last verified: 2026-05-27.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* SemVer of the RMD engine. Per ADR-0039 § 5 (reaffirmed in ADR-0041
|
|
39
|
+
* Position #4a): major bump = math change; minor = additive output; patch =
|
|
40
|
+
* numerical correction. NOT bumped for cosmetic refactors.
|
|
41
|
+
* - 1.0.0: initial v1 lift from rmd-engine.js (S141 Wave 1B).
|
|
42
|
+
*/
|
|
43
|
+
export declare const ENGINE_VERSION = "1.0.0";
|
|
44
|
+
/**
|
|
45
|
+
* Which IRS RMD distribution-period table was applied. For v1 always
|
|
46
|
+
* `"uniform-lifetime"` (mirrors the site source). The `"joint-life-expectancy"`
|
|
47
|
+
* variant is reserved for a future minor when the site engine adds Table II.
|
|
48
|
+
*/
|
|
49
|
+
export type RmdTableUsed = "uniform-lifetime" | "joint-life-expectancy";
|
|
50
|
+
/** Inputs to the RMD distribution-amount engine. */
|
|
51
|
+
export interface RmdDistributionAmountInput {
|
|
52
|
+
/** Prior year-end account balance, USD. */
|
|
53
|
+
accountBalance: number;
|
|
54
|
+
/** Owner's age this calendar year (must be ≥ SECURE 2.0 RMD age, typically 73). */
|
|
55
|
+
ownerAge: number;
|
|
56
|
+
/**
|
|
57
|
+
* Spouse's age (optional). RESERVED FOR FUTURE Joint Life Expectancy Table
|
|
58
|
+
* support. v1 ignores this input — the Uniform Lifetime Table is always
|
|
59
|
+
* applied (matches site parity). Documented in tool description so callers
|
|
60
|
+
* know not to rely on it for v1.
|
|
61
|
+
*/
|
|
62
|
+
spouseAge?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the spouse is the sole IRA beneficiary (optional). RESERVED FOR
|
|
65
|
+
* FUTURE Joint Life Expectancy Table support. v1 ignores this input.
|
|
66
|
+
*/
|
|
67
|
+
isSpouseSoleBeneficiary?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Result payload returned by the engine. */
|
|
70
|
+
export interface RmdDistributionAmountResult {
|
|
71
|
+
/** Required minimum distribution this year, USD. */
|
|
72
|
+
rmdAmount: number;
|
|
73
|
+
/** Years (life-expectancy factor from the applied IRS table). */
|
|
74
|
+
distributionPeriod: number;
|
|
75
|
+
/** Duplicates distributionPeriod under a more semantic name. */
|
|
76
|
+
lifeExpectancyFactor: number;
|
|
77
|
+
/** Which IRS table was applied. Always `"uniform-lifetime"` for v1. */
|
|
78
|
+
tableUsed: RmdTableUsed;
|
|
79
|
+
/**
|
|
80
|
+
* Default excise tax owed if the RMD is missed (SECURE 2.0 §302, 25%).
|
|
81
|
+
* Reduced to 10% if corrected within the IRS correction window (typically
|
|
82
|
+
* 2 years) — callers should surface this nuance in the UI; the field
|
|
83
|
+
* itself reports the 25% default.
|
|
84
|
+
*/
|
|
85
|
+
penaltyIfMissed: number;
|
|
86
|
+
/** Provenance metadata. */
|
|
87
|
+
meta: {
|
|
88
|
+
/** IRS table edition / tax year. */
|
|
89
|
+
tableYear: number;
|
|
90
|
+
/** Source authority (IRS publication / SECURE 2.0 citation). */
|
|
91
|
+
source: string;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compute the required minimum distribution for the given inputs.
|
|
96
|
+
*
|
|
97
|
+
* 1-to-1 logic port of site/src/utils/rmd-engine.js `calculateRMD` +
|
|
98
|
+
* `getDistributionPeriod` (single-year path; multi-year projection out of
|
|
99
|
+
* scope). The site engine clamps lookups to [72, 120]; we additionally
|
|
100
|
+
* require ownerAge ≥ 73 (SECURE 2.0 floor for the 1951-1959 cohort, the
|
|
101
|
+
* common case in 2026) at the tool boundary via RMD_BOUNDS.
|
|
102
|
+
*
|
|
103
|
+
* Throws on undefined-period lookup — should be unreachable because bounds
|
|
104
|
+
* validation in the tool wrapper rejects out-of-range ages first.
|
|
105
|
+
*/
|
|
106
|
+
export declare function distributionAmount(input: RmdDistributionAmountInput): RmdDistributionAmountResult;
|
|
107
|
+
//# sourceMappingURL=rmd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rmd.d.ts","sourceRoot":"","sources":["../../src/engines/rmd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAUH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,uBAAuB,CAAC;AAExE,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,6CAA6C;AAC7C,MAAM,WAAW,2BAA2B;IAC1C,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,SAAS,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,2BAA2B;IAC3B,IAAI,EAAE;QACJ,oCAAoC;QACpC,SAAS,EAAE,MAAM,CAAC;QAClB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,0BAA0B,GAChC,2BAA2B,CAyC7B"}
|