// Builds release CSVs, analyses and the FactHub dataset content for // dataset:wa-cca-spending-cost-per-ton. Run: node build_dataset.js (after dl/rows.json exists). const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const rows = require('./dl/rows.json'); const REL = path.join(__dirname, 'release'); fs.mkdirSync(REL, { recursive: true }); const KEY = 'wa-cca-spending-cost-per-ton'; const TODAY = '2026-11-03'; const EXEC = '2026-11-03T18:00:00Z'; const PRICE_MAX = 70.86; // highest quarterly settlement price through 2025 (Dec 2025) const PRICE_AVG = 43.66; // mean of the 8 auctions Aug 2023 - Jun 2025 (FY2024-25) const SCC_2030 = 230; // EPA 2023, SC-CO2 for 2030 emissions, 2% near-term rate (2020$) const PRIOR = { nodeHandle: 'dataset:wa-cca-gasoline-price-premium-2015-2026:2', contentDigest: 'sha256:c77d8375a8e2ea65d0bec15dc19fe31695c6ec1a414a1ebe261a2c7a209f9d24' }; const CATS = [ ['Ferry hybrid-electric conversion', r => /Marine Construction|144-car Hybrid/i.test(r.title)], ['Industrial and clean-fuel projects', r => /Hard-to-Decarbonize|Industrial expansion|Port of Everett|Port Electrification|Port of Anacortes/i.test(r.title)], ['Solar and grid', r => /Solar|Grid Integration|Clean Energy Generation|Tribal Clean Energy Development/i.test(r.title)], ['Home appliance and electrification rebates', r => /Home Elec|Electrification and Appliance/i.test(r.title)], ['Home weatherization', r => /Weatherization/i.test(r.title)], ['Public and commercial building upgrades', r => /Building|Energy and Operational|Energy Efficiency|HVAC|Decarbonization Planning|Academic Complex|Local Emission Reduction|Legislative Campus|Indoor Air|Climate and Clean Energy|Tribal Clean Energy Training/i.test(r.title)], ['State agency vehicles and buildings', r => /^Greenhouse Gas Emissions Reductions|BEV 4x4/i.test(r.title)], ['EV charging', r => /EV Charging|EV Supply|ZEV State Infrastructure/i.test(r.title)], ['Transit buses and service', r => /Bus and Facility|Transit|Special Needs|Green Transportation|Connecting Communities|ZEV Access|Electric Fire Engines/i.test(r.title)], ['Walking, biking and e-bikes', r => /Pedestrian|Bike|Safe Routes/i.test(r.title)], ['Farms, soils and waste', r => /Agricultur|Organic Materials|Waste Material|Sustainable Farms|Digester/i.test(r.title)], ['Habitat and forests', r => /Riparian|Salmon|Carbon Offset/i.test(r.title)], ['Tribal consultation capacity', r => /Tribal Consultation/i.test(r.title)], ]; const catOf = r => (CATS.find(([, f]) => f(r)) || ['Other'])[0]; // Second line items that repeat a project's full lifetime tonnage (see qualityReport). const isDup = r => /Electrical Connection for Hybrid/.test(r.desc) || (/^91000043/.test(r.title) && r.tons === 3600) || (/^Transit Support Grants/.test(r.title) && r.tons === 219.5); const sum = (a, f) => a.reduce((s, x) => s + f(x), 0); const median = a => { const s = [...a].sort((x, y) => x - y); const m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; }; const r2 = x => Math.round(x * 100) / 100, r4 = x => Math.round(x * 10000) / 10000; const all = rows; const recs = rows.filter(r => r.quant === 'Yes' && r.tons > 0).map(r => ({ ...r, cat: catOf(r), cpt: r.exp / r.tons, dup: isDup(r) })) .sort((a, b) => a.cpt - b.cpt || a.pdfPage - b.pdfPage); recs.forEach((r, i) => { r.id = 'p' + String(i + 1).padStart(3, '0'); }); const notEst = rows.filter(r => r.quant === 'Yes' && !(r.tons > 0)); const totExp = sum(recs, r => r.exp), totT = sum(recs, r => r.tons); const shareAbove = (th, set = recs, tonsOf = r => r.tons) => { const above = set.filter(r => r.exp / tonsOf(r) > th); return { projects: r4(above.length / set.length), spending: r4(sum(above, r => r.exp) / sum(set, r => r.exp)), tons: r4(sum(above, tonsOf) / sum(set, tonsOf)) }; }; // ---------- CSVs ---------- const csvq = v => { const s = String(v ?? ''); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; const toCsv = (hdr, data) => [hdr.join(',')].concat(data.map(d => hdr.map(h => csvq(d[h])).join(','))).join('\n') + '\n'; const projHdr = ['project_id', 'agency', 'program', 'recipient', 'county', 'category', 'fy2025_cca_spending_usd', 'lifetime_reduction_tco2e', 'cost_per_ton_usd', 'reported_cost_per_ton_usd', 'possible_duplicate_tonnage', 'report_pdf_page', 'description']; fs.writeFileSync(path.join(REL, 'projects.csv'), toCsv(projHdr, recs.map(r => ({ project_id: r.id, agency: r.agency, program: r.title, recipient: r.recip, county: r.county, category: r.cat, fy2025_cca_spending_usd: r.exp.toFixed(0), lifetime_reduction_tco2e: r.tons, cost_per_ton_usd: r.cpt.toFixed(2), reported_cost_per_ton_usd: r.cost ?? '', possible_duplicate_tonnage: r.dup ? 'yes' : 'no', report_pdf_page: r.pdfPage, description: r.desc, })))); const byCat = {}; for (const r of recs) { const c = byCat[r.cat] || (byCat[r.cat] = { projects: 0, spending: 0, tons: 0, cpts: [] }); c.projects++; c.spending += r.exp; c.tons += r.tons; c.cpts.push(r.cpt); } const catRows = Object.entries(byCat).map(([k, c]) => ({ category: k, projects: c.projects, spending: Math.round(c.spending), tons: Math.round(c.tons * 10) / 10, costPerTon: Math.round(c.spending / c.tons), medianProjectCostPerTon: Math.round(median(c.cpts)), timesHighestAuctionPrice: Math.round(c.spending / c.tons / PRICE_MAX * 10) / 10, })).sort((a, b) => a.costPerTon - b.costPerTon); fs.writeFileSync(path.join(REL, 'categories.csv'), toCsv(['category', 'projects', 'spending', 'tons', 'costPerTon', 'medianProjectCostPerTon', 'timesHighestAuctionPrice'], catRows)); const ceilings = [[50, 'Up to $50'], [PRICE_MAX, 'Up to $71 (top auction price)'], [100, 'Up to $100'], [SCC_2030, 'Up to $230 (EPA social cost)'], [500, 'Up to $500'], [1000, 'Up to $1,000'], [5000, 'Up to $5,000'], [10000, 'Up to $10,000'], [Infinity, 'All projects']]; const cumRows = []; ceilings.forEach(([c, label], i) => { const under = recs.filter(r => r.cpt <= c); cumRows.push({ order: i + 1, costCeiling: label, measure: 'Share of tons cut', share: r4(sum(under, r => r.tons) / totT) }); cumRows.push({ order: i + 1, costCeiling: label, measure: 'Share of spending', share: r4(sum(under, r => r.exp) / totExp) }); }); fs.writeFileSync(path.join(REL, 'cumulative.csv'), toCsv(['order', 'costCeiling', 'measure', 'share'], cumRows)); const totals = [ { scope: '2023-25 biennium, Ecology report as first published (Nov 2025)', spending_usd: 386278814, lifetime_reduction_tco2e: 8635900, cost_per_ton_reported_usd: 40, source: 'Ecology transmittal letter, March 19, 2026' }, { scope: '2023-25 biennium, Ecology revised report (Mar 2026)', spending_usd: 386278814, lifetime_reduction_tco2e: 1201286, cost_per_ton_reported_usd: 322, source: 'Ecology Pub. 25-14-107, p. 43' }, { scope: 'Fiscal year 2025 projects with estimates, revised report (sum of Appendix B rows)', spending_usd: Math.round(totExp), lifetime_reduction_tco2e: Math.round(totT * 10) / 10, cost_per_ton_reported_usd: '', source: 'Ecology Pub. 25-14-107, Appendix B; FactHub sum' }, ]; fs.writeFileSync(path.join(REL, 'report-totals.csv'), toCsv(['scope', 'spending_usd', 'lifetime_reduction_tco2e', 'cost_per_ton_reported_usd', 'source'], totals)); const dups = recs.filter(r => r.dup).map(r => ({ project_id: r.id, agency: r.agency, program: r.title, description: r.desc.slice(0, 160), fy2025_cca_spending_usd: r.exp.toFixed(0), repeated_tco2e: r.tons, report_pdf_page: r.pdfPage })); fs.writeFileSync(path.join(REL, 'possible-duplicates.csv'), toCsv(['project_id', 'agency', 'program', 'description', 'fy2025_cca_spending_usd', 'repeated_tco2e', 'report_pdf_page'], dups)); // copy scripts const SCR = 'C:/Users/mikes/AppData/Local/Temp/claude/C--dev-FactHub-Tester/052fefbf-9b7d-4c59-912c-26146f42bd16/scratchpad/pdfjs'; fs.copyFileSync(path.join(SCR, 'dump.mjs'), path.join(REL, 'dump.mjs.txt')); fs.copyFileSync(path.join(SCR, 'parse.mjs'), path.join(REL, 'parse.mjs.txt')); fs.copyFileSync(__filename, path.join(REL, 'build_dataset.js')); // ---------- analyses ---------- const dupFree = recs.filter(r => !r.dup); const fy = { projects: recs.length, spending: Math.round(totExp), tons: Math.round(totT * 10) / 10, costPerTon: r2(totExp / totT), medianProjectCostPerTon: r2(median(recs.map(r => r.cpt))), highestAuctionPrice: PRICE_MAX, allFy2025Rows: all.length, allFy2025Spending: Math.round(sum(all, r => r.exp)), expectedButNotEstimatedRows: notEst.length, expectedButNotEstimatedSpending: Math.round(sum(notEst, r => r.exp)), }; const thresholds = [ ['Highest auction price ($70.86)', PRICE_MAX], ['Twice the highest auction price ($141.72)', 2 * PRICE_MAX], ['EPA social cost of carbon, 2030 emissions ($230)', SCC_2030], ['$1,000 per ton', 1000], ].map(([label, t]) => ({ threshold: label, thresholdUsdPerTon: r2(t), ...Object.fromEntries(Object.entries(shareAbove(t)).map(([k, v]) => ['share' + k[0].toUpperCase() + k.slice(1) + 'Above', v])) })); const ferryInd = recs.filter(r => /Ferry|Industrial/.test(r.cat)); const cheap = { categories: 'Ferry hybrid-electric conversion; Industrial and clean-fuel projects', spending: Math.round(sum(ferryInd, r => r.exp)), tons: Math.round(sum(ferryInd, r => r.tons)), shareOfSpending: r4(sum(ferryInd, r => r.exp) / totExp), shareOfTons: r4(sum(ferryInd, r => r.tons) / totT) }; const dupSens = { removedRows: recs.length - dupFree.length, removedTons: Math.round(sum(recs.filter(r => r.dup), r => r.tons) * 10) / 10, tonsWithoutDuplicates: Math.round(sum(dupFree, r => r.tons) * 10) / 10, costPerTonWithoutDuplicates: r2(totExp / sum(dupFree, r => r.tons)), shareSpendingAboveHighestPriceWithoutDuplicates: shareAbove(PRICE_MAX, dupFree).spending, }; const tonSens = [1, 2, 3, 5].map(m => ({ tonsMultiplier: m, shareSpendingAboveHighestPrice: shareAbove(PRICE_MAX, recs, r => r.tons * m).spending, pooledCostPerTon: r2(totExp / (totT * m)) })); const analyses = [ { id: 'biennium-totals', title: 'Ecology report totals before and after the correction', question: 'How much did the 2023-25 lifetime emission-cut total and average cost per ton change when Ecology revised its report?', operation: 'Transcription of Ecology figures; cost per ton = spending / lifetime tons.', scope: '2023-25 biennium, all 37 agencies, projects Ecology counted as directly cutting emissions.', resultShape: 'table', structuredResult: totals.slice(0, 2).map(t => ({ version: t.scope, spending: t.spending_usd, tons: t.lifetime_reduction_tco2e, reportedCostPerTon: t.cost_per_ton_reported_usd, computedCostPerTon: r2(t.spending_usd / t.lifetime_reduction_tco2e) })), displayResult: 'Lifetime cuts fell from 8,635,900 to 1,201,286 metric tons (-86.1%); Ecology\u2019s average cost rose from $40 to $322 per ton. Dividing the same $386.3 million by the original tons gives $44.73, so Ecology\u2019s first-published $40 used slightly different inputs.', methodologicalNotes: ['Both versions use the same $386,278,814 of 2023-25 spending on projects expected to cut emissions, as stated in Ecology\u2019s March 19, 2026 transmittal letter.'], limitations: ['Lifetime tons are agency projections from calculator tools, not measured cuts.', 'Biennium totals include fiscal year 2024 projects that this dataset does not list one by one.'], executedAt: EXEC, }, { id: 'fy2025-summary', title: 'Fiscal year 2025 projects with emission estimates: totals', question: 'What did the fiscal year 2025 projects with emission estimates cost, and what do they claim to cut, in the revised report?', operation: 'Sum of Appendix B rows marked "Yes" (quantifiable reductions expected) with a numeric lifetime reduction; pooled cost per ton = total spending / total tons; median of per-project spending / tons.', scope: 'Fiscal year 2025 (July 2024 - June 2025), 567 line items from 37-agency Appendix B of Ecology Pub. 25-14-107 (revised March 2026).', resultShape: 'scalar', structuredResult: fy, displayResult: `${fy.projects} line items, $${(fy.spending / 1e6).toFixed(1)} million, ${Math.round(fy.tons).toLocaleString('en-US')} lifetime tons: $${fy.costPerTon.toFixed(0)} per ton pooled; median line item $${Math.round(fy.medianProjectCostPerTon).toLocaleString('en-US')} per ton.`, methodologicalNotes: ['Spending and tons match the report\u2019s own fiscal year 2025 totals exactly ($333,128,350 and 866,116 tons including 123,128 tons Ecology labels as enabled by EV chargers), which confirms the extraction.', 'Cost per ton uses only the fiscal year 2025 CCA expenditure on each line, matching Ecology\u2019s pooled method; the report\u2019s own per-row cost column sometimes uses other cost bases.'], limitations: ['A "line item" is one row of Appendix B; some projects span several rows.', `${fy.expectedButNotEstimatedRows} rows ($${(fy.expectedButNotEstimatedSpending / 1e6).toFixed(1)} million, mostly Department of Natural Resources forest work) were expected to cut emissions but had no estimate; they are excluded, as in Ecology\u2019s totals.`], executedAt: EXEC, robustnessChecks: [ { analysisId: 'duplicate-sensitivity', checkType: 'sensitivity', summary: `Dropping ${dupSens.removedRows} line items that repeat another line\u2019s full tonnage removes ${Math.round(dupSens.removedTons).toLocaleString('en-US')} tons and raises the pooled cost to $${dupSens.costPerTonWithoutDuplicates.toFixed(0)} per ton.` }, { analysisId: 'tonnage-sensitivity', checkType: 'sensitivity', summary: 'Even if every project cut three times the tons it reported, 82% of spending would still cost more per ton than the highest auction price.' }, ], }, { id: 'share-above-benchmarks', title: 'Share of projects, spending and tons above carbon-price benchmarks', question: 'What share of fiscal year 2025 emission-cutting projects, dollars and tons cost more per lifetime ton than the carbon price and other benchmarks?', operation: 'For each benchmark B, count line items with spending / tons > B, and sum their spending and tons; divide by totals.', parameters: { highestAuctionPrice: PRICE_MAX, averageFy2024to25AuctionPrice: PRICE_AVG, eraSocialCost2030: SCC_2030 }, scope: 'Same 567 fiscal year 2025 line items as fy2025-summary.', resultShape: 'table', structuredResult: thresholds, displayResult: thresholds.map(t => `${t.threshold}: ${(t.shareSpendingAbove * 100).toFixed(1)}% of spending, ${(t.shareProjectsAbove * 100).toFixed(1)}% of line items, ${(t.shareTonsAbove * 100).toFixed(1)}% of tons above`).join('; ') + '.', datasetReferences: [PRIOR], methodologicalNotes: ['The highest quarterly settlement price through 2025 was $70.86 (December 2025), from the auction-prices table of dataset wa-cca-gasoline-price-premium-2015-2026 version 2. The eight auctions held during fiscal years 2024-25 averaged $43.66, so using the highest price favors the projects.', 'EPA\u2019s 2023 social cost of carbon dioxide is $190 for 2020 emissions and $230 for 2030 emissions (2020 dollars, 2% near-term discount rate); $230 is used as a benchmark that also favors the projects.'], limitations: ['Allowance prices measure what it costs emitters to cut a ton under the cap at the margin, not the full social value of a cut.', 'Projects also have benefits other than emission cuts, which a cost per ton does not count.'], executedAt: EXEC, }, { id: 'cumulative-by-cost', title: 'Cumulative share of spending and of tons by cost per ton', question: 'How much of the money, and how much of the emission cuts, came from projects at or below each cost per ton?', operation: 'For each cost ceiling C, sum spending and tons of line items with spending / tons <= C, divided by totals.', scope: 'Same 567 fiscal year 2025 line items as fy2025-summary.', resultShape: 'table', structuredResult: cumRows, displayResult: `Projects at or below the top auction price ($71/ton) got ${(cumRows[3].share * 100).toFixed(1)}% of spending and delivered ${(cumRows[2].share * 100).toFixed(1)}% of the tons. Projects above $1,000/ton got ${(100 - cumRows[11].share * 100).toFixed(1)}% of spending for ${(100 - cumRows[10].share * 100).toFixed(1)}% of the tons.`, limitations: ['Tons include about 71,000 tons that appear to be counted twice (see duplicate-sensitivity).'], executedAt: EXEC, }, { id: 'category-cost-per-ton', title: 'Cost per lifetime ton by project type', question: 'How does the cost per ton of emission cuts vary by type of project?', operation: 'FactHub assigned each line item to one of 11 types by its appropriation title; cost per ton = type spending / type tons; also the median of line-item costs.', scope: 'Same 567 fiscal year 2025 line items as fy2025-summary.', resultShape: 'table', structuredResult: catRows, displayResult: catRows.map(c => `${c.category}: $${c.costPerTon.toLocaleString('en-US')}/ton`).join('; ') + '.', methodologicalNotes: ['Types are FactHub groupings of appropriation titles (rules in build_dataset.js); Ecology groups projects differently in its Figure 20.', `Cheapest two types (ferries and industrial retrofits) received ${(cheap.shareOfSpending * 100).toFixed(1)}% of spending and delivered ${(cheap.shareOfTons * 100).toFixed(1)}% of the tons.`], limitations: ['Several types mix projects whose main purpose is not cutting emissions, such as walkability or salmon habitat, so their cost per ton overstates the cost of the emission cut itself.'], executedAt: EXEC, }, { id: 'category-shares', title: 'Share of spending and share of tons cut, by project type', question: 'Which types of projects got the money, and which delivered the emission cuts?', operation: 'For each FactHub project type: type spending / total spending, and type tons / total tons. One row per type and measure, types ordered from lowest to highest cost per ton.', scope: 'Same 567 fiscal year 2025 line items as fy2025-summary.', resultShape: 'table', structuredResult: catRows.flatMap(c => [ { category: c.category, measure: 'Share of spending', share: r4(c.spending / totExp), costPerTon: c.costPerTon }, { category: c.category, measure: 'Share of tons cut', share: r4(c.tons / totT), costPerTon: c.costPerTon }, ]), displayResult: catRows.map(c => `${c.category}: ${(c.spending / totExp * 100).toFixed(1)}% of spending, ${(c.tons / totT * 100).toFixed(1)}% of tons`).join('; ') + '.', limitations: ['Shares of tons include about 71,000 tons that appear to be counted twice, almost all in the ferry type (see duplicate-sensitivity).'], executedAt: EXEC, }, { id: 'duplicate-sensitivity', title: 'Sensitivity: line items that repeat a project\u2019s full tonnage', question: 'How do the totals change if line items that repeat another line\u2019s full lifetime tonnage are counted once?', operation: 'Remove the second line item where two rows describe the same project and list the identical tonnage: three ferry-terminal "Electrical Connection for Hybrid" rows, a Washington State University building row, and a Clallam Transit vehicle row.', scope: 'Same 567 fiscal year 2025 line items as fy2025-summary.', resultShape: 'scalar', structuredResult: dupSens, displayResult: `Removing ${dupSens.removedRows} repeated rows cuts ${Math.round(dupSens.removedTons).toLocaleString('en-US')} tons (${(dupSens.removedTons / totT * 100).toFixed(1)}% of fiscal year 2025 tons) and raises the pooled cost from $${fy.costPerTon.toFixed(0)} to $${dupSens.costPerTonWithoutDuplicates.toFixed(0)} per ton.`, methodologicalNotes: ['For each ferry terminal, the report\u2019s own cost-per-ton column equals the two rows\u2019 combined spending divided by the tonnage once (for example $69 at Bainbridge Island), which indicates the agency meant the tonnage once; the report\u2019s total counts it twice.'], limitations: ['FactHub could not confirm with the agencies which rows are repeats. Other rows with the same tonnage (such as three Yakima County solar rows of 2,042.4 tons) may be separate projects and were kept.'], executedAt: EXEC, }, { id: 'tonnage-sensitivity', title: 'Sensitivity: if projects cut more than reported', question: 'Would most spending still exceed the highest auction price if every project cut two, three or five times the reported tons?', operation: 'Multiply every line item\u2019s tons by M and recompute the share of spending on line items above $70.86 per ton.', scope: 'Same 567 fiscal year 2025 line items as fy2025-summary.', resultShape: 'table', structuredResult: tonSens, displayResult: tonSens.map(t => `x${t.tonsMultiplier}: ${(t.shareSpendingAboveHighestPrice * 100).toFixed(1)}% of spending above $70.86`).join('; ') + '.', limitations: ['A uniform multiplier is a stress test, not an estimate of actual under-counting.'], executedAt: EXEC, }, ]; fs.writeFileSync(path.join(__dirname, 'analyses.json'), JSON.stringify(analyses, null, 1)); // ---------- artifacts ---------- const art = (id, file, role, mediaType, description) => { const b = fs.readFileSync(path.join(REL, file)); return { id, role, fileName: file, mediaType, byteLength: b.length, contentDigest: 'sha256:' + crypto.createHash('sha256').update(b).digest('hex'), description, access: 'public' }; }; const artifacts = [ art('projects', 'projects.csv', 'curatedTable', 'text/csv', 'One row per fiscal year 2025 Appendix B line item with a numeric lifetime emission-cut estimate (567 rows).'), art('categories', 'categories.csv', 'derivedTable', 'text/csv', 'Spending, tons and cost per ton by FactHub project type.'), art('cumulative', 'cumulative.csv', 'derivedTable', 'text/csv', 'Cumulative share of spending and tons at or below each cost per ton.'), art('report-totals', 'report-totals.csv', 'derivedTable', 'text/csv', 'Ecology totals before and after the March 2026 revision, and the fiscal year 2025 sum.'), art('possible-duplicates', 'possible-duplicates.csv', 'qualityReport', 'text/csv', 'Line items that repeat another line item\u2019s full lifetime tonnage.'), art('extract-script', 'dump.mjs.txt', 'transformationManifest', 'text/plain', 'Node.js script (pdfjs-dist 4.10.38) that dumps every text item with its page and position from PDF pages 63-565.'), art('parse-script', 'parse.mjs.txt', 'transformationManifest', 'text/plain', 'Node.js script that rebuilds Appendix B table rows from the text positions.'), art('build-script', 'build_dataset.js', 'transformationManifest', 'text/javascript', 'Node.js script that assigns types and computes every table and saved analysis.'), ]; const f = (name, type, description, units, valueType, sourceIds, derivation = null, nullable = false) => ({ name, type, description, units, nullable, valueType, derivation, sourceIds }); const content = { key: KEY, status: 'Final', rulesVersion: '2.23', title: 'Washington Climate Commitment Act spending: cost per ton of emission cuts, fiscal year 2025', description: 'Every fiscal year 2025 Climate Commitment Act (CCA) project that Washington agencies expected to cut greenhouse gases, from the Department of Ecology\u2019s revised March 2026 investment report, with its spending, projected lifetime cut and cost per ton, plus totals before and after the state corrected the report.', selectionRationale: 'Ecology publishes project-level spending and projected emission cuts only inside a 565-page PDF. Putting the 567 line items with estimates in a table lets anyone compare cost per ton across projects and against the carbon allowance price, and check the state\u2019s corrected totals.', topics: ['Washington energy and climate policy', 'Government spending'], geography: ['Washington, United States'], temporalCoverage: 'Fiscal year 2025 (July 1, 2024 - June 30, 2025) project spending; 2023-25 biennium totals; lifetime emission projections extend decades beyond.', unitOfObservation: 'One Appendix B line item: a CCA expenditure by one agency under one appropriation, as reported to Ecology.', updateCadence: 'One-time release; a new version may follow Ecology\u2019s next CCA investment report.', curator: 'FactHub Research Team', reproducibilityStatus: 'Reproducible from public sources', reproducibilityExplanation: 'Download Ecology Pub. 25-14-107 (revised March 2026), run dump.mjs on pages 63-565, parse.mjs on its output, then build_dataset.js. The fiscal year 2025 sums match the report\u2019s stated totals ($333,128,350 and 866,116 tons).', limitations: [ 'Emission cuts are agency projections over each project\u2019s lifetime from calculator tools, many adapted from California\u2019s; none are measured.', 'Since fiscal year 2025, Ecology has credited CCA money with a co-funded project\u2019s whole emission cut, not a share in proportion to the CCA dollars, so cost per CCA dollar can understate a project\u2019s full cost per ton.', 'Cost per ton divides only the fiscal year 2025 CCA expenditure by the lifetime tons; projects paid over several years may report a share of tons each year.', 'Many projects have other goals (resilience, health, walkability, habitat). Their cost per ton is not a measure of their full value.', 'Under a binding cap, a cut at a covered emitter can free allowances for others; this dataset reports the state\u2019s projections and does not adjust for that.', 'Rows were rebuilt from PDF text positions. Totals match the report exactly, but individual descriptions may contain line-break artifacts.', 'Project types are FactHub groupings, not Ecology categories.', ], sources: [ { id: 'ecology-25-14-107', name: 'Washington Department of Ecology, Climate Commitment Act Investments Report: Fiscal Year 2025 (Pub. 25-14-107), updated March 2026', url: 'https://apps.ecology.wa.gov/publications/SummaryPages/2514107.html', retrievedAt: TODAY, role: 'Project-level fiscal year 2025 spending and projected lifetime emission cuts (Appendix B); biennium totals and cost-effectiveness (pp. 13, 40-45).', licenseName: 'Washington State public record', licenseUrl: null, redistribution: 'Curated values redistributed with attribution; the PDF itself is linked, not hosted.', reliability: { publisherKind: 'government', interest: 'aligned', note: 'Ecology runs the CCA and compiles this report from figures each agency reports; it revised the report in March 2026 after errors in Commerce data. The agency benefits when CCA spending appears effective.' } }, { id: 'ecology-transmittal-2026-03', name: 'Washington Department of Ecology, transmittal letter to legislators on the updated CCA investment report, March 19, 2026', url: 'https://apps.ecology.wa.gov/publications/parts/2514107part2.pdf', retrievedAt: TODAY, role: 'Original and revised biennium totals (8,635,900 and 1,201,286 tons; $40 and $322 per ton) and the 61 corrected Commerce projects.', licenseName: 'Washington State public record', licenseUrl: null, redistribution: 'Linked as a public record.', reliability: { publisherKind: 'government', interest: 'against', note: 'Ecology\u2019s own account of its correction, which lowers the program\u2019s reported results.' } }, { id: 'commerce-correction-2026-01', name: 'Washington Department of Commerce, "Commerce submits corrected greenhouse gas emissions data for state climate report," January 6, 2026', url: 'https://www.commerce.wa.gov/commerce-submits-corrected-greenhouse-gas-emissions-data-for-state-climate-report/', retrievedAt: TODAY, role: 'Commerce\u2019s correction of eight rebate projects (7.5 million to 78,000 tons) and its own total of nearly 308,000 tons.', licenseName: 'Washington State public record', licenseUrl: null, redistribution: 'Quoted with attribution.', reliability: { publisherKind: 'government', interest: 'against', note: 'Commerce made the data-entry error and announced the correction.' } }, { id: 'wpc-myers-2026-01-06', name: 'Todd Myers, Washington Policy Center, "State CO2 report shows 86% of Washington\u2019s claimed climate benefits are probably fake," January 6, 2026', url: 'https://www.washingtonpolicy.org/publications/detail/state-co2-report-shows-86-of-washingtons-claimed-climate-benefits-are-probably-fake', retrievedAt: TODAY, role: 'Statement: Myers\u2019 estimate that 86% of the claimed cuts were wrong.', licenseName: 'Copyrighted; short quotation for commentary', licenseUrl: null, redistribution: 'Short quotation only.', reliability: { publisherKind: 'advocacy', interest: 'aligned', note: 'The Washington Policy Center is a free-market think tank that opposes the CCA\u2019s design; Myers first identified the error.' } }, { id: 'wpc-myers-2026-02-03', name: 'Todd Myers, Washington Policy Center, "HB 2251, Weakening Oversight While Expanding Climate Spending," February 3, 2026', url: 'https://www.washingtonpolicy.org/publications/detail/hb-2251-weakening-oversight-while-expanding-climate-spending', retrievedAt: TODAY, role: 'Statement: the 308,000-ton figure applied to all CCA-funded projects.', licenseName: 'Copyrighted; short quotation for commentary', licenseUrl: null, redistribution: 'Short quotation only.', reliability: { publisherKind: 'advocacy', interest: 'aligned', note: 'Legislative memo opposing HB 2251 by a think tank critical of CCA spending.' } }, { id: 'facthub-cca-gas', name: 'FactHub, Washington Gasoline Price Premium and Carbon Allowance Costs, 2015-2026 (dataset version 2), auction-prices table', url: 'https://facthub.org/n/dataset/wa-cca-gasoline-price-premium-2015-2026/2', retrievedAt: TODAY, role: 'Quarterly auction settlement prices used as the carbon-price benchmark; each price is sourced there to Ecology auction reports.', licenseName: 'FactHub dataset', licenseUrl: null, redistribution: 'Cited by exact version.', reliability: { publisherKind: 'other', interest: 'none', note: 'An earlier FactHub dataset; prices transcribed from Ecology auction summary reports and secondary reports citing them.' } }, { id: 'epa-scghg-2023', name: 'U.S. Environmental Protection Agency, Report on the Social Cost of Greenhouse Gases, November 2023, Table ES.1', url: 'https://www.epa.gov/system/files/documents/2023-12/epa_scghg_2023_report_final.pdf', retrievedAt: TODAY, role: 'Social cost of CO2 ($190 for 2020 emissions, $230 for 2030, 2% near-term rate) as an alternative benchmark.', licenseName: 'U.S. federal government work', licenseUrl: null, redistribution: 'Linked as a public record.', reliability: { publisherKind: 'government', interest: 'none', note: 'Peer-reviewed federal estimate of climate damages per ton; higher values make emission cuts look more worthwhile.' } }, ], artifacts, tables: [ { id: 'projects', title: 'Fiscal year 2025 CCA line items with lifetime emission-cut estimates', description: 'Each Appendix B row marked as expected to cut emissions and given a numeric estimate, sorted from lowest to highest cost per ton.', artifactId: 'projects', unitOfObservation: 'Appendix B line item', fields: [ f('project_id', 'string', 'FactHub row id, ordered by cost per ton.', null, 'derived', [], 'Assigned after sorting by cost_per_ton_usd.'), f('agency', 'string', 'Reporting state agency.', null, 'observed', ['ecology-25-14-107']), f('program', 'string', 'Appropriation title as printed.', null, 'observed', ['ecology-25-14-107']), f('recipient', 'string', 'Pass-through recipient, or N/A when the agency spent the money itself.', null, 'observed', ['ecology-25-14-107']), f('county', 'string', 'County or Statewide.', null, 'observed', ['ecology-25-14-107']), f('category', 'string', 'FactHub project type.', null, 'derived', [], 'Keyword rules on program in build_dataset.js.'), f('fy2025_cca_spending_usd', 'number', 'Fiscal year 2025 expenditure (sum of all CCA accounts on the row).', 'US dollars', 'observed', ['ecology-25-14-107']), f('lifetime_reduction_tco2e', 'number', 'Projected greenhouse gas cut over the project\u2019s lifetime.', 'metric tons CO2-equivalent', 'estimated', ['ecology-25-14-107']), f('cost_per_ton_usd', 'number', 'Spending divided by lifetime tons.', 'US dollars per metric ton CO2e', 'derived', ['ecology-25-14-107'], 'fy2025_cca_spending_usd / lifetime_reduction_tco2e'), f('reported_cost_per_ton_usd', 'number', 'Cost per ton as printed in the report, which some agencies computed on another cost basis.', 'US dollars per metric ton CO2e', 'observed', ['ecology-25-14-107'], null, true), f('possible_duplicate_tonnage', 'string', 'yes if the row repeats another row\u2019s full tonnage for the same project.', null, 'derived', [], 'Rule isDup in build_dataset.js.'), f('report_pdf_page', 'integer', 'Page of the PDF file (not the printed page number) where the row starts.', null, 'observed', ['ecology-25-14-107']), f('description', 'string', 'Project description as printed.', null, 'observed', ['ecology-25-14-107']), ] }, { id: 'categories', title: 'Cost per ton by project type', description: 'Totals and cost per ton for each FactHub project type.', artifactId: 'categories', unitOfObservation: 'Project type', fields: [ f('category', 'string', 'FactHub project type.', null, 'derived', []), f('projects', 'integer', 'Number of line items.', null, 'derived', ['ecology-25-14-107']), f('spending', 'number', 'Fiscal year 2025 CCA spending.', 'US dollars', 'derived', ['ecology-25-14-107']), f('tons', 'number', 'Projected lifetime cut.', 'metric tons CO2e', 'derived', ['ecology-25-14-107']), f('costPerTon', 'number', 'spending / tons, rounded.', 'US dollars per metric ton CO2e', 'derived', ['ecology-25-14-107']), f('medianProjectCostPerTon', 'number', 'Median line-item cost per ton.', 'US dollars per metric ton CO2e', 'derived', ['ecology-25-14-107']), f('timesHighestAuctionPrice', 'number', 'costPerTon / 70.86.', 'ratio', 'derived', ['ecology-25-14-107', 'facthub-cca-gas']), ] }, { id: 'cumulative', title: 'Cumulative share of spending and tons by cost per ton', description: 'For each cost ceiling, the share of spending and of tons from line items at or below it.', artifactId: 'cumulative', unitOfObservation: 'Cost ceiling and measure', fields: [ f('order', 'integer', 'Sort order of the ceiling.', null, 'derived', []), f('costCeiling', 'string', 'Cost-per-ton ceiling label.', null, 'derived', []), f('measure', 'string', 'Share of spending or Share of tons cut.', null, 'derived', []), f('share', 'number', 'Cumulative share (0-1).', 'fraction', 'derived', ['ecology-25-14-107']), ] }, { id: 'report-totals', title: 'Report totals before and after the correction', description: 'Ecology\u2019s 2023-25 totals as first published and as revised, and the fiscal year 2025 sum from Appendix B.', artifactId: 'report-totals', unitOfObservation: 'Report version or scope', fields: [ f('scope', 'string', 'Which total.', null, 'observed', ['ecology-transmittal-2026-03']), f('spending_usd', 'number', 'Spending on projects expected to cut emissions.', 'US dollars', 'observed', ['ecology-transmittal-2026-03', 'ecology-25-14-107']), f('lifetime_reduction_tco2e', 'number', 'Projected lifetime cut.', 'metric tons CO2e', 'estimated', ['ecology-transmittal-2026-03', 'ecology-25-14-107']), f('cost_per_ton_reported_usd', 'number', 'Ecology\u2019s stated average cost per ton.', 'US dollars per metric ton CO2e', 'observed', ['ecology-transmittal-2026-03'], null, true), f('source', 'string', 'Where the figure appears.', null, 'observed', []), ] }, ], events: [ { id: 'report-published', eventType: 'publication', date: '2025-11-26', label: 'Ecology publishes FY2025 CCA investment report', description: 'Report projects 8,635,900 lifetime tons from 2023-25 spending at $40 per ton.', sourceIds: ['ecology-transmittal-2026-03'] }, { id: 'myers-flags-error', eventType: 'publication', date: '2026-01-06', label: 'WPC says 86% of claimed cuts are probably wrong', description: 'Todd Myers attributes most of the claimed cuts to eight projects.', sourceIds: ['wpc-myers-2026-01-06'] }, { id: 'commerce-correction', eventType: 'correction', date: '2026-01-06', label: 'Commerce corrects eight rebate projects', description: 'Eight projects corrected from 7.5 million to 78,000 tons; Commerce total nearly 308,000 tons.', sourceIds: ['commerce-correction-2026-01'] }, { id: 'report-revised', eventType: 'correction', date: '2026-03-19', label: 'Ecology publishes revised report', description: '61 Commerce projects corrected; total 1,201,286 tons at $322 per ton.', sourceIds: ['ecology-transmittal-2026-03'] }, ], derivedFrom: [], analyses, replications: [ { id: 'fy2025-tons-match', analysisId: 'fy2025-summary', externalSourceId: 'ecology-25-14-107', externalLocator: 'p. 40 (866,116 MT CO2e for fiscal year 2025)', externalValue: 866116, facthubValue: fy.tons, unit: 'metric tons CO2e', note: 'Exact match after rounding; confirms the row extraction.' }, { id: 'fy2025-spending-match', analysisId: 'fy2025-summary', externalSourceId: 'ecology-25-14-107', externalLocator: 'p. 41 ($333,128,350 spent on projects with readily calculable reductions in 2025)', externalValue: 333128350, facthubValue: fy.spending, unit: 'US dollars', note: 'Exact match.' }, { id: 'median-vs-ecology', analysisId: 'fy2025-summary', externalSourceId: 'ecology-25-14-107', externalLocator: 'pp. 43-44 (median per-project cost effectiveness $1,730/MT CO2e, 2023-25 biennium)', externalValue: 1730, facthubValue: fy.medianProjectCostPerTon, unit: 'US dollars per metric ton CO2e', note: 'Ecology\u2019s median covers both years of the biennium and may define projects differently; FactHub\u2019s covers fiscal year 2025 line items.' }, ], statements: [ { id: 'myers-86-percent', speaker: 'Todd Myers', speakerRole: 'Vice President for Research, Washington Policy Center', date: '2026-01-06', venue: 'Washington Policy Center article', quote: 'About 86 percent of the total CO2 reductions claimed by the report are probably fake.', context: 'Myers wrote this the day Commerce corrected its data, after noting that eight of nearly 3,600 projects accounted for most of the claimed cuts at $1 or $4 per ton. Ecology\u2019s March 2026 revision lowered the 2023-25 total from 8,635,900 to 1,201,286 tons.', claimType: 'assertion', sourceId: 'wpc-myers-2026-01-06' }, { id: 'myers-308000', speaker: 'Todd Myers', speakerRole: 'Vice President for Research, Washington Policy Center', date: '2026-02-03', venue: 'Washington Policy Center legislative memo on HB 2251', quote: 'So far, CCA-funded projects are projected to reduce just 308,000 metric tons of CO2 over their lifetime', context: 'The memo opposed HB 2251. The sentence before says more than $1.5 billion of CCA revenue has been spent with poor results. The memo gives no source for 308,000; the Department of Commerce had reported nearly 308,000 tons across its own CCA-funded programs on January 6, 2026.', claimType: 'assertion', sourceId: 'wpc-myers-2026-02-03' }, { id: 'commerce-308000', speaker: 'Washington Department of Commerce', speakerRole: 'State agency that runs most CCA rebate and grant programs', date: '2026-01-06', venue: 'Commerce news release', quote: 'With this correction, Commerce is now reporting a total reduction in emissions of nearly 308,000 tons across all their CCA-funded programs.', context: 'The release corrected eight home electrification and appliance rebate projects from 7.5 million to 78,000 lifetime tons and said Ecology would publish a corrected report for all agencies.', claimType: 'assertion', sourceId: 'commerce-correction-2026-01' }, ], forecasts: [], changelog: [{ date: TODAY, summary: 'Initial release.' }], }; fs.writeFileSync(path.join(__dirname, 'dataset-content.json'), JSON.stringify(content, null, 1)); // Large arrays travel by reference to a staged JSON artifact. const PARTS = ['limitations', 'sources', 'tables', 'events', 'analyses', 'replications', 'statements']; fs.writeFileSync(path.join(REL, 'content-parts.json'), JSON.stringify(Object.fromEntries(PARTS.map(p => [p, content[p]])), null, 1)); artifacts.push(art('content-parts', 'content-parts.json', 'documentation', 'application/json', 'The dataset’s sources, tables, saved analyses, replications and statements as JSON.')); const skeleton = { ...content }; for (const p of PARTS) skeleton[p] = { $artifact: 'content-parts', pointer: '/' + p }; fs.writeFileSync(path.join(__dirname, 'dataset-skeleton.json'), JSON.stringify(skeleton)); fs.writeFileSync(path.join(__dirname, 'artifacts.json'), JSON.stringify(artifacts, null, 1)); console.log(JSON.stringify({ fy, thresholds, cheap, dupSens, tonSens, cum: cumRows.filter((_, i) => i < 12) }, null, 1)); console.log(artifacts.map(a => a.fileName + ' ' + a.byteLength).join('\n'));