We built this dbt project, bigshop, to test the review at the size of a real estate. It is not a client's, and its model names show it. It has the problems real projects have at this size: most models untested, copied logic, and main tables with no declared grain. The review raised every finding below, and a reviewer cut them to five things to do first.
Summary
Of the 113 checks in the suite, 31 ran on the artefacts supplied and raised 811 findings. The 289 findings selected for this report are 47 high, 242 medium.
The weakest areas are testing (10) and maintainability (63). Two more areas score below 70 on part of their checks: modelling correctness 0 (1 of 5 checks) and documentation 5 (2 of 3 checks). Those are narrower claims than a full pass, and the basis column says what is missing. One area came back clean on every check: lineage integrity.
The first three things to do:
- Give every model at least one test — 180 findings, medium effort.
- Stop partial reloads from duplicating rows — 7 findings, low effort.
- Collapse duplicated model logic to a single definition — 22 findings, medium effort.
Who this review is for
No engagement profile was supplied, so the findings below are ranked by the engine's default ordering and measured against a full pass on every check. That is a bar most teams neither reach nor should aim for. Tell us the size of the business, what the data touches and what good enough looks like for you. We can then rank and score the same evidence against that instead.
Platform health
| Dimension |
Score |
Basis |
| Documentation |
5 |
based on 2 of 3 checks — 20 of 200 models have a description; 0 of 6 data sources have a description. 1 check needs an artefact that was not collected. |
| Testing |
10 |
20 of 200 models have at least one test; 20 of 200 models are tested and checked for duplicate rows |
| Source monitoring |
100 |
based on 1 of 3 checks — 6 of 6 data sources say how late their data may arrive. 2 checks need an artefact that was not collected. |
| AI lifecycle |
n/a |
not assessed — the artefact these checks read was not supplied |
| Orchestration & reliability |
n/a |
not assessed — the artefact these checks read was not supplied |
| BI semantic layer |
n/a |
not measurable — 2 checks had nothing of that kind in this project; 6 checks need an artefact that was not collected |
| Execution health |
n/a |
not assessed — the artefact these checks read was not supplied |
| Lineage integrity |
100 |
200 of 200 models read their inputs through the project, not by fixed table names; 200 of 200 models are free of circular dependencies |
| Modelling correctness |
0 |
based on 1 of 5 checks — 0 of 7 models that update in place declare a key. 1 check had nothing of that kind in this project; 3 checks need an artefact that was not collected. |
| Consumption & cost |
n/a |
not assessed — the artefact these checks read was not supplied |
| Maintainability |
63 |
51 of 200 models list their columns explicitly; 200 of 200 models are short enough to read |
| Ownership & accountability |
n/a |
not measurable — 2 checks had nothing of that kind in this project; whether models name an owner and whether data sources name an owner could not be assessed from the artefacts supplied |
| Repository hygiene |
n/a |
not assessed — the artefact these checks read was not supplied |
A score is the average of its checks' pass rates. Each check's rate is the share of the entities it examined that passed. So a check covering only tested models counts as much as one covering every model. It is not a weighted sum of findings, which would measure project size. n/a means the artefacts supplied could not answer, which differs from a zero. The basis column says which, and how many checks a score rests on.
What this exposes you to
Each of these is a consequence the platform currently carries, not a style preference. They are ordered by the cost of leaving them alone.
HIGH · Give every model at least one test
- This is a presentation-layer model, the layer reports normally read, so a silent data error here can reach one before anyone notices. No consumer is declared, so the report cannot say which.
- Threatens: Silently wrong numbers
- Reaches, at its widest: 14 models built from the affected entity
- Affects 180 entities:
fct_mart_1, fct_mart_2, fct_mart_4, fct_mart_5, fct_mart_7, +175 more
HIGH · Stop partial reloads from duplicating rows
- Any re-run that re-reads an already-loaded window appends duplicate rows instead of replacing them. Totals drift upward and a full refresh is the only repair.
- Threatens: Silently wrong numbers
- Affects 7 entities:
fct_mart_0, fct_mart_9, fct_mart_18, fct_mart_27, fct_mart_36, +2 more
MEDIUM · Collapse duplicated model logic to a single definition
- The same definition exists in more than one place, so a fix applied to one copy leaves the others wrong — this is how two dashboards come to disagree.
- Threatens: Hard to change safely · Silently wrong numbers
- Reaches, at its widest: 6 models built from the affected entity
- Affects 22 entities:
fct_mart_8, fct_mart_14, fct_mart_32, fct_mart_49, fct_mart_59, +17 more
MEDIUM · Describe every column of each main table
- Column names and descriptions are all an AI agent or a new analyst reads. Where a description is missing, they guess what the column means, and the guess reaches a dashboard.
- Threatens: Silently wrong numbers · Visible outside the team
- Affects 40 entities:
fct_mart_1, fct_mart_2, fct_mart_4, fct_mart_5, fct_mart_7, +35 more
MEDIUM · Declare what one row of each main table is
- A count or a join assumes one row per key. If the table holds one row per key per day, an agent's totals are multiplied and nothing fails.
- Threatens: Silently wrong numbers
- Affects 40 entities:
fct_mart_1, fct_mart_2, fct_mart_4, fct_mart_5, fct_mart_7, +35 more
What an AI agent would get wrong
An AI agent answering questions from a data estate gets three kinds of question wrong. This is where this estate stands on each, worst first, sized by the findings in this review. It is not a score.
- What does this field mean? 40 findings across 40 items, the worst rated medium. An agent reads names and descriptions. Where a field has no description it guesses, and where a table has several undescribed dates it may filter on the wrong one.
- What does one row stand for? 40 findings across 40 items, the worst rated medium. Without a declared key, an agent cannot tell a table of customers from a table of customers per day, so its counts and totals can double.
- Which number is right? Not assessed: the artefacts supplied could not show it. Asked for a figure such as revenue, an agent picks one definition. Where two definitions disagree, it answers confidently with either one.
Not assessed: whether the estate keeps a set of checked questions with known answers, used to test an agent before release. Tools that hold such a set keep it in their own service, not in project files, so the files supplied cannot show whether one exists.
Roadmap to recovery
Work grouped by when it should happen. Each stage assumes the ones above it are done, and the projected scores are what the scorecard reaches once that stage closes.
Fix immediately
227 findings across 3 workstreams:
- Give every model at least one test — 180 findings, medium effort
- Stop partial reloads from duplicating rows — 7 findings, low effort
- Declare what one row of each main table is — 40 findings, low effort
Takes it to: Modelling correctness 0 → 100 · Testing 10 → 100
Next 30 days
62 findings across 2 workstreams:
- Collapse duplicated model logic to a single definition — 22 findings, medium effort
- Describe every column of each main table — 40 findings, medium effort
Not every stage moves a score: the scorecard measures the checks it can count, and some of this work is worth doing for the consequence rather than the number.
What to do first
This review ran 31 checks and produced 811 findings. A reviewer read them and selected the 5 below, accounting for 289 of them; the remainder are held and available on request.
The scores, the coverage table and the scope statement in this report describe everything that was examined, not only what is listed here.
1. Give every model at least one test
- Impact: very high · Effort: medium · Confidence: high · When: Fix immediately
- 180 findings (40 high, 140 medium):
fct_mart_1, fct_mart_2, fct_mart_4, fct_mart_5, fct_mart_7 (+175 more)
- Why: This is a presentation-layer model, the layer reports normally read, so a silent data error here can reach one before anyone notices. No consumer is declared, so the report cannot say which.
Example, for fct_mart_1 — values in angle brackets are yours to fill in:
# the schema.yml beside the model
models:
- name: fct_mart_1
columns:
- name: <key column> # the column one row is unique on; confirm before applying
data_tests: [unique, not_null]
2. Stop partial reloads from duplicating rows
- Impact: high · Effort: low · Confidence: high · When: Fix immediately
- 7 findings:
fct_mart_0, fct_mart_9, fct_mart_18, fct_mart_27, fct_mart_36 (+2 more)
- Why: Any re-run that re-reads an already-loaded window appends duplicate rows instead of replacing them. Totals drift upward and a full refresh is the only repair.
Example, for fct_mart_0 — values in angle brackets are yours to fill in:
-- top of models/marts/fct_mart_0.sql
{{ config(materialized='incremental', unique_key='<key column>') }} -- confirm the key before applying
-- then rebuild once: dbt build -s fct_mart_0 --full-refresh
3. Collapse duplicated model logic to a single definition
- Impact: high · Effort: medium · Confidence: high · When: Next 30 days
- 22 findings:
fct_mart_8, fct_mart_14, fct_mart_32, fct_mart_49, fct_mart_59 (+17 more)
- Why: The same definition exists in more than one place, so a fix applied to one copy leaves the others wrong — this is how two dashboards come to disagree.
4. Describe every column of each main table
- Impact: high · Effort: medium · Confidence: high · When: Next 30 days
- 40 findings:
fct_mart_1, fct_mart_2, fct_mart_4, fct_mart_5, fct_mart_7 (+35 more)
- Why: Column names and descriptions are all an AI agent or a new analyst reads. Where a description is missing, they guess what the column means, and the guess reaches a dashboard.
5. Declare what one row of each main table is
- Impact: high · Effort: low · Confidence: high · When: Fix immediately
- 40 findings:
fct_mart_1, fct_mart_2, fct_mart_4, fct_mart_5, fct_mart_7 (+35 more)
- Why: A count or a join assumes one row per key. If the table holds one row per key per day, an agent's totals are multiplied and nothing fails.
Where one edit closes several findings
40 models carry 120 findings between them that one YAML entry per model closes — whichever of a description, columns, a key test and an owner it is missing.
fct_mart_1 — 3 findings
fct_mart_2 — 3 findings
fct_mart_4 — 3 findings
fct_mart_5 — 3 findings
fct_mart_7 — 3 findings
fct_mart_8 — 3 findings
fct_mart_10 — 3 findings
fct_mart_11 — 3 findings
fct_mart_13 — 3 findings
fct_mart_14 — 3 findings
fct_mart_16 — 3 findings
fct_mart_17 — 3 findings
- …and 28 more models
Whose area this is
Nothing in this review can be routed to a named owner. The project declares no dbt groups: and no ownership-shaped meta key, so the manifest carries nobody to send a finding to. That is a limit of what a manifest records, not a statement that the work is unowned. It does mean whoever picks up this review must assign every item by hand first.
Test coverage by layer
The project is laid out in layers, so the same test findings regroup by the one a model sits in. This covers all 200 models in this project.
| Layer |
Models |
No test at all |
Tested |
No grain assertion |
No not_null |
| Staging |
60 |
60 of 60 (100%) |
0 |
— |
— |
| Intermediate |
80 |
80 of 80 (100%) |
0 |
— |
— |
| Marts |
60 |
40 of 60 (67%) |
20 |
none |
none |
Each figure is counted against the column to its left: no test at all out of Models, the two assertion columns out of Tested. There is no single coverage figure for a layer, because a model with no tests cannot also be a model whose tests miss its grain. These are counts, not scores — nothing here is comparable with Platform health above.
The gap sits equally across staging (60 of 60) and intermediate (80 of 80) — no one layer is thinner than the others.
What this review could not see
This pass read one artefact: the dbt manifest. The following are unassessed, not passing — each needs evidence that pass did not have.
| Unassessed |
Needs |
| Whether two dashboards define a metric differently |
a comparison of metric definitions ACROSS layers — dbt metrics against the BI tool's own measures. Each layer is read on its own; the two are not yet compared |
| Whether a credential-shaped file actually contains a live secret |
a human opening it — the engine reads filenames, never contents |
| Dependency vulnerabilities and license compatibility |
a dependency manifest scan (not yet built) |
Leaf models are consumed by something (check model-possibly-unused could not run) |
the project declares no exposures, metrics or semantic models, so there is no way to tell a used leaf model from an abandoned one |
Models declare an owner (check model-unowned could not run) |
this project declares no dbt groups: and no ownership-shaped meta key, so there is no convention to measure its models against — ownership held outside dbt (CODEOWNERS, a rota, a wiki) is not visible in a manifest |
Sources declare an owner (check source-unowned could not run) |
this project declares no dbt groups: and no ownership-shaped meta key, so there is no convention to measure its sources against — ownership held outside dbt (CODEOWNERS, a rota, a wiki) is not visible in a manifest |
| Query cost, full scans and partition use |
a warehouse query history (--query-history <path>) — collected by a read-only query the client runs themselves, with no credential granted |
| Table sizes, row counts and storage cost |
catalog.json, or a warehouse connection |
| Repository hygiene: CI, tests, documentation, credential-shaped filenames |
a repository inventory (--repo owner/name) |
| Whether the declared tests actually pass, and what a failure is blocking |
target/run_results.json from a recent dbt build (--run-results <path>) — the same directory as the manifest |
| Whether the monitored sources are actually arriving on time |
target/sources.json from dbt source freshness (--source-freshness <path>) |
| Whether the warehouse actually matches what the project declares |
target/catalog.json from dbt docs generate (--catalog <path>) — the same directory again |
| How this project is scheduled — retries, alerting, ownership, and what a redeploy replays |
the DAG or job definition files themselves (--dag <path>, one per file). They are read for schedule, owner, retry and alert declarations only, and nothing in them is retained |
| Which scheduled jobs have actually been failing, and for how long |
the orchestrator's own run history — Airflow's metadata database or REST API, or the equivalent. Not reachable from a repository at any depth: a DAG file states intent, not what happened. Reaching it needs a credential to the scheduler, which this review does not ask for |
| How the BI layer reads this project — whether dashboards sit on the models dbt builds, and whether their joins count correctly |
the LookML project, if there is one (--lookml <dir>). Only .lkml files are read; a measure's or dimension's SQL is reduced to a fingerprint and discarded, and every other SQL body is not read at all |
| Which dashboards people open, and which nobody has opened in months |
a BI usage export the client produces (--bi-usage <csv>): view counts per dashboard, named by the tool's own id. An export naming people is refused |
| Which models change most often, and whether those are the tested ones |
the change history — one git log command, run from the dbt project directory in a full clone, printing commit hashes, dates and file paths only (no names, no messages). No access is needed, and we send the exact command to run |
| How often CI fails, and whether changes are reviewed before they merge |
the repository host's own records — Actions run history and pull-request reviews. A change history has neither, and both need token permissions (Actions: read, Pull requests: read) this review does not ask for |
Evidence
dbt project
|
|
| Source |
dbt manifest.json |
| Project |
bigshop |
| dbt version |
1.10.23 |
| Manifest schema |
https://schemas.getdbt.com/dbt/manifest/v12.json |
| Warehouse adapter |
duckdb |
| Manifest generated |
2026-09-20T17:20:53.005637Z |
| Project size |
200 models · 6 sources · 40 tests · 0 exposures · 0 metrics |
| Test attribution |
attached_node (authoritative) |
| Dependency map |
manifest child_map |
| Scope |
bigshop — no installed package contributed entities |
Exactly what was read
| Artefact |
Supplied as |
SHA-256 |
Size |
| dbt manifest |
bigproj-manifest.json |
849fad1cbb88b7e0… |
1000.0 KB |
Produced by Metacenta check suite v1.30 (113 checks). The digests are of the files as supplied, and shasum -a 256 <file> reproduces them. So every finding traces to the exact artefact it came from, and a later disagreement about what was reviewed is a matter of record rather than recollection.
Findings fingerprint: 4f466ba5284e1a60…, covering 811 findings and the outcome of each of 43 checks, taken before the shortlist was cut. Re-running the same check suite on these files gives the same fingerprint. If you have portal access, its Verify page does that in your own browser.
Ownership and routing
|
|
| Convention in use |
none declared in dbt |
| Findings routed to a named owner |
0 of 811 |
This project declares no dbt groups: and no ownership-shaped meta key, so no finding in this review can be addressed to a named owner from the manifest alone. A manifest cannot show ownership held elsewhere, such as in a CODEOWNERS file, a rota or a wiki. So this is a limit of what was read, not a statement about the team.
Every finding below is traceable to a field in the artefacts above. This review connected to no warehouse and no dashboard tool, and everything was read from files. Nothing here is inferred from a test run, a source-freshness reading or the warehouse's own account of what it holds. No finding is inferred from the contents of any file in the repository. See What this review could not see.
What was examined
| Artefact |
|
| dbt entities (models, sources, tests, exposures, metrics) |
246 |
| Findings raised |
811 |
This is the whole of what was read. Everything in this report is derived from it, and nothing outside it was assumed.
Check coverage
Every check, including the ones that found nothing. A check that could not run is listed as such — so an absent finding is never mistaken for a passing grade.
| Check |
Dimension |
Outcome |
Examined |
Findings |
Corresponds to |
Note |
incremental-without-unique-key |
Modelling correctness |
ran |
7 |
7 |
— |
|
dependency-cycle |
Lineage integrity |
ran |
200 |
0 |
— |
|
hardcoded-table-reference |
Lineage integrity |
ran |
200 |
0 |
fct_hard_coded_references |
|
model-untested |
Testing |
ran |
200 |
180 |
fct_test_coverage (related) |
|
model-grain-not-asserted |
Testing |
ran |
20 |
0 |
fct_missing_primary_key_tests (narrower) |
|
source-no-freshness |
Source monitoring |
ran |
6 |
0 |
fct_sources_without_freshness |
|
duplicate-model-logic |
Maintainability |
ran |
200 |
22 |
— |
|
public-model-without-contract |
Modelling correctness |
not applicable |
0 |
0 |
fct_public_models_without_contracts |
no model is declared access: public |
model-possibly-unused |
Lineage integrity |
could not run |
0 |
0 |
— |
the project declares no exposures, metrics or semantic models, so there is no way to tell a used leaf model from an abandoned one |
model-undocumented |
Documentation |
ran |
200 |
180 |
fct_undocumented_models; fct_undocumented_public_models (broader) |
|
model-columns-undocumented |
Documentation |
ran |
20 |
0 |
— |
|
model-no-column-schema |
Documentation |
ran |
200 |
180 |
— |
|
source-undocumented |
Documentation |
ran |
6 |
6 |
fct_undocumented_source_tables |
|
source-parent-undocumented |
Documentation |
ran |
1 |
1 |
fct_undocumented_sources |
|
duplicate-source |
Modelling correctness |
ran |
6 |
0 |
fct_duplicate_sources |
|
source-freshness-no-timestamp |
Source monitoring |
ran |
6 |
0 |
— |
|
model-no-not-null-test |
Testing |
ran |
20 |
0 |
fct_missing_primary_key_tests (narrower) |
|
select-star |
Maintainability |
ran |
200 |
149 |
— |
|
high-cte-count |
Maintainability |
ran |
200 |
0 |
— |
|
layer-naming-mismatch |
Maintainability |
ran |
200 |
0 |
fct_model_naming_conventions (related) |
|
test-not-beside-model |
Maintainability |
ran |
40 |
0 |
fct_test_directories (narrower) |
|
deep-lineage |
Lineage integrity |
ran |
200 |
0 |
fct_chained_views_dependencies (related) |
|
fan-out-hotspot |
Lineage integrity |
ran |
200 |
0 |
fct_model_fanout (related) |
|
staging-depends-on-staging |
Modelling correctness |
ran |
60 |
0 |
fct_staging_dependent_on_staging |
|
staging-depends-on-downstream |
Modelling correctness |
ran |
60 |
0 |
fct_staging_dependent_on_marts_or_intermediate |
|
downstream-depends-on-source |
Modelling correctness |
ran |
140 |
0 |
fct_marts_or_intermediate_dependent_on_source |
|
direct-join-to-source |
Modelling correctness |
ran |
200 |
0 |
fct_direct_join_to_source |
|
root-model |
Lineage integrity |
ran |
200 |
0 |
fct_root_models |
|
source-fanout |
Modelling correctness |
ran |
6 |
6 |
fct_source_fanout (broader) |
|
multiple-sources-joined |
Modelling correctness |
ran |
200 |
0 |
fct_multiple_sources_joined |
|
rejoining-upstream-concept |
Modelling correctness |
ran |
200 |
0 |
fct_rejoining_of_upstream_concepts |
|
exposure-parent-not-materialized |
Modelling correctness |
not applicable |
0 |
0 |
fct_exposure_parents_materializations |
the project declares no exposures |
exposure-on-private-model |
Modelling correctness |
not applicable |
0 |
0 |
fct_exposures_dependent_on_private_models |
the project declares no exposures |
model-unowned |
Ownership & accountability |
could not run |
0 |
0 |
— |
this project declares no dbt groups: and no ownership-shaped meta key, so there is no convention to measure its models against — ownership held outside dbt (CODEOWNERS, a rota, a wiki) is not visible in a manifest |
source-unowned |
Ownership & accountability |
could not run |
0 |
0 |
— |
this project declares no dbt groups: and no ownership-shaped meta key, so there is no convention to measure its sources against — ownership held outside dbt (CODEOWNERS, a rota, a wiki) is not visible in a manifest |
group-owner-unreachable |
Ownership & accountability |
not applicable |
0 |
0 |
— |
the project declares no groups |
exposure-owner-unreachable |
Ownership & accountability |
not applicable |
0 |
0 |
— |
the project declares no exposures |
semantic-column-not-built |
BI semantic layer |
not applicable |
0 |
0 |
— |
the project declares no semantic models — whether it should have is a choice, not a finding |
semantic-primary-entity-unasserted |
BI semantic layer |
not applicable |
0 |
0 |
— |
the project declares no semantic models — whether it should have is a choice, not a finding |
metric-undocumented |
Documentation |
not applicable |
0 |
0 |
— |
the project declares no metrics — whether it should is a choice, not a finding |
model-date-ambiguous |
Documentation |
not applicable |
0 |
0 |
— |
no core model declares two or more date columns (core models are the marts) |
core-model-columns-undocumented |
Documentation |
ran |
60 |
40 |
— |
|
core-model-grain-undeclared |
Modelling correctness |
ran |
60 |
40 |
fct_missing_primary_key_tests (narrower) |
|
Where these checks come from
- dbt_project_evaluator (dbt Labs) — 21 of its 29 rules were answered by checks in this review. A further 4 have checks that could not run on the artefacts supplied — see the coverage table for which, and why. https://dbt-labs.github.io/dbt-project-evaluator/
The 4 rules not checked, and why
Listed rather than left out. Each is a position you can disagree with, not a gap being glossed over.
| Rule |
Not checked because |
Reasoning |
fct_too_many_joins |
it needs evidence this review does not collect |
dbt applies its join threshold to COMPILED SQL. This review holds each model's raw code with its Jinja unrendered, so a join count taken from it would be wrong wherever a macro expands to a join — and picking our own threshold to compare it against would be an invented bar wearing somebody else's citation. |
fct_documentation_coverage |
it is a project-wide measure, not an assignable defect |
A project-wide documentation percentage has no entity to attach to and nobody to assign. The underlying defect is reported per model by model-undocumented and model-columns-undocumented, which name which models and can be worked through. |
fct_source_directories |
checking it would impose a layout you did not choose |
The rule requires each source to be defined in a staging subdirectory named after it. That is dbt Labs' preferred layout, not a correctness property, and a project keeping one sources.yml per staging folder is not wrong. Unlike layer naming and test placement — both of which this review measures before enforcing — a project with a handful of sources gives too weak a signal to tell a deliberate layout from an inconsistent one. |
fct_model_directories |
checking it would impose a layout you did not choose |
Same ground as fct_source_directories: it requires staging models to sit in folders named after their source. layer-naming-mismatch covers the part of this that can be self-calibrated — whether a project applies its OWN naming convention consistently — without asserting which convention it should have chosen. |
22 checks here correspond to a rule published by somebody else. Each is named in the table above, so you can look it up and disagree with the criterion rather than with us. The other 9 have no published counterpart. They cover how the last run went, whether the warehouse matches what the project declares, whether the build is reproducible, and who is accountable for each asset.
Correspondence, not conformance. Citing a rule does not make its publisher a party to this review, and nothing here is a certification against any standard.