Singapore publishes everything Parliament says. A typical sitting runs 40,000 to 90,000 words, and most of it is written answers nobody reports. Parsnips marks up the parts that matter inside the full transcript. A sitting takes about 40 minutes, and you can still check any line against the record. Getting those marks right took three attempts. Two of them were wrong, and both are worth writing down.

What it is
Every sitting of Parliament as a reading page. The whole transcript, with the parts that matter highlighted where they sit.
Behind it
Every sitting since 2016. Tens of millions of words, across more than ninety thousand speaker turns.
Published
Every summary sentence points back at the exact turn it came from. If a sentence cannot be pinned down, it is marked as ambiguous rather than guessed at.
Kept current
A watcher checks for the next sitting, waits for the Hansard, then runs the same pipeline. New sittings arrive on their own.
Cost
$10.97 in model credits for the whole archive. That is under a fifth of a cent per brief, measured from the pipeline's own log rather than estimated.
Not
The published copy is read-only. The ask box needs a model, so that part runs on my machine only.

What I set out to do

I want to know what the Singapore government is doing. I am not a lawyer or a political scientist, and I am not going to read Hansard. A typical sitting covers Bills, ministerial statements, oral answers and Budget debate. At 150 words a minute, one day of Parliament is about eight hours of reading.

Both existing options fail, in opposite ways. News coverage gives you a phrase picked for how it reads. The transcript gives you everything, and it is unusable. I wanted the middle: enough to know what was decided, in under an hour, without it being someone else's pick.

So I turned that into a number to build against. A sitting should take 30 to 60 minutes. Working back through the hundreds of speaker turns, that is about ten seconds per speaker, or 25 to 35 words of summary. That one number decided most of what follows, because a summary that short only works if it is reliably accurate.

I also wrote the correctness rule before any code existed. These are parliamentary transcripts. One quote in the wrong mouth would sink the credibility of the whole thing. And the failure is quiet: a summary that reads well is the one you would not think to check. So I made it structural. Every claim has to show the exact words it came from, in a place I can point to.

Not "the model should quote accurately." Verifiable.

Version one: I let the model write the quotes

The first build was the obvious one. Send each item to a model, ask for the important points with the sentences behind them, then assemble. It worked, in the sense that it always produced something.

Two things were wrong. It repeated itself, often using the same sentence to back more than one point. A brief that looked like six findings was sometimes two findings said six ways. And some of the quotes were not in the transcript at all. Close, plausible, and wrong.

So I added a checker to compare every quote against the source afterwards. That did not fix it. The reason is the part I had not thought through. The checker only ever sees what the model produced, so a near-miss paraphrase gets waved through. There is nothing to compare it against except the summary itself.

Asking a model nicely to quote accurately is not a guarantee. Adding a checker after the model does not create one. The guarantee has to come from how you build it.

Version two: retrieve first, then summarise

The second design put search in front of the model. Embed everything, pull the passages that look most like the item being summarised, then let the model work only on those. It sent far less text, and it was cheaper and faster.

What it pulled was the problem. It kept surfacing procedure instead of substance: the lines that open and close a debate, requests for leave, the roll of members present. Those lines repeat almost word for word across ten years, so they look similar to everything, including a question about housing. I had built a machine that reliably handed the model the dullest material in the room.

Embeddings have a second blind spot. They blur exact strings, and this archive is full of them: dollar amounts, flat counts, abbreviations. A search that cannot tell one figure from a similar figure is the wrong tool for a record of what was promised and spent.

What actually fixed it: a database, then retrieval on top

The missing piece was never the transcript or the summary. Both existed, and both were right. What did not exist was the link between them: which turn a summary sentence came from, and where inside that turn.

So I put the whole archive into SQLite and described how the parts relate. A turn belongs to a report. A chunk belongs to a turn. A summary sentence points at a turn by foreign key, and carries the character range it came from. A broken link became impossible to insert, instead of something to hunt for later.

With the database as the source of truth, search became a tool rather than the whole design. It now runs two kinds of search over the same tables: one that matches exact words, figures and abbreviations, and one that matches meaning.

I also noticed procedural lines kept getting highlighted. Chair business, motions, requests for leave, the lines that open and close a debate. They crowded out what I actually wanted. They kept rising to the top because they repeat almost word for word, which makes them look similar to everything. So I tightened the rules there too, and wrote procedure out of what counts as a summary point at all, rather than leaving it to a similarity score.

The last change was to stop the model writing quotes at all. It returns the id of the sentence it means, and the system drops the real text in afterwards. A model can be wrong about which sentence matters. It cannot invent one, because it never types any. That is what makes the guarantee structural instead of hopeful.

