What bloomery proves¶
This page states the correctness claim precisely: what property bloomery establishes, from which facts, at what time, and what stays outside it. It exists because both directions of over- and under-statement are easy. "Artifact generator" undersells a compiler that refuses arithmetic; "guaranteed correct metrics" oversells one that has only ever read your declarations.
The claim in one sentence:
The core claim
bloomery does not prove that source data or business definitions are true. It proves that generated representations and accepted query plans preserve the semantics declared to bloomery, under a finite set of documented rules.
Shorter, and worth memorising, because every other sentence on this page is a consequence of it:
bloomery does not prove truth. It proves preservation of declared semantics.
If you tell bloomery that shipping is an order-grain measure in dollars, it will refuse
every representation and every plan that would silently make it mean something else. If
shipping is actually per-parcel, or the column holds euros, nothing here will save
you — that is a wrong declaration, and bloomery has no access to the world the
declaration is about.
Representation and answerability are different questions¶
These two look contradictory the first time you meet them together, which is why they are stated side by side rather than discovered.
Representation safety — a mart or emitted relation must not expose a measure in a shape where ordinary downstream aggregation silently changes its declared meaning. This is what the guardrails enforce, and it is a property of the artifact.
Query answerability — a request may still be answerable when bloomery can construct a plan that preserves each measure at its origin grain until the correct aggregation point. This is a property of a plan, decided per request.
The same measure can fail the first and pass the second:
An order-level shipping measure is unsafe when repeated into an order-item mart. The same
measure may be safely answered by customer country when bloomery can aggregate from Order
to Country through grain-preserving relationships.
Nothing has been relaxed between those two sentences. The mart is refused because storing
the measure at line grain makes every later SUM overstate. The query is answerable
because a plan exists that aggregates at order grain first and joins after — and note
the condition: it is answered when such a plan can be built, and
refused when one cannot, not
answered approximately in the meantime.
Without the distinction stated, the refusal reads as arbitrary — and an arbitrary-looking refusal is one somebody works around.
Unknown is not safe¶
The closed-world rule
bloomery does not use heuristic schema inference as evidence of correctness. New capabilities are added by introducing explicit proof rules for additional safe operations.
A fact bloomery was not told is not a fact it assumes benign. Nothing here reads your SQL and concludes what a column must have meant, and no capability arrives by relaxing a check — it arrives as a named rule stating what it admits and what evidence closes it, so the set of accepted requests only ever grows deliberately.
The practical consequence is that a refusal can mean "this is unsafe" or "no rule covers this yet", and both come out as a refusal rather than a guess.
The planner claim
If bloomery returns a semantic query plan, every multiplicity-changing operation in that plan is justified by declared or mechanically derived semantic facts and a documented inference rule. If bloomery cannot construct that derivation, it refuses the request.
This is stronger and more testable than "correct analytics", and it was false until recently — which is why it is worth knowing what changed. Every request the planner answers now carries a derivation. Four request shapes used to be answered without one: a metric computed from others, a semi-additive measure, a cumulative one, and two metrics restricted differently. They were answered correctly — the number was never the problem — but nothing could say why, and while that was so the claim above could not honestly be made about the whole system.
What it does not claim is that the SQL is generated from the plan. bloomery decides what to compute and states it; the query is still produced alongside. The derivation is evidence about the answer, not the machinery that produces it.
A guardrail that stayed silent, and a proof that closed¶
Both run, and they are not two spellings of one check.
A guardrail asks did anything I know how to look for go wrong?, and its green answer is the absence of a complaint. Nothing in that silence separates "this was checked and holds" from "no rule here looked at it at all", because silence carries no detail — which is why a guardrail can only ever tell you what bloomery did not find.
A proof asks which named rule admits this, resting on which facts?, and its answer is an
object: the rule that closed the obligation, the premises under it, and a provenance on
every leaf. bloomery explain prints it, so the distinction is one a reader meets rather
than one the architecture keeps to itself.
Guardrails keep their own aggregate refusal throughout — one error carrying every violation found, not the first — while consuming the same semantic facts the proofs rest on. One store of facts, two ways of reading it, so the answer and the argument for it cannot drift apart quietly.
The provenances that may never close one¶
A proof is only as strong as the weakest fact any of its leaves admitted, so whether a fact can close an obligation is decided by where the fact came from:
| Provenance | Closes an obligation |
|---|---|
declared — authored in a spec |
yes |
derived — mechanically implied by declared facts |
yes |
imported_verified — read from an external artifact under an exact documented rule |
yes |
inferred_heuristic — a guess, however good |
never |
unknown — no fact at all |
never |
The last two are carried rather than discarded, and the reason is the refusal: one that found a heuristic match can say what it found instead of only what it lacked. They may inform a diagnostic; they may not close an acceptance, so a project resting on one does not compile.
The line runs through one place — Provenance.closes in
src/bloomery/semantic/proof.py — rather than through each rule that needs it, because a
second copy of the line that drifted would not look like a safety change in review.
It is not the line the grades below draw. derived closes an obligation and grades
ASSUMED: closure is about soundness, the grade about authorship, and a project resting
entirely on derived facts is sound and entirely undeclared.
Targets are not the proof¶
The target boundary
SQLMesh, dbt, MetricFlow, Cube and the SQL dialects are compilation or execution
targets. Their ability to generate executable SQL is not itself a bloomery correctness
proof. Semantic acceptance happens before target lowering. The retrieval target has
no consumer on this side of the boundary at all — it emits a manifest no framework
reads, so nothing downstream of it accepts anything, and no runtime's acceptance of it
would be evidence either.
That SQLMesh plans your models, or that Cube loads your data model, says those tools accepted the syntax. It says nothing about grain, additivity, units or tax basis, because none of them models those. Semantic acceptance has already happened by the time an artifact is written — a project that will not compile never reaches a target at all, and a target that happily runs a wrong query would not have been given one.
What bloomery cannot establish¶
The boundary, stated plainly, because a guarantee whose edges are vague is quoted well past them:
- Correctness of source values. Whether the number in the column is the right number.
- Truth of authored business definitions. Whether
revenueshould have included refunds is a question about your business, not about your specs. - Undeclared business constraints. A rule nobody wrote down constrains nothing.
- Data-dependent cardinality, unless represented by a runtime audit. Whether a relationship is actually one-to-many in today's data is a fact about rows; quality rules measure it at run time, and the compiler cannot.
- Correctness of arbitrary custom code outside the semantic contract. A step declares its input and output types and those are enforced; what its body computes is its author's claim, not bloomery's.
- Semantic facts inferred only heuristically from SQL. Anything reverse-engineered from a query rather than declared.
This list is not the same as what bloomery does not do — that one is about execution and orchestration: it runs no SQL, schedules nothing, reads no warehouse. This one is about evidence: things bloomery runs no check for, and will not be able to, because the facts they need never arrive in a spec.
How a fact was obtained, and asking for better¶
Every fact under a proof carries where it came from, and bloomery explain prints the
grade beside it:
$ bloomery explain specs/ --metrics line_discount,shipping_count
Evidence (2 locked, 2 assumed)
ASSUMED branch:order_items
order_items is aggregated to the requested grain before the join, so it holds
one row per key
ASSUMED branch:orders
orders is aggregated to the requested grain before the join, so it holds one
row per key
LOCKED mart:order_items.line_discount
line_discount is a measure of order_items, whose grain is order_item
LOCKED mart:orders.shipping_count
shipping_count is a measure of orders, whose grain is order
Three grades, and they answer one question — did a human here write this down?
| Grade | Means |
|---|---|
LOCKED |
Somebody declared it in a spec, or it follows necessarily from something they did. |
ASSUMED |
The compiler obtained it soundly on its own — a proof rule, a propagation, an exact read of an external artifact. |
OPEN |
Closes nothing. A project resting on one does not compile at all, so you will not be handed one. |
ASSUMED is not a criticism. A derived fact is sound; the grade records where it came
from, not whether it is right. Treating it as doubt pushes you to declare things you have
not thought about, and a LOCKED fact nobody considered is worse than the default it
replaced.
Requiring the stronger kind¶
A mart can say it will not rest on anything the compiler worked out for itself:
marts:
statutory_revenue:
grain: order
base: order
measures: [net_revenue]
requires_evidence: locked # declared premises only
The default is assumed, which is what every project does today — leaving the key out is
byte-for-byte the same as writing it. There is no open: that would mean "accept
anything", which is the absence of the annotation rather than a third setting.
When a premise is weaker than the mart asked for, the refusal names the consumer, its measures, the column, and how the compiler reached it:
mart 'statutory_revenue' requires 'locked'; its measures ('net_revenue') rest on column
'customer_tier', which the compiler reached by '<basis>' rather than from anything an
author wrote (S-0070/the-grades-and-what-they-project-from). Fix: declare the relationship that carries
'customer_tier', or set 'requires_evidence: assumed' on this mart
The basis above is a placeholder rather than a name you could grep for, because every way the compiler believes a dependency from your own specs is something an author declared or follows necessarily from one — an entity's key determining its own columns is the second kind. That refusal is waiting for a weaker premise the compile path does not yet mint.
A declared key is a LOCKED premise, and the build checks it. Every proof that a
mart holds one row per key rests on the entity's key: meaning what it says — and that is
a property of the data, which no compiler can see. So the premise is not only trusted: an
entity with a declared key and no dedupe: carries a generated blocking audit,
<entity>_key_unique, that stops the run if two rows share the key. A build whose data
breaks the premise stops before a mart or a rollup reads it. See
Data quality for what the audit reads and how dedupe: removes it.
A premise from another project's artifacts does reach it. A relationship carrying
imported_from: was read out of an artifact rather than written here, so it grades
ASSUMED however ordinary its cardinality — and a strict mart whose columns are carried
through one is refused, in its own words:
relationships:
- name: item_of_order
from: order_item
to: order
via: {order_id: order_id}
cardinality: many_to_one
imported_from: metricflow:semantic_manifest.json
mart 'statutory_revenue' requires 'locked'; its measures ('net_revenue') rest on column
'order_customer_id', carried by 'item_of_order' — read out of
'metricflow:semantic_manifest.json' rather than written here (S-0075/D-1). Fix: author
the relationship in this project and drop its 'imported_from:', or set
'requires_evidence: assumed' on this mart
The fix differs from the one above, and that is why the sentence does: the relationship is declared, so "declare the relationship" would send you to write a line that already exists. What a strict mart is asking is whether somebody here wrote it.
imported_from: names the artifact and is written by an importer, not by hand. Nothing
checks that — bloomery cannot tell a hand-typed one from a generated one, and writing it
on a relationship you authored silently lowers that relationship's grade.
A fact from an upstream project grades ASSUMED here. An entity, mart or metric this
project imports was proven, or not, where it was authored — and however strong it was
there, it was not written here. So a strict mart over an imported base, a strict mart
listing an imported metric, and a strict exposure reading an imported mart or metric are each
refused, naming the alias and the upstream's fingerprint:
mart 'lines' requires 'locked'; it rests on entity 'order_item', bound from upstream
'platform' (fingerprint 3f9c…) rather than written here, and a fact that crosses a
project boundary grades 'assumed' however the upstream obtained it (S-0085/D-2). Fix:
set 'requires_evidence: assumed' on this consumer, or carry the requirement on the
upstream's mart; it cannot be authored in this project
Those are the two repairs open to you: relax the requirement here, or put
requires_evidence: locked on the upstream's mart, where the fact can be judged. An
upstream whose own strict mart would refuse a fact cannot launder it by being imported.
The consumer is often not the mart¶
A dashboard is the thing somebody signs off, and it usually reads several marts none of which knows it is feeding a statutory report. So an exposure carries the same key, and its requirement applies to everything beneath it:
exposures:
exec_dashboard:
kind: dashboard
owner: finance@example.com
depends_on:
metrics: [net_revenue]
requires_evidence: locked # of every mart below this, too
That reaches the marts named under depends_on.marts, and — this is the point — every
mart carrying a metric named under depends_on.metrics. Every such mart, not the one
the compiler happens to emit the measure on: which mart serves a query is chosen when the
query is planned, so a guarantee that held for only one of them would not hold for the
number on the dashboard.
The two refusals mirror the mart's, naming the metric that reached the mart and sending you to the exposure's own key:
exposure 'exec_dashboard' requires 'locked'; it reads mart 'statutory_revenue' (carrying
metric 'net_revenue'), whose column 'customer_tier' the compiler reached by '<basis>' rather
than from anything an author wrote (S-0070/the-grades-and-what-they-project-from). Fix: declare the relationship that
carries 'customer_tier', or set 'requires_evidence: assumed' on this exposure
exposure 'exec_dashboard' requires 'locked'; it reads mart 'statutory_revenue' (carrying
metric 'net_revenue'), whose column 'order_customer_id' comes in through 'item_of_order' —
read out of 'metricflow:semantic_manifest.json' rather than written here (S-0075/D-1). Fix:
author the relationship in this project and drop its 'imported_from:', or set
'requires_evidence: assumed' on this exposure
A mart that carries its own requires_evidence: locked is not reported twice: its own
refusal says the same thing with the same repair, so the exposure adds nothing to it.
Use it on few consumers. A finance mart feeding a statutory report is the case it exists for: someone signs that number, and "the compiler worked it out" is not an answer they can give a regulator. An exploration mart is the counter-example — it wants whatever compiles, and annotating it strictly buys a wall of declarations whose cheapest fix is deleting the requirement, which loses the guarantee everywhere at once.
Two notes worth having before you adopt it:
- This sits above the proof floor and never below it. Every fact a strict mart refuses is one that already closed its obligation — the project is sound and would compile without the annotation. You are asking a different question, not a harder version of the same one.
LOCKEDhere andLOCKEDon an RFC decision are analogous, not the same. One grades a design decision, the other grades a fact about your project.
Reading a correctness claim¶
Any sentence in this documentation asserting that bloomery is correct about something answers four questions, or it gets rewritten:
- What exact property is proven?
- From which declared facts?
- At compile time or at run time?
- What remains outside the guarantee?
Applied to the grain guardrail, for example: a measure is never embedded in a mart at a
grain coarser than its own (property), from the measure's declared grain: and the
mart's declared grain (facts), at compile time (when) — and it establishes nothing
about whether the measure's own declared grain is the truth (residue).
A claim that cannot answer all four is one nobody can check, which makes it marketing rather than documentation.