Decision Spine
Blog
Agentic Analytics25 min read

The AI-Readiness Repair Matrix

Simple questions make AI analysts look ready. Real business questions combine several primitives, and small errors compound. From 1,488 graded runs, a repair method for AI-ready data: find the primitive that failed, find where its grounding lives, and move that grounding where the agent cannot skip it.

By Dmitry Ustimov

Simple questions make AI analysts look ready. Count users. Filter one segment. Pull one metric.

Real business questions are different. They combine several primitives. Each primitive is another place where the analyst can be right or wrong.

That is why reliability does not degrade slowly. It compounds. A system that is excellent on one-primitive questions can become unreliable once the question asks for four or five things to be resolved at once.

The standard response is a shopping question: a semantic layer? A better model? After 1,488 graded runs against eight versions of one warehouse, I think readiness is more specific than that. It is a property of each question you ask, and repairing it is a routine, not a purchase:

  1. Find the primitive that failed. Decompose the question into the primitives it forces; one of them broke.
  2. Find where its grounding lives. The fact that settles that primitive is stored somewhere, or nowhere.
  3. Move it to the cheapest place the agent cannot skip.

Earlier in this series I measured how much structure buys, what the agent does when there is no answer, and how to make single claims inspectable. All three treated the question as a unit. This piece takes it apart: the routine above, with the measured evidence behind each step.

The short version#

A question is a list of primitives#

Take one ordinary business question and read it the way the agent has to.

One question, glossed

How muchmeasurebilled revenuecame fromsegmentPaid SearchgraincustomersincoverageJune?

and with no words in the question

additivity

Can annual cash be summed?

join path

Which customer relationship?

segment default

Do staff accounts count?

Some primitives are named in the question. Others are silent defaults the agent still has to resolve.

Each highlighted piece is a primitive: one irreducible choice that must be resolved before the query means anything. (The test harness uses the same word, so this piece matches the repo.) The gloss shows what makes such questions dangerous: some primitives have no words. "Billed revenue" does not announce that annual plans bill once, as a full year's cash. Nothing says whether staff accounts count. The agent resolves the silent primitives anyway, with defaults nobody chose.

To resolve a primitive correctly, the agent needs grounding for it: the piece of business knowledge that settles the primitive. Staff accounts are not customers settles the segment. Annual amounts enter monthly recurring revenue as one twelfth settles additivity. The data ends on 12 July settles coverage. A primitive with no grounding available does not go unanswered; it gets resolved by a guess that looks like a default. My first experiment measured grounding in bulk, one warehouse against another. This piece assigns it per primitive, because the agent does not need a well-grounded warehouse in general. It needs one specific fact, for one specific primitive, at the moment it writes the clause.

Two consequences carry the whole method.

Errors compound. At 90% per primitive, a five-primitive question comes out right 59% of the time (0.9⁵). A system that looks excellent on simple questions can become unreliable on real business questions without anything visibly breaking.

And a failure has an address. When the answer is wrong, some specific primitive failed. The useful question is which primitive failed and where its grounding lives.

Grounding lives in one of five places#

The homes are concrete: a column, a table's grain, a comment, a wiki page, a metric definition, a lookup table that cleans raw values, or a runtime check. Stack them the way a real warehouse does, and write the same grounding, staff accounts are not customers, at every layer.

One fact, five layers of the same stack
enforcedruntime checks (agent guardrails)not skippable
before serving: the number must be thegoverned metric result — otherwise refuseactive_customers includes user_type = 'customer'

sits between the answer and the user; there is no other route out

declareda metric definition (inside the semantic layer)skippable while optional
metric: active_customers  filter: user_type = 'customer'

a separate route: the agent can write raw SQL instead and never meet the definition

modelledthe star schemahardest to skip
dim_users.user_type  →  'staff' | 'customer'

on the route itself: query the table and the column arrives with the data; being wrong now means ignoring a value in front of it

documenteda commentskippable
comment on column users.internal:  'true for staff and test accounts'

rides beside the data: the table can be queried without reading or applying it

implicitthe source tableskippable
idemailinternal
184anna@internal-test.com1
185mark@gmail.com0

the values are visible but nothing says what they mean; the fact must be guessed

skippable = the layer is beside the agent's route · not skippable = the layer is the route

The same fact can live in raw data, comments, model structure, metrics, or runtime checks. The question is whether the agent must pass through that layer before it serves an answer.

The chip on each layer says whether the agent can ignore that fact and still answer. A comment can go unread. An optional metric can be bypassed. A runtime check can stop the answer. I will call that property skippable, and it explains much of the matrix later in the article.

