📇  Infrastructure layer · backfill

how-to-data-dictionary

The only document in a data platform that everybody trusts and nobody tests. Everrest has one: 36 rows, a description on every single one, last touched eight months ago. This page audits it against the warehouse it claims to describe, then rebuilds it so it cannot rot. Seed 42, learn-python env, every number below measured against 131,832 real rows.

raw dump→lake→warehouse→the doc that says what it means→marts, scorecard, agent
Case: Everrest · B2B2C retail platform seed = 42 DuckDB · pyyaml · matplotlib 4 detectors · 67 findings · 88 claims tested gate: FAIL → PASS → FAIL → PASS mutation suite 12/12
01
Step 1

Input - a warehouse, and three documents that describe it

The star schema from the Infrastructure flagship is already live and already described twice: a data contract that the pipeline enforces, and a metric registry the agent is grounded on. The dictionary is the third document about the same 39 columns, which is exactly how a platform ends up with four versions of the truth. So the job is not only to fix the dictionary. It is to make it point at what the other two already own.

the warehouse 39 columns
  • fact_orders · 107,841 rows, 16 cols
  • dim_merchant · 400 rows
  • dim_customer · 18,226 rows
  • dim_product · 5,000 rows
  • dim_date · 365 rows
  • 131,832 rows in total
legacy_data_dictionary.csv 36 rows
  • description · filled on 36 of 36
  • owner · filled on 7
  • unit · filled on 6
  • grain · one sentence, on the fact
  • last_reviewed · 2025-11-14
  • never checked against the data
data_contract.yaml enforced
  • owned by how-to-schema-and-warehouse
  • dtype, nullable, allowed values
  • referential integrity rules
  • what the pipeline guarantees
metric_registry.yaml signed off
  • owned by how-to-agentic-on-your-warehouse
  • 5 metrics, each with a control SQL
  • grain, unit, status policy, take rate
  • what a number is allowed to mean
Three documents, one rule

The contract owns guarantees. The registry owns metric formulas. The dictionary owns column meaning, and points at the other two instead of restating them. Every check on this page reads the contract and the registry as inputs, so a disagreement between documents is itself a finding.

02
Step 2

Sample data - a legacy document with 19 planted defects

There is no honest way to generate a human's prose from a seed, so the legacy dictionary is synthesized rather than sampled - and every single departure from the live warehouse is deliberate and listed in the generator docstring. That turns Step 5 into a recall test: which of the 19 does the audit find, and which does it miss? The answer at the bottom of Step 5 includes one it misses.

make_legacy_dictionary.py ↗ · excerpt from the planted-defect list
  D8  wrong uniqueness    order_id documented as the primary key; 2.16 rows each
  D9  stale value set     status listed as Delivered|Shipped|Cancelled - wrong
                          case, and 'paid' is missing entirely
  D15 wrong label         net_amount described as revenue. The metric registry
                          defines platform revenue as net_amount x take rate
  D17 wrong grain         fact_orders documented as one row per order; it is one
                          row per order LINE
  D18 wrong timezone      order_ts documented as Asia/Singapore local; the
                          contract says UTC. Prose against prose - EXPECTED MISS
👻 Ghost columns2 columns the doc still describes were removed from the warehouse
🆕 Undocumented columns5 arrived after the doc was written and nobody added them
🧱 Wrong grain"one row per order" on a table that is one row per order line
🏷 Wrong labelnet_amount called revenue, when revenue is that column x 0.11
📋 Stale value sets4 columns list values the warehouse stopped using, or never had
📏 Wrong unitdiscount documented as a percentage; the column is a 0 to 1 fraction
⭕ Wrong nullability2 columns documented "always populated" that are nullable by design
🕗 Wrong timezoneprose against prose - planted knowing no detector here can reach it
03
Step 3

Objective - the question an analyst actually arrives with

Nobody opens a data dictionary to admire it. They open it at 4pm with one question, take the first answer they find, and put it in a deck.

