Data semantics change. When they do, a consumer needs to tell "this bill moved" from "we changed how we read this bill" — those look identical in a diff and mean opposite things. Anything marked changes what a field means below is worth reading before you trust a comparison across that date.
Empty results now say why they are empty
changes what a field means
Three endpoints — /people, /committees and /events — are published in our OpenAPI schema and return zero rows for every query, because nothing populates those tables. They returned a bare {"data": []}, which an agent reasonably reads as "Florida has no legislators". Worse, the MCP hearings tool explained its emptiness as a "daily refresh target" with possible "ingestion lag" — for a table that has never held a single row and that no code writes to. That is not a gap in the data; it is the product describing itself inaccurately in the one surface a machine reads.
Every empty response now carries data_status — not_collected or no_match — and a notice stating the inference not to draw. The distinction is probed per request rather than hardcoded, so it stays true if these datasets are ever ingested. The MCP server's instructions now also declare what is out of scope entirely: Congress, city and county ordinances, prior sessions, hearings, and legislator records.
Four new topic trackers, and alerts you can scope to one state
coverage
Three curated topics existed. Four more now cover the areas people actually ask about: youth online safety, platform accountability, cybersecurity, and local government & preemption — 160, 150, 154 and 6,325 bills respectively. Patterns were tuned against the corpus rather than guessed, and two plausible-looking ones were dropped after inspection: %section 230% matches "section 2307 of the public health law", and %parental consent% is mostly minors' mental-health bills.
Email alerts were national-only, so subscribing to a topic meant a digest spanning all 51 jurisdictions. /alerts/subscribe now takes an optional jurisdiction, which is what makes this usable for a city or county affairs office tracking its own legislature. The scope appears in the subject line, because a scoped digest and a national one are otherwise indistinguishable in an inbox.
Bills are searchable by sponsor
coverage
Just under a million sponsorship records were loaded and unreachable — there was no way to ask what a given member filed. /api/v1/bills?sponsor= now answers that, backed by a trigram index so a substring search does not scan the table. Read the names for what they are: they are stored as the source published them, which is usually a bare surname ("Rouson") and sometimes a committee. There is no legislator directory behind this, so a surname shared by two members returns both, and you cannot filter by party or district.
Bills are no longer declared dead by a predicted adjournment date
changes what a field means
died_on_adjournment was applied once a session's end date passed. That date is the expected adjournment reported upstream — a prediction — and the rule never consulted the active flag in the same record. A chamber sitting longer than predicted therefore had its whole docket marked dead by the calendar. 29,227 bills across 8 jurisdictions were affected, 18,343 in Massachusetts alone, and the national adjournment-death share falls from 45.2% to 31.3%.
The new rule: death requires the source to report the session closed. Where it still reports the session active, we publish the action-derived status if the chamber filed real legislative business in the last 30 days, and no status at all otherwise — 7,861 bills in AZ, MS, SC and VA. Clerical filings do not count as evidence a chamber is sitting; South Carolina's were "Act No. 250" and a scrivener's-error correction. If you compared adjournment mortality across dates or states before today, re-pull it.
Evidence packets are citable: permalink, snapshot id, downloadable JSON
correctness fix
A packet used to be a JSON blob in an agent transcript — nothing a reader could open, and no way to tell later whether the record had moved. Packets now carry a how_to_cite block: a one-line cite_as sentence, a human permalink at /evidence/{bill_id}, a JSON download, and a snapshot_id that changes if and only if a cited fact changes. Snapshots are not archived. The id is a change detector, not a way to retrieve the version you cited — so keep your own copy. The derived-status caveat rides inside the citation sentence itself, because a footnote is exactly where that distinction gets lost.
Offset pagination is bounded at 50,000 rows
correctness fix
page was bounded below and not above, so ?page=1000000 became OFFSET 50,000,000 — and Postgres walks every one of those rows before discarding them. On a public, anonymous API that is a one-line denial of service that costs the caller nothing. The limit is on how deep one result set can be paged, not on how much data you can reach: filter by jurisdiction or session and page within that.
Usage figures no longer count our own uptime monitor
correctness fix
The read-path monitor calls a real MCP tool every two minutes on purpose — a handshake succeeds against a dead database, so nothing cheaper detects an outage. Within two hours of shipping usage telemetry, that monitor was 65 of the 67 recorded tool calls, and /stats/usage was publishing the total while its own note claimed health probes were excluded. Monitor calls are now tagged at the MCP edge, excluded from every figure, and reported separately as self_probe_calls so the subtraction is checkable. The earlier rows were attributed rather than deleted: all 65 sit on an exact 120-second cadence with no interleaved call.
Adjournment and clock-deaths are not comparable across states
changes what a field means
Whether a bill that ran out of time is recorded as dead or died_on_adjournment depends on whether the legislature files an action recording the death — not on what happened to the bill. Eleven jurisdictions report zero of one bucket; three report zero of the other. A new did_not_pass field (the sum) is the cross-state comparable figure, and terminal_split_is_degenerate flags states where the split carries no information. If you were comparing states on adjournment mortality, switch to did_not_pass. The headline 35% figure is unaffected.
Enrolled bills no longer wait forever
changes what a field means
A bill enrolled in a session that adjourned more than 180 days ago is no longer described as awaiting executive action — every state bounds that window at roughly 5–45 days from presentment. The status stays enrolled, but a new enrolled_outcome_uncaptured flag marks that the final signature or veto was never captured. This affected 3,274 of 4,918 enrolled bills.
Ambiguous bill numbers are reported as ambiguous
correctness fix
search_legislation's bill-number path returned match_type: bill_number_exact however many sessions matched, in non-deterministic order, silently truncated. It now orders deterministically, limits in SQL, and returns bill_number_ambiguous with the candidate count when a number spans sessions. Texas HB 1 matches three.
Coverage warnings now fire for degraded jurisdictions
correctness fix
Coverage severity was ranked by lifecycle position, where the fault states sit after the healthy one — so a wholly degraded jurisdiction produced no warning at all, and a blocked session was hidden by any healthy sibling row. Every tool's empty-result path depended on that one function.
Evidence packets distinguish derived conclusions from official record
changes what a field means
build_legislative_evidence_packet labelled its record "official" over a payload containing the derived status — beside the state's own source URL, which does not contain that claim. Packets now name their derived_fields. An empty hearings section is labelled "not collected" rather than official.
Bill detail pages are no longer submitted for indexing
coverage
Bill pages remain publicly addressable and are unchanged for API and direct-link use, but carry noindex and were removed from the sitemap. Search Console reported them as never reached; asking a search engine to evaluate 200,000 largely repetitive records was starving the pages worth citing.
Known limitations
Standing gaps, stated plainly. These are not bugs awaiting a fix; they are the current edges of the system.
No hearing data at all. A hearings tool exists and returns an empty list. That means "not collected", never "none scheduled".
No legislator or committee records./people and /committees are published and permanently empty. Sponsors are available per bill, but as bare names — no party, district, or chamber — so you cannot ask how a given member voted across a session, or who chairs anything.
No federal legislation. 50 states and DC only.
Nothing below the state legislature. No city or county ordinances, commissions, or boards. State bills that regulate municipalities are collected — those are a different thing, and a city tracking its own ordinances will not find them here.
No historical sessions. Current session or biennium only — this is not an archive.
Status is derived, not reported.died_on_adjournment in particular has no filed action behind it; the session simply ended. Cite it as our conclusion.
Roughly 5% of bills carry no status, deliberately. States classify identical wording differently, so the derivation returns nothing rather than guess. passed_both is never assigned at all — no evidence source supports it.
status_date is unpopulated for every bill. Date a status from the session end date, not from this field.
Ambiguity is common. 981 (jurisdiction, identifier) pairs match more than one bill, 726 of them in Texas. Ambiguous is never reported as not-found.
The change feed omits derivation changes. Real transitions emit events; a wholesale re-derivation of the status logic deliberately does not, so one maintenance run cannot flood every watchlist with changes that did not happen.