Methodology
This tracker records where 2027 Chicago mayoral candidates stand on housing, based on what they say in public media — podcasts, interviews, candidate forums, and news coverage. It is an accountability tool: every position links back to the source it came from.
How positions get here
- Discovery. On a schedule, the system scans local-outlet news feeds, video and podcast channels, and candidate social posts for new appearances.
- Ingestion. Articles are read as text; videos use their captions; audio is transcribed. We store only the extracted quotes and a link to the source — not full transcripts or media files.
- Extraction. An AI model reads the transcript and drafts each housing position with the exact supporting quote.
- Review. A second, independent AI model checks that each quote is real and correctly attributed. Then a human reviews and approves every position before it appears here.
What we do and don't claim
Summaries are our paraphrase; the linked quote and source are the record. A blank cell means a candidate has not addressed that issue on the record that we've found — not that they have no view. We track housing closely; other issues a candidate raises are noted internally but not published here.
Saying you support housing is not a position
Every candidate is for affordable housing. The question worth asking is how — so for each position we record the specific mechanism the candidate named: a program, a rule change, a funding source, a quantity, or a deadline. It has to be present in their own quoted words.
Where a candidate voiced support but named no mechanism, the cell is marked ● and the profile says "No specific mechanism stated." We show those rather than hiding them: a blank cell cannot tell you the difference between a candidate we have little coverage of and one who talks about housing constantly without ever committing to anything.
Borderline cases are recorded as no mechanism. "Streamline permitting" is a direction; "cut permit review to 30 days" is a mechanism. We would rather understate a candidate's specificity than credit them with a plan they did not describe — a second model checks every claimed mechanism against the transcript, and flags any it cannot find.
This makes some candidates look thin, and for a few that is because we have found little coverage of them rather than because they have said nothing. The remedy for that is more sourcing, not a lower bar. If we have missed a place where a candidate laid out specifics, tell us — see Corrections.
Under the hood
For the technically curious: this is a scheduled pipeline that runs on GitHub Actions and publishes a static site. Everything is open source, and every language-model step is dependency-injected so the whole flow is tested offline on fixtures before it ever touches the live models.
Sources it watches
- News articles — direct RSS feeds from Chicago outlets
(Block Club Chicago, WTTW News, Chicago Reader, The TRiiBE, Chicago
Sun-Times), read as text with
trafilaturabehind a real browser user-agent. (Google News RSS is kept as a fallback but disabled by default — its links are redirects a plain fetch can't read.) - YouTube — each candidate's campaign channel plus
standing channels (WTTW / Chicago Tonight, WGN News, City Club of Chicago),
via YouTube's per-channel RSS. Audio is pulled with
yt-dlpand transcribed. - Podcasts — Chicago-politics shows (The Ben Joravsky Show, The Fran Spielman Show, City Cast Chicago) via their RSS audio enclosures. Long episodes are re-encoded to 16 kHz mono before transcription so they fit the transcription API's upload limit.
- Bluesky — each candidate's public posts via the
app.bsky.feed.getAuthorFeedAPI. The post text is the record (no audio); reposts and media-only posts are skipped, and extraction is scoped to the account owner so first-person posts are attributed correctly.
X, Instagram, and TikTok are not auto-tracked (no open feed / terms of service), but a link to any of them can still be submitted manually.
The pipeline
discover → ingest → extract → propose → review → human approval → publish
- Discover polls each source daily, de-duplicates against a ledger, and a cheap AI triage call decides whether an item plausibly features a candidate discussing policy.
- Ingest turns the item into clean text — article readability, audio → transcript, or a social post's text directly. Transcripts are never stored; only the extracted quote and a source link are kept (copyright).
- Extract asks a model to pull each housing position with a verbatim quote. Any quote that doesn't literally appear in the transcript is dropped, as is any statement about a candidate or topic that isn't in our registry.
- Propose writes the evidence and a draft position and opens a pull request.
- Review — a second, different-family model re-fetches the source, re-verifies every quote, and labels the PR verified or flagged. For audio there is a wrinkle worth stating plainly: we don't store transcripts, so the reviewer re-transcribes the recording, and two transcriptions of the same audio are never quite identical — filler words, hyphens and trailing clauses move. When a quote isn't word-for-word in the new transcription, we show the reviewer the closest passage and ask whether it is the same statement: same numbers, same polarity, same promise-versus-possibility. That verdict is labelled reconciled rather than verified, and the PR shows the passage and a similarity score so the human deciding can see exactly what differed.
- Human approval — nothing publishes automatically; a person merges the PR, which rebuilds and redeploys the site.
The models
- Extraction & triage: DeepSeek V3.2
(
deepseek/deepseek-v3.2). - Independent review: Moonshot Kimi K2
(
moonshotai/kimi-k2-0905) — deliberately a different model family than the extractor, so it catches the extractor's mistakes (a common one is attributing an opponent's words to the candidate). - Transcription: Whisper large-v3-turbo, via Groq.
Language models are accessed through OpenRouter; the model choices live in a config file and can change. The extractor is trusted only to draft: a wrong or fabricated quote has to get past a verbatim-quote check, a different-family reviewer, and a human before it can reach this site — and the auto-publish switch ships off. The verbatim check is never relaxed at extraction: a quote that isn't literally in the transcript the model read is dropped, always. The reconciliation described above applies only to the reviewer's second, independent transcription of audio.
Candidates we don't track
We focus on candidates with a realistic path in the race. These declared candidates are not shown in the matrix; we'll add anyone here if that changes or they put a housing position on the record.
- Danielle Carter-Walters — Not actively tracked — a long-shot candidate with no media-sourced housing position on the record. Will revisit if that changes.
- Liam Stanton — Withdrew from the race 2026-08-10 and endorsed Alexi Giannoulias. `status: withdrawn` alone does not remove a candidate from the site — loadTrackedCandidates() filters on `tracked`, not `status` — so this flag is what actually takes him off the matrix and profiles. His media hits stay committed as history.
Corrections
Found a misquote, a wrong attribution, or a missed appearance? Corrections are welcome and taken seriously. Please open an issue — you can also use that form to submit a media link for us to process.