The repair algorithm#

That is all the vocabulary the routine needs. Here it is on one page, built to be scanned; the three sections after it walk each step with the tools, the traps, and the measured evidence.

The repair algorithm
One routine: name the broken primitive, find the fact it needed, then move that fact somewhere the agent has to use it. Every copper name links to the section that walks that tool, under the same name.

Step one: find the primitive that failed#

When a number comes back wrong, you have three instruments, ordered by how much of the agent's work you can see.

Calculate the wrong answers#

Do not only calculate the correct answer. For the failed question, also calculate the answers you would get if one primitive were missing.

The reason is simple: a wrong number is often the right answer to a broken version of the same question. Match the agent's wrong number to that broken version, and you know which primitive to inspect first.

For example, take: How much MRR came from Paid Search customers in June? Suppose the correct answer is $12k, but the agent returns $48k.

Broken version you calculateAnswerIf the agent matches it
MRR in June, without the Paid Search filter$40kSegment is probably not the first problem.
Paid Search revenue, but annual plans counted as full cash$48kAdditivity is the failed primitive.
Paid Search revenue joined through current customer state$15kJoin path is probably not the first problem.
Paid Search rows counted instead of customers or MRR180Grain or measure is probably not the first problem.

In this example, the agent's $48k is not random. It is exactly the number you get when annual plan cash is summed as if it were monthly recurring revenue. That tells you where to look next: the additivity grounding. Does the model expose a customer-month snapshot? Is the MRR metric enforced? Or is the rule only written in prose where the agent can skip it?

This is triage, not final proof. It narrows the repair from "the answer is wrong" to "inspect this primitive first".

Confirm it in the SQL trace#

The wrong-answer test gives you a suspect. The SQL trace confirms whether that suspect actually failed.

Continue the example above. The agent returned $48k, and $48k is what you get when annual cash is summed directly. Open the SQL trace and ask one question: did the query use the monthly MRR snapshot or governed MRR metric, or did it sum raw billing cash?

If the trace says sum(r.amount) from fct_billing, additivity failed in the aggregate expression and the fact table. If the trace uses fct_customer_month_snapshot and mrr_amount, additivity is probably not the first failure; go back to segment, join path, or coverage.

Use the same check for every primitive:

SQL trace checklist
suspected primitive familyopen the trace and askfailure looks like
SegmentIs the `WHERE` filter the right population?
where raw_channel = 'Paid Search'
GrainIs it counting the right entity?
select count(*) as customers
Join pathAre the `JOIN`s the declared relationship?
join dim_customer c
  on u.current_customer_id = c.customer_id
Measure / aggregationIs the aggregate the governed measure?
select count(*) as active_customers
from events
AdditivityIs it using the right snapshot or roll-up?
select sum(r.amount) as mrr
from fct_billing r
Answerability gateDid the trace check period and scope?
where month = '2026-08'
-- no coverage check
Deliberately wrong SQL examples. The highlighted text is the first place to inspect when that primitive is the suspect.

The result is not just "the SQL is wrong". It is more precise: now you know whether additivity actually failed, and where it failed.

The peel: remove one primitive and ask again#

If you only have the chat answer, simplify the question and ask again. Remove the segment. Remove the join. Remove the date window. When the answer starts working, the primitive you just removed is where to inspect.

In the same example, peel the question into a shorter version each time:

Peel the question

1

original

How much additivityMRR came from segmentPaid Search graincustomers in coverageJune?

$48kincorrect

Incorrect.

2

− segment

How much additivityMRR came from graincustomers in coverageJune?

$160kincorrect

Still incorrect. Segment is probably not the first failure.

3

− segment− additivity

How much measurebilled revenue came from graincustomers in coverageJune?

$160kcorrect

Correct. The primitive just removed is the suspect: additivity.

Illustrative answers. Remove one primitive at a time; when the answer starts working, inspect the primitive you just removed.

The peel does not prove the repair. It gives you a fast next move when all you have is the chat window.

This is the manual version of what the evidence graph automates: every claim is tied back to the query result that supports it.

Step two: find where the grounding lives#

Once you know which primitive failed, the usual advice turns vague: improve the model, add more context. Three tools make it mechanical.

Name the grounding fact#

Before you can repair the failure, name the fact the agent needed but did not use. Keep it concrete.

  • Staff accounts are not customers.
  • ppc, paid-search, and Paid Search are the same channel.
  • Annual amounts enter monthly recurring revenue as one twelfth.
  • The data ends on 12 July.

Find where it lives#

