// Builds the curated tables and saved-analysis results for the // "[TEST] Washington general-fund growth" dataset. Run: node build.js // All budget figures are $ thousands from the Legislative Budget Notes (LBN) // "Budget vs. Budget" total-state / functional-area tables; see SOURCES below. const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const out = __dirname; // ---- Budget by biennium ($K). final = prior-biennium column of the next odd-year LBN. const budget = [ { biennium: '2005-07', midYear: 2006, enactedNgf: null, finalNgf: 30171802, finalAllFunds: 51719959, basket: 'NGF-S (GF-S + HSA, PSEA, VRDE, WQA, SAF, PFSA, ELTA)', finalSource: '2007 LBN', status: 'final' }, { biennium: '2007-09', midYear: 2008, enactedNgf: 33364407, finalNgf: 32597107, finalAllFunds: 57359986, basket: 'NGF-S', finalSource: '2009 LBN', status: 'final' }, { biennium: '2009-11', midYear: 2010, enactedNgf: 31388596, finalNgf: 30274330, finalAllFunds: 60082507, basket: 'NGF-S (most related accounts merged into GF-S in 2009)', finalSource: '2011 LBN', status: 'final' }, { biennium: '2011-13', midYear: 2012, enactedNgf: 31976802, finalNgf: 31470672, finalAllFunds: 61319558, basket: 'NGF-S + Opportunity Pathways', finalSource: '2013 LBN', status: 'final' }, { biennium: '2013-15', midYear: 2014, enactedNgf: 33631274, finalNgf: 33899735, finalAllFunds: 68829730, basket: 'NGF-P (GF-S + ELTA + Opportunity Pathways)', finalSource: '2015 LBN', status: 'final' }, { biennium: '2015-17', midYear: 2016, enactedNgf: 38219179, finalNgf: 38487824, finalAllFunds: 79664510, basket: 'NGF-P', finalSource: '2017 LBN', status: 'final' }, { biennium: '2017-19', midYear: 2018, enactedNgf: 43708381, finalNgf: 44850662, finalAllFunds: 89314698, basket: 'NGF-O (funds subject to outlook)', finalSource: '2019 LBN', status: 'final' }, { biennium: '2019-21', midYear: 2020, enactedNgf: 52498622, finalNgf: 52562318, finalAllFunds: 112396510, basket: 'NGF-O (adds Workforce Education Investment Account)', finalSource: '2021 LBN', status: 'final' }, { biennium: '2021-23', midYear: 2022, enactedNgf: 59066595, finalNgf: 63346479, finalAllFunds: 132593772, basket: 'NGF-O', finalSource: '2023 LBN', status: 'final' }, { biennium: '2023-25', midYear: 2024, enactedNgf: 69804354, finalNgf: 72743525, finalAllFunds: 142419627, basket: 'NGF-O', finalSource: '2025 LBN', status: 'final' }, { biennium: '2025-27', midYear: 2026, enactedNgf: 77857672, finalNgf: 80203984, finalAllFunds: 157299314, basket: 'NGF-O', finalSource: '2026 LBN (after 2026 supplemental)', status: 'in progress' }, ]; // ---- Benchmarks by calendar year const popOfm = { 2006: 6420220, 2008: 6608230, 2010: 6724540, 2012: 6831660, 2014: 7005210, 2016: 7237220, 2018: 7463480, 2020: 7706310, 2022: 7864400, 2024: 8035700, 2026: 8176300 }; const cpiUs = { 2006: 201.6, 2008: 215.303, 2010: 218.056, 2012: 229.594, 2014: 236.736, 2016: 240.007, 2018: 251.107, 2020: 258.811, 2022: 292.655, 2024: 313.689 }; const cpiSea = { 2006: 207.6, 2008: 224.719, 2010: 226.693, 2012: 238.663, 2014: 246.018, 2016: 254.886, 2018: 271.089, 2020: 282.693, 2022: 322.167, 2024: 353.488 }; const pi = { 2005: 232783.8, 2006: 253199.9, 2007: 274770.4, 2008: 289381.1, 2009: 277928.7, 2010: 285504.2, 2011: 302541.4, 2012: 324964.1, 2013: 331032.6, 2014: 355389.1, 2015: 376470.4, 2016: 397376.1, 2017: 423117.1, 2018: 451716.6, 2019: 484654.4, 2020: 522979.4, 2021: 572962.1, 2022: 592762.4, 2023: 636764.3, 2024: 675399.6, 2025: 711640.8 }; const r = (x, d = 1) => Math.round(x * 10 ** d) / 10 ** d; const pct = (a, b) => r((b / a - 1) * 100, 1); const bench = budget.filter(b => cpiUs[b.midYear]).map(b => { const y = b.midYear; const piBien = 0.5 * pi[y - 1] + pi[y] + 0.5 * pi[y + 1]; // $M, two years of income centred on the biennium midpoint return { biennium: b.biennium, midYear: y, population: popOfm[y], cpiUs: cpiUs[y], cpiSeattle: cpiSea[y], personalIncomeBiennialMillions: r(piBien, 1) }; }); const B = Object.fromEntries(bench.map(x => [x.biennium, x])); const N = Object.fromEntries(budget.map(x => [x.biennium, x])); // ---- Functional areas, final NGF ($K) const areas = [ // area, 2013-15 (2015 LBN, NGF-P), 2023-25 (2025 LBN, NGF-O) ['Legislative', 141249, 279098], ['Judicial', 243052, 607082], ['Governmental Operations', 464511, 2374916], ['Other Human Services', 6219402, 15638934], ['Dept of Social & Health Services', 5791717, 11152008], ['Natural Resources', 270208, 1123790], ['Transportation', 69871, 169963], ['Public Schools (K-12)', 15298272, 31352738], ['Higher Education', 3090849, 6105088], ['Other Education', 205808, 108120], ['Special Appropriations', 2104796, 3831788], ]; const sum = (i) => areas.reduce((s, a) => s + a[i], 0); if (sum(1) !== N['2013-15'].finalNgf || sum(2) !== N['2023-25'].finalNgf) throw new Error(`area sums ${sum(1)} ${sum(2)}`); const totalInc = N['2023-25'].finalNgf - N['2013-15'].finalNgf; const areaRows = areas.map(([area, a, b]) => ({ area, ngf2013_15: a, ngf2023_25: b, changeThousands: b - a, pctChange: pct(a, b), shareOfIncreasePct: r((b - a) / totalInc * 100, 1) })) .sort((x, y) => y.changeThousands - x.changeThousands); // ---- Growth comparisons function growth(from, to) { const f = N[from], t = N[to], bf = B[from], bt = B[to]; const popG = bt.population / bf.population, cpiG = bt.cpiUs / bf.cpiUs, seaG = bt.cpiSeattle / bf.cpiSeattle; const ngfG = t.finalNgf / f.finalNgf; return [ { measure: 'Near General Fund, final appropriations (nominal)', fromValue: f.finalNgf, toValue: t.finalNgf, unit: '$ thousands', pctChange: pct(f.finalNgf, t.finalNgf) }, { measure: 'All budgeted funds, final appropriations (nominal)', fromValue: f.finalAllFunds, toValue: t.finalAllFunds, unit: '$ thousands', pctChange: pct(f.finalAllFunds, t.finalAllFunds) }, { measure: 'Population (OFM, April 1 of middle year)', fromValue: bf.population, toValue: bt.population, unit: 'persons', pctChange: r((popG - 1) * 100) }, { measure: 'Population x US CPI-U', fromValue: null, toValue: null, unit: 'index', pctChange: r((popG * cpiG - 1) * 100) }, { measure: 'Population x Seattle CPI-U', fromValue: null, toValue: null, unit: 'index', pctChange: r((popG * seaG - 1) * 100) }, { measure: 'Real per-capita NGF, US CPI-U', fromValue: null, toValue: null, unit: 'index', pctChange: r((ngfG / popG / cpiG - 1) * 100) }, { measure: 'Real per-capita NGF, Seattle CPI-U', fromValue: null, toValue: null, unit: 'index', pctChange: r((ngfG / popG / seaG - 1) * 100) }, { measure: 'WA personal income (BEA, biennial)', fromValue: bf.personalIncomeBiennialMillions, toValue: bt.personalIncomeBiennialMillions, unit: '$ millions', pctChange: pct(bf.personalIncomeBiennialMillions, bt.personalIncomeBiennialMillions) }, { measure: 'NGF if it had grown with population x US CPI-U', fromValue: f.finalNgf, toValue: Math.round(f.finalNgf * popG * cpiG), unit: '$ thousands', pctChange: r((popG * cpiG - 1) * 100) }, ]; } const g1 = growth('2013-15', '2023-25'); const g2 = growth('2005-07', '2023-25'); // Index series (2013-15 = 100) for the chart const index = []; for (const b of bench) { const n = N[b.biennium], base = N['2013-15'], bb = B['2013-15']; index.push({ biennium: b.biennium, series: 'NGF appropriations', index: r(n.finalNgf / base.finalNgf * 100) }); index.push({ biennium: b.biennium, series: 'Population x US inflation', index: r(b.population * b.cpiUs / (bb.population * bb.cpiUs) * 100) }); index.push({ biennium: b.biennium, series: 'WA personal income', index: r(b.personalIncomeBiennialMillions / bb.personalIncomeBiennialMillions * 100) }); } const share = bench.map(b => ({ biennium: b.biennium, ngfShareOfPersonalIncomePct: r(N[b.biennium].finalNgf / 1000 / b.personalIncomeBiennialMillions * 100, 2) })); const k12 = { from: 15298272, to: 31352738 }; const exK12 = [ { measure: 'K-12 (Public Schools) NGF', from: k12.from, to: k12.to, pctChange: pct(k12.from, k12.to) }, { measure: 'NGF excluding K-12', from: N['2013-15'].finalNgf - k12.from, to: N['2023-25'].finalNgf - k12.to, pctChange: pct(N['2013-15'].finalNgf - k12.from, N['2023-25'].finalNgf - k12.to) }, { measure: 'DSHS + Other Human Services combined', from: 5791717 + 6219402, to: 11152008 + 15638934, pctChange: pct(5791717 + 6219402, 11152008 + 15638934) }, ]; const enacted = { from: N['2013-15'].enactedNgf, to: N['2023-25'].enactedNgf, pctChange: pct(N['2013-15'].enactedNgf, N['2023-25'].enactedNgf) }; // ---- CSV writers const csv = (rows, cols) => [cols.join(','), ...rows.map(o => cols.map(c => o[c] === null || o[c] === undefined ? '' : /[,"]/.test(String(o[c])) ? `"${String(o[c]).replace(/"/g, '""')}"` : o[c]).join(','))].join('\n') + '\n'; const files = { 'wa-ngf-by-biennium.csv': csv(budget, ['biennium', 'midYear', 'enactedNgf', 'finalNgf', 'finalAllFunds', 'basket', 'finalSource', 'status']), 'wa-benchmarks-by-biennium.csv': csv(bench, ['biennium', 'midYear', 'population', 'cpiUs', 'cpiSeattle', 'personalIncomeBiennialMillions']), 'wa-ngf-by-functional-area.csv': csv(areaRows, ['area', 'ngf2013_15', 'ngf2023_25', 'changeThousands', 'pctChange', 'shareOfIncreasePct']), 'wa-growth-index.csv': csv(index, ['biennium', 'series', 'index']), }; for (const [f, c] of Object.entries(files)) fs.writeFileSync(path.join(out, f), c); const results = { g1, g2, index, share, areaRows, exK12, enacted }; fs.writeFileSync(path.join(out, 'results.json'), JSON.stringify(results, null, 2)); for (const f of [...Object.keys(files), 'build.js']) { const b = fs.readFileSync(path.join(out, f)); console.log(f, b.length, 'sha256:' + crypto.createHash('sha256').update(b).digest('hex')); } console.log(JSON.stringify({ g1, g2, share, exK12, enacted }, null, 1)); console.table(areaRows);