個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Free, independent tracker of Wisconsin politics: bills, votes, campaign finance, elections, and city councils, plus Wisconsin's members of Congress. Built from official public records, with open data and a free API.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Explore Wisconsin politics through official public records.
Badger Politics is a free, independent,
open-source resource for residents, journalists, researchers, and civic
developers. Follow bills, votes, campaign finance, and elections across
Wisconsin state government, its members of Congress, and covered city councils.
Records link back to official sources so you can check the evidence yourself.
See coverage for available history and gaps.
Badger Politics is an independent project, not affiliated with the State of Wisconsin.檢視完整 README ↗Explore Wisconsin politics through official public records.
Badger Politics is a free, independent, open-source resource for residents, journalists, researchers, and civic developers. Follow bills, votes, campaign finance, and elections across Wisconsin state government, its members of Congress, and covered city councils.
Records link back to official sources so you can check the evidence yourself. See coverage for available history and gaps.
Badger Politics is an independent project, not affiliated with the State of Wisconsin.
Working on the code? Start with local development, architecture, and data sources.
| Section | What it holds |
|---|---|
| Bills | Every proposal in every session since 2009, with status, sponsors, the LRB plain-language analysis, fiscal estimates, companion bills (from the Legislature's own See Also links), and the full official history with legislator names linked |
| Roll calls | Every recorded floor vote, name by name, with party splits and the reader's own legislators pinned |
| Legislators | Profiles with bills led, key votes selected by rule, votes against party, floor attendance by day, committees, campaign money while in office, compensation, and the complete paged record |
| Districts and Find My Legislators | Address to district entirely in the browser (Census geocoder for coordinates, bundled LTSB boundaries for the match); nothing is stored anywhere but the device |
| Committees | Members, hearings, and the bills that died in each committee without a hearing |
| Hearing None | The graveyard: bills that were referred, never heard, and failed at session's end |
| Calendar | Hearings and election days, with iCal feeds and WisconsinEye recordings where they exist; readers who saved an address in a covered city also see their own council's upcoming meetings, on that device only |
| New Laws, Governor's Desk, Veto Tracker, Partial Veto | Acts by biennium with passage tallies; bills awaiting signature; every veto, partial veto and override attempt; how the partial veto works |
| Campaign Money | Receipts to sitting legislators by two-year reporting period (the current period by default, all available history on request, an optional filter to receipts during recorded service), contributing committees, and outside spending filing by filing with the Ethics Commission's transaction IDs and report links |
| Federal Delegation | Both U.S. senators and all eight House members, with every floor roll call from the Senate's and House Clerk's own XML (Senate from the 112th Congress, House from 2005) |
| City Councils | Milwaukee, Madison, Appleton, Waukesha and West Allis recorded votes, member profiles, attendance, source links, feeds and JSON. Partial history is labeled. Verified district maps and address lookup currently cover Milwaukee and West Allis. |
| 2026 Ballot | Statewide offices and every legislative seat, with a personal "what is on my ballot" view from the saved district |
| Following | Device-only follows for bills, legislators, committees, districts and races, with what changed since the last visit |
Every page links its official source and carries the independence disclaimer.
openstates-scrapers (wi, pinned, CLI-only) ┐
docs.legis member pages, subject index, LRB │
WEC ballot access + certified canvasses ├─→ archived raw data (_data/) → SQLite → integrity gates → data products + Astro static build → Firebase Hosting
CFIS tRPC API (campaignfinance.wi.gov) │
WisconsinEye (private archive import) │
Senate LIS + House Clerk roll-call XML ┘
SQLite is the only database and is rebuilt from the archived raw data on every run (pipeline/run.sh). The served site is fully static: no servers, no functions, no runtime LLM calls. Target infrastructure cost is about $2 a month plus domains.
Releases run in GitHub Actions, never from a laptop: a push to main
rehearses on dev, and production is a deliberate promotion
(workflow_dispatch). CI builds from the newest database snapshot in a
private bucket and runs every gate before publishing. See
docs/deploys.md. The nightly parser uses sequential,
resumable GitHub Actions jobs and private GCS checkpoints. Scheduled runs
require a seeded archive and NIGHTLY_PARSER_ENABLED=true; see the
parser runbook.
| Data | Source | Mechanism |
|---|---|---|
| Bills, actions, votes, hearings | docs.legis.wisconsin.gov | openstates-scrapers pinned as a git submodule, invoked only as a CLI (os-update); fixes live in pipeline/patches/ |
| LRB analyses, companion bills, fiscal documents | docs.legis bill and proposal pages | fetched once per URL into an on-disk cache; companions come only from the page's own See Also links |
| Legislator roster, photos, committees, service terms | openstates people files, docs.legis membership listings, openstates legacy CSV (2009-2012) | session-windowed rosters; human-verified curation tables for merges, aliases, terms and departures |
| Capitol office contacts | docs.legis member pages | refreshed each run; sitting members only |
| Subject index | docs.legis subject index | headings as printed; each entry attributed to the heading its document path names (the index's continuation pages overlap), matched by exact session and identifier |
| Hearing recordings | WisconsinEye | metadata matched by exact date and committee title; links out, never hosted |
| Candidates and election results (legislature, statewide offices, U.S. House) | Wisconsin Elections Commission | before the primary, the ballot-access report, every row accounted for against each office's printed subtotal; after it, each party's nominee from the certified primary canvass under Wis. Stat. 8.16 plus the independents the Commission approved, with anything the records cannot settle shown as open (rulings with an official basis in importer/wec_rulings.json); certified ward-by-ward canvasses for results |
| Statewide ballot questions | Commission referendum notice; docs.legis enrolled resolutions | each question kept only when its enrolled resolution states it for that election, names a first consideration whose history agrees, and carries the constitution text word for word as the preceding Legislature approved it (importer/import_amendments.py) |
| District boundaries | LTSB 2024 official files | bundled GeoJSON; the Census geocoder is used for address-to-point only |
| Campaign finance | CFIS tRPC API (campaignfinance.wi.gov) | legislator receipts in monthly windows since 2008-01 through a verified committee map; every other filer's money since 2025-01, including independent expenditures with their report IDs; the newest two months are re-read nightly and older months on a rotating audit |
These are hard rules. A change that violates one is wrong even if it works.
person_merges, person_aliases, person_terms, term_events,
vote_corrections, candidate_committees), each entry with its
basis.source='legiscan', if ever
present, never appear in any export. Site pages may show them, credited
to LegiScan and linked, under its CC BY 4.0 license. FollowTheMoney data is
cross-check input only and is never read by the importer or the site
build. Org logos render only for entities in
site/src/data/org-domains.json, each entry human-verified with a
dated basis string, re-verified by the fetcher before download.Pipeline (importer/checks.py, every run; a failure aborts before any
snapshot is written):
Site (site/scripts/, run against a build; CI runs all of them before a
release):
| Script | Checks |
|---|---|
preflight.mjs | the built tree matches the database it came from: every session, one page per bill, legislator and roll call, a search index at least as large as the bill count, data products present, disclaimer shipped |
verify.mjs | 11 functional checks in headless Chrome: search quality and junk-match rejection, address lookup to the right two legislators, roll-call pinning, rep highlighting, Hearing None banner, LRB analysis, disclaimer, analytics |
a11y.mjs | axe-core WCAG 2.2 AA over 41 representative pages at desktop and 360px; 0 violations required |
responsive.mjs | no horizontal overflow on the same 41 pages at 344/360/412/540/768/1280/1920px |
links.mjs | every internal path and fragment anchor resolves; --external probes deduplicated outbound URLs with per-host pacing |
csp.mjs | run against the released site: the Content-Security-Policy breaks neither search (WebAssembly) nor the address lookup (Census JSONP) |
measure.mjs | diagnostic, not a gate: names the element that overflows one page at one width, with --wide-font to reproduce a Linux-only failure on Windows |
The two browser gates share one page list (scripts/lib/serve.mjs), so a
page added to one is scanned by both.
Everything on the site is also data, keyless: a static JSON API under
/api/v1/ (more than 41,000 files: bills, roll calls, legislators, committees
and hearings, subjects, seats and election results, the federal
delegation's roll calls, city councils, and campaign money in separate
files, with individual donors only in aggregate; the layout is in
api.py), Atom feeds per bill, legislator,
committee, seat and subject, plus a weekly digest and feeds of new laws,
vetoes and outside-spending filings (more than 20,600), iCal calendars for every
hearing and for election days (845), per-session CSVs, and a
provenance-filtered SQLite snapshot. See /data/.
The bulk files are published to GitHub Releases by each production deploy (never served from the site origin), always at the same addresses:
https://github.com/uprightsleepy/badger-politics/releases/latest/download/badger-politics-csv.ziphttps://github.com/uprightsleepy/badger-politics/releases/latest/download/wi-filtered.sqlite.gzhttps://github.com/uprightsleepy/badger-politics/releases/latest/download/SHA256SUMShttps://github.com/uprightsleepy/badger-politics/releases/latest/download/README.txtLike the API and feeds, they leave out LegiScan rows and lobbying registrations, and individual donors appear only in aggregate (their receipts keep amount, date and recipient, without name, occupation or filer id).
| Data | Coverage |
|---|---|
| Bills, actions, roll calls | 2011-12 through 2025-26 full; 2009-10 partial (official pages list vote totals, not names) |
| Legislator campaign finance | electronic records 2008 to present, for members with a verified committee link (docs/curation-worklist.md holds the human-verification queue) |
| Committee, PAC and outside spending | every filer's transactions since January 2025 |
| Lobbying | Withheld from public pages and exports pending an approved source |
| Election results | certified WEC canvasses: legislative seats in every general election from 2014 (each labeled with the district maps it was held under), statewide offices 2014 and 2022, Supreme Court and Court of Appeals races from 2020, statewide referendum questions of 2014 and 2023-2025 |
| Federal roll calls | U.S. Senate from the 112th Congress (2011); U.S. House from 2005 |
| Council votes | Milwaukee from 2008; Madison from 2025 with five new meetings per run and visible backfill progress; West Allis from 2015 (earlier minutes record votes inconsistently). Expansion order and source review. |
Prerequisites: uv, Node 22, Docker (scrapes
only), Google Chrome (headless test harnesses; Edge is the fallback, or
set BROWSER_PATH).
cd pipeline
uv run pytest # 114 tests
uv run ruff check .
uv run ./run.sh --local --skip-deploy # full chain: scrape, import, enrich, check, build
uv run python -m importer.checks ../data/wi.sqlite
uv run python -m scraper.backfill # historical sessions (idempotent)
Scrapes run in the vendored container: python -m scraper.scrape bills
wraps docker compose run --rm scrape against
pipeline/vendor/openstates-scrapers/docker-compose.yml. os-update
wipes its output at run start, so never run two scrapes concurrently, and
never build the site while the importer is rebuilding the database.
The optional FTM_API_KEY lives in gitignored pipeline/.env; its
enrichment degrades gracefully without it.
cd site
npm ci
npm run dev # against ../data/wi.sqlite
npm run build # current biennium only, for fast iteration
BUILD_SESSIONS=all npm run build # full history; what CI releases
npx astro check # TypeScript; 0 errors expected
node scripts/preflight.mjs
node scripts/responsive.mjs && node scripts/a11y.mjs && node scripts/verify.mjs && node scripts/links.mjs
CYCLE (default 2026) selects the active election year in run.sh.
| Path | Contents |
|---|---|
pipeline/scraper/ | fetchers for every source; http.py is the one HTTP session (identifying User-Agent, retries, page cache), cfis_api.py the one CFIS client |
pipeline/importer/ | importers, enrichment, checks.py (the gates), schema.sql (the entire data model), and the human-verified curation JSON |
pipeline/dataproducts/ | JSON API, Atom feeds, iCal, bulk exports |
pipeline/patches/ | documented fixes applied to the pinned scraper at runtime |
pipeline/_data/ | archived raw scrapes and API responses (the rebuild source; gitignored) |
site/ | Astro 7 + Tailwind 4 + Pagefind; src/lib/db.ts is the only database access, src/lib/wire.ts the contract between build-time JSON and browser scripts |
site/scripts/ | the build-time logo fetcher, preflight, and the browser harnesses |
.github/workflows/ | ci.yml (lint, tests, typecheck, workflow guards, tofu validate) and deploy.yml (gated release from a snapshot) |
infra/ | OpenTofu for the CI deploy identity: the enabled APIs, the Workload Identity pool and provider GitHub federates into, the deployer service account, and its hosting and snapshot-bucket IAM bindings |
docs/ | plan, deploy runbook, backfill record, money methodology, cross-check reports, curation worklist, and research/ notes behind each module |
Corrections are the highest-priority issues:
open one here.
Pull requests to main need the code owner's approval and a green CI run
(lint, tests, schema, typecheck, workflow guards, tofu validate); only the
owner pushes directly. Please keep the data mandates above in mind: a
change that makes a number look better by guessing is not a fix.
Code is Apache-2.0. Legislative, election and campaign finance data comes from Wisconsin public records. Archived lobbying data is withheld from publication; federal roll calls are U.S. government works. openstates-scrapers is GPL-3.0, subprocess-only (see mandate 7). FollowTheMoney data is CC BY-NC-SA and is not redistributed.
| Data and API | Static JSON API, Atom feeds, iCal calendars, bulk CSV and a provenance-filtered SQLite snapshot, all keyless |
| Lobbying registrations | Withheld | Collection removed; archived data is excluded from public pages and exports pending an approved source |
| Federal roll calls | senate.gov LIS XML, clerk.house.gov EVS XML, unitedstates/congress-legislators roster | per-vote files mirrored once and cached forever; positions keyed by each chamber's own member id |
| Council votes and attendance | Legistar Web API (milwaukee, madison, cityofappleton, waukesha, westalliswi); each meeting's InSite page for item links | one shared collector/importer; meetings cached once minutes are settled (Approved for Madison, Final otherwise); votes retain each tenant's person IDs and original labels |
| Aldermanic district boundaries | Milwaukee open data portal (CC BY shapefile); West Allis city GIS server | one-time generator (importer/local_shapes.py), committed GeoJSON; the robots-disallowed city map host is not used |
| Curated council seats | official West Allis, Appleton and Waukesha rosters | shared table (importer/local_seats.json), keyed by tenant and person ID with a basis URL for each verified district |
| Legislator filer ids kept alongside a curated one | pipeline/importer/retained_committees.json, each row with how it was matched and when first seen | re-validated against the same name rules on every run; a row that stops passing fails the run rather than lingering |
| Council members carried under two ids | importer/local_person_merges.json, each entry with its basis (identical record name, complementary service, no item voted under both) | folded into one member; the importer refuses a merge the votes contradict |
| Council member names, portraits, contacts, committees | Legistar person/office IDs and public department listings; reviewed city district pages for Milwaukee and West Allis | exact identity and source attribution; Milwaukee profile retrieval is currently paused with its complete archive retained; Madison districts use the same person ID's official council URL, without portrait downloads |
| Cross-check only | FollowTheMoney API (CC BY-NC-SA) | verification input, never imported or republished |
| Org logos | Local assets for hand-verified org domains | missing logos use monogram tiles; automatic retrieval is paused |
| Address suggestions (optional) | Google Places API (New), only in builds where the PUBLIC_PLACES_KEY Actions variable is set | as-you-type suggestions in the address lookups, disclosed in the privacy copy; the submitted address still resolves through the Census geocoder; without the variable the feature is dormant and the site fully keyless |
pipeline/patches/ and documented for
upstreaming.uv.lock, package-lock.json, npm ci --ignore-scripts).