Now look for that fact in the system. Is it in the raw values? In a comment or wiki? In the data model? In a metric definition? In a runtime check?

Sometimes it is not in the system at all. It only lives in someone's head. Then the agent cannot use it reliably.

The common failure is not missing knowledge. It is knowledge in the wrong place. In my test, the additivity rule was written above the revenue column, and the agent still summed annual cash as if it were monthly revenue. The fact existed, but it lived somewhere the agent could skip.

The skip test: can it answer without using the fact?#

By skip, I mean this: the agent can produce and serve an answer without using the grounding fact you found. It may not read the comment. It may choose raw SQL instead of the metric. It may join through another path. The fact exists, but it is not on the route the answer must travel.

Ask one practical question: can the agent still answer if this fact is ignored?

Where you found the factCan the agent skip it?Why
Comment or wikiYesIt only helps if the agent reads it and applies it correctly.
Optional metric definitionYesThe agent can still write its own SQL unless the metric route is required.
Data model structureHarderIf the agent queries the modelled table, the cleaned value, grain, or join path is already there.
Runtime checkNoThis is an enforced guardrail inside the agent path: the answer is blocked unless the check passes.

Step two ends with a verdict, and there are only three:

what you foundthe grounding isstep three's verb
nobody can write the fact downabsent: the business has not defined itauthor it, or make sure the agent refuses questions that depend on it
the fact exists, in a place the agent can skipmis-housed: the usual case in this studymove it
the fact is in the right place, and the answer is still wrongincomplete: the artifact is missing a pieceaugment it, like the label beside the key

Step three implements the verbs.

Step three: move the fact where it cannot be skipped#

Do not fix this with another prompt note. Use the result of step two:

  • If the fact is missing, define it.
  • If the fact is in a skippable place, move it.
  • If the fact is incomplete, add the missing piece.

The type of fact usually tells you where it should go. These are the repair patterns this study measured.

the fact sounds likewhich isthe recipewhere the grounding moves
ppc and Paid Search mean the same channela vocabularythe decodeData model: one cleaned value
this table is one row per subscription terma grainthe declared grainData model: named grain
platform is a user attributean attribute's homethe label on the dimensionData model: dimension the query already uses
include all accounts unless asked otherwisea defaultthe stated defaultDocs: clear default
annual cash must become monthly revenuea stock or unitthe snapshot and the ruleSemantic layer: snapshot plus enforced metric
the data ends on 12 Julya boundarythe published windowGuardrail: coverage window plus refusal

The six recipes below are reference material, not stages: read the one your fact matched. Each follows the same pattern: the situation, the move, and what it measured on the bench.

The decode: put the vocabulary in the data#

The situation: the same business value appears in several forms. ppc, paid-search, and Paid Search all mean the same channel. FR means France.

The repair is a small data modelling exercise with exactly two moves:

  1. Normalise the code. Store one machine value: paid_search.
  2. Store the name beside it. Store one human label: "Paid Search". This is Kimball's thirty-year-old rule: keep the decode beside the code.

Both columns live on the dimension the agent already queries. Nothing is renamed, nothing breaks; the repair is pure addition.

The decode, as a modelling exercise

spend.chanraw

  • ppc
  • paid-search
  • Paid Search
  • paid_search
  • organic
  • Organic
  • referral
  • Referral

the tinted values are one channel, four spellings

dim_channelmodelled

channelchannel_name
paid_searchPaid Search
organicOrganic
referralReferral

one row per channel: the key for machines, the name for people

Illustrative values. The raw data has many spellings for the same channel. The dimension table stores one stable key and one readable name, so the agent has a clean value to filter.

Why two columns, and not just clean values? Because I built the one-column version, and it is a trap.

What it measured. A vocabulary written only in a comment did not work. A vocabulary built into the data model did. Synonym questions went from 3% with prose to 75–100% with modelled values. Code questions, such as FR meaning France, showed the same pattern.

Four kinds of filter value, measured separately“Filter to the right people” is four skills, not one — and prose handles exactly one of them. Read it →

The segment questions split by how much repair the filter's value needs before it can be matched against the data.

This is one slice of the bench described later; the questions and runs are in the harness repository.

Four repair jobs, 36 runs each
repair jobrawraw + docsstarstar + docsverdict
stated“excluding staff accounts” — the filter is in the question53%89%92%97%prose is enough
caseANDROID / Android / android — nine spellings81%39%58%86%prose hurts; structure must be findable
codeFR must become “France”56%58%92%94%structure required
synonymppc = paid-search = “Paid Search”11%3%75%100%structure required; prose useless
One slice of the bench: segment questions grouped by matching difficulty. Each row is 36 runs. Documentation helps when the filter is stated, but fails on synonyms: 3% with prose, 75–100% when the mapping is in the data model.

