Clarification & refusal¶
Ask-don't-guess is a guarantee, not a model behavior. Every promise on this page is
enforced in code that runs before or after the model — never by a prompt rule alone,
because measurement showed that prompt rules alone fail. In particular, the cheap
first-tier generation path renders a leaner prompt that never includes definition
bodies at all, so any promise that lived only in the prompt would silently not apply
there (services/runtime/pipeline.py:235).
Contested terms: clarify, deterministically¶
A governed term can be ambiguous — carrying possible_mappings instead of one
meaning. A question that uses the term without explicitly picking a meaning returns a
ClarificationRequest before any LLM or warehouse touch
(services/runtime/pipeline.py:74). The lens ranks the likely readings and
presents them; it never silently decides.
Clarify
"Average value per customer?" — value maps to lifetime value
(customers.customer_lifetime_value) or order amount (orders.amount).
dst returns the options and waits; naming a meaning in the question
answers normally.
source: services/runtime/pipeline.py:74
Certified answers are exempt by design: a human approved that exact question→SQL pair, so there is nothing left to clarify.
Excluded scope: refuse, before anything runs¶
A question naming a metric the lens's selection deliberately dropped is rejected before
any model call or warehouse touch (services/runtime/pipeline.py:59) — a curator who
excluded conversion_rate drew a boundary, and the refusal must not depend on a
model's mood. The boundary covers shape, not just name: generated SQL that would
compose a dropped metric from raw columns — a conversion rate rebuilt as a
flag-grouped count, divided in prose — is refused the same way. Both doors speak one
refusal (services/runtime/shape_guard.py, one shared text): the metric, the path
that would make it governed (select it in lens.yaml, or certify the answer), and —
when exactly one other lens in the org carries the metric — that lens by name. One
knob covers both doors too: serve_ungoverned_shapes: true lets the question run and
serve at confidence: unverified, whether it asked by name or composed the shape. A
rejected response never includes the SQL it refused to run
(services/runtime/pipeline.py:156); the trace keeps it for review.
Absent data: decline, never a confident zero¶
When the data to answer doesn't exist, the correct output is a decline — not an
empty-set aggregate dressed up as 0. An all-NULL aggregate row counts as no
evidence in verification, and the benchmark grades declining as the correct response
for questions whose data does not exist (services/benchmark/grading.py:55).
Scoring everywhere in dst is three-way — correct / wrong / declined — with wrong-rate as co-headline, because a wrong answer is worse than no answer: an ungoverned lane's characteristic failure is a confident "0" for data that doesn't exist.
A related failure hides one layer down: the data EXISTS but the filter is written in
the question's vocabulary instead of the column's — WHERE country = 'Finland'
against a column holding 'FI' returns zero rows with nothing to decline about.
And the zero has a disguise: COUNT(*) over an empty match returns one row holding
0, which row-presence alone reads as evidence. Serving investigates before either
zero leaves the building, deterministically and without an extra model call:
- Known value dictionary (a committed
dst probeartifact): a=/INstring literal outside a column's complete dictionary is caught before execution and repaired with the real values in the feedback. - No dictionary: a zero-evidence result (no rows, or the all-aggregate
0/NULL row) with string-literal filters buys one governed probe (≤2SELECT DISTINCTreads, read-only, row-capped, PII-names skipped). A literal proven absent from an enumerable column repairs the query; a literal the column really holds means the zero is honest and serves unchanged.
Then the escalation floor, when repair cannot use what the investigation learned:
- Proven absent, repairs exhausted → ask. The response is a
clarificationwithkind: unknown_value—termnames the column,optionsare the values it actually holds, and the question says the absence may itself be the answer. The ask-don't-guess doctrine, extended from governed terms to warehouse values. - Nothing learned → serve, graded honestly. A zero the probes could not check
serves with the
empty_result_investigationcheck failed and confidence capped atpartial— neververified— which is exactly whatauto_review: "partial"routes into the review queue. An investigated-and-confirmed absence keeps its badge: that zero is the data's answer, and now there is a receipt saying so.
An invented figure: withheld, never shipped¶
The same doctrine covers the prose itself. On a generated serve, if the composed
answer fails the numeric-grounding check — a figure in the sentence with no source
in the rows — composition retries once with the failure named in the prompt. Failing again,
the prose is withheld entirely: the response sets composition: "fallback" and the
answer becomes a code-generated frame over the result — the question restated, the
row and column count, and a preview of the rows themselves, written entirely by code
(services/runtime/pipeline.py:1027). The rows ride the fallback because they were
never the problem: withholding is about the prose, and the deterministic table is a
rendering of the data the prose failed to describe.
The failing check stays on the report on purpose — it is the evidence for the
fallback, it grades the serve unverified, and that is what routes it into the
review queue; re-grading the deterministic frame would launder the incident. A
degraded-but-true answer is an outcome; an invented figure is not.
The lens-less door declines too¶
The router ("just ask", no lens named) either routes to a lens the caller may use or
declines with the reason and the nearest miss (services/api/route.py:153). Thresholds
are two-gated — floor 0.78, confident 0.95, margin 0.07
(services/router/__init__.py:56) — and there is deliberately no ungoverned
full-warehouse catch-all behind a decline. With no embedder configured at all, a stdlib
lexical fallback makes matching ≈ token overlap, so the cold-start path declines
rather than guesses (services/api/route.py:56).
Declines are not dead ends: they persist, cluster into named coverage gaps, and become
the to-do list for the next lens (GET /mgmt/surface).