// Builds the tables for dataset `wa-millionaires-tax-household-reach`. // Usage: node build_reach.js // Outputs: data/reach/*.csv and data/reach/results.json const fs = require("fs"), path = require("path"); const [irsDir, rcFile] = process.argv.slice(2); const out = path.join(__dirname, "data", "reach"); fs.mkdirSync(out, { recursive: true }); function parseCsv(text) { const rows = []; let row = [], f = "", q = false; for (let i = 0; i < text.length; i++) { const c = text[i]; if (q) { if (c === '"') { if (text[i + 1] === '"') { f += '"'; i++; } else q = false; } else f += c; } else if (c === '"') q = true; else if (c === ",") { row.push(f); f = ""; } else if (c === "\n" || c === "\r") { if (c === "\r" && text[i + 1] === "\n") i++; row.push(f); rows.push(row); row = []; f = ""; } else f += c; } if (f.length || row.length) { row.push(f); rows.push(row); } return rows; } const csv = rows => { const h = Object.keys(rows[0]); const esc = v => (v === null || v === undefined) ? "" : (/[",\n]/.test(String(v)) ? '"' + String(v).replace(/"/g, '""') + '"' : String(v)); return [h.join(","), ...rows.map(r => h.map(k => esc(r[k])).join(","))].join("\n") + "\n"; }; // --- Table 1: IRS SOI WA returns, all returns and AGI classes $500k-$1M and $1M+ --- const CLASS = { "0": "All returns", "9": "$500,000 under $1,000,000", "10": "$1,000,000 or more" }; const irs = []; for (const yy of [19, 20, 21, 22]) { const rows = parseCsv(fs.readFileSync(path.join(irsDir, `in${yy}.csv`), "latin1")); const h = rows[0].map(s => s.trim()); const n = (r, k) => Number(String(r[h.indexOf(k)]).replace(/,/g, "")); for (const r of rows.filter(r => r[h.indexOf("STATE")] === "WA" && CLASS[r[h.indexOf("AGI_STUB")]])) { const k = r[h.indexOf("AGI_STUB")]; irs.push({ taxYear: 2000 + yy, agiClass: CLASS[k], returns: n(r, "N1"), jointReturns: n(r, "MARS2"), agiDollars: n(r, "A00100") * 1000, wageReturns: n(r, "N00200"), wagesDollars: n(r, "A00200") * 1000, pensionReturns: n(r, "N01700"), pensionDollars: n(r, "A01700") * 1000, iraReturns: n(r, "N01400"), iraDollars: n(r, "A01400") * 1000, taxableSocialSecurityReturns: n(r, "N02500"), taxableSocialSecurityDollars: n(r, "A02500") * 1000, partnershipSCorpReturns: n(r, "N26270"), partnershipSCorpDollars: n(r, "A26270") * 1000, charityReturns: n(r, "N19700"), charityDollars: n(r, "A19700") * 1000, }); } } irs.sort((a, b) => a.taxYear - b.taxYear || a.agiClass.localeCompare(b.agiClass)); fs.writeFileSync(path.join(out, "irs_wa_top_returns.csv"), csv(irs)); // --- Analysis: income mix of WA $1M+ returns, 2022 --- const t = irs.find(r => r.taxYear === 2022 && r.agiClass === "$1,000,000 or more"); const pct = (a, b) => +(100 * a / b).toFixed(1); const retirementDollars = t.pensionDollars + t.iraDollars + t.taxableSocialSecurityDollars; const mix = [ { item: "Returns with AGI of $1,000,000 or more", returns: t.returns, sharePctOfReturns: 100, dollars: t.agiDollars, sharePctOfAgi: 100 }, { item: "Joint returns", returns: t.jointReturns, sharePctOfReturns: pct(t.jointReturns, t.returns), dollars: null, sharePctOfAgi: null }, { item: "Wages and salaries", returns: t.wageReturns, sharePctOfReturns: pct(t.wageReturns, t.returns), dollars: t.wagesDollars, sharePctOfAgi: pct(t.wagesDollars, t.agiDollars) }, { item: "Taxable pensions and annuities", returns: t.pensionReturns, sharePctOfReturns: pct(t.pensionReturns, t.returns), dollars: t.pensionDollars, sharePctOfAgi: pct(t.pensionDollars, t.agiDollars) }, { item: "Taxable IRA distributions", returns: t.iraReturns, sharePctOfReturns: pct(t.iraReturns, t.returns), dollars: t.iraDollars, sharePctOfAgi: pct(t.iraDollars, t.agiDollars) }, { item: "Taxable Social Security benefits", returns: t.taxableSocialSecurityReturns, sharePctOfReturns: pct(t.taxableSocialSecurityReturns, t.returns), dollars: t.taxableSocialSecurityDollars, sharePctOfAgi: pct(t.taxableSocialSecurityDollars, t.agiDollars) }, { item: "Pensions, IRA distributions and taxable Social Security combined (dollars only; returns overlap)", returns: null, sharePctOfReturns: null, dollars: retirementDollars, sharePctOfAgi: pct(retirementDollars, t.agiDollars) }, { item: "Partnership and S corporation net income", returns: t.partnershipSCorpReturns, sharePctOfReturns: pct(t.partnershipSCorpReturns, t.returns), dollars: t.partnershipSCorpDollars, sharePctOfAgi: pct(t.partnershipSCorpDollars, t.agiDollars) }, { item: "Charitable contributions deducted (itemizers)", returns: t.charityReturns, sharePctOfReturns: pct(t.charityReturns, t.returns), dollars: t.charityDollars, sharePctOfAgi: pct(t.charityDollars, t.agiDollars) }, ]; const charityAboveCapLowerBound = t.charityDollars - t.charityReturns * 100000; // --- Table 2: House and Senate floor amendments and their roll calls --- const rc = fs.readFileSync(rcFile, "utf8").split("").slice(1).map(r => { const g = tag => (r.match(new RegExp(`<${tag}>([^<]*)`)) || [])[1]; return { agency: g("Agency"), motion: (g("Motion") || "").replace(/\s+/g, " ").trim(), seq: +g("SequenceNumber"), date: (g("VoteDate") || "").slice(0, 10), yeas: +(r.match(/\s*(\d+)/) || [])[1], nays: +(r.match(/\s*(\d+)/) || [])[1] }; }); const AM = [ ["6346-S.E AMH WALE TAYT 601", "2596", "House", "Rep. Walen", "Not adopted", "Applies the $1,000,000 standard deduction to the individual, not the household, resulting in a combined standard deduction of $2,000,000 for state registered domestic partners and persons who are married."], ["6346-S.E AMH REEV TAYT 572", "2604", "House", "Rep. Reeves", "Not adopted", "Increases the total combined standard deduction for state registered domestic partners and married persons to $1,500,000."], ["6346-S.E AMH MARS HARA 481", "2543", "House", "Rep. Marshall", "Not adopted", "Applies the $1,000,000 standard deduction to the individual, not the household, resulting in a combined standard deduction of $2,000,000 for persons filing jointly. Adds intent language that the legislature will not ever lower the standard deduction amount to anything below $100,000."], ["6346-S AMS WARN S5217.1", "693", "Senate", "Sen. Warnick", "Not adopted", "Increases the standard deduction to $2 million for joint filers."], ["6346-S.E AMH KLIC REIN 502", "2555", "House", "Rep. Klicker", "Not adopted", "Modifies the definition of \"Washington taxable income\" to be based on a five-year average of such income ."], ["6346-S.E AMH STOK TAYT 587", "2587", "House", "Rep. Stokesbary", "Not adopted", "Excludes non-cash value of qualified equity compensation received by an individual from a qualified employer."], ["6346-S.E AMH DENT TAYT 583", "2583", "House", "Rep. Dent", "Not adopted", "Provides a deduction from a taxpayer's Washington base income for the proceeds of the sale of the business assets of a retiring farmer. Provides a deduction from a taxpayer's Washington taxable income for gains from the sale or exchange of Section 1250 property that is used primarily for agricultural purposes. Provides that any gains deferred under Section 1031 for federal purposes will also be deferred for Washington income taxes."], ["6346-S.E AMH REEV TAYT 575", "2602", "House", "Rep. Reeves", "Not adopted", "Provides a deduction from a taxpayer's Washington base income for gains attributable from the sale of certain depreciable personal property by an agricultural producer."], ["6346-S.E AMH JACO TAYT 588", "2588", "House", "Rep. Jacobsen", "Not adopted", "Exempts income earned through a pass-through entity from income tax."], ["6346-S.E AMH GRIF HARA 467", "2566", "House", "Rep. Griffey", "Not adopted", "Excludes public pensions from being included as income for the purposes of the income tax."], ["6346-S.E AMH BARN MORI 404", "2545", "House", "Rep. Barnard", "Not adopted", "Allows a taxpayer to claim a charitable deduction equal to the amount claimed for federal income tax purposes."], ["6346-S.E AMH DUFA HARA 474", "2573", "House", "Rep. Dufault", "Not adopted", "Requires a referendum if any reduction to the standard deduction amount is made."], ["6346-S.E AMH RULE TAYT 602", "2615", "House", "Rep. Rule", "Adopted", "Clarifies that the exemption to Initiative 2111 for the state income tax is only effective so long it only applies to millionaires."], ]; const votes = AM.map(([amendment, floor, chamber, sponsor, result, effect]) => { const v = rc.filter(x => x.agency === chamber && x.motion.startsWith(floor + " ")); if (v.length !== 1) throw new Error(`roll call for ${floor}: ${v.length}`); return { amendment, floorNumber: floor, chamber, sponsor, voteDate: v[0].date, rollCallSequence: v[0].seq, yeas: v[0].yeas, nays: v[0].nays, result, officialEffect: effect }; }); fs.writeFileSync(path.join(out, "amendment_votes.csv"), csv(votes)); // --- Table 3: illustrative households under ESSB 6346 as enacted --- // Base = federal AGI less long-term gains (Sec. 302(1)); if WA capital gains tax is owed, add back // WA capital gains plus the capital gains standard deduction (Sec. 302(3)). Tax = 9.9% x (base - $1,000,000), // less a nonrefundable credit for WA capital gains tax (Sec. 205). WA capital gains tax: 7% of gains above the // standard deduction plus 2.9% of WA capital gains above $1,000,000 (RCW 82.87.040). Deduction held at the 2025 // figure, $278,000; the 2028 figure will be higher after indexing. const CGD = 278000; function calc(h) { const waGains = Math.max(0, h.longTermGainTaxable - CGD); const cgTax = Math.round(0.07 * waGains + 0.029 * Math.max(0, waGains - 1e6)); const ordinary = h.wages + h.retirementIncome + h.otherOrdinaryIncome; const base = ordinary + (cgTax > 0 ? waGains + CGD : 0); const pre = Math.round(0.099 * Math.max(0, base - 1e6)); const credit = Math.min(pre, cgTax); return { ...h, waCapitalGainsTax: cgTax, washingtonBaseIncome: base, incomeTaxBeforeCredit: pre, capitalGainsCredit: credit, millionairesTaxOwed: pre - credit, totalWashingtonTax: cgTax + pre - credit }; } const H = (id, household, wages, retirementIncome, otherOrdinaryIncome, longTermGainTaxable, exemptGain, note) => calc({ id, household, wages, retirementIncome, otherOrdinaryIncome, longTermGainTaxable, exemptGain, note }); const examples = [ H("couple-550k-each", "Married couple, each spouse earns $550,000 in wages", 1100000, 0, 0, 0, 0, "Spouses share one $1,000,000 deduction (Sec. 314)."), H("singles-550k-each", "The same two earners if unmarried (per person)", 550000, 0, 0, 0, 0, "Each single filer has a $1,000,000 deduction."), H("rsu-vest", "Household with $250,000 salary and a one-time $900,000 RSU vest", 1150000, 0, 0, 0, 0, "Vested RSUs are wages in federal AGI."), H("retiree-lump-sum", "Retired couple with $150,000 of pension and taxable Social Security who withdraw $1,000,000 from a 401(k) in one year", 0, 1150000, 0, 0, 0, "Retirement distributions are ordinary AGI; no averaging."), H("retiree-no-lump-sum", "The same retired couple without the withdrawal", 0, 150000, 0, 0, 0, ""), H("inherited-ira-lump", "Heir with $200,000 other income who takes a $4,000,000 inherited IRA in one year", 200000, 4000000, 0, 0, 0, "SECURE Act 10-year rule allows spreading."), H("inherited-ira-spread", "The same heir taking $400,000 a year for 10 years (one year shown)", 200000, 400000, 0, 0, 0, ""), H("wages-plus-stock-gain", "Household with $800,000 wages that sells stock for a $600,000 long-term gain", 800000, 0, 0, 600000, 0, "Gains taxed by the capital gains tax count toward the $1,000,000 line; that tax is credited."), H("wages-only-800k", "The same household without the stock sale", 800000, 0, 0, 0, 0, ""), H("home-sale", "Household with $400,000 wages that sells its home for a $1,500,000 gain", 400000, 0, 0, 0, 1500000, "First $500,000 excluded federally (IRC 121); the rest is real estate gain, exempt from the capital gains tax (RCW 82.87.050) and removed by Sec. 302."), ]; fs.writeFileSync(path.join(out, "household_examples.csv"), csv(examples)); // --- Table 4: payer projection under Sec. 316 vs annual indexing, holding real incomes constant --- const thr = parseCsv(fs.readFileSync(path.join(__dirname, "data", "threshold_real_value.csv"), "utf8")).slice(1) .filter(r => r[0] === "2.5").map(r => ({ taxYear: +r[1], realAsWritten2026: +r[3], realAnnualIndex2026: +r[5] })); const base28 = thr.find(r => r.taxYear === 2028).realAsWritten2026; const projection = thr.filter(r => [2028, 2030, 2035, 2040, 2045, 2050].includes(r.taxYear)).map(r => ({ taxYear: r.taxYear, inflationPct: 2.5, thresholdIn2026DollarsAsWritten: r.realAsWritten2026, thresholdIn2026DollarsAnnualIndex: r.realAnnualIndex2026, payersAsWrittenAlpha19: Math.round(25000 * Math.pow(r.realAsWritten2026 / base28, -1.9) / 100) * 100, payersAsWrittenAlpha15: Math.round(25000 * Math.pow(r.realAsWritten2026 / base28, -1.5) / 100) * 100, payersAnnualIndex: Math.round(25000 * Math.pow(r.realAnnualIndex2026 / base28, -1.7) / 100) * 100, })); fs.writeFileSync(path.join(out, "payer_projection.csv"), csv(projection)); const t9 = irs.find(r => r.taxYear === 2022 && r.agiClass === "$500,000 under $1,000,000"); const alphaLocal = Math.log2((t9.returns + t.returns) / t.returns), alphaMean = (t.agiDollars / t.returns) / (t.agiDollars / t.returns - 1e6); fs.writeFileSync(path.join(out, "results.json"), JSON.stringify({ mix, charityAboveCapLowerBound, votes, examples, projection, alphaLocal, alphaMean }, null, 2)); console.log(JSON.stringify({ charityAboveCapLowerBound, alphaLocal, alphaMean, projection }, null, 1)); console.table(examples.map(e => ({ id: e.id, base: e.washingtonBaseIncome, cg: e.waCapitalGainsTax, owed: e.millionairesTaxOwed, total: e.totalWashingtonTax }))); console.table(votes.map(v => ({ a: v.amendment, y: v.yeas, n: v.nays, r: v.result })));