When the question states the filter in words ("excluding staff accounts"), a comment is enough: 53% jumps to 89%. That is the case people imagine when they say documentation will fix their warehouse for AI, and there they are right. Every other row needs the value repaired first, and prose contributes almost nothing. A vocabulary has to be in the data model.

The declared grain: people are not rows#

The situation: the fact says what one row of a table means. "One row is a subscription term, not a customer".

The repair is a modelling exercise with two moves:

  1. Name the grain in the table itself. A table where one row is one subscription term is called subscription_terms, not subscriptions. The name is grounding the agent cannot avoid reading; declaring the grain is a core Kimball dimensional modelling rule.
  2. Expose both counts as separate metrics. Where "how many" has two honest readings, define both: one metric counts rows, one counts people.
The declared grain, as a modelling exercise

subscriptionsgrain unstated

user_idstartedamount
1842026-03-0296
1842026-06-1412
1852026-05-0112

user 184 appears twice: rows are terms, not people

“how many customers?” → count(*) = 3 ✗

subscription_termsgrain named

metric: subscription_terms
count(*) = 3

metric: subscribers
count(distinct user_id) = 2

two questions, two metrics: the choice is visible

Illustrative rows. The same table can answer two questions: three subscription terms or two people. Name the grain and expose both counts, so the agent has to choose the right one.

What it measured. Grain failures look like this: you ask for people, and the agent counts rows. Naming the table grain and exposing both counts moved grain accuracy from 56% on raw tables to 86% on the star and 94% with star + docs. This is still basic modelling: no new rule, no semantic layer, just a clear grain and the right metrics.

The label on the dimension: keep the attribute on the route#

The situation: the question filters by an attribute, but the readable label lives one join away. For example, users have a platform key, and the platform name sits in a lookup table.

The repair is a modelling exercise with two moves:

  1. Put common labels on the dimension the query already reads. If most questions start from users, platform_name belongs on dim_users.
  2. Keep lookup tables for real vocabularies. A channel can have its own dim_channel. A small platform lookup can still exist, but the agent should not need an extra join just to read the label.

This is the Kimball avoid-snowflaking guidance applied to an AI analyst: if it describes the user, put it on dim_users. If it is its own business thing, give it its own dimension.

The label on the dimension, as a modelling exercise

dim_users · dim_platformsnowflaked

dim_users(user_id, plan, platform_key)dim_platform(platform_key, platform_name)

the name is one join away, and joins are where these arms failed

dim_userslabel on the dimension

dim_users(user_id, plan,  platform, platform_name)

the name arrives with every query of the dimension

The same attribute in two homes. The probes measured the trade both ways, and the gaps sit near the noise floor; the durable observation is in the note on the left: a lookup can still work as machine-readable metadata, when the schema names the key.

What it measured. I tested two versions of the same star: one with the platform label on dim_users, and one with the label one join away. The label-on-dimension version did better (89% vs 75%). The gap is close to the noise floor, so I would not over-read the exact numbers. The lesson is simpler: put common labels where the agent already looks.

The stated default: document what to do, not just what it means#

The situation: the question leaves something unsaid. Should staff accounts count? Should test accounts count? The agent will still pick a default.

The repair is simple:

  1. Describe the column. Keep the normal definition.
  2. State the default. Say what to do when the question is silent.
The stated default, as one added sentence

comment on users.internaldescribes the column

“true for staff and test accounts”

read as an instruction: staff excluded from questions that never asked

comment on users.internalstates the default

“true for staff and test accounts Every account counts by default; do not exclude staff unless the question asks”.

the failing questions flipped; removing the sentence brought them back

The same comment, before and after. The first version describes what the column means, and a description gets read as an instruction. The added sentence states the default the agent has to resolve when the question is silent.

What it measured. A comment that only describes the column was not enough. The missing piece was the default: what to do when the question says nothing. Adding that sentence fixed the failing questions; removing it brought the failures back. Document the action, not only the meaning.

Documentation amplifies whatever base it lands onThe same comments, written onto two different warehouses, with opposite signs. Read it →
The same comments, two different bases

raw → raw + docs

49%47%-2pp

silent error 47% → 49%

star → star + docs

74%85%+11pp

silent error 18% → 9%

Accuracy across all 186 runs per version. Documentation hurt the raw extract and helped the clean star. It amplifies the base it lands on.

