// Builds the wa-millionaires-tax-revenue-estimates dataset tables and saved-analysis results. // Inputs are typed in from the primary documents named in each row (no downloads at run time). // Run: node build.js -> writes the CSVs and results.json next to this file and prints digests. const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const out = (f) => path.join(__dirname, f); // ---- Table 1: gross income-tax revenue by fiscal year, by estimate ($ millions) ---- // value_type: observed = printed in the source; derived = our calculation (method in note). const est = []; const add = (id, name, publisher, date, fy, v, type, note) => est.push({ estimate_id: id, estimate_name: name, publisher, published_date: date, fiscal_year: fy, gross_income_tax_musd: v, value_type: type, note }); // ESSB 6346 fiscal note, DOR "Tax on Income" line (FNS pkg 77653, p. 116 of the package). [[2029, 2698], [2030, 3732], [2031, 3167], [2032, 3297], [2033, 3439], [2034, 3591], [2035, 3753]].forEach(([fy, v]) => add('fiscal-note', 'ESSB 6346 fiscal note (Mar 2026)', 'OFM / Dept. of Revenue', '2026-03-16', fy, v, 'observed', 'Tax on Income line, account 001, cash basis; static (no behavioral response).')); // ERFC June 2026: FY29 = 2027-29 IIT ($2,698M, WRC 9/25: Sept $3,503M less $805M increase); // FY30 = 5% Fair Start transfer FY31 ($186.6M = 251.6 - 65.0) / 0.05; FY31 = 2029-31 IIT ($6,899M = 9,435 - 2,536) - FY30. add('erfc-june', 'ERFC forecast (June 2026)', 'Economic and Revenue Forecast Council', '2026-06-26', 2029, 2698, 'observed', 'Individual Income Tax, 2027-29 biennium, June 2026 forecast column (ERFC Sept 2026 publication, Table 3.7); only FY29 has collections.'); add('erfc-june', 'ERFC forecast (June 2026)', 'Economic and Revenue Forecast Council', '2026-06-26', 2030, 3732, 'derived', 'June Fair Start transfer for FY31 ($251.6M - $65.0M = $186.6M) divided by 5%.'); add('erfc-june', 'ERFC forecast (June 2026)', 'Economic and Revenue Forecast Council', '2026-06-26', 2031, 3167, 'derived', '2029-31 income-tax forecast ($9,435M - $2,536M = $6,899M) less FY30.'); // OFM fiscal impact statement for I-645 (July 2026), Table 1 total revenue loss (includes Fair Start transfer). [[2029, 3108], [2030, 4432], [2031, 3892]].forEach(([fy, v]) => add('ofm-fis', 'OFM fiscal impact statement for I-645 (Jul 2026)', 'Office of Financial Management', '2026-07-23', fy, v, 'observed', 'Table 1 total revenue loss, state general fund + Fair Start for Kids account. Excludes $0.414M federal funds in FY27-28.')); // ERFC September 2026: FY29 = 2027-29 IIT ($3,503M, WRC 9/25); FY30 = Fair Start FY31 transfer $251.6M / 5%; // FY31 = 2029-31 IIT $9,435M - FY30. add('erfc-sept', 'ERFC forecast (Sept 2026)', 'Economic and Revenue Forecast Council', '2026-09-25', 2029, 3503.1, 'observed', 'Individual Income Tax, 2027-29 biennium, Sept 2026 forecast (ERFC publication Table 3.7); only FY29 has collections. Fair Start FY30 transfer $175.2M / 5% = $3,504M agrees.'); add('erfc-sept', 'ERFC forecast (Sept 2026)', 'Economic and Revenue Forecast Council', '2026-09-25', 2030, 5032, 'derived', 'ERFC Fair Start transfer for FY31 ($251.6M) divided by 5%. Our calculation; +/- $2M rounding.'); add('erfc-sept', 'ERFC forecast (Sept 2026)', 'Economic and Revenue Forecast Council', '2026-09-25', 2031, 4403, 'derived', '2029-31 income-tax forecast ($9,435M, WRC 9/25/2026) less FY30. Our calculation. ERFC Table 3.9 "Other (including Individual Income Tax)" FY30 $5,089.0M and FY31 $4,461.8M agree after about $57-59M of other revenue.'); // ---- Table 2: fiscal note, whole-bill net state revenue vs gross income tax ($ millions) ---- const netRaw = { 2026: 0, 2027: -46.478, 2028: -52.338, 2029: 2353.348, 2030: 2902.265, 2031: 2440.364, 2032: 2546.823, 2033: 2664.641, 2034: 2790.72, 2035: 2927.969 }; const grossFN = { 2029: 2698, 2030: 3732, 2031: 3167, 2032: 3297, 2033: 3439, 2034: 3591, 2035: 3753 }; const pkg = Object.keys(netRaw).map((k) => { const fy = +k, g = grossFN[fy] ?? 0, n = netRaw[fy]; return { fiscal_year: fy, gross_income_tax_musd: g, net_state_revenue_musd: n, revenue_side_relief_musd: +(n - g).toFixed(3) }; }); // ---- Table 3: 2027-29 budget outlook ($ millions) ---- const outlook = [ { item: 'NGF-O projected ending balance, 2027-29 (enacted 2026 supplemental outlook)', value_musd: 653, value_type: 'observed', source_id: 'erfc-outlook-apr-2026' }, { item: 'ESSB 6346 counted in the outlook, 2027-29 (net, after revenue-side relief)', value_musd: 2309.18, value_type: 'observed', source_id: 'erfc-outlook-apr-2026' }, { item: 'Gross income-tax revenue assumed for 2027-29 (FY29)', value_musd: 2698, value_type: 'observed', source_id: 'wrc-i645-brief' }, { item: 'Ending balance without the tax, if its relief were also removed (653 - 2,309.18)', value_musd: +(653 - 2309.18).toFixed(2), value_type: 'derived', source_id: 'erfc-outlook-apr-2026' }, { item: 'Ending balance without the tax, relief kept as under I-645 (653 - 2,698)', value_musd: 653 - 2698, value_type: 'derived', source_id: 'erfc-outlook-apr-2026' }, { item: 'Free school meals in FY2029 projected expenditures (not mandatory)', value_musd: 140, value_type: 'observed', source_id: 'erfc-outlook-apr-2026' }, { item: 'City and county fiscal health in FY2029 projected expenditures (not mandatory)', value_musd: 200, value_type: 'observed', source_id: 'erfc-outlook-apr-2026' }, ]; // ---- CSV writer ---- const csv = (rows) => { const cols = Object.keys(rows[0]); const q = (v) => (typeof v === 'string' && /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : String(v)); return cols.join(',') + '\n' + rows.map((r) => cols.map((c) => q(r[c])).join(',')).join('\n') + '\n'; }; fs.writeFileSync(out('estimates_by_fy.csv'), csv(est)); fs.writeFileSync(out('fiscal_note_net_vs_gross.csv'), csv(pkg)); fs.writeFileSync(out('outlook_2027_29.csv'), csv(outlook)); // ---- Saved-analysis results ---- const r3 = (x) => Math.round(x * 1000) / 1000; const sum = (id, fys) => est.filter((e) => e.estimate_id === id && fys.includes(e.fiscal_year)) .reduce((s, e) => s + e.gross_income_tax_musd, 0); const byFy = est.map((e) => ({ estimate: e.estimate_name, fiscalYear: e.fiscal_year, grossBillions: r3(e.gross_income_tax_musd / 1000), valueType: e.value_type })); const totals = [ { window: 'Fiscal note, FY2029-32 (four years of collections)', estimate: 'ESSB 6346 fiscal note (Mar 2026)', billions: r3(sum('fiscal-note', [2029, 2030, 2031, 2032]) / 1000) }, { window: 'Fiscal note, FY2029-35 (ten-year table)', estimate: 'ESSB 6346 fiscal note (Mar 2026)', billions: r3(sum('fiscal-note', [2029, 2030, 2031, 2032, 2033, 2034, 2035]) / 1000) }, { window: 'Fiscal note, FY2029-31 (three years of collections)', estimate: 'ESSB 6346 fiscal note (Mar 2026)', billions: r3(sum('fiscal-note', [2029, 2030, 2031]) / 1000) }, { window: 'ERFC June, FY2029-31 (2027-29 + 2029-31 biennia)', estimate: 'ERFC forecast (June 2026)', billions: r3(sum('erfc-june', [2029, 2030, 2031]) / 1000) }, { window: 'OFM I-645 fiscal impact, FY2027-31 (collections FY29-31)', estimate: 'OFM fiscal impact statement for I-645 (Jul 2026)', billions: 11.432 }, { window: 'ERFC Sept, FY2029-31 (2027-29 + 2029-31 biennia)', estimate: 'ERFC forecast (Sept 2026)', billions: r3((3503.1 + 9435) / 1000) }, ]; const outlookRows = outlook.map((o) => ({ item: o.item, billions: r3(o.value_musd / 1000), valueType: o.value_type })); const ratios = totals.filter((t) => !t.window.includes('FY2029-31 (three')).map((t) => ({ comparedWith: t.window, billions: t.billions, ratio: Math.round((300 / t.billions) * 10) / 10 })); const relief = pkg.filter((p) => p.fiscal_year >= 2029 && p.fiscal_year <= 2032).map((p) => ({ fiscalYear: p.fiscal_year, grossBillions: r3(p.gross_income_tax_musd / 1000), netBillions: r3(p.net_state_revenue_musd / 1000), reliefBillions: r3(-p.revenue_side_relief_musd / 1000), netShareOfGross: Math.round((p.net_state_revenue_musd / p.gross_income_tax_musd) * 1000) / 1000 })); const results = { byFy, totals, outlookRows, ratios, relief }; fs.writeFileSync(out('results.json'), JSON.stringify(results, null, 2) + '\n'); for (const f of ['estimates_by_fy.csv', 'fiscal_note_net_vs_gross.csv', 'outlook_2027_29.csv', 'results.json', 'build.js']) { const b = fs.readFileSync(out(f)); console.log(f, b.length, 'sha256:' + crypto.createHash('sha256').update(b).digest('hex')); } console.log(JSON.stringify(results, null, 1));