What the model sees¶
Authoring stops being guesswork when you know what reaches the prompt, in what order, and what never reaches it at all. This page is the map: what each model call sees, what is deliberately withheld, and the one command that renders all of it for a real question with no LLM and no warehouse call.
Every serving call is single-turn — a cacheable system block plus one user message, text in, JSON out. No tool use, no structured-output API: the surface is the lowest common denominator any OpenAI-compatible endpoint can serve, which is what keeps bring-your-own-model honest.
The grounded prompt, anatomically¶
The system block carries the rules and the whole serialized semantic model — no
retrieval gamble on the facts the SQL must bind to. Tables render as
<physical> AS <entity> so the model never resolves physical names itself
(services/runtime/generator.py:172); metric lines are emitted by the same
deterministic compiler that produces real SQL, so the prompt teaches the exact
expression the guards will later demand; joins carry their type and cardinality;
definitions render in full, with their about: binding, any enforceable
[sql: …], and the [AMBIGUOUS — MUST clarify, never guess] marker
(services/runtime/generator.py:247). The only cap in the serializer is
common_questions[:5] — the scale lever is upstream scoping (a lens
selects few entities), not truncation.
The user turn carries the per-question context: the certified-definition page
first, ahead of every retrieved chunk (services/runtime/assembly.py), then up
to 6 retrieved context chunks, then the answer contract last, adjacent to the
question — project exactly the quantities the question names plus grouping keys,
results at the asked grain, full precision unless rounding is asked. Certified
exemplars enter anonymously: approved pairs render identically to lens-authored
sample queries — no score, no provenance, no "prefer these" instruction. The caller
is told an answer was certified-assisted; the model is not.
The intent path is the blind path¶
When a lens has metrics, the cheap first pass is the intent generator: the model
emits a structured QueryIntent and code compiles it to SQL
(services/runtime/intent_generator.py). Its prompt is deliberately leaner, and
knowing what it lacks is the single most important thing about the governance model:
| Rendered | Grounded prompt | Intent prompt |
|---|---|---|
| Physical table names | yes (AS entity) |
never — SQL is compiled by code |
| Metrics | compiled SQL + REQUIRES filters |
prose only |
| Joins | with type and cardinality | a bare "entities you may combine" list |
| Definitions | full body + about + [sql: …] + AMBIGUOUS marker |
full body + [filter: …], no AMBIGUOUS marker |
| Certified exemplars | yes, as sample queries | never |
| Common questions | first 5 | never |
ai_instructions |
yes | never — by design, below |
The cheap tier cannot see the ambiguity marker, the join keys, the exemplars, or the instructions. So ask-don't-guess is a guarantee carried by code, not a model behavior: deterministic clarification and exclusion, the filter and time guards, and the compiler's name resolution all run regardless of tier — any promise that lived only in the prompt would silently not apply on this path. See Clarification & refusal.
ai_instructions is absent from the lean pass because the gap is grammar, not
knowledge: the structured QueryIntent shape cannot express rulings like "list
DISTINCT" or "order by a count you do not project", so the lean pass could read such
a ruling and still be unable to obey it — its only lever is escalating to the
raw-SQL tier. dst apply warns intent_tier_escalation_only
(services/validate/report.py:583), naming what a metric lens loses; a ruling that
must hold on every answer belongs on the dimension, metric, or definition it is
about, which the lean pass does render.
The composer, and the about: rule¶
The second model call writes the English. It sees the question, the SQL, up to
max_rows_to_compose rows (default 200), the truncation flag — and two curated
extras (services/runtime/answer.py:97):
- Governing definitions — the full bodies of definitions bound to the columns the answer projects ("they decide what the values mean"), plus whatever definition the generator reports it applied. Never capped, never truncated: a silently dropped definition is the exact bug this scoping exists to prevent.
- Data notes — declared facts about the projected columns, taken from the table
profile and handed over explicitly with an attribution rule, so a useful caveat
becomes a cited one instead of an invention
(
services/runtime/answer.py:152).
It does not see the semantic model, the retrieved chunks, or definitions not bound to a projected column. Which produces the one authoring trap worth memorizing:
A caveat in a definition body does not reach the prose on its own
A definition binds to the composer through about: entity.column (or, failing
that, a result column literally named after the term). A definition without that
binding still steers SQL generation — that is a different prompt — but the model
writing the sentence will not have it, so a caveat you wrote into the body
("returned orders still count toward this") silently fails to appear in the
answer. If you care that a caveat reaches the reader, give the definition an
about: pointing at a column answers actually project.
source: services/runtime/answer.py:97
What the model never sees¶
Some facts are withheld by construction, and where they stop is the point:
| Withheld | Why, and where it stops |
|---|---|
| Caller identity and groups | authorization and the trace only; never in any serving prompt |
excluded_metrics |
the model is never told what it must not compute — the refusal runs in code before the LLM, so the boundary cannot be argued with |
| PII values | structurally absent: pii: true fields contribute shape only to profiles, never a literal (services/lenses/profile_enrich.py) |
| PII values for a redacting caller | redacted off the one served result the composer's prompt, the checks, and the payload all read — the prose cannot carry what the rows do not (services/runtime/redact.py) |
data_as_of, certified provenance |
response-only trust signals; the model neither knows nor can fake them — see Receipts |
| Rejected SQL | never returned to the caller; kept in the trace for review |
dst lens prompt — stop guessing, just look¶
This verb renders everything above for a real question — assembled, no LLM call, no
warehouse call (services/cli/main.py). It is the only way to answer "did the thing
I authored actually reach the model?". Abridged output, annotated:
$ dst lens prompt customer_value "How many repeat customers are there?"
lens customer_value · tier intent · prompt-set cb6c4324 · assembled, no LLM call
=== first pass: metric-layer prompt === # the lean tier, verbatim
=== escalation: raw-SQL prompt === # what only an escalation would see
=== context (user turn) === context_chunks: 0 · certified_exemplars: 0 · ...
=== reaching the model === # per authored asset kind: n/n counts
entity 2/2 · field 11/11 · metric 5/5 · join 1/1 · definition 3/3
=== reaching it only if generation escalates to the raw-SQL tier ===
join 1 orders -> customers # the per-lens version of the
sample_query 1 ... # intent_tier_escalation_only warning
instructions 1 ai_instructions
=== second call: compose prompt (writes the English) ===
=== definitions that can reach the composer ===
lifetime_value sent when the answer projects `customer_lifetime_value`
=== NOT reaching the composer === # named, with the reason
repeat_customer no `about: entity.column` — reaches the composer only if a
result column is literally named after the term
The counts panel is reassurance; the two "NOT reaching" panels are the debugging surface. An asset listed there, by name, with its reason, is an authored fact that will not influence the next answer — fix the binding, or accept that it only steers the tier that renders it.