.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 indbt_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:
.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 inadapters/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 singleProjectAdapterprotocol withload()andwrite_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)
- Re-read the dbt project and warehouse on every transform or maintain run.
- 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. - Diff the current dbt project and warehouse against the snapshot; propose edits.
- On any ambiguous or overwriting change, surface a diff and ask. Never silently overwrite.
references/canonical-model.md (edit on main). This page is generated; change the source, not the site.