Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/scraper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Pulls in government content like bills, court cases, and White House content and

## Active data sources

Only these five are registered and run by `all`:
Only these six are registered and run by `all`:

| CLI name | Source and data fetched | Stored/used as |
| ------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
Expand All @@ -13,6 +13,7 @@ Only these five are registered and run by `all`:
| `scotus` | CourtListener opinion clusters, dockets, and sub-opinion text for the Supreme Court | `court_case`; powers court content and AI/feed enrichment |
| `scc-cvig` | Hand-configured Santa Clara County voter-guide PDFs | Candidate statements in `CivicApiCache`; the API matches statements to candidates |
| `ca-sos-statements` | California SOS statewide-office candidate-statement pages | Candidate statements in `CivicApiCache`; the API reads the cache and can fall back to the live source |
| `ncsbe` | Current-cycle NCSBE candidate CSV, referendum PDFs, and result ZIPs | Provider-neutral election tables; powers `civic.getNcElectionData` with exact file provenance |

`vote411`, `ca-lao-fiscal`, and `ca-vig-archive` remain under
`src/scrapers/disabled/` and do not run. Their caches had no application
Expand Down Expand Up @@ -124,6 +125,7 @@ CONGRESS_MAX_ITEMS=10 pnpm --filter @acme/scraper run start congress
| `SCOTUS_MAX_ITEMS` | 50 | CourtListener opinion clusters |
| `SCC_CVIG_MAX_ITEMS` | 10 | Voter-guide PDF documents |
| `CA_SOS_MAX_ITEMS` | 9 | Statewide-office candidate-statement pages |
| `NCSBE_MAX_ITEMS` | 4 | Current-cycle candidate/referendum/result files |
| `SCRAPER_MAX_NEW_ITEMS_PER_RUN` | 10 | New records receiving expensive AI/image enrichment |

These are per-run limits, not durable calendar-day quotas. Schedule one run per
Expand All @@ -132,6 +134,11 @@ invocation gets a fresh allowance. Source limits cap API/page work;
`SCRAPER_MAX_NEW_ITEMS_PER_RUN` separately caps expensive enrichment while
still storing additional raw records for later backfill.

The NCSBE integration is intentionally current-cycle only and excludes voter
history plus candidate contact/address fields. See
[`docs/ncsbe-election-data.md`](../../docs/ncsbe-election-data.md) for discovery,
idempotency, provenance, API, and deterministic Civic-matching details.

---

## Congress bills (`congress.ts`)
Expand Down
3 changes: 3 additions & 0 deletions apps/scraper/src/fixtures/ncsbe/candidates-2026.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"election_dt","county_name","contest_name","name_on_ballot","party_candidate","has_primary","is_partisan","vote_for","term"
"03/03/2026","DURHAM","US HOUSE OF REPRESENTATIVES DISTRICT 04","Nida Allam","DEM","TRUE","TRUE","1","2"
"03/03/2026","WAKE","WAKE COUNTY BOARD OF COMMISSIONERS AT-LARGE","Marguerite Creel","DEM","TRUE","TRUE","2","4"
12 changes: 12 additions & 0 deletions apps/scraper/src/fixtures/ncsbe/referendums-2026.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
REFERENDUM CHOICES LIST GROUPED BY REFERENDUM
GATES BOARD OF ELECTIONS
CRITERIA: Election: 03/03/2026, County: ALL COUNTIES
GATES
GATES COUNTY LOCAL SALES AND USE TAX REFERENDUM
For Local sales and use tax at the rate of one-quarter percent (0.25%) in addition to all other State and local sales and use taxes.
Against Local sales and use tax at the rate of one-quarter percent (0.25%) in addition to all other State and local sales and use taxes.
GRANVILLE BOARD OF ELECTIONS
GRANVILLE
GRANVILLE COUNTY LOCAL SALES AND USE TAX REFERENDUM
For Local sales and use tax at the rate of one-quarter percent (0.25%) in addition to all other State and local sales and use taxes.
Against Local sales and use tax at the rate of one-quarter percent (0.25%) in addition to all other State and local sales and use taxes.
3 changes: 3 additions & 0 deletions apps/scraper/src/fixtures/ncsbe/results-2024.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
county election_date precinct contest_group_id contest_type contest_name candidate party vote_for election_day early_voting absentee_by_mail provisional total_votes real_precinct
DURHAM 11/05/2024 01 1373 S US PRESIDENT Kamala D. Harris DEM 1 100 200 30 2 332 Y
WAKE 11/05/2024 01-01 1373 S US PRESIDENT Kamala D. Harris DEM 1 150 250 40 3 443 Y
3 changes: 3 additions & 0 deletions apps/scraper/src/fixtures/ncsbe/results-2026.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
County Election Date Precinct Contest Group ID Contest Type Contest Name Choice Choice Party Vote For Election Day Early Voting Absentee by Mail Provisional Total Votes Real Precinct
DURHAM 03/03/2026 04 2114 S US HOUSE OF REPRESENTATIVES DISTRICT 04 (DEM) Nida Allam DEM 1 371 0 0 0 371 Y
WAKE 03/03/2026 06-05 1 C WAKE COUNTY BOARD OF COMMISSIONERS AT-LARGE (DEM) Marguerite Creel DEM 2 12 0 0 0 12 Y
2 changes: 2 additions & 0 deletions apps/scraper/src/scraper-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ScraperEnvContract } from "@acme/env";
import { caSosStatementsConfig } from "./scrapers/ca-sos-statements.config.js";
import { congressConfig } from "./scrapers/congress.config.js";
import { federalregisterConfig } from "./scrapers/federalregister.config.js";
import { ncsbeConfig } from "./scrapers/ncsbe.config.js";
import { sccCvigConfig } from "./scrapers/scc-cvig.config.js";
import { scotusConfig } from "./scrapers/scotus.config.js";