Practitioner conclusion 1: do not use documentation to compensate for raw structure. On raw tables, comments can give the agent more ways to be wrong.

Practitioner conclusion 2: documentation works after the model is clean. Use it for defaults, row meaning, and coverage; do not ask it to repair grain, joins, or messy values.

The snapshot and the rule: additivity cannot live in prose#

The situation: the sentence says an amount must be transformed before it may be summed. A stock, a unit, an annual amount.

This repair has three parts:

  1. Build the snapshot. Put MRR on a periodic snapshot fact: one row per customer per month. The annual amount is already converted into a monthly value.
  2. Define the metric there. Put mrr on that snapshot in the semantic layer. MetricFlow's non_additive_dimension is the relevant dbt field: MRR can be summed across customers, but not blindly across time.
  3. Enforce the metric route. If the agent can ignore the metric, it will. The guardrail makes the policy explicit: no governed metric result, no answer.
The snapshot and the rule, as a modelling exercise

billinga flow: one row per charge

user_idplanamount
184annual144
185monthly12

“June MRR?” → 144 + 12 = 156 ✗

one row holds a year, the other holds a month; a plain sum adds them anyway

fct_customer_month_snapshota stock: customer × month

user_idmonthmrr
1842026-0612= 144 ÷ 12
1852026-0612billed monthly

metric mrr → 12 + 12 = 24 ✓

the metric reads the snapshot, and the rule makes it the only route to a served number

Illustrative rows. The plan column is the grounding: an annual plan bills a year of cash in one row, so summing amounts mixes twelve months with one. The snapshot spreads the lump into monthly rows, the metric is defined on the snapshot, and the rule makes the metric the only way a number reaches the answer.

What it measured. Additivity was the cleanest failure in the study. Raw tables and raw + docs got every MRR question wrong: 0 of 12 in both cases. The comment explained the rule, but the agent still summed annual amounts as if they were monthly revenue.

The fix was structural:

MRR additivity repair
versionMRR questionswhat changed
raw0 / 12annual amounts summed as stored
raw + docs0 / 12the rule was written down, but skippable
star10 / 12monthly snapshot added
star + docs12 / 12snapshot plus explanation
+ semantic11 / 12metric defined, but still optional
+ guardrails12 / 12governed metric route enforced
Four MRR questions, three runs each. The score cell is coloured by correct runs out of 12.

Practitioner conclusion: prose does not fix additivity. Put the monthly value in the model, define the metric on that model, and require the agent to use it. A metric the agent can ignore is still advice; an enforced metric is policy.

The published window: answerability needs fact and restraint#

The situation: some questions are outside the data. The period may not exist yet. The term may not be defined. The agent needs to know the boundary, and it needs a rule for what to do when the question crosses it.

There are two repairs:

  1. Publish the boundary as data. Compute the min and max dates for each dataset and expose them as a lookup the agent can check. Recompute it with each load. This is metadata, not a wiki sentence someone maintains by hand.
  2. Enforce refusal. If the requested period, term, metric, or path is outside the governed contract, the agent should refuse. This is a runtime check inside the agent: no governed path, no answer.
The published window, as computed metadata

the agent's viewno boundary stated

“How many habits were completed in August?”
answer: 0 completions in August ✗

the data ends 12 July; a zero is a confident answer to a question the warehouse cannot address

coveragecomputed from the data

habits: 2026-01-03 → 2026-07-12
answer: “August is outside the data I hold” ✓

those questions went from 0 of 6 to 6 of 6

The coverage window is computed metadata, not hand-written documentation. Once exposed as a lookup, the agent could see the data boundary and refuse out-of-range questions itself; the backup guardrail never fired.

What it measured. In the mini matrix, data modelling made answerable questions much stronger. It did not make the agent good at saying no. On the no-answer questions, most warehouse versions stayed weak: 36–47% correct. The provenance guardrail reached 78%.

The lesson is simple: accuracy and honesty are different controls. A better data model helps the agent answer. A runtime check defines when it must stop.

The Repair Matrix#

The matrix is built from four repair areas: data modelling, documentation, semantic layer design, and agent guardrails. Each area has a different owner and a different reference point.

Four areas, one per layer of the stack

data modelling

modelled

repairs it ownsthe decode · the declared grain · the label on the dimension · the snapshot

the canonKimball essentials · snowflaked dimensions · fact tables

documentation

documented

repairs it ownsdefaults · row meaning · edge cases, on top of a clean star

the canonDatabricks grounding hierarchy

semantic layer

declared