Nearly every summary sentence points back to exactly one turn. The few that cannot be pinned down are repeated procedural lines, one of which appears eighteen times. They are marked ambiguous rather than guessed at. I would rather show the same speaker twice than the wrong one once.

How it reads now

Open a sitting and the whole day is there in order, grouped into topics that stay folded until you open one. A summary sits directly above the words it came from, and those sentences are highlighted inside the transcript rather than pulled out of it.

That is the decision I would defend hardest. The highlighted sentences are about a sixth of the transcript. A page showing only those would hand you a fraction of what was said and call it the record. Keeping the transcript whole means the summary is a reading aid you can check, not a replacement you have to trust.

Every record gets the same treatment, written answers included. They are a quarter of a sitting's words and get almost none of the coverage. On the day in the screenshots, 141 of the 165 records were written answers.

A sitting in Parsnips: 163 topics, 558 turns and 18 summarised briefs, with the day's topics folded closed and each one showing its turn count, word count and highlighted passages
What a sitting looks like on arrival. Every topic from the day, closed, with the day's shape at the top: 558 turns, 723 highlighted passages, and how much of the transcript they cover. Nothing is ranked or scored. The moment a panel ranks the participants, it stops being a record.
Parsnips at phone width: a sitting's header showing 163 topics, 558 turns and 723 highlighted passages, above the day's topics
Built phone-first. The base layout is the 390px phone, so nothing had to be retrofitted later. Same day as the hero, at the width most of its reading happens.

You can ask it questions in plain English

The same data answers questions. Ask "how many employers got Jobs Support Scheme payouts" in plain English and you get the passages that answer it. Each one is quoted, and links to the sentence it came from. It looks things up rather than making them up, so an answer is always something Parliament actually said. When it cannot find support, it says so instead of answering anyway.

That part is switched off on the published site. Every question costs a model call, and an open door on a public page is an invitation for someone to run up my bill. So reading is public and free, and asking runs on my machine. The site says so plainly rather than hiding it.

If you want to try it, get in touch. I am happy to set up access for anyone genuinely interested in the record. I would rather hand it out on purpose than leave it open.

What I took from building it

The lesson is about where I put the model. Early on I reached for the AI answer by default, because it is the interesting one. Embed everything, search it, let a model write the brief. Every time, the boring option held up better. A foreign key beat a similarity score at saying which turn a sentence came from. A simple text match beat a vector at knowing that procedure is not substance. Ordinary code that gives the same answer twice beat a model that gives a slightly different one each morning.

The correctness bar forced that, not a taste for older tools. I had promised every claim would be checkable against the record. Any design that could not guarantee it ruled itself out, and most of the pure-model designs did. I ended up with a model doing exactly one job, deciding what matters, and everything else ordinary and testable.

So the honest version is not that a model cannot do this. It is that doing everything with a model is the easy mistake, and the project's own rules pushed me back. A system like this is mostly a data problem with a model in it, not a model with some data attached.

Where it stands

Live at parliamentsnips.com, and I still use it. Every sitting since January 2016 is published. Tens of millions of words, with a reading page behind each one.

Keeping it current came last, because doing it by hand does not scale and quietly stops. A watcher reads Parliament's announcement of its next sitting, waits for the Hansard, then runs the same pipeline that built the archive: fetch, summarise, verify, render, publish. If any gate fails, nothing ships. The failure I care about is not a missing page. It is a wrong one going out while I am not looking. So the pipeline refuses to publish rather than publishing with a caveat.

The site is pre-rendered. Every sitting is built once, checked, and then placed there as a finished page. Nothing is put together when you open it. There is no API or server-side call behind it, so nothing can fail, slow down, or quietly answer differently than it did last time.

That is about citation, not about avoiding modern tools. A record you can cite needs an address that keeps working, and a page that keeps saying the same thing. If a sitting were assembled from a database on load, a citation would point at a query. The link would still resolve, and what it showed could change. This way a published sitting is close to immutable: reading it depends on a file being there, not on a service staying up.

It also removes a class of failure I cannot fix from here. There is no server to fall over, no key to expire, and no upstream API that can change its terms or start charging. The archive survives an outage at the source, and nobody can switch it off.

Rebuilding the set is still possible, which is how a new sitting gets in or a page-wide fix lands. What does not happen is a page being assembled at the moment you read it.

The earliest year has a known set of sentence mismatches against the current record, briefs whose source text moved after they went out. That is recorded rather than hidden. The coverage of every sitting is stored next to what the source promised, because the source API under-reports its own counts. What would kill a tool like this is a silent failure, so a point that fails verification gets dropped, not published with a caveat.