I need last month's revenue. Which column do I use, and can I trust what the dictionary says about it?
  • How much of the warehouse is documented, and does that number mean anything?
  • Where does the document contradict the warehouse itself?
  • Where does it contradict the two documents we already have?
  • What breaks when the schema moves and nobody edits the doc?
  • What can be generated, and what genuinely needs a human afternoon?
04
Step 4

Find-skills - the toolbox for a document you can test

Everything here exists to move facts out of a human's hands and into a query, and to make the result fail a build rather than sit in a wiki.

OSS engine

DuckDB

Profiles the whole warehouse in-process. Type, nullability, cardinality, uniqueness, the real value set - measured, never typed.

Standard

information_schema

The introspection surface every SQL engine exposes, so the profiler is not tied to one warehouse.

OSS tool

DataHub / OpenMetadata

Where the generated dictionary lands in a real company. This skill produces the payload; the catalog serves it.

OSS tool

pandera

Already enforcing the contract upstream. Read as an input here so the doc and the guarantee cannot drift apart.

Pattern

Docs as code

The semantic layer is a reviewed YAML file in the repo, and the gate runs on every pull request. A wiki page has no diff.

Skill

how-to-schema-and-warehouse

Built the star schema and the contract this page reads. The dictionary is the layer directly on top of it.

05
Step 5

Build - four detectors, then a document that regenerates itself

One script runs the whole thing against the live warehouse: profile it, audit the legacy document, fail the gate, generate the replacement, pass the gate, then move the schema for real on a copy and watch the gate fail again. Every number on this page comes out of that run.

The finding that decides everything else

Coverage is the number every catalog reports and the only one a document can reach 100 percent on while being wrong about a third of its rows.

Bar chart: 36 of 36 rows have a description, 13 of 36 rows state something the warehouse contradicts

Four detectors, because a document fails in four unrelated ways

Detector 1 · measured
existence

Set difference between the columns the document names and the columns that exist. Cheap, and it is the one nobody runs.

7 findings: 5 columns arrived, 2 were removed
Detector 2 · measured
structure

Type, nullability, uniqueness, value set, range, unit scale, grain and the foreign keys the contract declares. Each one is a query.

16 findings, 9 of them blocking
Detector 3 · text screen
agreement

The doc against the contract and the metric registry. Reads prose, so it can miss and it can flag something a human dismisses.

8 findings, including the expensive one
Detector 4 · text screen
policy

Rows with a description and nothing else, and rows nobody has reviewed inside a year. Never blocking - an incomplete row states nothing false.

36 findings, none blocking
Stacked bar chart of 59 findings by detector and severity

Every finding carries a proof field with three values. measured means a query settled it (27 of 67). screen means text or a distribution was read and a human must confirm (40 of 67). undecided means the check could not run at all - and that third value only exists because the review panel found that its absence was the whole design's blind spot. See Step 6.

Answering the question the analyst actually asked

Three bars: all-status net_amount 6.78M, delivered 5.25M, platform revenue 578k
107,841 rows in fact_orders versus 50,000 distinct order ids

Left: one column, two live definitions plus one wrong one. Delivered GMV and platform revenue are 9.1x apart - that is the take rate - and counting non-delivered lines adds a further 29%. The full traded window is shown, so these are not the registry's signed-off figures: it binds platform revenue to November 2025 only. The formula is the registry's, the window is this chart's. Right: the grain sentence at the top of the table, which multiplies every order count by 2.16 and leaves nothing in the data looking wrong.

the two findings above, exactly as the gate printed them
  [registry_label_conflict] fact_orders.net_amount
      claimed:  described as "Net revenue recognised on the order, in USD."
      measured: metric registry defines platform_net_revenue as a SCALED
                formula over net_amount, not the column itself

  [grain_mismatch] fact_orders
      claimed:  one row per order
      measured: 107,841 rows over 50,000 distinct order_id = 2.16 rows per key

What each wrong claim actually costs

