Skip to content

Clarity

Natural-language database interface

Ask the estate a question in English, get an answer with the SQL that produced it. Around thirty tenant databases, about twenty people daily. Generating SQL was the easy bit.

2025 → ongoing
~30 tenants, ~20 daily users
phosphor P11 · blue
> try it yourself

Five ways an answer can lie

Toggle it off for what the model said. On for what ships. All five are simplified from cases the detectors caught in production.

grounding layerONwhat users actually get
question

What are the freezer temperatures across my northern sites?

what actually ran
! runQuery → join across operational + telemetry stores
! error → cross-store join, relation not found
answer shown to the user
I couldn't answer that one. The operational and telemetry data live in separate stores and I can't join across them, so I need to query them one at a time. Want me to pull the telemetry side on its own?
fabricated_resulttriggers a correction round

Every query this turn failed, yet the answer presents data. Nothing produced those numbers.

how hard is text-to-sql, really?

Text-to-SQL demos in an afternoon

Point a decent model at a schema and you'll have something working before lunch. That part really is easy now.

Day two is the problem. It answers from a table that died a year ago. It quotes a number without running a query. It says your export is ready when nothing was ever written. Each one beats a stack trace for damage, because nobody can tell it happened.

So I stopped reading feature requests and read transcripts instead. The gap was never capability. It was trust.

where's the vector store?

No vector store. Anywhere.

The default move is embeddings. Chunk the schema, load it into a vector database, retrieve per question. I'd push back on anyone reaching for that first: a schema isn't an unbounded corpus, it's a few hundred tables you can simply describe.

A nightly job compiles a knowledge document per tenant. Business summary per table, a glossary, SQL recipes known to work. It goes into every conversation as cached context, byte-identical so the provider can actually cache it.

The model turns up already knowing the estate. Live discovery stays as a fallback, so a table added this morning works this morning. This is what it gets handed:

compiled nightly, injected as cached contextclick a highlighted line
## Schema Directory
Every table you can query is listed below with a one-line description.
This is the complete set.
 
### operational
- `work_order` — jobs raised against a site, with status and due date
- `site` — every physical location, with region and commissioning date
- `asset` — equipment installed at a site, keyed to its site
- `site_daily_rollup` (no new data since 2025-03) — legacy daily aggregates
- `alarm_event_archive` (no rows) — superseded, retained for audit
 
### telemetry
- `reading` — time-series sensor readings, one row per probe per interval
- `probe` — sensor metadata: type, unit, the asset it monitors
 
## Glossary
- **overdue**: a work order past its due date and not yet closed
- **estate**: every site belonging to one tenant
 
## Verified Query Recipes
- **open work by site** — how much outstanding work does each site have?
```sql
SELECT s.name, count(*) AS open_jobs
FROM work_order w JOIN site s ON s.id = w.site_id
WHERE w.closed_at IS NULL GROUP BY s.name ORDER BY 2 DESC
```
freshness marks

The compile probes each table's newest timestamp and stamps the result. A dead table announces itself, so the model routes around it instead of confidently reporting no data. This is the mark that exists because of a real bug.

Everything above is sanitised before it reaches the prompt. Table comments and sample values are attacker-influenceable, so headings, code fences and template tokens are defused at the rendering boundary. Injected knowledge is data, never instructions.

which sites are running hottest?

The bug that changed the design

Someone asked which sites were running hottest. Clarity said there was no data. There was loads of data. It had found a promisingly named table, dead for months, while the live telemetry sat somewhere less obvious.

A wrong "no data" is the nastiest failure in the set. Nobody escalates it. They decide the tool is useless and stop asking, and no metric ever tells you.

Now the compile probes each table for its newest timestamp and marks the directory: (no rows), (no new data since …). The model routes around dead tables because it can see they're dead. Freshness stays out of the change-detection hash, or a timestamp moving nightly would re-summarise the whole estate nightly to tell us nothing changed.

drop table sites;

Try to get something past it

Generated SQL is untrusted input that happens to be executable. The system prompt says read-only, which is worth nothing on its own, so the enforcement lives in the database.

Queries run as a dedicated read-only role, provisioned on every tenant database at startup. It fails closed: no pool, no query. It never falls back to the admin connection, which is the sort of helpfulness that ends up in an incident report.

try one, or write your own
model-generated SQL
what the validator actually sees
SELECT tablename FROM pg_tables

comments stripped · literals masked · identifiers unquoted and lower-cased

blockedclarity.sql.blocked{reason=system_table}

Access to system table 'pg_tables' is not permitted.

offending token: pg_tables

Beats naive string matching. Comments are stripped before any comparison happens.

identifiers: select, tablename, from, pg_tables
calls: none
schema refs: none

These are the real rule sets and reason codes, ported to run in your browser. Nothing is sent anywhere. In production this is the outermost layer: underneath it the query still runs as a SELECT-only role that fails closed.

Validation lexes SQL to canonical tokens before checking anything. String matching loses to quoting and comments, so FROM/**/pg_tables would walk straight past it. Failures return as data, not exceptions, so the model reads its own error and corrects itself.

how do you know it works?

I won't use a model to grade a model

LLM-as-judge is the obvious approach and I rejected it. A grader that hallucinates can't certify a system whose defining failure is hallucination. The bad case isn't a wrong score, it's two models agreeing confidently in the same wrong direction with a green dashboard on top.

Canned questions replay after every deploy, asserted against the audit record rather than the prose. Answer contains a number, SQL must have run. Fabricated-names list must be empty. Export claimed, report row must have completed. Nothing to argue with at 2am.

why is it built like that?

Two calls worth explaining

Before either of them: every answer carries the query that produced it, verbatim. It costs screen space and turns an oracle into a tool. These people can read SQL, and letting them check beats any amount of confident phrasing.

Two stores, routed, never joined

Operational data on a per-tenant database, telemetry in a shared time-series store. Generated SQL gets routed to exactly one. Cross-store joins are impossible by construction, not discouraged by prompt, which kills a whole category of confidently wrong answer.

Loops with a budget

Every turn carries a tool-call cap, enforced around the agent loop rather than requested in the prompt. Blow it and the model is told to summarise what it found and stop. A per-tenant token bucket sits on top, so a runaway conversation can't become runaway spend.

A test that fails when the docs drift

A unit test compares every registered metric against the documented table, both directions. That's how a series nobody could query survived for months, and how a total-outage signal sat there unalerted.

where does it stand?

Where it stands

~30

tenants, a database each

~20

daily users

5

lies detected per turn

500+

tests

Straight about status: the trust layer is built and running, and the proof it's driven fabrication to zero isn't in yet. The transcript analysis that started this hasn't been re-run against the current build. I'd rather say that than put up a number I can't back.

Thanks for reading.

If you're fighting hallucinations in production, or just want to argue about vector stores, I'm easy to find.