Skip to content
← Writing

PrinciplesTraceSarkar · 8 min read

No model in the path: why we hand-write the post and the PDF

TraceSarkar uses AI to read a photograph. It refuses to use AI to write a sentence about anyone. Here’s where we drew that line, and why.

For a civic app with no marketing budget, the share is the growth channel. Every WhatsApp forward of a pothole record is an ad, a piece of evidence and a nudge to a ward office at once. So the text of that share matters more than almost anything else we write. We decided no language model would write it.

The temptation#

We already use a vision-language model to classify photographs — pothole, open manhole, burst pipe — with a confidence floor below which it has to ask. Having the same kind of model write a friendly, punchy post about the result would take an afternoon. It would read better than anything we’d template.

“It would write better sentences than a template. It would also, eventually, write a fact.”
— ADR 0020 — share text is generated, not written by a model

That’s the whole argument. A model asked to describe a pothole on a contracted road will, one day out of thousands, add a contractor that isn’t in the record, or an amount, or a word like “negligent.” Filters catch vocabulary. They don’t catch claims.

What we do instead#

The share kit assembles the post from a fixed set of facts — the public view of an issue — using reviewed templates in English, Marathi and Hindi:

  1. 1

    Start from the public view

    Only fields in the public issue view can appear. What isn’t in it — the reporter, exact coordinates — is excluded by construction, not by remembering to leave it out.
  2. 2

    Fill reviewed templates

    Strings live as reviewable JSON with a review state. Marathi and Hindi ship marked as drafts until a native speaker has read them.
  3. 3

    Check the finished string

    A word-boundary check runs over the final post for three groups of terms: accusations of wrongdoing, insults, and political party names. If it fails, Compose refuses to return the text at all.

The tone is set by one rule from our brand notes: say “48 hours have passed,” never “BMC has failed you again.” State facts and joins; never conclusions.

“The failure mode is a stilted sentence, not a false one.”
— ADR 0020

The share kit caught our own bug#

Building the deterministic post forced us to say exactly which deadline applies to what. And it turned out we’d been sloppy: the High Court’s 48-hour rule binds potholes — not every road defect. A model would have happily kept writing “48 hours” under every cracked footpath. A template that pulls the deadline from a cited table couldn’t, and the mismatch surfaced the moment we wrote the test.

That’s also why no legal constant is a literal in our code. The 48-hour corporation deadline, the 24-hour contractor deadline during the warranty period, the ₹5,000-a-day penalty — each lives in a versioned table with an effective date and a citation.

The evidence PDF, written by hand#

The same thinking produced the strangest piece of code in the repo: a hand-written PDF writer of around 400 lines. Off-the-shelf libraries were fine at drawing pages. The problem was what they add quietly — a creation timestamp and a random document ID in every file.

An evidence record should be reproducible: the same facts should produce the same document, and anyone should be able to check that. So our PDF is deterministic, and it’s stamped with a hash over the facts, not the rendered bytes. Citations that would run off the page count as no citation at all. URLs never hyphenate. Only the person who reported a sighting can download it.

The line, stated plainly#

Use models where their mistakes are cheap and caught — sorting a photo into a category, with a human-facing “are you sure?” when they’re unsure. Never use them where a mistake becomes a published statement about a named person. That line costs us some nicer sentences. It’s the cheapest insurance we’ll ever buy.