status value counts, with paid flagged as absent from the documentation
An empty result, not an error. The doc lists three statuses in title case. The column is lower case and holds a fourth, so status = 'Delivered' returns 0 rows and looks like a quiet month.
Rows an inner join removes for product_id and customer_id
Two columns are nullable by design. The doc says "always populated", so an inner join quietly removes 2,880 delivered lines carrying $169,556 of GMV - $18,651 of platform revenue at the registry take rate. Value omitted from a report, not cash lost.
discount value distribution showing 0, 0.05, 0.1 and 0.2
A factor of 100. Documented as "percent, 0 to 100". Every stored value is a fraction, so anyone converting it lands two orders of magnitude out.
unit_price histogram with the documented ceiling of 1000 marked
The documented ceiling is not a ceiling. unit_price is written as 1 to 1000 and reaches 5,379.20; 186 rows sit above the line a validation rule would have been built on.
Columns documented and live, live but undocumented, documented but gone, per table
The schema moved and the doc did not. Five columns arrived after it was written, two it still describes are gone. Nothing in the document knows either happened.
Columns a reader can rely on, legacy document versus generated
Nothing to rely on. Not one of the 39 live columns survives the legacy document with a true claim, an owner and a unit all present.

The rebuild: measure everything measurable, write only what cannot be

The replacement dictionary is two files. One is generated on every build from the live warehouse. The other is about 200 lines of judgement a person writes once.

Gate exit codes: fail on the legacy document, pass on the generated one, fail on a breaking schema change, pass after regeneration

semantics.yaml is 5 owners, 5 grain sentences, 5 classifications, 39 definitions and 10 gotchas. That is the whole hand-written cost, and it is an afternoon at this size - 39 columns across 5 tables. A 2,000-column estate is not an afternoon, and nothing here makes it one.

semantics.yaml ↗ · the only hand-written half. No types, no counts, no ranges.
net_amount:
  definition: >-
    Merchandise value of the line after discount. Summed over delivered
    lines this is GMV, what buyers paid merchants. It is NOT platform
    revenue.
  unit: USD
  gotcha: >-
    Platform revenue is net_amount times the take rate, per the metric
    registry. Reporting net_amount as revenue overstates the P&L line by
    about 9x.
Measured, guaranteed, decided - three sources, and never the wrong one

Type, cardinality and the value set actually present are measured from the warehouse. Nullability and uniqueness are read from the data contract, because "will this column ever be null" is not a question today's rows can answer - a fully backfilled column has promised nothing, and publishing it as "never null" turns the first legitimate NULL next quarter into a blocking finding about a promise nobody made. Meaning, ownership and sensitivity are decided by a human. The first version of this page got the middle one wrong; a reviewer caught it.

Proving the detectors fire at all

The generated dictionary passes its own detectors because it is derived from the same profile they query. That is arithmetic, not evidence. So the document gets broken one field at a time, and each mutant has to produce its specific finding by name.

Mutation suite results: ten planted defects each caught by their expected finding, and two negative controls that stayed green

On its first run this suite scored 11 of 12. The miss was not in a detector - it was in the harness: the audit was reading the grain sentence from the semantic layer instead of from the claims being audited, so mutating a claim's grain changed nothing. Nothing else on this page would have found that. Fixing it also surfaced a second bug, a range parser reading 1-10 as (1, -10), which had been quietly firing a false out-of-range finding on qty.

Proving the gate is alive, not decorative

A gate that only ever passes teaches nothing. So the build copies the warehouse, adds a column and retypes another for real, and runs again.

1 · legacy doc → FAIL (11 blocking) 2 · generated → PASS (0 findings) 3 · schema moves → FAIL (drift) 4 · regenerated → PASS
gate_after_drift.txt ↗ · the real captured run, after ALTER TABLE on a copy
================================================================
DATA DICTIONARY GATE
================================================================
coverage           97.5% of live columns have a dictionary row
claims tested      75
findings           2 major, 3 minor
schema drift       BREAKING
  BREAKING  type changed     dim_date.year
  WARN      added column     fact_orders.refund_amount

