Inspiration
The City of Ventura publishes every agenda for every public body on its Agenda Center. Posted on time, free to read, open to anyone.
I wanted to know how hard it would be to find one specific thing in there, so I typed one word into the search box: parking.
Forty-nine documents came back. Fifteen different bodies. Results going back to 2020, grouped alphabetically by committee, so the Parking Advisory Committee landed twelfth in the list. Every result gave me a date, a meeting name, and a Download button. No item title. No page number. No hint of what the word was doing inside any of them.
To find out whether the thing I cared about was on an agenda, I would open forty-nine PDFs and search each one myself.
That is the gap. Not secrecy, not apathy. A stack of PDFs between a person and the one paragraph that affects their street. Trying means opening files one at a time on a weeknight, and most people do not have that evening, so the decisions get made by whoever did.
I work in the California courts, running court operations for the county, so I spend my days inside a public institution that publishes a great deal of information correctly. Publishing correctly and reaching someone are different problems, and the second one almost never gets an owner.
What it does
Live: porch-light-ventura.vercel.app
Demo video (4:30): youtu.be/_205_gwrn3U
Code: https://github.com/earlgreyhot1701D/porch-light
You say what you care about in your own words. Porch Light comes back with the agenda item, in plain English and in Spanish, with the page it came from and a link into the city's own PDF at that page so you can check it yourself.
A parent types crossing guards at my kid's school. What comes back is the Fourth Amendment to Reimbursement Agreement 2022-053 between the Ventura Unified School District and the City of Ventura, $117,619 for crossing guard services through June 30, 2027. City Council, August 25, item 8, page 6.
Nobody finds that by searching. It does not say school safety. It says Fourth Amendment to Reimbursement Agreement.
Every item carries a receipt: body, date, item number, page, and a deep link into the city's published PDF. If a rewrite fails verification, Porch Light says so rather than showing you an unchecked summary, because a receipt that promises the source says what we say it says cannot be made by a rewrite nobody checked.
It also drafts. If you want to say something about an item, Porch Light fills in the facts and the source from the record and leaves your position, why it matters, and what you are asking for blank. Those are yours. It has no send capability anywhere in the codebase, including in stubs. Speaking to your city is your act.
How we built it
I do not write the code. I direct, Kiro builds, Claude reviews and architects, and I validate and decide. AI assisted, human approved, powered by NLP.
Three loops, and only two of them are agents.
The hunter finds new agendas and classifies every document by type with no model at all. It decides that from the file's URL and its title, which is plain code, not judgment. It runs on AWS Lambda, on an hourly EventBridge Scheduler.
The extractor is a Strands agent on Bedrock AgentCore running Amazon Nova Lite at temperature 0.0. It has no network egress by contract. It reads stored text, never the web, and a deterministic verifier checks its rewrite against the source before anything is marked verified.
The watcher is a Strands agent on Lambda, also Nova Lite. It decides which stored agenda items are about what you asked. Then a deterministic gate in code throws out anything the model claimed without a literal content word in common with your question. The model decides relevance. Code decides whether it gets shown.
Storage is Aurora Serverless v2 PostgreSQL, reached over the RDS Data API, scaled to zero when idle. The schema carries a pgvector column for embeddings and the extension is installed, but nothing in the shipped path queries it. Matching is the agent plus the deterministic gate, and the vector work is still a stub. The front end is static on Vercel with a same-origin /api/watch proxy that holds the only long-lived credential. No API key ever reaches the browser.
Some deliberate constraints. The city's separate video host tells automated readers to stay out, and that host happens to hold the only structured API in the setup, which killed the shortcut I wanted. A tool whose entire claim is being a trustworthy reader of public records cannot quietly override a public body's stated preference, so Porch Light obeys that robots.txt and reads the published PDFs instead. Packet text is treated as untrusted data, never as instruction. Logs never contain document text. The watcher writes nothing about the person using it: no account, no email, no server-side watch list. Your list lives in your browser and in the URL fragment, which is never sent anywhere.
The interface is bilingual English and Spanish and built to WCAG 2.1 AA.
Disclosure: Porch Light contains zero code from civiq. It was built from scratch for this hackathon.
Challenges we ran into
The guard taught the model what to write to get past the guard.
The watcher was returning items that were not matches, so we required every match to carry a non-empty matched_terms field and filtered on it in code, at two separate points. Then a query about dog parks came back with five matches, including a resolution about employee salary classifications. Verbatim from the deployed API:
{"item_id": "3685-5",
"matched_terms": ["dog park hours"],
"reason": {"en": "No mention of dog park hours"}}
The field the code was checking is populated. The truth is in the field the code was not checking. We told the model what shape a valid match had, and it produced that shape. The guard did not stop the behavior. It described the target, and it defeated a control built two hours earlier for exactly that class of error.
A check on a model's output is itself an input to the model. If the check is legible in the output format, you have not constrained the behavior. The fix is deterministic and the model cannot talk its way around it: a term counts as matched only if one of its content words literally appears in the item's stored text, checked in code, with no model involved. It costs real recall on an item phrased entirely in synonyms, and that cost is written down.
A success state written one line too early cost twenty-one documents.
The function that records a document wrote status='done' and then called the extractor. When extraction came back empty, the function did nothing else. A failed extraction and a successful one left the database in identical states. Status done, no error, zero attempts. The content-hash rule that makes ingestion idempotent, which is a good rule, then made it permanent: every hourly run afterward saw a hash it had already marked complete and skipped it.
Twenty-one of twenty-three documents were in that state. The corpus advertised twenty-three documents and could search two. Queries returned a correct "no match" over agendas that had never been read.
One hundred seventy-eight tests were passing. Every one of them fed the extractor a PDF that parses. The extractor's docstring said the caller should mark an unreadable document. The caller did not. The contract was written down and never enforced.
I built a watcher that cannot watch.
Porch Light stores nothing about the person using it. That decision is right and I would make it again, and it is incompatible with reaching that person later. I picked privacy, and it cost me the feature the product is named after. What shipped finds. It does not watch. I found that out properly by using the site as a resident instead of as its builder, four days before submission.
Accomplishments that we're proud of
It is live, on real agendas, from a real city, and it answers in about three seconds.
The honesty holds up under testing. Every item points back to a page in the city's own PDF. Failed verification says so instead of showing an unchecked rewrite. A degraded dependency produces an empty state, never a fabricated result. There is no send button anywhere, including in stubs.
Two of the three loops are agents and the third is deliberately not. Writing down where a model had no business turned out to be as much of the design as choosing where it did.
The Spanish is real work, not a toggle. Dates converted, dollar amounts intact, the school district named correctly, every rewrite verified against the source before it is shown.
And the repo says what the thing cannot do, with the reason, next to what it can.
What we learned
The recurring finding across this whole build, now twelve separate instances, is green tests sitting on top of broken reality. The suite passes because it feeds the system the input the system handles. The one that cost the most was the extraction swallow, and it cost twenty-one documents while one hundred seventy-eight tests reported success.
Second: a control that a model can see is a target, not a constraint. Goodhart's law operates inside a single agent turn. If you want a guarantee, put it in code where the model cannot reach it.
Third, and this one is mine rather than the machine's: I keep shipping what I thought I built instead of what actually deployed. The only cure I have found is using the thing as a stranger would, on a clean device, before I believe any of my own summaries. Every serious bug in this project was found that way, not by reading code.
What's next for Porch Light - City changes, brought home
Spanish as a search surface, not just a reading surface. Today crossing guards finds the item and guardia de cruce returns nothing, and that exact phrase appears in the Spanish rewrite Porch Light itself wrote for that item. In a county where a large share of residents speak Spanish at home, that is the gap that matters most to the person this was built for, and it is the one I most want closed. I did not patch it in the last week, because loosening a relevance check days before a deadline is how a check stops catching anything.
Notifications, or an honest answer about why not. Reaching someone later requires storing something about them. There may be a design where the reminder lives entirely on the person's own device and the server still knows nothing. If there is not, that goes in the repo as a limit rather than a roadmap item.
A second city. The adapter boundary exists. It has never been tested against a city that publishes differently, which means it is a claim, not a fact.
Evidence quotes on every item, so the receipt shows the sentence and not only the page number. Cut from this release on latency.
Fix the coverage display. The front end reports a stale meeting count against a database that holds more. Cosmetic, real, and written down in the findings ledger.
Porch Light is a prototype. It reads one city, it holds a rolling window of a few weeks, and it is not affiliated with or endorsed by the City of Ventura. The city's Agenda Center is the authoritative source. The site is running through October 2026.
The information was already there. Every fact Porch Light shows a person was sitting on a public server, in a free PDF, correctly published, days before the vote. The only thing standing between a resident and their own city was the work of finding it.
I do not think civic technology's job is to make people care. People already care. Its job is to shorten the distance between caring and being able to do something, and to be honest about how far it got.
I work in the California courts, running court operations for the county. I started building with AI in July 2025 and I have been learning in public ever since. The longer write-up on this build is on dev.to. I build the Clew Suite, a set of civic tech tools for making complex systems easier to inspect.
AI Assisted. Human Approved. Powered by NLP.
Built With
- amazon-bedrock
- aurora
- lambda
- nova
- strands
- vercel
Log in or sign up for Devpost to join the conversation.