Inspiration
I've spent my career working with music artists and producers, and a couple of them are from Compton. One of them, AD a music artist and media personality, is a good friend and a former client. I've been in the studios. I've been on the blocks the records made on. Compton's creative output has shaped global culture for three decades, and nobody who has actually worked there needs that explained to them.
So when I saw Compton Tech Week, I didn't go looking for a problem. I went looking at the part of life here that nobody makes records about.
Here is what my career taught me that turned out to be the whole project:
In music, the gap between a talented artist and a working artist is almost never talent. It's whether somebody showed them the path. Who to call. What to send. What to keep a copy of. When to follow up. What to do when nobody answers. The people who make it are frequently not the most gifted people in the room — they're the ones who had someone in their corner who knew the process. I've been that person more times than I can count, and I've watched what happens to the ones who never got it.
Civic services work exactly the same way, and almost nobody says so out loud.
A resident with a busted streetlight and an artist with a finished record have the same problem: the thing they need exists, someone out there is responsible for it, and there is nobody in the room who can tell them what to do next. The information is public. The number is published. That has never been the same thing as knowing what to do.
So we built the thing I'd been doing informally for artists for years — for city services instead.
Then we went and checked whether the gap was real. It was worse than we assumed. We catalogued 22 City of Compton services by hand, from the resident's side, verifying every contact route against the City's own published pages.
13 of the 22 have no dedicated online form. They are phone-first.
That's not a knock on the City — every one of those channels made sense when it was created. But if you work days, or you're Deaf or hard of hearing, or you'd rather not conduct a service call in your second language, thirteen of twenty-two services aren't inconvenient. They're closed.
What it does
A resident describes what happened in their own words, in English or Spanish. No department name required. No form number. No account.
COMPTON ONE: FIX then does five things:
1. Routes it. The description is matched against 22 verified service paths. Routing is deterministic — no language model decides where a resident gets sent.
2. Shows its work. Every result displays the responsible entity, the verified contact method, the verification state of that fact, and the date it was last checked. If something is unconfirmed, it says so rather than hiding it.
3. Prepares the resident. An evidence checklist, a plain-language list of what not to include — no names, no plates, no photos of people — and a call script in the resident's own language.
4. Issues a Civic Action Receipt. What happened, who owns it, what to say, what to save, when to check back.
5. Tracks it to an outcome. A six-status lifecycle, a timeline, a field to save the confirmation number, a follow-up date that exports to a calendar, and controls to mark resolved, reopen, correct, or delete.
Then it says the thing most civic demos leave out:
This prototype does not submit your report to the City.
It prepares and tracks your action. Claiming otherwise would mean a resident walks away believing something was filed when it wasn't — in this category, that isn't a bug, it's a harm.
What's real: intake, classification, confidence gates, emergency gate, all 22 routes, evidence checklists, call scripts, receipts, status lifecycle, timeline, confirmation capture, calendar export, print view, opt-in local storage, full English/Spanish parity, browser voice input.
What's simulated, and labelled as such inside the interface: SMS reminders, city acknowledgment, staff dashboard, community trend map, and the sample cases on the dashboard.
What we deliberately did not build: direct submission to city systems, emergency dispatch, payments, identity-document collection, named-offender reporting, and any guaranteed response time.
How we built it
The core decision was to keep AI out of the routing path entirely.
The obvious build is a chatbot. A chatbot will confidently invent a department that doesn't exist and hand a resident a phone number that goes nowhere. In a civic tool that's the worst possible failure, and it fails silently — the resident doesn't find out until they've spent their lunch break on it.
So the architecture inverts the usual approach. Interpretation is deterministic; every civic fact is human-verified.
- Bilingual keyword and phrase matching with weighted confidence scoring
- A hand-built catalog where every phone number, URL, department name, and deadline carries a source link, a verification state, a named maintainer, and a next-review date
- Nothing reaches the screen unless it came from that catalog
Three rules are enforced by the type system rather than by convention:
1. The classifier can only emit a serviceId that exists in the catalog.
Free-form department invention is not possible.
2. An emergency result cannot carry a route. The type forbids it.
3. Confidence gates are explicit:
>= 0.85 route the resident
0.60 - 0.84 ask one clarifying question
< 0.60 show two options and let the resident choose
Spanish was authored, not translated. "No recogieron la basura" was never a translation of "the trash was skipped" — it was written as Spanish from the start. That's why Spanish routes as accurately as English instead of trailing it.
The stack
Languages — TypeScript (strict mode), HTML5, CSS3, JavaScript (compiled output).
Frameworks — None. No React, no Next.js, no Vue. Vanilla by decision, not by limitation: a framework would have added a build dependency and a runtime to an app that ships as one file.
Build tooling — esbuild bundles everything into a single self-contained HTML file. Node.js and npm are development-only and are not needed to run the app.
Testing — Vitest, 708 automated tests, plus custom browser harnesses for the journey, adversarial, and persistence suites.
Browser APIs — Web Speech API (SpeechRecognition, en-US and es-MX), localStorage (opt-in, device-only), Blob / URL.createObjectURL for calendar export, and window.print with a dedicated print stylesheet.
Databases — None. No Postgres, no Supabase, no Firebase, no server-side storage of any kind. Case data lives in localStorage on the resident's own device, only after explicit consent, and erase removes the key rather than blanking it.
Cloud services — None at runtime. The app is a static file. It can be served from Vercel, Netlify, Cloudflare Pages, or GitHub Pages interchangeably, and would run just as well from a USB stick.
Third-party APIs — None. The app makes no API calls of its own. The only outbound requests are links the resident chooses to click, going to official City pages.
AI / ML — None in the routing path. The classifier is deterministic bilingual keyword and phrase matching with weighted confidence scoring. This is the architectural centerpiece: a model that invents a department is the worst failure mode in a civic tool, so there is no model to invent one.
CI/CD — GitHub Actions running typecheck, the full test suite, and a gate that fails the build on any dangerous classification outcome.
Standards — WCAG 2.1 AA target, i18n with English/Spanish parity, progressive enhancement, static site.
It works on a bad connection, and it works with every permission on the phone denied — no camera, no location, no contacts, no account.
Challenges we ran into
We caught ourselves grading our own homework.
The classifier scored 100% on the initial test set. Then we looked at where those test phrases came from: they were derived from the classifier's own trigger lists. We were testing whether the code matched itself.
So we wrote an independent corpus of 212 resident phrasings — how people actually describe these problems — and labelled every item before running anything against it.
The honest score was 50.0%.
Worse than the number: it exposed that the emergency gate was missing half of all emergencies phrased in words we hadn't thought of. Ten emergency-gate defects, sitting in a build we would have shipped. Every one is now a permanent test.
That rewrite is documented in full, including the uncomfortable part — we used the frozen evaluation split to fix the defects it found, which burns it. The next clean measurement requires a new corpus. It's in the docs rather than quietly omitted.
An adversarial audit of every user path then found seven more defects, three of them trust failures rather than functional ones:
- Deleting a case cleared the state but left the resident's own words rendered on screen
- Delete produced no visible confirmation at all — you couldn't tell whether it had worked
- The previous case's confirmation number persisted into the next case, where it could be saved onto the wrong one
That last one bothered us most. A resident carefully saves their confirmation number and the app attaches it to the wrong case. Silent, plausible, and it destroys the exact thing the product exists to protect.
And one small bug taught us the most. The phrase "hay un gato herido" — there's an injured cat — was triggering the emergency gate. The matcher's tolerance for filler words was matching straight across gato. An injured animal is an animal control call, not a 911 call. Overtriggering an emergency gate isn't a safe failure; it teaches people to ignore it.
Accomplishments that we're proud of
Not the feature count.
That every claim in this submission has a test or a document behind it, and the failures are in the changelog instead of edited out of the story.
| Check | Result |
|---|---|
| Held-out corpus, 212 independently written phrasings | 95.7% correct top route |
| Emergency phrasings gated | 20 / 20 |
| Out-of-scope requests refused rather than misrouted | 12 / 12 |
| Dangerous outcomes — confident wrong route or missed emergency | 0 |
| Catalog trigger phrases routing correctly | 440 / 440 |
| Automated tests | 708 |
| Browser checks — journey · adversarial · persistence | 57 · 66 · 36 |
| Console errors on the demo path | 0 |
Stated limits, because a number without its limits is marketing: the corpus is author-written, not field-collected. The frozen split is burned. Eight corpus items still fail — all of them safe refusals or clarifying questions, documented rather than patched to make the number prettier.
We're also proud that a resident can decline every single permission and the app still works completely. No camera, no location, no contacts, no account. The microphone is only ever touched on an explicit press, and the app discloses that browser speech recognition sends audio to the browser vendor while typing sends nothing anywhere.
What we learned
The highest-severity bug in a civic app is not a crash. It's a wrong phone number, delivered confidently.
A crash is honest — the resident knows something broke and tries again. A confidently wrong department costs them a morning and teaches them the tool isn't worth using. That one realization drove the whole architecture: the sourced catalog, the visible verification dates, the CI gate that fails the build on any dangerous outcome, and a contribution template that refuses a catalog change without a source URL.
Our instinct about Compton was also inverted, and in a better way than expected. We assumed the challenge would be building something sophisticated enough to be worthy of the community. It wasn't. This community is not short on sophistication — the challenge was building something restrained enough to be trustworthy. Nearly every impressive feature we considered adding made the product less honest. What shipped is smaller than what we sketched and considerably more defensible.
And the parallel from the music side held all the way through. The bottleneck is never the person. It's whether anyone told them the process.
What's next for COMPTON ONE: FIX
Confirm the one remaining unverified contact detail with the City, and build a resident-facing "this number is wrong" path so the catalog self-corrects instead of decaying.
A field-collected corpus. Ours is author-written. The protocol is already costed out — real resident phrasings, two independent labellers, reported agreement.
A screen-reader pass with real assistive technology, plus 711 relay and TTY guidance. With 13 phone-first routes, that isn't an accessibility nicety — it's the difference between usable and not.
Tagalog and Samoan, human-authored and community-reviewed, with triggers written natively and their own corpus slice before any accuracy claim is made about them.
A supervised pilot with a block club or a community navigator — the people already doing this work by hand — plus a monthly catalog diff that flags the City when a page they own changes.
And the honest long answer: the best outcome for this project may not involve the app at all. The 13-of-22 finding is useful to the City whether or not they ever run our code. That audit is theirs, free, no conditions. If it becomes nine of twenty-two because someone stood up four forms, this was worth building.
Residents shouldn't have to become city employees to navigate city services.
One problem. One path forward.
Built With
- accessibility
- css3
- esbuild
- github-actions
- html5
- javascript
- localstorage
- los-angeles
- node.js
- npm
- progressive-enhancement
- static-site
- tagalog
- typescript
- vanilla-js
- vitest
- wcag
- web-speech-api
Log in or sign up for Devpost to join the conversation.