repairs it ownsgoverned metrics · member mappings · non-additive rules

the canonMetricFlow · dbt measures · Cube data model

guardrails & agent architecture

enforced

repairs it ownsthe provenance rule · enforced refusal · the coverage window as a tool

the canonrefusal experiment · OpenAI guardrails · Anthropic agent patterns

Some repairs stay in one area. Decode values, name the grain, or state the default. Those are cheap. Other repairs cross areas. Measure/aggregation needs a clear fact table and an enforced metric; additivity also needs a snapshot. Answerability needs computed coverage and refusal. This is the repair matrix: for each measured primitive family or subcase, find the cheapest location that worked.

Repair Matrix
rawraw+docsstar+semantic+guardrails
primitive familyimplicitdocumentedmodelleddeclaredenforcedfix
segment: filterDocument the filter meaning and default
segment: memberData modelling: normalise raw values into a clean dimension
grainData modelling: clear fact table grain and metrics
join pathData modelling: clear fact/dim tables and join keys
measure / aggregationData model + Semantic + Guardrail: clear fact table, enforced metric definition and aggregation
additivityData model + Semantic + Guardrail: clear snapshot table and enforced metric definition
answerability gateGuardrail: check coverage, definitions, and scope before answering

✓ works · [✓] reliable fix

One row per measured primitive family or repair subcase. The bold check is the cheapest location that worked. Plain checks also worked, but cost more. Measure/aggregation, additivity, and answerability need enforcement.

Read it as a budget. Most rows are fixed by ordinary data modelling. The right edge is needed for three rows: measure/aggregation, additivity, and answerability. The left side, where many facts live today, is usually too easy for the agent to skip.

The same four areas define ownership. A failed primitive should land in the backlog of the layer that can actually fix it.

Where each location lives on the big platformsThe recipes are written in dbt and Kimball vocabulary; the locations translate to every major stack. Read it →
locationDatabricksSnowflakePower BI / FabricLooker
documentedtable and column comments; Genie instructionscomments; Cortex Analyst descriptionsdescriptions in the semantic modelLookML descriptions
modelledUnity Catalog tablesdynamic tables and viewstables in the semantic modelviews and PDTs
declaredUnity Catalog metric viewsCortex Analyst semantic viewssemantic model measuresLookML measures and Explores
enforcedtrusted assets: an approved query or Unity Catalog function runs, and the reply is labelled TrustedAI_VERIFIED_QUERIES on the semantic view; the API reports whether a verified query was usedverified answers, matched to a saved visual by trigger phraseConversational Analytics compiles from LookML, so there is no raw SQL path

These controls are not equivalent. Some remove the raw-SQL bypass. Others still generate freely when no saved answer matches. The guardrail in this study checked the agent trace before serving: no governed result, no number.

One thing this bench does not test is permissions. Segment logic and row-level security can both affect the WHERE clause, but they answer different questions: who counts in the metric, and who is allowed to see the rows. A production system has to apply permissions first.

Run it as an AI-readiness audit#

The same anatomy that repairs a broken answer tells you, in advance, which answers will break. That audit is what I would ship.

Start from the questions, not the schema. Collect what people really ask: board, growth, finance, support questions. Decompose each one the way the gloss at the top does, and ask which primitive would fail first on your stack.

Build ladders, not a question list. Add one primitive per rung, the way the SQL figure climbs. The first rung should be almost boring; the fourth should read like a line from a board memo. The rung where answers start failing names the primitive, and the wrong answers confirm it.

Put in questions that have no answer. A period you do not hold, a term nobody defined, a false premise. Without them you can only measure whether the analyst is right, never whether it answers when it should not, and that second failure is the one that reaches a board deck.

Ask everything three times, and score by primitive, not by average. Errors compound as primitives stack. Here is why the ladder matters more than the question count:

Accuracy by primitives in the question
arm1 primitive2 primitives3 primitives4 primitives
raw100%67%40%53%
raw + docs100%100%87%80%
star100%100%100%80%
+ semantic100%100%93%93%
+ guardrails100%100%73%100%
spread, best to worst0pp33pp60pp47pp

a spread of 0pp means the test cannot tell the best warehouse from the worst

Questions with one primitive look solved in every arm. The gaps appear when a question combines two, three, or four primitives. That is where many demos and benchmarks stop too early.

On one-primitive questions, every warehouse looked perfect. The failures appeared only when questions combined multiple primitives. That is why a demo suite built from simple questions can look safe while real business questions still fail.