Expand All @@ -12,4 +13,5 @@ export const scraperContracts: readonly ScraperEnvContract[] = [
scotusConfig,
sccCvigConfig,
caSosStatementsConfig,
ncsbeConfig,
];
2 changes: 2 additions & 0 deletions apps/scraper/src/scrapers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Scraper } from "./utils/types.js";
import { caSosStatements } from "./scrapers/ca-sos-statements.js";
import { congress } from "./scrapers/congress.js";
import { federalregister } from "./scrapers/federalregister.js";
import { ncsbe } from "./scrapers/ncsbe.js";
import { sccCvig } from "./scrapers/scc-cvig.js";
import { scotus } from "./scrapers/scotus.js";

Expand All @@ -11,4 +12,5 @@ export const scrapers: readonly Scraper[] = [
scotus,
sccCvig,
caSosStatements,
ncsbe,
];
58 changes: 58 additions & 0 deletions apps/scraper/src/scrapers/ncsbe-parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";

import {
parseCandidateCsv,
parseReferendumLines,
parseResultsTsv,
restrictToCycle,
} from "./ncsbe-parsers.js";

const fixture = (name: string) =>
readFile(new URL(`../fixtures/ncsbe/${name}`, import.meta.url), "utf8");

test("parses current-cycle candidate CSV without private contact fields", async () => {
const rows = parseCandidateCsv(await fixture("candidates-2026.csv"));
assert.equal(rows.length, 2);
assert.deepEqual(rows[0], {
electionDate: "2026-03-03",
county: "DURHAM",
contest: "US HOUSE OF REPRESENTATIVES DISTRICT 04",
name: "Nida Allam",
party: "DEM",
voteFor: 1,
termYears: 2,
hasPrimary: true,
isPartisan: true,
});
assert.equal("email" in rows[0]!, false);
assert.deepEqual(
rows.map((row) => row.county),
["DURHAM", "WAKE"],
);
});

test("parses NCSBE result layouts from 2026 and 2024 fixtures", async () => {
const current = parseResultsTsv(await fixture("results-2026.tsv"));
const legacy = parseResultsTsv(await fixture("results-2024.tsv"));
assert.equal(current.length, 2);
assert.equal(current[0]?.totalVotes, 371);
assert.equal(current[0]?.earlyVotingVotes, 0);
assert.equal(legacy.length, 2);
assert.equal(legacy[0]?.choice, "Kamala D. Harris");
assert.deepEqual(restrictToCycle([...current, ...legacy], 2026), current);
});

test("parses current-cycle referendum PDF text across counties", async () => {
const rows = parseReferendumLines(
(await fixture("referendums-2026.txt")).split(/\r?\n/),
);
assert.equal(rows.length, 4);
assert.deepEqual(
[...new Set(rows.map((row) => row.county))],
["GATES", "GRANVILLE"],
);
assert.equal(rows[0]?.electionDate, "2026-03-03");
assert.equal(rows[0]?.choice, "For");
});
Loading
Loading