RESULT: FAIL - a breaking schema change since the recorded baseline
================================================================

Drift is tiered, and the tiering is the difference between a gate and a log line. A retyped column falsifies a claim the document already makes, so it fails. An added column falsifies nothing - it only leaves the document incomplete - so it warns. A gate that reddens the main branch every time somebody adds a column is a gate that gets switched off in week three.

The artifact: a dictionary somebody will actually open

One self-contained HTML file. Searchable, filterable, no build step, no CDN, opens from a file path. Every structural value on it was measured minutes before it was written.

📇 Open the live dictionary 39 columns · 5 tables · 131,832 rows described · regenerated by the build

Recall: 18 of 19 planted defects, and the one that got away

Planted defectCaught byWhat the run measuredStatus
Ghost columns (2)existence · blockinggross_amount, dim_merchant.status✓ caught
Undocumented columns (5)existence5 of 39 live columns✓ caught
Wrong types (2)structureqty decimal→BIGINT, dow string→BIGINT✓ caught
Wrong nullability (2)structure2,696 and 1,074 nulls✓ caught
Wrong uniquenessstructureorder_id: 2.16 rows per value✓ caught
Stale value sets (4)structurestatus, region, category, dow✓ caught
Wrong rangestructureunit_price max 5,379.20 vs 1,000✓ caught
Wrong unit scalestructure · 2 screensdiscount 0 to 0.2; span 500x too wide✓ caught
Wrong grain sentencestructure107,841 rows / 50,000 orders✓ caught
Wrong label (revenue)agreement · screenregistry scales net_amount by 0.11✓ caught
Missing status filteragreement · screenorder_id described as countable✓ caught
Governance fields blankpolicy · screen34 rows without owner, unit or grain✓ caught
Wrong timezone in prosenothingdoc says Asia/Singapore, contract says UTC✗ missed

The miss is structural, not a bug. No detector here compares one document's prose against another's, and building one would be a language model guessing rather than a query proving. It is planted, published and left unfixed so the boundary of the method is on the page rather than in a footnote. Six further findings appeared that were never planted: the same wrong claims re-detected as disagreements with the data contract, plus two owner conflicts nobody had noticed - the legacy doc assigns net_amount to finance and status to ops, where the contract assigns both to data-platform.

06
Step 6

Expert review - the panel found the same hole this page was built to expose

Five senior reviewer agents - 11 to 15 years each in warehouse modelling, governance, analytics engineering, data platform CI and commercial insight - were pointed at the code and the executed outputs. They did not describe defects; they ran them. Three of the five arrived independently at one sentence: a detector that cannot decide returns nothing, and nothing is indistinguishable from clean. This page had been written to argue that a document can look complete because nobody checked it. The audit had exactly that defect.

The finding that changed the design

The generated dictionary reported 0 findings and 100 percent coverage. That number included an untested grain claim, two unaudited foreign keys on a star schema, a value-set check that switches itself off above 40 distinct values, and a type check that returns silently for every BigQuery and Snowflake type. proof is now three-valued - measured, screen, undecided - and the gate prints a NOT CHECKED block. The current run is honestly at 0 undecided, which is a result rather than an assumption.

Principal Data Architect
15 yrs · warehouse modelling

"Your domain check disables itself exactly when the doc is most stale - past 40 distinct values it stops looking. And fk_resolution is defined and called from nowhere. On a star schema the join IS the claim, and it is the one thing you never test."

Fixed: value sets are now checked by a targeted anti-join that costs the size of the claimed set, not the cardinality. The contract's declared foreign keys are resolved and reported; an undeclared one comes back as fk_unaudited.
Data Governance & Catalog Lead
12 yrs · stewardship at 500+ table scale

"Zero hits for PII, classification or sensitivity anywhere in the plugin. You document a customer identity dimension and cannot say which columns are personal. Worse, the renderer prints the distinct values of any column under 40 - point that at a real warehouse and the dictionary becomes the disclosure."