The audit should return a repair backlog, not just a score. Each failed primitive should point to one kind of work:

  • Data cleaning: raw values need normalising.
  • Data modelling: grain, joins, or snapshots need to move into the model.
  • Documentation: defaults and edge cases need to be stated clearly.
  • Semantic layer: governed metrics and aggregations need to be declared.
  • Agent guardrails: the agent needs a runtime rule for refusal or provenance.

The bench behind the numbers#

The numbers come from one test warehouse, rebuilt eight ways. The data stayed the same. Only what the agent could see changed. Six versions carry the argument and appear in every figure below. The other two are structural probes of the star that move one label between a dimension and a lookup table; they are in the totals and in no chart.

armwhat it is
rawApplication-style tables: coded columns, abbreviations, and multiple spellings for the same value.
raw + docsThe same raw tables, with table and column comments added.
starA cleaned dimensional model: clearer names, resolved codes, and one grain per table.
star + docsThe cleaned star model, with documentation added.
+ semanticGoverned metric definitions in MetricFlow. The agent could still choose raw SQL instead.
+ guardrailsA runtime rule inside the agent: served numbers had to trace to governed metric results.
+ LLM judgeA verifier inside the agent flow checked the guarded answer before serving. This is a probe, not a ladder step.

Main run: 62 questions × 3 repetitions × 8 warehouse versions, using gpt-5-mini: 1,488 graded runs. Most questions combine one to four primitives. Twelve have no honest answer. The LLM-judge arm is a separate probe.

I also reran the headline scorecard once on gpt-5.6-terra. Read it for direction, not exact cell values.

Two caveats: the warehouse is synthetic, and repeated gpt-5-mini runs disagree in about 25% of question-version cells. The method and data are on GitHub.

How runs were graded, and what the rule enforcesDeterministic grading, the definition of a silent error, and the operational meaning of “bypass”. Read it →
  • Grading was deterministic. The expected answer was computed before the run. A numeric answer counted as correct if it was within 2% of that value.
  • Silent error means confidently wrong. The agent served a wrong number, and nothing in the answer warned the user.
  • No-answer questions had their own grading. A refusal, a clarification request, or a correction of a false premise counted as handled.
  • Each question appears in one matrix row. The row is the main primitive that question was built to test.
  • The provenance rule was enforced in code. A served number had to match a governed metric result in the trace. Otherwise the agent refused or retried through the governed route.
  • Bypass means raw SQL only. In the + semantic arm, the agent had both options: governed metrics and raw SQL. A bypass run used only raw SQL. In the + guardrails arm, the agent could still inspect data with raw SQL. But before showing a number to the user, it had to reproduce that number through a governed metric. If it could not, it refused.

The scoreboard#

Every arm, on the three metrics this series always reports.

The scoreboard: three metrics, every arm
armcoveragesilent errorbalanced accuracy
the modelling axis
raw97%47%45%
raw + docs99%49%43%
star95%18%64%
star + docs99%9%71%
+ semantic100%17%63%
enforcement
+ guardrails99%5%86%
+ LLM judgeprobe71%2%77%
Coverage is attempted answerable work. Silent error is confidently wrong output. Balanced accuracy averages answerable accuracy and no-answer accuracy. Metric definitions. The judge row is a probe; its verifier did not see the layer's notes.

Two things to notice in the mini run. First, coverage stays high until the judge, where refusals increase. Second, silent error changes much more than balanced accuracy. Two arms can look close on accuracy while one serves many more confident wrong answers.

What each improvement bought#

The same numbers, read as investments. Each row is one step up the ladder, and the chips are what that step changed.

What each improvement bought
the investmentΔ balanced accuracyΔ silent errorverdict
raw → raw + docs-2pp+2ppmade it worse
raw → star+19pp-29ppthe big accuracy buy
star → star + docs+7pp-9ppamplifies a clean base
star + docs → + semantic-8pp+8ppoptional route was not enough
+ semantic → + guardrails+23pp-12ppenforcement improved both dials
+ guardrails → + LLM judge-9pp-3ppprobe: coverage fell by 28pp

green = the dial moved the healthy way · balanced accuracy up, silent error down

Each row shows the change from one arm to the next. Green means the metric moved in the right direction. The largest gain came when the governed route was enforced.

On the mini run, the largest structural gain came from raw tables to a star model. Documentation helped on the star and hurt on raw tables. The optional semantic route did not improve this run. The largest overall gain came when the governed route was enforced.

Per primitive family: the matrix itself#

The third perspective explains everything the scoreboard averages: every measured primitive family against every version of the warehouse.

