Ingest California state bills via Open States - #276
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The incremental-walk cursor logic was written for congress.gov but is not federal in any way — any oldest-first walk with a per-item outcome needs exactly this. Move it to utils/ so the next scraper reuses the reasoning instead of reimplementing it, congress.ts re-exports both so its existing tests keep pointing at the same names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Billion could query state legislation on demand but never stored it, so CA SB 243 (companion chatbots, chaptered Oct 2025) was invisible in a feed that carried federal bills fine. The client and tRPC router already existed; what was missing was durable ingestion. Two constraints shaped this. State bills share a table with federal ones, so identity has to carry the state and session — billNumber is "CA SB 243 (2025-2026)" because SB 243 exists in most states and in every CA session as an unrelated bill. And the free Open States tier allows a few hundred requests a day, so the walk hydrates twenty bills per list request rather than fetching each bill's detail; at one request per bill a backfill would take weeks, which is what shelved the CourtListener scraper. --bulk-dir imports a session-CSV export through the same normalization for a zero-quota backfill. It takes a local directory because those archives sit behind a site login, not the API key, and automating it would mean storing account credentials in the scraper. Roll-call votes are not ingested: Bill has no column for them, and the openStates router already serves them live. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The data-source docs described state bills as planned, which is no longer true. Beyond flipping that, the notes worth keeping are the ones a future reader would otherwise have to rediscover: why billNumber has the shape it does (changing it duplicates every stored row), why the bulk export needs a manual download, and what the free tier actually allows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The column landed with the Browse sort fix and only congress.gov writes it. State bills share that feed, so a CA bill chaptered last week has to rank against a House bill passed last week — which only holds if both sources fill it from the same kind of event. Left unset, every state bill would fall back to its introduction date and sit below federal bills that moved the same week. The normalizer already assembles actions in the shape the shared helper reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
43a4d59 to
2090e7b
Compare
|
Rebased onto Added one commit: state bills now fill This matters more than it looks. State bills share the feed with federal ones, so a CA bill chaptered last week has to rank against a House bill passed last week — which only holds if both sources fill the column from the same kind of event. Left unset, every state bill would fall back to its introduction date and sit permanently below federal bills that moved the same week. The normalizer already assembles actions in the shape the shared 123 scraper tests pass, Still not merged on purpose. The blocker from the original description stands: |
Closes #158.
Billion could query state legislation on demand through the existing Open States client and tRPC router, but never stored it — so CA SB 243 (companion chatbots, chaptered Oct 2025) was invisible in a feed that carried federal bills fine. This adds durable ingestion into the normal
Billpipeline.Identity
billNumberis"CA SB 243 (2025-2026)",sourceWebsiteisopenstates.org. Every part is load-bearing: the unique constraint is(billNumber, sourceWebsite), SB 243 exists in most states, and SB 243 exists in every California session as an unrelated bill. Changing the format silently duplicates every stored row.Bill.congressstays null — it is a federal field, and the session lives in the bill number. Chamber uses the state vocabulary: CA mapslowerto Assembly, not House.Quota
The free Open States tier allows a few hundred requests/day. The walk asks
/billsfor sponsorships, abstracts, actions and versions inline, so one request returns 20 fully hydrated bills; bill text comes from leginfo and does not touch the quota. At one request per bill a backfill would take weeks — that is what shelved the CourtListener scraper.--bulk-dir <path>imports an unzipped session-CSV export through the identical normalize→upsert path for a zero-quota backfill, so a backfilled bill and its later incremental refresh are one row rather than two. It takes a local directory rather than downloading, because those archives sit behind a site login, not the API key — automating it would mean storing account credentials in the scraper. The import deliberately does not move the cursor: an export has no position in the update feed.Otherwise this mirrors the federal walk — ascending
updated_sincefrom a durable cursor, retry queue, same three-wayupsertContentoutcome contract.updated_sinceis date-granular so the cursor rounds back to its own day; a re-offered unchanged bill is a no-op, whereas rounding forward drops everything updated later that day.Acceptance criteria
pnpm --filter @acme/scraper run start open-states --max-items 1validates env and caps the runBillhas no column for them and adding one is a schema change beyond this issue. TheopenStatesrouter already serves votes live for the detail screen.Verification
tsc --noEmitclean on scraper/api/env. 124 scraper tests pass, including the live-query-format regression test. CLI help, env validation, and all three flag guards exercised.Live verification and remaining limitation:
includemust be repeated rather than comma-joined. The bill then completed the full local pipeline with a Big Mac FLUX fallback, and a second run produced zero new entries. The local database contains exactly one bill with its brief, dual lens, and header image.BulkExportShapeErrorlisting the headers it actually found rather than importing shifted data. Expect to adjust the mapping on first real use.Also note
--max-itemsis per state, not per run. Identical with one state today; worth knowing before adding a second.🤖 Generated with Claude Code