Fixed: classification is a required field, table-level with column overrides. The rendered page withholds values for anything not classified public or internal - withholding is now the default, not the opt-in.
Senior Analytics Engineer
13 yrs · dbt and SQL warehouses

"On a Snowflake catalog against a lower-case spreadsheet, nothing matches: 800 major findings, zero coverage, exit 0, and the report says every documented claim held. An empty CI clone blocks every documented primary key. BigQuery STRING and INT64 both resolve to nothing, so type checking is dead across the estate."

Fixed: keys are case-folded, empty tables emit not_evaluated instead of a false blocker, the type vocabulary covers Snowflake, BigQuery, Postgres and Redshift with parameterisation stripped, and an unrecognised type is reported rather than skipped.
Data Platform Engineer
11 yrs · CI and pipelines

"Three of the four ways a run can be meaningless exit 0 with a green banner - no dictionary, no tables, no baseline. And the one thing that reliably goes red, any added column, is the change least worth blocking on. Ship it and the team's first fix is --no-fail-on-drift."

Fixed: a run with nothing to audit is an argparse error, a missing baseline is an error, coverage has a floor that fails the build, and drift is tiered so removed and retyped columns fail while added columns warn. The baseline now records who regenerated it and when.
Commercial & Insights Lead
14 yrs · reporting to a CFO

"Your chart says the registry signs off $577K. It does not - the registry binds that metric to November 2025, which is $82,195. And '$156,982 of GMV' is all-status net_amount, so it is not GMV, it is not a loss, and the two bars overlap so they do not add up."

Fixed: the revenue chart states the 9.1x take-rate gap and the 29% status gap separately and says whose window it is using. The null-claims chart reports delivered GMV, the platform's own exposure at the take rate, and the union rather than the sum.
A defect the panel did not find
found by the fix itself

The mutation suite built in response to the panel scored 11 of 12 on its first run. The miss was in the harness, not a detector: the audit read the grain sentence from the semantic layer rather than from the claims under test, so a mutated grain was audited against the unmutated original.

Fixed, and it exposed a second bug behind it: parse_range read 1-10 as (1, -10), which had been firing a false out-of-range finding on qty for the whole build.

Before and after, on one of the five

the PASS line that asserted two things the gate never checked
# v1: printed whenever the exit code was 0 - including with 3 major findings
#     and real schema drift present but not gated.
lines.append("RESULT: PASS - every documented claim held, schema unchanged")

# v2: assembled from what was actually established.
held = ["0 blocking findings"]
if non_block:  held.append(f"{non_block} non-blocking")
if undecided:  held.append(f"{len(undecided)} NOT CHECKED")
if drift is None:        held.append("schema not compared")
elif drift.breaking: held.append("breaking drift present but not gated")
#   -> "PASS - 0 blocking findings, 4 non-blocking, schema unchanged"

Every reproduction the panel executed is committed as test_panel_regressions.py and runs green at 39 of 39. A fix without a test is a fix until the next refactor.

What the panel raised that is NOT fixed

Three of their findings are real and deferred rather than quietly dropped. Ownership is table-level, so a conformed column joined into two tables resolves to two owners - catalog_price is owned by data-platform on the fact and by commercial on the dimension. Concept-level ownership is the right shape and is not built. Profiling is up to three full scans per column, one query at a time, with no sampling or cost guard; fine on DuckDB, not safe to point at a metered warehouse. The fingerprint cannot see constraints, defaults, views, or any semantic change at constant structure - a currency switched from USD to SGD moves it by nothing. Each is named in the skill's own "what this does not do" section rather than left for a user to discover.

Run this on your own dictionary

Point it at the document you already have and the warehouse it claims to describe. The audit takes about ten minutes and is the highest-value thing on this page.

terminal
/plugin marketplace add phoebefu6/phoebe-data-skills
/plugin install how-to-data-dictionary@phoebe-data-skills