The map — accuracy per primitive family, per warehouse
primitive family (n)rawraw + docsstarstar + docs+ semantic+ guardrails+ LLM judge
segment4848%58%85%94%90%98%56%
grain3656%44%86%94%92%83%75%
join path3656%44%67%92%86%97%75%
measure / aggregation1275%67%67%100%58%100%75%
additivity120%0%83%100%92%100%75%
answerability gate3639%36%47%47%39%78%83%
Each cell is accuracy for that measured primitive family and arm. Read additivity first: prose scores zero. Then read answerability: it improves only when refusal is handled explicitly. Repeated runs moved often, so only large gaps matter.

And the same grid on the other dial, how often each cell misled instead of answering:

The map — silent error per primitive family, per arm
primitive family (n)rawraw + docsstarstar + docs+ semantic+ guardrails+ LLM judge
segment4850%40%13%6%10%0%2%
grain3642%56%6%3%8%17%3%
join path3642%56%33%8%14%3%0%
measure / aggregation1225%33%8%0%42%0%0%
additivity1292%100%8%0%8%0%0%
answerability gate3656%44%33%25%31%11%6%
Each cell is the silent error rate: wrong answers served with confidence. Green means 5% or below. Additivity is the row to watch: prose is wrong without warning. Only large gaps matter.

Read the matrix by row. The segment rows, grain and join path mostly need modelling. Additivity needs a snapshot. Answerability needs refusal handling. A single score hides this; the matrix shows which repair to make.

What a stronger model changes#

After the main run, I reran the headline scorecard on a stronger model: gpt-5.6-terra. I would not put the full primitive-by-primitive Terra table in the article. It is useful audit detail, but it asks the reader to compare too many cells. The top-line comparison is enough.

Model sensitivity check: mini → terra
armcoveragesilent errorheadline scoregoverned use
raw97% → 100%47% → 19%48% → 78%
raw + docs99% → 100%49% → 35%52% → 72%
star95% → 100%18% → 26%73% → 74%
star + docs99% → 100%9% → 3%85% → 92%
+ semantic100% → 100%17% → 2%78% → 96%74% → 48%
+ guardrails99% → 84%5% → 5%95% → 89%100% → 100%
+ LLM judge71% → 66%2% → 2%82% → 82%100% → 100%
Same questions and same warehouse arms, rerun on gpt-5.6-terra. Score is the headline accuracy figure from that comparison run. Governed use is the share of numeric answers that used the governed metric route; dash means that route did not exist in that arm.

Three things changed.

Terra raises the floor. The raw warehouse got much better, and silent error fell sharply. A stronger model can extract more from weak structure.

Clean structure still matters. The best unguarded result is not raw plus a stronger model. It is the clean star, documented, with governed metric definitions available.

Governance is a product choice, not just an accuracy trick. With the semantic layer optional, Terra scored well, but used the governed route less often. When the route was enforced, governed use stayed at 100%, but coverage fell. So the choice is explicit: allow more answers, or require every served number to have a governed trace.

Limits of this studyWhat I would not generalise from this bench. Read it →
  • This is not a model leaderboard. The repair matrix is the gpt-5-mini run; Terra is a sensitivity check.
  • The warehouse is small and synthetic. It does not include production scale, permissions, messy history, or slowly changing dimensions.
  • Repeated runs moved in 25% of question-arm cells. Treat small gaps as noise.
  • The findings I would defend are the wide ones: one-primitive questions looked solved, additivity hit zero in prose arms, synonyms needed data modelling, coverage needed a published window, optional metrics were bypassed, and enforcement changed the tradeoff.
  • Documenting what to do when the question is silent worked in one focused test. I would test it again before treating it as a general rule.
  • The recipes were developed against the same 62 questions that score them.
  • A question is scored under one primitive, but a different primitive can still be the real reason it failed.
  • I did not measure token cost.
  • These figures are not comparable with earlier articles in the series: different questions, different denominators.

There is no AI-ready warehouse#

There is no single switch that makes a warehouse AI-ready. A question can be ready for one primitive and fragile for another. Segment can be fine while additivity is broken. A metric can be defined while the agent is still allowed to ignore it.

The practical work is concrete: clean the values, name the grain, state the defaults, publish the coverage window, model stocks on snapshots, and enforce governed metric routes where silent errors are costly.

The cleanest definition I can now give: AI-ready data means every question primitive people rely on has a governed path, and the agent refuses when that path does not exist. The repair matrix is the checklist for finding which path is missing.

Sources & further reading

Read next

Want to build a clearer decision system?

Tell us where the numbers feel murky and we'll show you what a trustworthy decision system looks like for your team.