← All projects

2026

Know Your District

An AI agent, map, and report builder for Hawaiʻi's 76 legislative districts. It answers plain-language questions about who lives in a district and which programs could help them, and every number links back to its source.

Event
2026 Hawaiʻi Annual Innovation Challenge
Challenge
DBEDT Research & Economic Analysis Division
Type
AI agent and full-stack web app
Stack
TypeScript, React, Fastify, tRPC, SQLite, Python
  • AI Agents
  • Claude
  • Evals
  • TypeScript
  • React
  • tRPC
  • SQLite
  • MapLibre
  • Python
  • HAIC 2026
Know Your District title slide over a map of Oʻahu House districts with hazard layers

Overview

Know Your District is my entry for the 2026 Hawaiʻi Annual Innovation Challenge (HAIC). The challenge came from the state’s Research & Economic Analysis Division: legislators have no fast, reliable way to see who lives in their district or which state and federal programs could serve those residents, so people who already qualify for help never sign up.

Answering “who lives in my district, and what help is available to them?” by hand means pulling Census, state, and tax tables and matching ZIP codes and census tracts to district lines. That takes days. The app does it in about ten seconds.

A 13-page district profile report next to the claim that a report takes 10 seconds instead of days

It has four parts that work together:

  • Chat: a Claude agent answers questions about any district, county, or the state by writing read-only SQL against a facts database. It draws charts and changes the map as it goes.
  • Map: all 51 House and 25 Senate districts and the counties, with layers for hazard zones, Hawaiian Home Lands, census tracts, and ZIP areas.
  • Overview: key figures compared with the county and state, overlapping districts, and programs worth a look. It comes straight from the database with no model, so it appears instantly.
  • Reports: a PDF or Word report of an overview or a conversation, with charts, the map, and footnoted sources.

The map with House District 28 selected and its overview panel comparing the district with the county and state

Three rules

When an AI talks about people’s lives, a plausible number isn’t good enough. I built the whole app around three rules.

Every figure in an answer is a link to where it came from. Hovering shows the source, year, area, and margin of error, and opening it shows the full trail. If a number can’t be traced, it isn’t in the answer.

A chat answer where the figure 2,424 kūpuna shows a card with its source, period, margin of error, and reliability

The agent writes each figure as a Markdown link whose target is a small marker: a fact, a curated indicator, a program income limit, how much of one place overlaps another, or a calculation. A fact’s id is a hash of its measure, place, and period, so it stays the same across database rebuilds.

When an answer finishes, the server resolves every marker. If a link doesn’t resolve, or the number the agent wrote doesn’t match the value in the database, the answer goes back to the agent once to fix before the reader sees it.

2. The AI never does the math

The model doesn’t compute shares, differences, or margins of error. It names the operation and the inputs, such as “the share of this fact in that fact,” and tested code does the arithmetic and carries the margin of error through with the Census Bureau’s formulas.

A rejected unsourced claim next to an accepted one: 2,740 divided by 27,502 equals 10.0 percent, with the source trace panel showing the inputs and warnings

The same code raises flags the model would miss: adding up medians, mixing units, comparing figures from different years or sources, or calling a difference real when it’s smaller than its margin of error.

3. Tested like software

There are 122 test questions. Their answer keys are calculated by code from the data, not written by an AI. During a test run the agent can’t see those calculated values. It has to write its own queries against the raw tables.

The star analogy for writing code instead of drawing, next to a passed test case with its typed checks

Each question has typed checks that a script grades: the right figure, a margin of error next to it, the right source, the right places on the map, the right program limit. A small model only judges the wording checks that code can’t. An answer also has to be grounded, meaning every number in it is either a link or appears in one of the agent’s tool results.

The grading is strict on purpose. In the latest run the agent got every figure right and still scored 91%, because a few numbers weren’t linked to their source.

The test dashboard for run v12: 111 of 122 passed, 100 percent of figures correct, 96 percent grounded

The data build

Everything the app knows comes from an offline Python pipeline that downloads the sources, cleans them, and builds two SQLite databases. It rebuilds both from scratch in about six seconds, and the databases are never edited by hand.

Database What’s in it Who reads it
kyd.db The facts: about 460,000 values across 4,000 measures and 667 places, plus 34 programs and their rules, sources, and known gaps The agent’s SQL tool
kyd-derived.db What’s calculated from the facts: 251 curated indicators with margins of error, chart definitions, suggested questions The overview and charts, and the test answer keys

Keeping them apart is what keeps the tests honest. The agent’s SQL connection never opens the derived database, so hiding the answer keys is a matter of which file gets opened, not a list of rules.

