// Builds the curated tables for the WA millionaires-tax dataset. // Inputs: IRS SOI state files 19-22in55cmcsv.csv (path given as argv[2]). // Outputs: data/*.csv and data/results.json const fs = require("fs"), path = require("path"); const irsDir = process.argv[2]; const out = path.join(__dirname, "data"); // --- Table 1: IRS SOI, Washington returns with AGI >= $1M (AGI_STUB 10) --- 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 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 col = k => h.indexOf(k); const num = (r, k) => Number(String(r[col(k)]).replace(/,/g, "")); const wa = rows.filter(r => r[col("STATE")] === "WA"); const tot = wa.find(r => r[col("AGI_STUB")] === "0"), top = wa.find(r => r[col("AGI_STUB")] === "10"); irs.push({ taxYear: 2000 + yy, allReturns: num(tot, "N1"), returns1mPlus: num(top, "N1"), shareOfReturnsPct: +(100 * num(top, "N1") / num(tot, "N1")).toFixed(2), agi1mPlusBillion: +(num(top, "A00100") / 1e6).toFixed(1), shareOfAgiPct: +(100 * num(top, "A00100") / num(tot, "A00100")).toFixed(1), wages1mPlusBillion: +(num(top, "A00200") / 1e6).toFixed(1), netCapitalGain1mPlusBillion: +(num(top, "A01000") / 1e6).toFixed(1), partnershipSCorp1mPlusBillion: +(num(top, "A26270") / 1e6).toFixed(1), }); } // --- Table 2: real value of the $1M standard deduction, tax years 2028-2050 --- // ESSB 6346 sec. 316: adjusted each October of odd years from 2029 by the 12-month CPI-W change; // an October adjustment applies to taxes due the next calendar year, i.e. that tax year's return. // Model: tax year t has n = count of odd years y with 2029 <= y <= t adjustments at rate p; // prices in tax year t are (1+p)^(t-2026) times 2026 prices. const threshold = []; for (const p of [0.02, 0.025, 0.03]) { for (let t = 2028; t <= 2050; t++) { let n = 0; for (let y = 2029; y <= t; y++) if (y % 2 === 1) n++; const annualN = Math.max(0, t - 2028); // annual indexing from tax year 2029, as RCW 82.87.150 does for capital gains const prices = Math.pow(1 + p, t - 2026); const asWritten = Math.round(1e6 * Math.pow(1 + p, n) / 1000) * 1000; const annual = Math.round(1e6 * Math.pow(1 + p, annualN) / 1000) * 1000; threshold.push({ inflationPct: +(p * 100).toFixed(1), taxYear: t, nominalAsWritten: asWritten, realAsWritten2026: Math.round(asWritten / prices / 1000) * 1000, nominalAnnualIndex: annual, realAnnualIndex2026: Math.round(annual / prices / 1000) * 1000, }); } } // --- Table 3: WA capital gains excise tax net collections (DOR, as of June 9, 2026) --- const capGains = [ { taxYear: 2022, netCollectionsMillion: 782.0, netPayments: 3625, status: "observed" }, { taxYear: 2023, netCollectionsMillion: 394.7, netPayments: 3649, status: "observed" }, { taxYear: 2024, netCollectionsMillion: 583.8, netPayments: 6727, status: "observed" }, { taxYear: 2025, netCollectionsMillion: 1500.0, netPayments: 9050, status: "initial (DOR reports $1.5 billion, rounded)" }, ]; // --- Table 4: households expected to pay vs. all WA households --- const hh = 3168080; // ACS 2024 1-year B11001_001E, MOE +/- 9,393 const households = [ { estimate: "Legislature's fiscal note (November 2025 forecast)", payingHouseholds: 21000, waHouseholds: hh }, { estimate: "Department of Revenue revision (June 2026 forecast)", payingHouseholds: 25000, waHouseholds: hh }, ].map(r => ({ ...r, sharePct: +(100 * r.payingHouseholds / r.waHouseholds).toFixed(2) })); const toCsv = rows => [Object.keys(rows[0]).join(","), ...rows.map(r => Object.values(r).map(v => typeof v === "string" && /[,"]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v).join(","))].join("\n") + "\n"; fs.writeFileSync(path.join(out, "wa_irs_returns_1m_plus.csv"), toCsv(irs)); fs.writeFileSync(path.join(out, "threshold_real_value.csv"), toCsv(threshold)); fs.writeFileSync(path.join(out, "wa_capital_gains_collections.csv"), toCsv(capGains)); fs.writeFileSync(path.join(out, "paying_household_share.csv"), toCsv(households)); console.log(JSON.stringify({ irs, households }, null, 1)); for (const p of [2, 2.5, 3]) console.log(p, threshold.filter(r => r.inflationPct === p && [2028, 2030, 2035, 2040, 2045, 2050].includes(r.taxYear)).map(r => `${r.taxYear}:${r.realAsWritten2026}/${r.realAnnualIndex2026}`).join(" "));