Skip to content

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.