The sources are the Census survey (ACS), IRS tax statistics, BLS and BEA, CDC health estimates, FEMA risk ratings, and Hawaiʻi’s own DBEDT Data Book, Department of Taxation, Department of Human Services, and Office of Hawaiian Affairs data.

Apportioning

Most data isn’t published by legislative district. IRS data comes by ZIP code and FEMA risk by census tract, and neither lines up with district boundaries.

The build splits those numbers onto districts once, using 2020 Census block populations, and stores its working. Every apportioned number keeps the pieces it was built from, its possible range, and a quality rating, so the agent can explain it:

1,558 EITC claims in House District 24, tax year 2022. 410 come from ZIP 96826, which is entirely in the district; the rest are shares of 96813 and 96817. Possible range: 410–3,755. Quality: fair.

A few rules keep this from producing made-up precision. House and Senate districts don’t nest, so each level is calculated straight from the source. Census survey tables are never apportioned, because they’re already published for districts. County totals are never spread onto districts, because that only smears the county average.

No dead ends

When the data can’t answer a question, the database says why. Missing values carry a reason code, such as “not published for this geography,” and a list of known gaps points to the closest alternative. The agent cites the reason the same way it cites a number.

A statewide map shading House districts by households below the poverty line that don’t receive SNAP, up to 31,739 statewide Gap figures like this one show where outreach could reach people who already qualify.

The app

The app is one TypeScript monorepo with types shared end to end. A place, a chart, or an event is defined once as a zod schema and is the same type in the server, the page, and the tests.

Area Technology Details
Agent Anthropic TypeScript SDK Claude Sonnet 5.5 or Haiku 5.5, streaming, nine tools
Server Node 24, Fastify, tRPC HTTP for queries, WebSocket for streams
Database SQLite, better-sqlite3, Kysely Read-only, with types generated from the schema
Web React, Vite, Tailwind CSS, shadcn/ui Chat, map, and overview panes
Map MapLibre GL, PMTiles Self-hosted vector tiles, no license cost
Charts React SVG components The same components render on the page and in reports
Reports pandoc, LibreOffice, resvg Markdown to Word and PDF
Data build Python, GDAL Standard library only, checked with pytest
Quality Vitest, Playwright, Biome, GitHub Actions Unit, click-through, and theme contrast checks
Deployment Docker One image, three build stages

The agent’s tools

The agent has nine tools. Each one is a name, a description, a zod input schema, and a function. The registry turns the schemas into the model’s tool list and validates inputs before running them.

  • search_catalog and describe_table find what the database has and what each measure means.
  • run_sql runs a single read-only SELECT, with a time limit and row caps.
  • district_profile and show_chart return curated figures and draw charts.
  • highlight_places, shade_map, and clear_map drive the map.
  • create_report builds a PDF or Word report.

The database is read-only three ways: the files are opened read-only, SQLite’s query_only is set, and the SQL tool rejects any statement that would write.

Events

Each browser tab holds one WebSocket carrying typed events: answer text, steps, charts, map actions, and report progress. Every event has an increasing id, so after a dropped connection the page asks for everything after the last one it saw. The page’s state is a reducer over those events, and conversations are stored in SQLite, so a reload or a server restart rebuilds a conversation exactly.

Swappable models

The model is a single setting. The app runs Claude Sonnet 5.5 or Haiku 5.5, DeepSeek, or a small model served locally on Apple Silicon, which matters for a state agency that may need to keep everything on its own hardware. With Haiku an answer costs under half a cent.

How I built it

The first version was Python with plain JavaScript. Once it worked, I rebuilt the app in TypeScript and kept the Python data build. The old version stayed running as the reference: parity tests called both with the same inputs and compared the JSON until the new one matched.

I did the rebuild with several Claude Code agents working in parallel, coordinated by one main session:

  • Contracts first. The shared types, the API’s shape, and the event types were written before anything else, so each track could build against stubs instead of another track’s unfinished code.
  • One worktree per agent. Each agent worked in its own git worktree and branch, and every path in the repo had exactly one owner.
  • Small commits. Each step was merged only when typecheck, lint, and tests passed.

The test set drives the work the same way. After a run, Claude proposes a fix for each failed question and I approve or reject it. The test files themselves are locked by a fingerprint, so a graded run won’t start until a person approves any change to the questions or the grader.

What I learned

  • Make the model point, not compute. Having the agent cite a row and name an operation turns “is this number right?” into something code can check.
  • Separate what the agent can read from what grades it. Putting the answer keys in a different file was simpler and more trustworthy than any prompt rule.
  • Grade the reasoning, not just the answer. A correct but untraceable number counts as a failure, and that pushed the design further than accuracy alone would have.
  • A database should explain its own gaps. Reason codes for missing data stopped the agent from guessing and gave users a real answer.