The server-side half of the Legislative Intelligence Platform: how raw bill text becomes structured intelligence and a calibrated floor-vote probability. Ingestion, LLM feature extraction, a formal model selection evaluation, a procedural taxonomy built from nothing, and a three-stage survival model validated at 0.9944 AUC-ROC across 117,667 bills — running unattended every day.
Two sources, neither designed for this. LegiScan provides a structured API; the Illinois General Assembly publishes bulk files over FTP. Neither exposes a change feed, so the pipeline computes its own: a content hash per artifact, compared against the last known hash, with download conditional on a mismatch. A daily run that finds nothing new does almost no work.
Bill text arrives in wildly uneven lengths — a one-line technical amendment and a 400-page appropriations act enter the same pipeline. Text is normalized and chunked before extraction so the downstream model sees consistent input regardless of source length.
The application serves four sessions because that is the window stakeholders actually reason about. The model trains and validates across nine, because a survival model needs to see enough terminal outcomes across enough political regimes to be trusted. Generative work — personalized impact scoring, briefings, digests — runs only against the current session, since scoring a closed session against a live role profile produces analysis nobody can act on.
Extraction across the backfill corpus was an 11,673-bill, roughly 155-hour unattended job. A wrong model choice there is not a quick re-run; it is a week of GPU time and a corpus of low-quality structured output that everything downstream inherits. So the choice was made as a formal evaluation with the acceptance criteria written down first.
Two findings did the real work. First, a reproducible saturation signature appeared in one model-and-chunk-size cell — visible only because resource metrics were sampled concurrently with throughput rather than averaged afterward. Second, output quality degraded past roughly 2,000 tokens regardless of model. That second finding is what made the decision tractable: chunk size became separable from model choice, so the two could be decided independently instead of as a joint search.
The original grid selected Mistral 7B at 500 tokens, with cross-configuration variance used as the tiebreaker rather than peak score. For an unattended multi-night job, a model that behaves predictably across configurations is worth more than one that wins a single cell and misbehaves elsewhere.
The original evaluation ran on the previous inference backend. After the migration to vLLM (documented separately in the throughput case study), the comparison no longer held — different batching behaviour, different effective cost per token. Rather than carry forward a stale decision, the evaluation was re-run over the weekend of 17 May 2026 against a contemporary quantized lineup: Mistral 7B v0.3 AWQ, Qwen 2.5 7B AWQ, and Llama 3.1 8B AWQ.
Production moved to Llama 3.1 8B AWQ, upgrading from the 3B-class model the pipeline had been running. The larger model became affordable specifically because the serving migration had recovered the headroom to pay for it.
Each bill is reduced to a set of standardized attributes an analyst can filter, sort, and reason over: what the bill does, who it affects, what it changes, what it expands or contracts, its policy domain, and a university impact assessment. These are the fields every downstream surface reads — search, categories, impact scoring, and digests all resolve against them.
Two properties matter more than the field list. Raw model output is retained alongside the standardized field. Keyword matching runs against the raw text as ground truth and falls back to the standardized field only when a raw value is absent, so normalization never silently destroys a match. And extraction is decoupled from scoring: bill features are computed once per bill, while per-user impact scoring runs separately against each user's own prompts. Adding a user does not re-extract the corpus.
Predicting passage requires knowing where a bill sits procedurally, and legislative action text is free-form. "Placed on Calendar Order of 3rd Reading", "Rule 19(a) / Re-referred to Rules Committee", "Do Pass Licensed Activities; 008-000-000" — each carries stage, activity type, and semantic meaning, none of it labelled.
So the labels were built. A 47-category two-tier taxonomy reaching 99.0% coverage, applied to 1,388,400 individual action rows with procedural stage, activity type, and semantic stage at 99.65% coverage.
Most bills die. They die at different procedural stages, for different reasons, and the honest answer to "will this pass" depends entirely on when you ask. A single classifier over final outcomes throws that structure away. The system is staged instead, so each model answers the question that is actually answerable at its point in the lifecycle.
| Measure | Result | Note |
|---|---|---|
| Discrimination (AUC-ROC) | 0.9944 | 2025 holdout, never seen during training |
| 95% confidence interval | 0.9908 – 0.9971 | Bootstrap over the holdout |
| Precision @ operating threshold | 0.9602 | Threshold chosen for the production surface |
| Recall @ operating threshold | 0.9659 | |
| Year-by-year stability | 0.9961 ± 0.0018 | Sixteen years, across speaker and rule changes |
| Validation corpus | 117,667 bills | 2009 – 2026, nine General Assembly sessions |
The stability figure matters more than the headline. A model that scores well on one holdout may simply have learned one legislature's habits. Holding within ±0.0018 across sixteen years spanning multiple speakers, rule changes, and political transitions says the signal is procedural rather than incidental to a particular regime.
A stakeholder asked to act on a number will ask why it changed. Showing the score after every action makes the model's reasoning inspectable without requiring anyone to read a model: a bill that jumps from 46% to 85% on "Do Pass" is legible. A single current-value display would have been simpler to build and considerably less useful, and it would have concealed the defect described in Section 08.
The system was extended to California to test whether the approach generalized past one legislature. The first framing produced a suspiciously easy task — the target definition was leaking. After correction, Stage 2 validated at 0.8512 AUC across 31,798 California bills. That is the published number. The flattering pre-correction figure is not reported anywhere, because it was wrong.
0.9944 on passage prediction is the kind of result that should make a reviewer distrust the pipeline before they believe the model, and it made me distrust mine. Procedural history leaks by construction: the actions that record a bill passing live in the same table as the actions that predict it. Without a guard, the model learns to read the answer.
The last row is the one I would defend hardest. A leakage guard that cannot be independently checked is a claim, not a control. Keeping the raw labelled history means a reviewer can re-derive what the guard excluded rather than take my word for it.
The daily run executes as a single scheduled command on the institutional Linux analytics server. Every stage writes structured logs against the standardized logging contract used across all pipelines on that server, which means this pipeline is visible in the cross-pipeline job execution monitor without any bespoke monitoring code.
Database writes worked locally and failed from the deployed application. Not credentials, not permissions — the campus network was terminating the connection at the TLS layer, and only for traffic originating inside the institutional network. Rather than fight the network, the scoring workload was re-architected around it: bulk inference stays on-premises where the constraint lives, user-facing generation runs in the cloud where it does not. The constraint became an architectural decision instead of an outage.
Every defect below was found internally, before a stakeholder encountered it. That is the part worth documenting — not that the system had faults, but which mechanism surfaced each one.
Everything above produces a scored, structured corpus. It becomes useful only when a specific person can ask it a question in their own terms — which is the subject of Part 2: the authenticated multi-service application, the per-user personalization layer, team deliberation, and the cloud architecture behind it.