How legislative data flows from LegiScan → central → tenants
change_hash.
0 * * * *)
▼
stateCoverage + any state where an active tenant already has bills.bills table; for new bills, insert with change_hash='' sentinel (not queued). The stored hash is advanced only for matched bills — unmatched (match_type=null) changed bills are left stale so the next full pass still detects them (advancing it here would hide the change from the full pass and swallow it).match_type ∈ {'keyword','manual'} for at least one covering tenant AND the hash changed (the ingestor re-writes the hash via getBill). No stubOnly notifications — raw pass has no titles, so stubs go quiet until the next full pass picks up the (intentionally still-stale) hash.getSessionList (once daily at 5 ET only)getMasterList?state=, called via the getMasterListBySession wrapper. ~400 KB per session.sessions/{id}/masterlist.jsonbill_tenants.match_type='keyword'; non-matching → match_type=null stub link (still tracked metadata-wise, no AI).change_hash + masterlist fields to central bills table, then:
getBill): newly-matched bills, OR matched bills with changed hash.stubOnly messages for match_type=null links whose hash changed, so tenant refreshes denormalized status/last_action.getBill spend. A bill triggers a getBill call only when it is keyword- or manually-tracked by at least one covering tenant AND its change_hash moved (or it just newly matched). Unmatched changes update central state but never reach the ingestor.
central-ingestor
▼
getBill() call, drawn from the deployment's monthly API budget. The cron (Stage 2) is the API gate: it only queues bills whose change_hash moved, so unchanged bills never reach the ingestor. reprocess bypasses the ingestor entirely (zero calls); redownload-texts uses skipFetch: true.
skipFetch: true (bulk seed, redownload-texts) → download any missing texts from state legislature, skip getBill, jump to step 4.detectChanges → write bill_change_log rows for each diff → upsert bills row → delete+reinsert bill_history / bill_sponsors / bill_sasts / bill_subjects / bill_calendar / bill_referrals → upsert bill_texts / bill_supplements / bill_amendments / roll_calls → download any new text versions to bills/legiscan-{billId}/texts/{docId}.{ext} → stamp texts_fetched_at → derive text_status.roll_call_votes) are NOT written by this path — getBill returns summaries only. Bulk-seeded bills have them; live-ingested bills do not.
GET /bills/{id}GET /bills/{id}/texttext_status='no_texts' wait for LegiScan to publish text.change_hash dedup prevents redundant AI re-runs.
POST central/admin/ingest-billmatch_type='manual')match_type='manual'match_type=null (lightweight stub)POST central/admin/keyword-resync-tenantskipFetch: true — downloads text from the legislature site, no getBill() (zero API quota). Use to heal missing bill text.
central-bill-texts)bills/{id}/{docId}.html|pdf — bill textsessions/{id}/masterlist.json — masterlist cachecentral-bills-ls, LegiScan env)sessions, bills, bill_tenants, tenants, keyword_registrypeople, committees, roll_calls, bill_sponsors, bill_history, bill_texts, bill_amendments, bill_change_log, session_sync_log, api_call_logfloorvote-{id}, one per org)bills: match_type · text_status · ai_processed_at| Event | Mechanism | Latency |
|---|---|---|
| New bill introduced in LegiScan | Next full sync discovers it via getMasterList + keyword match | up to 24h |
| Tracked bill gets status/text update | Hourly change check (getMasterListRaw) detects changed hash → ingestor | ≤ 1h |
| Bill with no text → text published by LegiScan | Hash changes → ingestor fetches text → AI runs automatically | ≤ 1h |
| Keywords updated → remove old bills | keyword-resync cleanup pass runs synchronously on save | immediate |
| Keywords updated → add bills (in central DB) | keyword-resync scans central bills table | immediate |
| Keywords updated → add bills (never fetched) | keyword-resync scans R2 masterlist cache (populated by first full sync) | immediate if cache warm, else up to 24h |
| Manual "Add bill" by number | ingest-bill → ingestor → tenant queue → AI | 1–3 min |