Skip to main content
dex maintains no canonical model of its own. The repository is the source of truth, on two axes: the transformation project, which is dbt, and the semantic layer, which is dbt’s own or a native format that owns its definitions in files beside it. dex reads whichever are present, reasons over them together with warehouse introspection, and writes changes back as reviewable diffs. The only state dex keeps of its own is a non-canonical cache under .dex/.

Why dbt, not a dex-native model

An earlier design made the source of truth a dex-invented semantic model, with every other format a projection of it. That was an over-correction. The reasoning was “the interchange formats are immature, so build our own,” but the right response to an immature standard is to anchor on the mature thing, not to invent a third one: a representation dex invents has no spec, no tooling, and no users, so it is the least mature option, not the most stable. And the premise was largely wrong on the facts: dbt’s MetricFlow already expresses nearly all of the richness in question (entities; categorical and time dimensions with granularity; measures; and metrics of kind simple, ratio, derived, and cumulative, with time spines). The dex-native model was, in effect, a plan to re-implement MetricFlow, worse. dbt is mature, versioned, adopted, and where AEs already live, so it is the correct anchor for the transformation model. The decisive practical win: with the repository as the source of truth there is no parallel copy to reconcile, so the round-trip fidelity problem, the per-element provenance and identity machinery, and “the human’s file wins” conflict resolution all disappear. Human edits are authoritative by construction. That reasoning is about not inventing a model, and it does not argue against reading a format somebody else specifies. A semantic layer that owns its own definitions is read where it lives, on its own axis, with no translation into dbt’s vocabulary and no dbt project required. Those definitions are source of truth in exactly the same sense the dbt project is: they are files in git, dex holds no competing copy of them, and every change dex makes to them is a diff. See �0.

How dex reads and writes the dbt project

  • Read: load the project primarily from the compiled manifest.json (dbt’s own documented, versioned serialization of nodes, sources, tests, semantic models, metrics, and lineage), supplemented by the raw SQL and YAML for editing. This lives in dbt_project.py.
  • Reason: over that view plus warehouse introspection and the .dex/ cache.
  • Write: edits go back into the dbt source files as reviewable diffs. dex never holds a competing copy.

The .dex/ cache (not canonical)

.dex/ holds only what the repository has no home for, and it is a cache that informs proposals, never the source of truth:
Delete .dex/ and nothing canonical is lost: dex re-derives the cache from the repository and the warehouse. The cache types live in cache.py; secrets never live here. PII is recorded as (column, category, confidence) with no example values. The layout above is the CLI’s choice, not the engine’s. Where this state lands is a backend behind the Store protocol, injected at the entry point: the CLI picks the filesystem so subcommands stay stateless across processes and the session budget carries from one command to the next, while an in-process library caller gets an in-memory store by default and writes nothing at all. A host can implement the protocol over its own session store or database. The repository is unaffected by any of this: it is the source of truth and stays a git-reviewable filesystem artifact, which is why repo_root remains separate from the store. Credentials are in neither place. dex discovers them at connection time and never stores, caches, or refreshes one, in any backend. A host serving more than one end user can supply them itself, the warehouse connection and a hosted semantic layer token alike, so each request reaches its target as its own principal instead of as the process; that moves who the caller is, and nothing else. The cost gate is still built by the engine from the store, the source allowlist still only narrows, and no credential becomes persistent state.

The extension seam (more formats later)

Supporting more model formats over time does not require a neutral internal model. It requires a seam thin enough that a class with the right methods is a project, with no base class to inherit and no registration step. That seam is three nested protocols in adapters/project.py, and a format implements the tiers it can serve: DbtProject is the one implementation dex ships. Future sources (SQLMesh, Cube, an orchestrated asset graph) become new implementations of the same protocols without touching the engine that reasons over a project. A semantic layer that builds nothing is not one of them: it owns no model graph, no compilation, and no targets, so it sits on a separate seam described in �1 and satisfies none of the tiers above. Nothing behind the seam is a rich neutral model: definitions() returns the engine’s existing ProjectDefinitions rather than a parallel vocabulary that would have to be mapped at every call site, and dex does not project the dbt model back out into other formats. dex reasons over the project and authors into it directly; there is no one-way exporter path. Tiers rather than a capability flag, because a flag is a claim the engine has to trust while isinstance(project, EditableProject) is checkable. A format that cannot receive an edit declines tier 3 and cannot accidentally claim otherwise. �0 is the contract itself: what each tier owes its callers, the one rule that is not visible in the signatures, how a format is named in configuration, and the conformance suite that proves an implementation works. Read that before writing a format; this section only says where the seam sits.
An earlier version of this section named a single ProjectAdapter protocol with load() and write_edits(). That protocol still exists, unreferenced, because it has been public since v1, but it is superseded and satisfies no tier. Anything written against it would be refused as “not a project”.

The round-trip rule (reconcile, simplified)

  1. Re-read the dbt project and warehouse on every transform or maintain run.
  2. Human edits are already authoritative (the repository is canonical); there is no internal copy to reconcile against, only the .dex/ snapshot used to detect change.
  3. Diff the current dbt project and warehouse against the snapshot; propose edits.
  4. On any ambiguous or overwriting change, surface a diff and ask. Never silently overwrite.
Source: references/canonical-model.md (edit on main). This page is generated; change the source, not the site.