Lenses¶
A lens is a use case — a purpose — that needs a set of data, a set of context, for a set of callers. The purpose comes first: a lens is not a bundle of config that happens to have a name; it is a job someone needs done — commission questions, churn questions, the board's monthly numbers — and the three things it declares are what serving that job requires:
- A set of data — which entities are in scope. That already settles which warehouse and which tables, because an entity names its own source.
- A set of context — everything needed to read the data correctly: what the words mean (governed definitions, including the contested ones), what one row is, when to use a table and when not to, and the prose you have attached.
- A set of callers — who may ask: the deny-by-default access boundary. Keys belong to the people and apps behind the agents, so this is a list of principals, not tools.
Mechanically, a lens is a selection over the semantic layer: which entities, which
governed definitions, which certified patterns this use case may draw on — bound to
warehouse connection(s), a model config, and the access list
(services/contracts/lens_config.py). Callers ask a lens questions; the lens decides
what the answer is allowed to touch and what the words in the question mean. Lenses
are named for use cases (sales_comp), never for tables.
The purpose is declared, not implied. use_when in queries.yaml is the field
that states it, and it is not decorative: those lines become the lens's routing
anchors (services/router/profiles.py:41), so when a caller asks a bare question with
no lens named, the purpose you wrote is what the question is matched against. A lens
with an empty use_when is a lens that has not said what it is for.
One metric, one definition — the word is what varies¶
A lens is not a private dialect, and you should not give two teams two "revenue"
metrics. Metrics are defined once, in the shared semantic layer
(semantic/entities/*.yaml, semantic/definitions/*.md), each one clearly itself:
bookings is bookings, recognized revenue is recognized revenue. What varies is which
of them the word "revenue" points to when a particular person says it — sales says
revenue and means bookings; finance says revenue and means recognized. Both are
legitimate and neither is a redefinition; the word is just underspecified.
dst's job at that fork is to not guess. Mark the term ambiguous with its possible
mappings and the question comes back as a question, naming the options, before
anything runs. A lens's scope often settles it on its own — a commission lens has
only one revenue-ish metric in play — and where scope does not settle it, the
clarification does. Fragmentation is treated as a defect to fix, not a feature:
when one word is claimed by several metrics, dst apply warns by name — govern the
others, or mark the term ambiguous.
Selection, not copy¶
lens.yaml names what the lens takes from the shared layer; dst apply compiles
the selection into the runtime model (services/project/compile.py:50). A term defined
both shared-and-selected and lens-locally is a compile error, never a silent winner
(services/project/compile.py:8). Metrics the selection dropped become
excluded_metrics, refused deterministically before any model call — see
Clarification & refusal. The refusal covers their shape
too: an answer that would rebuild a dropped metric from raw columns declines with
the same path (select it in lens.yaml, or certify the answer) — one refusal text
for both doors. serve_ungoverned_shapes: true in lens.yaml opts back into serving
either — the by-name ask and the composed shape — at confidence: unverified
(services/contracts/lens_config.py).
The managed file layout (services/project/loader.py:26):
semantic/ # project scope, shared by every lens
entities/deals.yaml # table, grain, use/avoid, fields, metrics, joins
definitions/commission.md # governed term: frontmatter + prose
lenses/sales_comp/
lens.yaml # the selection + policy (below)
queries.yaml # use_when + sample queries, lens-local
definitions/*.md # lens-LOCAL terms
certified_answers.yaml # approved question→SQL pairs
evals/cases.yaml # behavioral cases (expect: clarify | refuse)
# lenses/sales_comp/lens.yaml (abridged)
name: sales_comp
connections: [bigquery]
select:
entities:
- name: reps
- name: deals
- name: payouts
definitions: [commission, earnings]
access:
allow: [] # deny-by-default; grant callers or groups explicitly
Versioned like code¶
A lens is stored as a bundle and materialized to a browseable file tree on read
(services/lenses/repo.py); every publish snapshots a lens_version
(services/lenses/store.py:301), and version-to-version diffs are unified diffs over
the rendered tree (services/api/mgmt_lenses.py:534). You review a lens change the way
you review a pull request, because structurally it is one.
A lens ranks likely intent — it never silently decides¶
When a question hinges on a term with more than one governed meaning, the lens does not
pick the likelier reading. An ambiguous definition lists its possible mappings, and a
question that uses the term without naming a meaning returns a clarification —
deterministically, in code, before any generation
(services/runtime/pipeline.py:74).
Clarify
"What is the average value per customer?" — value is ambiguous here:
lifetime value (customers.customer_lifetime_value) or order amount
(orders.amount)? The answer is a question, not a guess.
source: services/runtime/pipeline.py:74
Contested vocabulary usually surfaces through the drift audit — the same metric computed N different ways in the warehouse's own query history — and is recorded as an ambiguous definition rather than a silent winner.
One agent, one lens¶
Because a lens is the whole answering surface — vocabulary, scope, and access in one
object — the natural shape for AI agents is one scoped lens per agent, reached over MCP:
what list_lenses returns under the agent's key is its entire world
(services/mcp/server.py). See Agents over MCP.