Skip to content

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 probe artifact): a =/IN string 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 (≤2 SELECT DISTINCT reads, 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 clarification with kind: unknown_valueterm names the column, options are 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_investigation check failed and confidence capped at partial — never verified — which is exactly what auto_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).