Audit dbt models and connector configurations against the data-flow conventions in .cf-studio/config/rules/architecture.md. Verifies engine=ReplacingMergeTree, order_by=['unique_key'], silver delete+insert, read-time dedup of every RMT read (FINAL/QUALIFY/LIMIT 1 BY/union_by_tag), unique_key formula, bronze→RMT promotion, ephemeral usage for Rust-owned tables, and Airbyte append-only sync mode. Reports deviations with file paths and line numbers.
This skill audits the codebase against the dbt pipeline conventions. It is LLM-based correctness validation — complements cfs validate which only checks artifact structure and code-marker presence (not engine config content).
Before reporting anything, read this THIS turn:
Where that file and the checks below disagree, the rules file wins — update the checks to match it.
For every .sql file under src/ingestion/silver/ (excluding crm.disabled):
engine must be 'ReplacingMergeTree(_version)' OR 'ReplacingMergeTree' (versionless, only for materialized='table')order_by must be ['unique_key']materialized must NOT be 'view' (views are forbidden for silver)materialized='incremental' → incremental_strategy='delete+insert' AND unique_key='unique_key'
(silver is physically deduplicated so any
consumer — gold views, the product — can read silver WITHOUT FINAL).
incremental_strategy='append' in a silver model is now a FAIL.Bash discovery:
find src/ingestion/silver -name "*.sql" -not -path "*disabled*"For each file: read the {{ config(...) }} block. Report violations with file path + the offending line.
For every .sql file under src/ingestion/connectors/*/dbt/:
materialized is incremental or table → engine='ReplacingMergeTree(_version)' + order_by=['unique_key']materialized is view → confirm it's a thin pass-through (no GROUP BY / window) AND the bronze upstream has been promotedmaterialized is ephemeral → confirm it's a pass-through over a staging table dbt does not own (none exist today; the Jira field history is derived in dbt)unique_key column (either propagated from bronze: u.unique_key AS unique_key, or computed: CAST(concat(...) AS String) AS unique_key)Bash discovery:
find src/ingestion/connectors -name "*.sql" -path "*/dbt/*" -not -path "*disabled*"unique_key formula in connector record producersFor every Airbyte YAML connector at src/ingestion/connectors/*/connector.yaml:
AddFields block with path: [unique_key] (or path: [unique] — that's a deviation){{ config['insight_tenant_id'] }}-{{ config['insight_source_id'] }}-{{ record['id'] }} or composite)For every Python CDK connector at src/ingestion/connectors/*/source_*/:
streams/base.py (or equivalent) for a _make_unique_key helpertenant_id and source_id as the first two argumentsKnown deviation: claude-admin uses field name unique instead of unique_key and omits tenant/source prefix. Tracked as follow-up — flag it but don't double-report.
promote_bronze_to_rmt calls in bootstrap modelsFor every <connector>__bronze_promoted.sql under src/ingestion/connectors/*/dbt/:
promote_bronze_to_rmt(table=..., order_by='unique_key') for each bronze table the connector has (every table in its sources: block)-- depends_on: {{ ref('<connector>__bronze_promoted') }}Every connector MUST have a <connector>__bronze_promoted.sql — a missing one is a FAIL.
Known exceptions: claude-admin is a tracked follow-up (its bronze lacks a unique_key
column, so promotion is blocked until the connector emits one — flag, don't double-report).
src/ingestion/airbyte-toolkit/connect.sh must have dest_sync_mode = "append" literalappend_dedup or overwrite anywhereephemeral must SELECT only from source(...) (not ref(...)) — i.e., it's a thin wrapper for a non-dbt-managed tableunion_by_tag macro must contain the ephemeral handling branch (check src/ingestion/dbt/macros/union_by_tag.sql for materialized == 'ephemeral')Every silver model and every dbt-owned staging model with append/event semantics MUST be materialized='incremental'. materialized='table' is allowed ONLY in three justified cases:
class_people, class_hr_working_hoursmtr_git_person_totals, mtr_git_person_weeklyjira__changelog_items, jira__issue_field_snapshotFor each .sql file under src/ingestion/silver/ and src/ingestion/connectors/*/dbt/:
materialized='table' AND model name is in the allow-list above → PASSmaterialized='table' AND model name is NOT in the allow-list → FAIL with suggestion: "Convert to materialized='incremental'. For silver use incremental_strategy='delete+insert' + unique_key='unique_key'; for staging use incremental_strategy='append'. Scope the incremental boundary to one source instance with {{ silver_incremental_watermark([...]) }} — never a table-wide max(_version), which lets one producer's boundary permanently exclude a slower producer's rows. If upstream lacks _version, amend the SELECT to project toUnixTimestamp64Milli(_airbyte_extracted_at) AS _version."materialized='view' for a silver class_* / fct_* / mtr_* → FAIL (views forbidden in silver per check 1)materialized='ephemeral' → cross-checked by Check 6Bash discovery:
grep -lE "materialized\s*=\s*'table'" src/ingestion/silver/ src/ingestion/connectors/*/dbt/ -r 2>/dev/nullCross-reference each match against the allow-list and report PASS / FAIL.
RMT only collapses duplicates on
background merge (never guaranteed at query time), so every read of an RMT
relation must be deduplicated at read time unless the producer is physically
unique. A duplicate that slips through inflates metrics (e.g. the
slack_active_days = 42 incident).
A read is OK if ANY of:
union_by_tag macro (which dedups the union by
unique_key via QUALIFY ROW_NUMBER() … ORDER BY _version DESC, or LIMIT 1 BY
for versionless), ORFINAL / argMax / QUALIFY ROW_NUMBER / LIMIT 1 BY in its
own subquery scope, ORincremental_strategy='delete+insert' or materialized='table'
(physically unique → reader needs no dedup).FAIL conditions (scan connectors/*/dbt/ + silver/ + gold migrations
scripts/migrations/*.sql):
ref()/source() read of an RMT producer (append/incremental) with
no dedup in scope and not via union_by_tag.count()/sum()/avg() over a
source('bronze_*') read without FINAL (RMT bronze) or LIMIT 1 BY unique_key
(non-promoted MergeTree bronze). Inflation is baked into one output row and is
unrecoverable downstream (e.g. bitbucket_cloud__commits, claude_admin__ai_dev_usage).snapshot() macro call whose source is read without FINAL (spurious SCD2
versions). The shared macros/snapshot.sql must read FROM {{ source_ref }} FINAL.scripts/migrations/*.sql reading a staging.*/silver.*
append+RMT table without FINAL. (Reads of delete+insert silver are OK.
Note migrations are immutable history — verify against the latest migration
that (re)defines the view.)Deterministic complement: src/ingestion/dbt/audit_rmt_read_dedup.py performs
this scan mechanically and exits non-zero on a gap — runnable in CI. Use it to
generate candidates, then confirm each by reading (it cannot resolve outer-scope
dedup or column-level semantics).
When user invokes /check-dbt-conventions:
DESIGN.md + 4 ADRs) THIS turn — confirm understandingGlob / Grep / Read (optionally run python3 src/ingestion/dbt/audit_rmt_read_dedup.py for Check 8)order_by='(insight_source_id, comment_id)' to order_by=['unique_key']")=== check-dbt-conventions ===
Rules read: .cf-studio/config/rules/architecture.md (this turn)
Check 1 — Silver engine + order_by — PASS (34 models, 0 violations)
Check 2 — Connector staging engine/order_by/unique_key — FAIL (2 violations)
- src/ingestion/connectors/.../foo.sql:8 — order_by='(...)' should be ['unique_key']
- src/ingestion/connectors/.../bar.sql:14 — missing unique_key projection in SELECT
Check 3 — unique_key formula — FAIL (8 violations in claude-admin — tracked)
Check 4 — promote_bronze_to_rmt bootstrap — PASS (1 connector covered: jira)
Check 5 — Airbyte append-only — PASS
Check 6 — Ephemeral wrapping — PASS
Check 7 — Incremental-by-default — PASS
Check 8 — Read-time dedup of RMT reads — FAIL (1 violation)
- src/ingestion/connectors/.../foo.sql:42 — count()/sum() over source('bronze_*') without FINAL/LIMIT 1 BY
Summary: 6/8 PASS, 2 FAIL (Checks 2, 8 — fixes above), 1 known-tracked (Check 3 — claude-admin)Refuse to report PASS without having read each spec doc this turn (anti-pattern: stale reasoning). Refuse to invent file paths — only report files actually scanned.
User-driven, on demand:
dbt build to surface convention driftc31d302
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.