loom-dbt 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loom/.agents/skills/loom-adoption/SKILL.md +399 -0
- loom/.agents/skills/loom-adoption/references/adoption-playbook.md +158 -0
- loom/.agents/skills/loom-adoption/references/first-project/local/registrations/customer_order_counts.yml +12 -0
- loom/.agents/skills/loom-adoption/references/first-project/metadata/instances/hubs.yml +7 -0
- loom/.agents/skills/loom-adoption/references/first-project/metadata/instances/staging.yml +29 -0
- loom/.agents/skills/loom-adoption/references/first-project/metadata/patterns/current_hub.yml +8 -0
- loom/.agents/skills/loom-adoption/references/first-project/metadata/patterns/staging.yml +8 -0
- loom/.agents/skills/loom-adoption/references/first-project/models/sources.yml +25 -0
- loom/.agents/skills/loom-adoption/references/metadata-reference.md +347 -0
- loom/.agents/skills/loom-ask/SKILL.md +241 -0
- loom/.agents/skills/loom-authoring/SKILL.md +125 -0
- loom/.agents/skills/loom-authoring/references/metadata-reference.md +347 -0
- loom/.agents/skills/loom-bootstrap/SKILL.md +136 -0
- loom/.agents/skills/loom-bootstrap/references/first-project/local/registrations/customer_order_counts.yml +12 -0
- loom/.agents/skills/loom-bootstrap/references/first-project/metadata/instances/hubs.yml +7 -0
- loom/.agents/skills/loom-bootstrap/references/first-project/metadata/instances/staging.yml +29 -0
- loom/.agents/skills/loom-bootstrap/references/first-project/metadata/patterns/current_hub.yml +8 -0
- loom/.agents/skills/loom-bootstrap/references/first-project/metadata/patterns/staging.yml +8 -0
- loom/.agents/skills/loom-bootstrap/references/first-project/models/sources.yml +25 -0
- loom/.agents/skills/loom-core/SKILL.md +260 -0
- loom/.agents/skills/loom-core/references/commands.md +378 -0
- loom/.agents/skills/loom-core/references/terminology.md +88 -0
- loom/.agents/skills/loom-maintenance/SKILL.md +100 -0
- loom/__init__.py +27 -0
- loom/__main__.py +5 -0
- loom/check.py +411 -0
- loom/cli.py +3865 -0
- loom/contracts.py +723 -0
- loom/declared_generic_tests.py +319 -0
- loom/declared_unit_tests.py +387 -0
- loom/dialects.py +545 -0
- loom/equivalence.py +490 -0
- loom/errors.py +2 -0
- loom/generator_registry.py +129 -0
- loom/inventory.py +1021 -0
- loom/inventory_corpus.py +1182 -0
- loom/inventory_patterns.py +371 -0
- loom/inventory_records.py +2676 -0
- loom/inventory_relationships.py +1559 -0
- loom/manifest.py +597 -0
- loom/migration_assessment.py +5794 -0
- loom/migration_plan.py +327 -0
- loom/output_columns.py +624 -0
- loom/output_layout.py +172 -0
- loom/project_files.py +260 -0
- loom/properties.py +308 -0
- loom/renderers/__init__.py +53 -0
- loom/renderers/base.py +491 -0
- loom/renderers/bridge.py +115 -0
- loom/renderers/bronze.py +104 -0
- loom/renderers/dimension.py +163 -0
- loom/renderers/fact.py +211 -0
- loom/renderers/gold.py +284 -0
- loom/renderers/hub.py +304 -0
- loom/renderers/link.py +351 -0
- loom/renderers/mart.py +92 -0
- loom/renderers/normalized_table.py +118 -0
- loom/renderers/obt.py +116 -0
- loom/renderers/pit.py +107 -0
- loom/renderers/reference.py +220 -0
- loom/renderers/satellite.py +692 -0
- loom/renderers/scd2_dimension.py +341 -0
- loom/renderers/silver.py +204 -0
- loom/renderers/staging.py +217 -0
- loom/renderers/view.py +87 -0
- loom/reporting.py +2862 -0
- loom/selection.py +1544 -0
- loom/similarity.py +452 -0
- loom/source_structure.py +481 -0
- loom_dbt-0.1.0.dist-info/METADATA +192 -0
- loom_dbt-0.1.0.dist-info/RECORD +74 -0
- loom_dbt-0.1.0.dist-info/WHEEL +4 -0
- loom_dbt-0.1.0.dist-info/entry_points.txt +3 -0
- loom_dbt-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loom-adoption
|
|
3
|
+
description: Adopt Loom in an existing dbt project - assess its hand-written models, convert what fits, register the rest. Use when the practitioner points Loom at a project with existing models and a manifest - running register/convert, reading readiness, working drafts in local/drafts/, promoting an instance, or proving equivalence before a swap. Not for a project with no models yet (loom-bootstrap) or routine upkeep of an adopted project (loom-maintenance).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adoption - bringing an existing dbt project onto Loom
|
|
7
|
+
|
|
8
|
+
The core rules apply throughout (`loom-core`): Loom copies, you
|
|
9
|
+
propose, the practitioner decides; mutating commands preview by
|
|
10
|
+
default and write only under `--write`; escalation beats improvisation.
|
|
11
|
+
|
|
12
|
+
The short version, from the [adoption playbook](https://github.com/lincolnp-oss/loom-dbt/blob/main/docs/adoption-playbook.md)
|
|
13
|
+
(which governs where this skill paraphrases it): **convert first,
|
|
14
|
+
register the rest, promote only what is ready, prove each conversion
|
|
15
|
+
against the original before swapping.** One assessment drives
|
|
16
|
+
everything. A hybrid project - some models generated, the rest
|
|
17
|
+
registered and hand-written - is a legitimate permanent end state,
|
|
18
|
+
not a waypoint.
|
|
19
|
+
|
|
20
|
+
This skill ships with bundled copies of the documents it defers
|
|
21
|
+
to, under `references/` beside this file: the adoption playbook
|
|
22
|
+
(`references/adoption-playbook.md`), the metadata reference
|
|
23
|
+
(`references/metadata-reference.md`), and the first-project
|
|
24
|
+
example's metadata files (`references/first-project/`, verbatim
|
|
25
|
+
excerpts of the shipped example at their example paths). They
|
|
26
|
+
work offline; when a detail matters, read the section you need
|
|
27
|
+
rather than the whole file.
|
|
28
|
+
|
|
29
|
+
## 1. Assess
|
|
30
|
+
|
|
31
|
+
Prerequisite: a current, parse-produced `target/manifest.json` (see
|
|
32
|
+
`loom-core`). Run both reads - reads never write:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
loom register --project-dir . --format json
|
|
36
|
+
loom convert --project-dir . --format json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both report the same `summary`; `register` presents the inventory,
|
|
40
|
+
`convert` presents the drafts. Read, in this order:
|
|
41
|
+
|
|
42
|
+
- `warnings` and `manifest_provenance` - warnings label the evidence
|
|
43
|
+
itself (substitute-adapter mismatch; build-produced manifest).
|
|
44
|
+
Report them before any numbers, and act on the provenance warning
|
|
45
|
+
as `loom-core` directs.
|
|
46
|
+
- `summary.readiness` - the **readiness distribution** over the
|
|
47
|
+
project's non-controlled models: `ready_to_promote`,
|
|
48
|
+
`needs_authoring`, `not_draftable`, plus `readiness_transitions`
|
|
49
|
+
(how many unpromoted drafts changed account since the run that
|
|
50
|
+
wrote them). Readiness is derived, never stored.
|
|
51
|
+
- `summary` state counts - `controlled_by_loom`,
|
|
52
|
+
`known_not_controlled`, `unknown_to_loom`, plus
|
|
53
|
+
`seen_but_not_assessed` (non-model resources - snapshots, seeds,
|
|
54
|
+
tests, and the semantic layer: semantic models, metrics, saved
|
|
55
|
+
queries - counted, not assessed as models).
|
|
56
|
+
- `models[]` - each model's `state`, `draft_registration` where
|
|
57
|
+
drafted, `fit` (a generator proposal with drafted parameters and
|
|
58
|
+
evidence, or a `no_fit` reason), `readiness` (state, `basis` -
|
|
59
|
+
`fit` or `authoring` - `holes` of kind `authoring` or `evidence`,
|
|
60
|
+
and `blockers`), and `interface_flags`. Consumer counts are
|
|
61
|
+
decomposed by kind (`consumers_by_kind`), and the metrics and
|
|
62
|
+
saved queries reachable from a model via its semantic models are
|
|
63
|
+
*named* in its record and flags - treat semantic-layer consumers
|
|
64
|
+
as real consumers when sequencing a conversion.
|
|
65
|
+
|
|
66
|
+
On a large project, narrow any pass with `--select` (the shared
|
|
67
|
+
grammar in `loom-core`): selection narrows within the command's pool
|
|
68
|
+
and an unmatched selector errors by name. Present the readiness
|
|
69
|
+
distribution, the states and the per-model picture to the
|
|
70
|
+
practitioner. Nothing has been written yet.
|
|
71
|
+
|
|
72
|
+
## 2. Convert first - write the drafts
|
|
73
|
+
|
|
74
|
+
With the practitioner's go-ahead, preview, then write:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
loom convert --project-dir . --format json # preview
|
|
78
|
+
loom convert --project-dir . --write --format json # drafts_written
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Convert drafts for its pool - by default the models **unknown** to
|
|
82
|
+
Loom. Registered models are reported but not drafted: registration is
|
|
83
|
+
the withdrawal mechanism (step 3). `--include-registered` re-opens a
|
|
84
|
+
registered model for drafting when its circumstances change.
|
|
85
|
+
|
|
86
|
+
Convert writes **one draft class**. Every pooled model that earns a
|
|
87
|
+
draft - fit or layered no-fit - gets a single **unified draft** under
|
|
88
|
+
`local/drafts/`:
|
|
89
|
+
|
|
90
|
+
- a **draft record** `<model>.yml`: the generator and its basis
|
|
91
|
+
(`fit` - output shape matched a fit rule - or `authoring` - a
|
|
92
|
+
target generator named by layer), the evidence or the no-fit
|
|
93
|
+
reason, the pre-wired inputs and named authoring hole (authoring),
|
|
94
|
+
the declared interface copied verbatim, the readiness account,
|
|
95
|
+
`review_todos`, interface flags, and a `promotion` list;
|
|
96
|
+
- a **final-form instance document** `instances/<model>.yml`, in the
|
|
97
|
+
shape `metadata/instances/` consumes, carrying the drafted
|
|
98
|
+
parameters (for an authoring draft, the transform parameters
|
|
99
|
+
deliberately absent - the hole);
|
|
100
|
+
- where the project has no pattern for the generator, one
|
|
101
|
+
**synthesised pattern** `patterns/<generator>_pattern.yml` per
|
|
102
|
+
generator for the run. Its name is **final** - drafts reference it
|
|
103
|
+
from the start and promotion moves it unchanged; there is no
|
|
104
|
+
rename step.
|
|
105
|
+
|
|
106
|
+
`local/drafts/` is a review area - Loom never loads it, so an
|
|
107
|
+
incomplete draft cannot break validation. What a draft carries:
|
|
108
|
+
|
|
109
|
+
- **The declared interface, copied verbatim** - the exact input set,
|
|
110
|
+
an enforced contract's columns/types/constraints, dbt unit tests'
|
|
111
|
+
given/expect fixtures, generic tests, singular tests (copied with
|
|
112
|
+
their SQL), semantic models' entities/dimensions/measures, and
|
|
113
|
+
config facts. Copied, never inferred. It is the proof record of
|
|
114
|
+
what a swap must not lose.
|
|
115
|
+
- **Carriage** - a fit draft copies the model's manifest generic
|
|
116
|
+
tests into the instance's `generic_tests`, keyed on the model the
|
|
117
|
+
test is declared on, so they generate with the model after
|
|
118
|
+
promotion. Singular tests are files, not declarations; they travel
|
|
119
|
+
as files and are never copied into an instance.
|
|
120
|
+
- **Macro drafting, in tiers** (staging fits). A slot-shaped macro
|
|
121
|
+
call - one macro over one source column as the projection - drafts
|
|
122
|
+
as a `macro:` / `macro_args:` column item, but only after
|
|
123
|
+
verification against the manifest. References come in two forms: a
|
|
124
|
+
**bare name** is a project macro (validation verifies it); a
|
|
125
|
+
**`package.name`** reference names a package macro - validation
|
|
126
|
+
cannot resolve those and warns "unverified package macro" without
|
|
127
|
+
blocking, while `loom check` and `loom convert` verify existence
|
|
128
|
+
and arity against the manifest, and `loom generate` renders the
|
|
129
|
+
invocation exactly as declared. Where a call's arguments place the
|
|
130
|
+
source column somewhere other than first, the draft pins the
|
|
131
|
+
position with **`macro_column_param`** (the 1-based argument
|
|
132
|
+
position the column fills; `macro_args` carries the rest). A
|
|
133
|
+
slot-shaped call that cannot be verified is an **evidence hole**
|
|
134
|
+
naming the macro - never silently passed or dropped. Macro calls
|
|
135
|
+
in any other shape (inside a CTE, embedded in a larger expression)
|
|
136
|
+
are named in the no-fit reason and wait for authoring.
|
|
137
|
+
- **Named blockers, not silent losses** - a model carrying a row filter, a plain-SQL column expression or a row limit is `not_draftable` with the construct named, never a fit that drops it: a row filter (WHERE) - a fit draft cannot carry a filter, so a fit would silently drop it (the staging generator can express a filter as authored vocabulary under ADR 0038, but convert does not draft one).
|
|
138
|
+
|
|
139
|
+
An authoring draft is **not** a fit and the record says so: its
|
|
140
|
+
generator (staging → `staging`; intermediate → `normalized_table`, or
|
|
141
|
+
`view` when declared a view; mart → `fact` / `dimension` by
|
|
142
|
+
classification, else `mart`) names where the authoring is headed,
|
|
143
|
+
nothing about the model's shape.
|
|
144
|
+
|
|
145
|
+
## 3. Register the rest
|
|
146
|
+
|
|
147
|
+
Register describes the models the practitioner is keeping
|
|
148
|
+
hand-written - no-fit models they decide not to author toward a
|
|
149
|
+
generator, flagged models whose coordination is not worth a
|
|
150
|
+
conversion, stateful resources, and models whose bespoke SQL is
|
|
151
|
+
simply their right form:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
loom register --project-dir . --write --format json # registrations_written
|
|
155
|
+
loom validate --project-dir .
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Drafted registrations land in `local/registrations/` directly - that
|
|
159
|
+
*is* the load path; there is no promotion for a registration. Review
|
|
160
|
+
each draft with the practitioner: the classification and its
|
|
161
|
+
evidence, the harvested `description` and `owner` (verbatim from the
|
|
162
|
+
manifest where declared); fix any remaining `owner: TODO`, complete
|
|
163
|
+
any description the manifest did not document, correct any
|
|
164
|
+
mis-classification. Those completions are holes - propose,
|
|
165
|
+
practitioner ratifies, then edit.
|
|
166
|
+
|
|
167
|
+
Registration does three things: the model joins the inventory; it
|
|
168
|
+
**leaves the convert pool**, so later convert runs report it without
|
|
169
|
+
drafting it; and it stands as a complete, legitimate end state.
|
|
170
|
+
Deleting a registration returns the model to the unknown pool.
|
|
171
|
+
|
|
172
|
+
## 4. Work the draft - one at a time
|
|
173
|
+
|
|
174
|
+
Pick one draft with the practitioner. Never batch. The draft record's
|
|
175
|
+
readiness account tells you which kind of work it is.
|
|
176
|
+
|
|
177
|
+
**A fit draft asks for review:**
|
|
178
|
+
|
|
179
|
+
1. Read the record's `evidence`; it is the audit trail. Check the
|
|
180
|
+
drafted parameters in `local/drafts/instances/<model>.yml`
|
|
181
|
+
against the original SQL by eye - especially the staging column
|
|
182
|
+
mapping (items are `{source, alias, …}`; a rename inference is
|
|
183
|
+
recorded as such, verify it; a mapping the manifest's documented
|
|
184
|
+
columns could not pair is drafted as an unresolved placeholder,
|
|
185
|
+
`source: __TODO__` - draft each such mapping from the original
|
|
186
|
+
SQL, because `loom validate` rejects the sentinel and no
|
|
187
|
+
placeholder may survive promotion), a silver `dedup_order`, the
|
|
188
|
+
drafted macro calls (an unverifiable one sits in the account as
|
|
189
|
+
an evidence hole; the remedy is a current manifest or a corrected
|
|
190
|
+
call - not deleting the hole), the carried `generic_tests`, and
|
|
191
|
+
any `TODO` the draft could not derive. Fill `description` and
|
|
192
|
+
`owner` where they are TODOs. A fit certifies output shape, not
|
|
193
|
+
semantics - step 6's proof is what verifies a conversion.
|
|
194
|
+
2. Work the remaining named holes as proposals: answer each
|
|
195
|
+
`review_todos` item, and propose where the `source_rationale`
|
|
196
|
+
comments belong (keep, prune, or move into documentation) -
|
|
197
|
+
verbatim text, your placement proposal.
|
|
198
|
+
|
|
199
|
+
**An authoring draft asks for the transform:**
|
|
200
|
+
|
|
201
|
+
1. Check the copied facts against the model's SQL - layer, target
|
|
202
|
+
generator, pre-wired inputs (the consolidated `input_models`
|
|
203
|
+
list; a link target's `endpoints`, a vault target's `parent`
|
|
204
|
+
block), declared interface. A copy is not a verification; where
|
|
205
|
+
manifest and SQL disagree, the SQL is truth and the record is
|
|
206
|
+
stale - regenerate the manifest and re-assess.
|
|
207
|
+
2. Author the transform - the named hole. The record's
|
|
208
|
+
`authoring_hole` lists the target generator's required parameters
|
|
209
|
+
the draft could not wire; express the transform as those
|
|
210
|
+
parameters in `local/drafts/instances/<model>.yml`, labelled as
|
|
211
|
+
your proposal. Everything else was bookkeeping Loom already
|
|
212
|
+
copied. (For a `mart` target the hole is the nested `aggregation`
|
|
213
|
+
map - `group_by` plus measures with `column`/`expression`/`alias`;
|
|
214
|
+
a second graph input has no slot in a singular-input target -
|
|
215
|
+
express it inside a measure expression, restructure upstream, or
|
|
216
|
+
stop.) If the target generator cannot express the transform, stop:
|
|
217
|
+
the model stays hand-written and registered (step 3), the draft
|
|
218
|
+
remains its starting point. A valid end state - report it as one.
|
|
219
|
+
|
|
220
|
+
A staging instance also binds to its source, in exactly one of
|
|
221
|
+
two ways - declare one of them, never both, never neither (the
|
|
222
|
+
exactly-one rule is a validation error either way it is broken):
|
|
223
|
+
|
|
224
|
+
- `source_ref: {name, table}` - a pointer to a source the
|
|
225
|
+
practitioner has declared in their own schema YAML. Both keys
|
|
226
|
+
are required and no others are accepted; Loom emits nothing
|
|
227
|
+
for it, and the generated staging SQL references the declared
|
|
228
|
+
source.
|
|
229
|
+
- `inline_source: {name, table, ...}` - a declaration Loom
|
|
230
|
+
itself emits into the generated sources document. `name` and
|
|
231
|
+
`table` are required; `schema`, `loaded_at_field` and
|
|
232
|
+
`freshness` are optional (a `freshness` block takes
|
|
233
|
+
`warn_after` and/or `error_after`, each `{count, period}`).
|
|
234
|
+
|
|
235
|
+
(Medallion bronze takes `inline_source` only, and requires it.)
|
|
236
|
+
|
|
237
|
+
**Seed-fed staging drafts arrive with the source binding as a
|
|
238
|
+
hole.** A seed-fed staging model reaches convert as an authoring
|
|
239
|
+
draft, because every staging fit rule needs a dbt source
|
|
240
|
+
dependency and a seed read through `ref()` is not one; the draft
|
|
241
|
+
record names the seed dependencies the model actually has, and
|
|
242
|
+
the binding is the one authoring choice the draft cannot make
|
|
243
|
+
for you. A staging instance binds exactly one dbt source, so
|
|
244
|
+
choose how the seeds become a source. Either declare the seeds
|
|
245
|
+
as a dbt source in your own schema YAML and bind the instance
|
|
246
|
+
with `source_ref: {name, table}` (the source declaration stays
|
|
247
|
+
practitioner-owned YAML that you maintain by hand, Loom emits
|
|
248
|
+
nothing for it, and the generated staging SQL references the
|
|
249
|
+
declared source), or bind with `inline_source: {name, schema,
|
|
250
|
+
table}` (Loom generates the source declaration itself, into its
|
|
251
|
+
generated sources document, so the declaration is Loom-owned and
|
|
252
|
+
regenerates with the project instead of being maintained by
|
|
253
|
+
hand). Both are complete bindings, and the choice is only about
|
|
254
|
+
who owns the source declaration; it is the same exactly-one
|
|
255
|
+
choice every staging instance makes, and the metadata reference
|
|
256
|
+
states both shapes in full. The schema in an `inline_source`
|
|
257
|
+
binding is the warehouse schema the seed materialises into
|
|
258
|
+
under the project's dbt profile (the target schema, adjusted
|
|
259
|
+
by any `+schema` configured for the seed), not the name of the
|
|
260
|
+
seeds directory; if unsure, look the seed up in the manifest
|
|
261
|
+
(`target/manifest.json`), where each seed entry records the
|
|
262
|
+
schema it resolved to.
|
|
263
|
+
|
|
264
|
+
A computation on one column (an `amount / 100` division, say) is
|
|
265
|
+
carried by the macro slot: the column item is
|
|
266
|
+
`{source, alias, macro}`.
|
|
267
|
+
|
|
268
|
+
For a plain-SQL computation with no templating, staging columns
|
|
269
|
+
also take an `{expression, alias}` item; the expression-item
|
|
270
|
+
section of `references/metadata-reference.md` states the uniform
|
|
271
|
+
grammar and every surface that takes the form. Dimension `attribute_columns` and fact `measure_columns` take the same three item forms (a plain column name, a `{source, alias}` rename, or an `{expression, alias}` item); a rename's `source` is existence-checked against the input's output columns, an expression item's identifiers are practitioner-asserted and never checked, and scd2 `tracked_columns` and satellite `attribute_columns` stay name-only.
|
|
272
|
+
|
|
273
|
+
A row restriction is carried the same way, as authored vocabulary rather than as a fit: six generators take a `filter` parameter, a non-empty verbatim SQL predicate rendered into the generated model's WHERE clause (staging, silver, dimension, fact, scd2_dimension, normalized_table). Silver applies it before dedup ranking, scd2_dimension on the snapshot's input select, and changing a filter between loads is a population change; the filter parameter section of `references/metadata-reference.md` states the full surface and the incremental composition. Convert does not draft a filter, so a model carrying one is `not_draftable` with the construct named (see step 2).
|
|
274
|
+
|
|
275
|
+
## 5. Promote - move-only, practitioner only
|
|
276
|
+
|
|
277
|
+
Promotion is one pathway for both draft kinds, and it is
|
|
278
|
+
**move-only**: the draft was authored in final form, so nothing is
|
|
279
|
+
retyped, renamed or edited at promotion time. Confirm readiness
|
|
280
|
+
first - re-run `loom convert` and read the account and any
|
|
281
|
+
`readiness_transition`, rather than trusting remembered state. Then
|
|
282
|
+
the practitioner:
|
|
283
|
+
|
|
284
|
+
1. If the draft references a synthesised pattern
|
|
285
|
+
(`<generator>_pattern`), moves
|
|
286
|
+
`local/drafts/patterns/<generator>_pattern.yml` into
|
|
287
|
+
`metadata/patterns/` **unchanged**. Where several patterns already
|
|
288
|
+
exist for the generator, the instance document's `pattern:` is a
|
|
289
|
+
`TODO`: choosing among them happens *in the draft, before
|
|
290
|
+
promotion* - present the candidates; choosing is the
|
|
291
|
+
practitioner's.
|
|
292
|
+
2. Moves the instance document into `metadata/instances/`
|
|
293
|
+
unchanged, or merges its instance into an existing instances
|
|
294
|
+
document for the same pattern.
|
|
295
|
+
3. If the model has a registration, deletes
|
|
296
|
+
`local/registrations/<model>.yml` now, before the gate - while
|
|
297
|
+
both exist, `loom validate` fails with `duplicate inventory name`.
|
|
298
|
+
4. Gates on `loom validate`. The record's `promotion` list repeats
|
|
299
|
+
these steps for the specific draft in hand.
|
|
300
|
+
|
|
301
|
+
Your job in this step is the checklist and the readiness evidence -
|
|
302
|
+
not the moves.
|
|
303
|
+
|
|
304
|
+
## 6. Prove equivalence - before any swap, every time
|
|
305
|
+
|
|
306
|
+
Build the candidate in a **scratch copy** of the project: the
|
|
307
|
+
practitioner removes the hand-written model file in the copy (their
|
|
308
|
+
act - `loom generate` refuses to overwrite it, by design), then
|
|
309
|
+
`loom generate` to preview and `loom generate --write` to write,
|
|
310
|
+
`dbt seed` if applicable, and `dbt build` against the **same** source
|
|
311
|
+
data as the original. Building and proving happen in the
|
|
312
|
+
practitioner's environment; prepare the commands, the decisions and
|
|
313
|
+
the comparison, and run only what you are authorised to run there.
|
|
314
|
+
|
|
315
|
+
**The properties-document decision comes first.** `loom generate`
|
|
316
|
+
publishes a properties document for each generated model (under
|
|
317
|
+
`models/_generated/tests/`), and the project's schema YAML may still
|
|
318
|
+
describe the model too - dbt refuses a resource described twice.
|
|
319
|
+
Decide by what the schema entry carries:
|
|
320
|
+
|
|
321
|
+
- Only description, columns and tests that carriage or the generator
|
|
322
|
+
covers → keep Loom's generated document and delete the
|
|
323
|
+
practitioner's entry for the model.
|
|
324
|
+
- Content Loom does not generate (a semantic model defined alongside
|
|
325
|
+
the entry, generic tests kept by hand) → keep the schema YAML and
|
|
326
|
+
declare `skip_properties_document: true` in the instance params, so
|
|
327
|
+
generate stops publishing its document for that model. One
|
|
328
|
+
combination is refused by validation: `skip_properties_document`
|
|
329
|
+
**plus** a non-empty `generic_tests` - carriage needs the document
|
|
330
|
+
the opt-out suppresses. If a model needs both, the generic tests
|
|
331
|
+
are declared in the schema YAML by hand and the carried
|
|
332
|
+
`generic_tests` comes out of the instance. Never propose deleting
|
|
333
|
+
Loom's generated document after every generate as a workaround -
|
|
334
|
+
the opt-out is what it is for.
|
|
335
|
+
|
|
336
|
+
For an authored model, also confirm the declared interface held:
|
|
337
|
+
contract columns/types/constraints; the copied dbt unit tests as an
|
|
338
|
+
oracle (`dbt test` - `given` fixtures are inputs, `expect` the
|
|
339
|
+
required outputs); the generic tests passing.
|
|
340
|
+
|
|
341
|
+
**Then the static stage - `loom prove`.** `dbt compile` in the
|
|
342
|
+
original project and in the scratch copy; copy the two manifests to
|
|
343
|
+
the fixed names `local/equivalence/original.manifest.json` and
|
|
344
|
+
`local/equivalence/candidate.manifest.json` in the scratch copy
|
|
345
|
+
(copy each into place before the next dbt run overwrites `target/`);
|
|
346
|
+
run `loom prove --format json`. Per model the stage reports
|
|
347
|
+
**statically proven** (the warehouse comparison may be skipped - cite
|
|
348
|
+
the proof record, its JSON, in step 7), **inconclusive** (proceed to
|
|
349
|
+
the warehouse comparison exactly as before), or **not run** because
|
|
350
|
+
the manifest pair is absent. The stage only proves or abstains - it
|
|
351
|
+
never disproves - and candidate-only `__`-prefixed system columns are
|
|
352
|
+
declared differences by rule.
|
|
353
|
+
|
|
354
|
+
**Then, for every model the static stage did not prove, the warehouse
|
|
355
|
+
comparison** - candidate vs original over the original model's
|
|
356
|
+
columns: equal row counts; equal contents (ordered full-row selects,
|
|
357
|
+
or per-column checksums plus a whole-row hash count); generator-added
|
|
358
|
+
`__` system columns listed explicitly as accepted differences, never
|
|
359
|
+
allowed to mask a real one.
|
|
360
|
+
|
|
361
|
+
**Proof-data standard.** A proof is only as strong as its data. For
|
|
362
|
+
dedup-shaped conversions (silver, and any shape whose rule treats
|
|
363
|
+
keys specially) the data must contain duplicate keys (including the
|
|
364
|
+
rows the `dedup_order` tie-break decides), **null keys**, and
|
|
365
|
+
boundary rows (first/last row of each group, empty strings,
|
|
366
|
+
earliest/latest timestamps). A proof on null-free data cannot
|
|
367
|
+
distinguish a shape that keeps null-key rows from one that drops
|
|
368
|
+
them: if the data cannot supply such rows, record the proof as
|
|
369
|
+
**partial** in step 7 and name the cases it did not cover. A model
|
|
370
|
+
that cannot be built twice against identical data is not converted by
|
|
371
|
+
this pathway - it stays registered and hand-written.
|
|
372
|
+
|
|
373
|
+
## 7. Swap and record - practitioner only, on a pass
|
|
374
|
+
|
|
375
|
+
In the real project the practitioner: removes the hand-written model
|
|
376
|
+
file, runs `loom generate --write` (the step 6 properties-document
|
|
377
|
+
decision applies to this generate too), `dbt build`, and `loom
|
|
378
|
+
coverage` (`ok`, `missing_models`, `orphan_inventory_entries`,
|
|
379
|
+
`misplaced_generated_models`). If the model carried an enforced
|
|
380
|
+
contract or attached tests, they declare `contract_enforced` in its
|
|
381
|
+
Loom metadata (with `column_types` completing its types), per the
|
|
382
|
+
step 1 flags, and confirm the attached tests still run against the
|
|
383
|
+
generated model - generated data tests are additional where the
|
|
384
|
+
generator emits them, and **staging emits none**, so for a staging
|
|
385
|
+
conversion the kept or carried tests are the whole test story. The
|
|
386
|
+
registration was already deleted at promote time (step 5); a re-run
|
|
387
|
+
of `loom convert` now reports the model as controlled.
|
|
388
|
+
|
|
389
|
+
**Record the outcome** in the commit message: model, generator, the
|
|
390
|
+
equivalence evidence (the static proof record, or row counts and
|
|
391
|
+
checksums), any accepted differences, and - for a partial proof - the
|
|
392
|
+
cases it did not cover. Your job in this step is the checklist and
|
|
393
|
+
the evidence record - not the acts.
|
|
394
|
+
|
|
395
|
+
Use `loom plan --from <old> --to <new>` before a swap that renames or
|
|
396
|
+
replaces a model others depend on: it inventories direct/transitive
|
|
397
|
+
dependents and observed manifest consumers (decomposed by kind,
|
|
398
|
+
semantic layer included), plus its phased actions. External
|
|
399
|
+
(non-manifest) consumers are always unknown - say so.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Adoption playbook: bringing an existing dbt project onto Loom
|
|
2
|
+
|
|
3
|
+
This playbook is the adoption pathway (design: [ADR 0025](adr/0025-migration-pathway.md)). It is written for a practitioner working with an AI agent, and every step is review-first: the agent drafts, the practitioner decides, and no model is converted without an empirical equivalence proof.
|
|
4
|
+
|
|
5
|
+
The short version: **convert first, register the rest, promote only what is ready, prove each conversion against the original before swapping.** One assessment drives everything: `loom convert` drafts replacements for the models it can - a fit where a generator's shape matches, an authoring draft where none does - and `loom register` describes the models you decide to keep hand-written. Registering a model is also how you withdraw it from conversion: the convert pool is the models Loom does not know, so a registered model is never drafted unless you ask for it back. A hybrid project - some models generated, the rest registered and hand-written - is a legitimate permanent end state, not a waypoint.
|
|
6
|
+
|
|
7
|
+
Terms in this playbook follow [Loom terminology](terminology.md); command detail is the [commands reference](commands.md); mechanism is the [architecture document](architecture.md).
|
|
8
|
+
|
|
9
|
+
## Step 0 - Prerequisites
|
|
10
|
+
|
|
11
|
+
Producing `target/manifest.json` is the adoption's first wall: the assessment reads the manifest, not the warehouse, and everything downstream inherits its provenance. Before Step 1:
|
|
12
|
+
|
|
13
|
+
- **Install the dbt adapter for the project's profile** - the `type` of the active target in `profiles.yml` - in the environment that will run the parse.
|
|
14
|
+
- **Make the profile authenticatable, or substitute deliberately.** The profile must be able to authenticate for a parse, *or* parse with a substitute adapter (for example a local DuckDB profile) purely to produce the manifest. A substitute parse is blessed for assessment only: the assessment detects it - the manifest's recorded adapter will not match the project profile's adapter - and warns, because dialect-sensitive conclusions (fit shapes, interface flags, drafted configuration) then derive from the substitute adapter's parse, not the project's own toolchain. Nothing converted under the equivalence proof may rest on a substitute parse; the proof build uses the real profile. One abstention case: if the project's profile is fully templated - its adapter `type` itself resolved from `env_var(...)` - no profile adapter can be read, so the warning stays silent even when the manifest came from a substitute parse; silence on a templated profile is not evidence that the adapters match.
|
|
15
|
+
- **Match the dbt version.** Parse with a dbt that satisfies the project's `require-dbt-version`. Loom's tested line is dbt Core 1.12.x, and Loom reads manifest schemas v10–v12 (dbt Core 1.6–1.12); a manifest from outside that range is rejected, not misread.
|
|
16
|
+
- **Set the required env vars.** Profiles and packages routinely read `env_var(...)`; a parse in a shell without them fails before it starts. Set them in the parse command's environment.
|
|
17
|
+
- **Produce a current manifest with `dbt parse`.** The assessment consumes a *parse-produced* manifest: `dbt parse` does not compile model SQL, so the manifest's model nodes carry no compiled code and the fit rules read the authored SQL - the input fit calibration assumes. A manifest from `dbt build` / `dbt compile` is *build-produced*: its model nodes carry macro-expanded compiled SQL, the fit rules read that instead, and proposals can differ. The assessment declares which provenance it was given (`manifest_provenance` in the report, and a header line in text output) and warns on a build-produced manifest - if you see that warning, regenerate the manifest with `dbt parse` and re-run the assessment before acting on any fit proposal. (`dbt build` remains the right command for actually building the project; it is just not the manifest the assessment should consume.)
|
|
18
|
+
- **Check `git status` afterwards.** Pre-flight commands can write tracked files - `dbt deps` has been observed rewriting a tracked `package-lock.yml`. Restore anything the pre-flight touched that you did not intend to change.
|
|
19
|
+
- **Work on a branch.** Nothing in this playbook requires committing until the very end.
|
|
20
|
+
|
|
21
|
+
## Step 1 - Assess
|
|
22
|
+
|
|
23
|
+
One assessment, read through its two operations:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
loom register --project-dir .
|
|
27
|
+
loom convert --project-dir .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Both commands run the same assessment and report the same `summary`; `register` presents the inventory (states and drafted registrations), `convert` presents the drafts. Read the reports (human-readable text by default; agents and scripts should pass `--format json` for the structured payload):
|
|
31
|
+
|
|
32
|
+
- `summary` - leads with the **readiness distribution**: `ready_to_promote` (drafts with zero holes and zero blockers), `needs_authoring` (drafts whose named holes stand between the draft and promotion - *authoring* holes only the practitioner can supply, *evidence* holes Loom could not verify against the manifest) and `not_draftable` (no draft, or a named blocker no generator can express), counted across the project's non-controlled models, plus `readiness_transitions`: how many unpromoted drafts changed account since the run that wrote them. Expect the states to differ by a lot: converting is only for the few whose shape is a closed generator shape, authoring drafts cover most of the rest, and registering is for every model a practitioner decides to keep hand-written. The summary then breaks project models down by state: `controlled_by_loom` (a generated instance exists; Loom generates the model), `known_not_controlled` (a practitioner registration exists; Loom knows the model but does not generate it), and `unknown_to_loom` (manifest only) - plus drafted registrations, flagged models, and `seen_but_not_assessed`: counts of the non-model resources (snapshots, seeds, tests, semantic models, metrics, saved queries, …) the assessment saw in the manifest but does not assess as models. Model counts are model counts; a stateful snapshot in the project shows up here even though it is never proposed for conversion. In `loom convert`'s text output the conversion drafts are listed first, so the convert yield can be triaged without reading every registration.
|
|
33
|
+
- Per model: its `state`; for unknown models, `draft_registration` (classification + the evidence for it); for unknown and registered models, `fit` (a generator proposal with drafted parameters and recorded evidence, or a `no_fit` reason naming the blocking signals); for a no-fit model in a dbt layer, an authoring draft (the layer, the target generator, the pre-wired inputs, the declared interface, the authoring hole); and `interface_flags`. Controlled models are summarised in text output - they need no action.
|
|
34
|
+
|
|
35
|
+
If the report carries a `warnings` list (printed as `Warning:` lines ahead of the model sections in text mode), read it first: it records a provenance problem with the manifest itself - currently the adapter mismatch, meaning the manifest came from a substitute-adapter parse (Step 0) and dialect-sensitive conclusions in this report derive from the substitute, not the project's own toolchain. A warning never blocks the assessment; it labels the evidence.
|
|
36
|
+
|
|
37
|
+
Treat the flags as conversion blockers until addressed:
|
|
38
|
+
|
|
39
|
+
- **Enforced dbt contract** - a Loom-generated replacement drops enforcement unless `contract_enforced` is declared in the Loom metadata for the replacement (with `column_types` completing its types, ADR 0032). Declare it, or accept the drop explicitly.
|
|
40
|
+
- **Public/protected access (Mesh)** - external projects may depend on the interface; a conversion is a coordinated change.
|
|
41
|
+
- **Attached tests / downstream consumers** - the swap must keep or deliberately replace them; consumers are listed by `loom plan` in detail. Counts include semantic models, metrics, saved queries and unit tests from the manifest.
|
|
42
|
+
- **Incremental / ephemeral materialization** - a Loom-generated replacement carries no incremental configuration, so converting an incremental model would silently turn its builds into full rebuilds, and converting an ephemeral model would persist a model that is never persisted today. Keep the model hand-written, or accept the change explicitly. One exception: where a bronze or silver conversion draft exists, the model's declared `unique_key` / `incremental_strategy` / `on_schema_change` are drafted into the draft's `incremental` block and this flag does not fire - review the drafted block like any other drafted parameter instead.
|
|
43
|
+
- **Group / deprecation date** - group membership gates cross-group Mesh references and cannot be re-declared in Loom metadata today; re-declare the group (and any deprecation date) by hand after converting.
|
|
44
|
+
- **Grants / physical configuration** - grants, partitioning, clustering and similar warehouse configuration are not carried by a conversion; re-apply them by hand or warehouse cost, performance and access control may silently regress.
|
|
45
|
+
|
|
46
|
+
Documented models keep their documentation: a manifest description is drafted into the registration and the conversion draft, an owner declared in the manifest's `meta.owner` is drafted with it, and documented column descriptions and types are drafted into the `column_docs` / `column_types` parameters (the types as declared, marked UNVERIFIED in the draft evidence). The source model's SQL comments - all of them, verbatim, in source order - are carried into the conversion draft as `source_rationale`; prune what does not belong and decide where that rationale belongs before swapping.
|
|
47
|
+
|
|
48
|
+
## Step 2 - Convert first: write the drafts
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
loom convert --project-dir . --write
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Convert drafts for the models in its pool - by default the models **unknown** to Loom (`unknown_to_loom`). Registered models are counted and reported but not drafted: registration is the withdrawal mechanism, and it is how the models you have already decided to keep hand-written stay out of the drafting pool. `--include-registered` drafts registered models too, when you deliberately re-open one.
|
|
55
|
+
|
|
56
|
+
Convert writes **one draft class**. Every pooled model that earns a draft - a model with a fit proposal, and every model with no fit that sits in a dbt layer (on a real project, most of them) - gets a single unified draft under `local/drafts/`:
|
|
57
|
+
|
|
58
|
+
- a draft record `<model>.yml`: the generator and its basis (`fit` - the model's output shape matched a fit rule - or `authoring` - a target generator named by layer for a model no rule matched), the evidence basis per parameter (for a fit) or the no-fit reason, the layer, the pre-wired inputs and the named authoring hole (for an authoring draft), the declared interface copied verbatim, the model's **readiness account** (its state, its holes, its blockers), a `review_todos` list, the interface flags, and a `promotion` list spelling out exactly how to adopt the draft;
|
|
59
|
+
- a final-form instance document `instances/<model>.yml`, in the pattern/instance shape `metadata/instances/` consumes, carrying the drafted parameters (for an authoring draft, the inputs wired from the manifest dependency graph and the transform parameters deliberately absent - the hole) and referencing the pattern to use - an existing one where the project has exactly one for the generator, otherwise the synthesised pattern;
|
|
60
|
+
- when the project has no pattern for the generator, one synthesised pattern definition `patterns/<generator>_pattern.yml` per generator for the whole run, in the shape `metadata/patterns/` consumes, with the required params taken from the generator contract. Its name is **final**: drafts reference it from the start and promotion moves it unchanged - there is no rename step.
|
|
61
|
+
|
|
62
|
+
That directory is a review area - Loom never loads it as inventory, so an incomplete draft cannot break validation. Drafts are authored in final form, so once the review todos are completed, promotion is a file move plus registration cleanup - no content edits at promotion time (Step 5). Readiness is derived, never stored in the instance: every assessment run re-derives each unpromoted draft's account from its current content plus current evidence, and a change is reported as a readiness transition.
|
|
63
|
+
|
|
64
|
+
What a draft carries, beyond the fitted parameters:
|
|
65
|
+
|
|
66
|
+
- **The declared interface, copied verbatim** - bookkeeping the project has already declared: the exact input set, an enforced contract's columns/types/constraints, dbt unit tests' given/expect fixtures, generic tests, singular tests (copied with their SQL), semantic models' entities/dimensions/measures, and config facts (materialization, partition/cluster keys, access, group). Copied, never inferred. The declared interface is standard furniture of every draft, fits included - it is the proof record of what a swap must not lose.
|
|
67
|
+
- **Carriage** - a fit draft also copies the model's manifest generic tests into the instance's `generic_tests`, so the tests generate with the model after promotion instead of needing to be re-declared by hand. The copy is keyed on the model the test is declared on (a `relationships` test lands on its declaring model, not its target), drops the arguments dbt injects itself, and preserves a declared warn severity. Singular tests are bespoke SQL files, not declarations, and are never copied - they travel as files.
|
|
68
|
+
- **Macro drafting, in tiers.** A staging fit whose select items are macro calls in the slot shape - one macro invocation over one source column, as the projection - is verified against the manifest (the macro exists; the argument count fits its declared signature) and drafted as a `macro:` / `macro_args:` column; a call whose arguments place the source column elsewhere drafts with `macro_column_param` naming the argument position. A slot-shaped call that cannot be verified is an **evidence hole** naming the macro - it blocks readiness, and it is never silently passed or silently dropped. Macro calls in any other shape (inside a CTE, embedded in a larger expression) are named in the no-fit reason; they wait for authoring, not for a wider fit rule.
|
|
69
|
+
- **Named blockers, not silent losses.** A model carrying a row filter, a plain-SQL column expression or a row limit is `not_draftable` with the construct named, never a fit that drops it: a row filter (WHERE) - a fit draft cannot carry a filter, so a fit would silently drop it (the staging generator can express a filter as authored vocabulary under ADR 0038, but convert does not draft one).
|
|
70
|
+
|
|
71
|
+
An authoring draft is **not** a fit and the record says so: its generator (staging → `staging`; intermediate → `normalized_table`, or `view` when the model is declared a view; mart → `fact` / `dimension` by classification, else `mart`) names where the authoring is headed, nothing about the model's shape.
|
|
72
|
+
|
|
73
|
+
## Step 3 - Register the rest
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
loom register --project-dir . --write
|
|
77
|
+
loom validate --project-dir .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Register describes the models you are keeping hand-written: the no-fit models you decide not to author toward a generator, the flagged models (Step 1) whose coordination is not worth a conversion, stateful resources, and any model whose bespoke SQL is simply its right form. Drafted registrations land in `local/registrations/` (never overwriting an existing file). A model's declared description and `meta.owner` are drafted in verbatim where the manifest carries them; review each draft: fix any remaining `owner: TODO`, complete any description the manifest did not document, correct any mis-classification.
|
|
81
|
+
|
|
82
|
+
Registration does three things: the model joins the inventory (searchable, covered, counted); it leaves the convert pool, so future `loom convert` runs report it without drafting it; and it stands as a complete, legitimate end state - most projects should live with most models registered for a while, and many for good. Deleting a registration returns the model to the unknown pool and to future drafting. `--include-registered` on convert re-opens a registered model for drafting without deleting anything, for when a model's circumstances change.
|
|
83
|
+
|
|
84
|
+
## Step 4 - Work the draft
|
|
85
|
+
|
|
86
|
+
Work one draft at a time. The draft record's readiness account tells you which kind of work it is. The [metadata reference](metadata-reference.md) annotates every parameter and nested shape the drafts use, in place in its worked examples, when a draft asks for a value this playbook does not spell out.
|
|
87
|
+
|
|
88
|
+
**A fit draft** asks for review, in this order:
|
|
89
|
+
|
|
90
|
+
1. Read the record's `evidence` array; it is the audit trail for the fit. Check the drafted parameters (in the instance document under `local/drafts/instances/`) against the original SQL by eye - especially the staging column mapping (a rename inference is recorded as such; verify it; a mapping the manifest's documented columns could not pair is drafted as an unresolved placeholder, `source: __TODO__`, naming the output in the record's review todos - draft each such mapping from the original SQL, because `loom validate` rejects the sentinel and no placeholder may survive promotion), a silver `dedup_order` drafted from the original dedup's ORDER BY, the drafted macro calls (each was verified against the manifest; an unverifiable one is sitting in the account as an evidence hole, and the remedy is a current manifest or a corrected call - not deleting the hole), and any `TODO` the draft could not derive. Fill `description` and `owner`. A fit certifies the model's output shape, not its semantics - the equivalence proof in Step 6 is what verifies a conversion.
|
|
91
|
+
|
|
92
|
+
**An authoring draft** asks for the transform, in this order:
|
|
93
|
+
|
|
94
|
+
1. Check the copied facts against the model's SQL: the layer and target generator, the pre-wired inputs (copied from the manifest dependency graph - what dbt resolved), and the declared interface. A copy is not a verification; where the manifest and the SQL disagree, the SQL is the truth and the record is stale - regenerate the manifest and re-assess.
|
|
95
|
+
2. Author the transform - the named hole. The record's `authoring_hole` lists the target generator's required parameters the draft could not wire. Express the model's transform as those parameters in `local/drafts/instances/<model>.yml`. This is the one authoring task; everything else about the model was bookkeeping. If the target generator cannot express the transform, stop here: the model stays hand-written and registered (Step 3), and the draft remains its starting point for a later attempt. That is a valid end state, not a failure.
|
|
96
|
+
|
|
97
|
+
For a `mart` target the hole is the `aggregation` parameter, a nested map - `group_by` lists the key columns, and each measure is an entry with `column`, `expression` and `alias`:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
aggregation:
|
|
101
|
+
group_by: [customer_id]
|
|
102
|
+
measures:
|
|
103
|
+
- {column: order_id, expression: "COUNT(order_id)", alias: order_count}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A second graph input has no slot in a singular-input target: express what you need from it inside a measure expression (a scalar subquery, for instance), restructure the model upstream, or stop - the contract is not stretched by hand.
|
|
107
|
+
|
|
108
|
+
For a `staging` target fed by dbt seeds, the hole includes the source binding. A seed-fed staging model reaches convert as an authoring draft, because every staging fit rule needs a dbt source dependency and a seed read through `ref()` is not one; the draft record names the seed dependencies the model actually has, so the decision below is the one authoring choice the draft cannot make for you. A staging instance binds exactly one dbt source, so choose how the seeds become a source. Either declare the seeds as a dbt source in your own schema YAML and bind the instance with `source_ref: {name, table}`: the source declaration stays practitioner-owned YAML that you maintain by hand, Loom emits nothing for it, and the generated staging SQL references the declared source. Or bind with `inline_source: {name, schema, table}`: Loom generates the source declaration itself, into its generated sources document, so the declaration is Loom-owned and regenerates with the project instead of being maintained by hand. Both are complete bindings, and the choice is only about who owns the source declaration; it is the same exactly-one choice every staging instance makes, and the metadata reference states both shapes in full. The schema in an `inline_source` binding is the warehouse schema the seed materialises into under the project's dbt profile (the target schema, adjusted by any `+schema` configured for the seed), not the name of the seeds directory; if unsure, look the seed up in the manifest (`target/manifest.json`), where each seed entry records the schema it resolved to.
|
|
109
|
+
|
|
110
|
+
## Step 5 - Promote
|
|
111
|
+
|
|
112
|
+
Promotion is one pathway for both draft kinds, and it is **move-only**: the draft was authored in final form, so nothing is retyped, renamed or edited at promotion time. A draft is promotable when its readiness account shows no remaining holes - re-run `loom convert` and read the account (and any transition) rather than trusting the state you remember.
|
|
113
|
+
|
|
114
|
+
1. If the draft references a synthesised pattern (`<generator>_pattern`), move `local/drafts/patterns/<generator>_pattern.yml` into `metadata/patterns/` unchanged - its name is final and the instance document already references it. Where several patterns already exist for the generator, the instance document's `pattern:` is a `TODO`: choosing among them is yours - a choice you make in the draft, before promotion - and the document is not loadable until you set it.
|
|
115
|
+
2. Move the instance document into `metadata/instances/` unchanged, or merge its instance into an existing instances document for the same pattern.
|
|
116
|
+
3. If the model has a registration (you registered it in an earlier pass, or it was drafted under `--include-registered`), delete `local/registrations/<model>.yml` now, before the gate: the promoted instance replaces the registration as the model's inventory record, and while both exist `loom validate` fails with `duplicate inventory name`. The registration is retired here, not at swap time.
|
|
117
|
+
4. Gate on `loom validate`. The record's `promotion` list repeats these steps for the specific draft in hand.
|
|
118
|
+
|
|
119
|
+
## Step 6 - Prove equivalence
|
|
120
|
+
|
|
121
|
+
Promotion puts the instance in the inventory; the swap (Step 7) waits on proof. Build the candidate in a scratch copy of the project: remove the hand-written model file in the copy (an explicit act - `loom generate` will refuse to overwrite it, by design), run `loom generate --write`, then `dbt seed` (if applicable) and `dbt build` in the copy against the **same** source data as the original.
|
|
122
|
+
|
|
123
|
+
**The properties-document decision comes first**, because the candidate build hits it immediately: `loom generate` publishes a properties document describing the generated model (under `models/_generated/tests/`), and your schema YAML may still describe the model too - dbt refuses a resource described twice. Decide by what your schema entry carries:
|
|
124
|
+
|
|
125
|
+
- **Only description, columns and tests that carriage or the generator covers** → keep Loom's generated document and delete your entry for the model. Carried generic tests (Step 2) generate inside Loom's document; generated data tests live there too.
|
|
126
|
+
- **Content Loom does not generate** - a semantic model defined alongside the entry, or generic tests you keep declaring by hand - → keep your schema YAML and declare `skip_properties_document: true` in the model's instance params, so `loom generate` stops publishing its document for the model instead of you deleting it after every generate. One combination is refused by validation: the opt-out plus a non-empty `generic_tests` - carriage needs the document the opt-out suppresses. If a model needs both, declare the generic tests in your schema YAML by hand, drop the carried `generic_tests` from the instance, and the opt-out stands.
|
|
127
|
+
- For an authored (ex-fit or ex-authoring-draft) model, also confirm the declared interface held: the contract's columns, types and constraints; the dbt unit tests as a ready-made oracle (`dbt test` - their given fixtures are the inputs and their expect fixtures the required outputs); the generic tests passing.
|
|
128
|
+
|
|
129
|
+
**Then the static stage.** `dbt compile` in the original project and in the scratch copy; copy the two manifests to the fixed names `local/equivalence/original.manifest.json` and `local/equivalence/candidate.manifest.json` in the scratch copy (copy each manifest into place before the next dbt run overwrites `target/`); run `loom prove`. The stage compares the two compiled forms under normalisation and reports one of three outcomes per model: **statically proven** (the swap may skip the warehouse comparison below - record the proof record, its JSON output, with the conversion in Step 7), **inconclusive** (proceed to the warehouse comparison exactly as before - the warehouse decides), or no verdict because the manifest pair is absent (the stage did not run). The stage never disproves anything; candidate-only `__`-prefixed system columns are declared differences by rule, not mismatches.
|
|
130
|
+
|
|
131
|
+
**Then, for every model the static stage did not prove, the warehouse comparison** - the candidate's table against the original's, over the original model's columns:
|
|
132
|
+
|
|
133
|
+
- equal row counts;
|
|
134
|
+
- equal contents - e.g. in DuckDB, compare ordered full-row selects, or a per-column checksum such as `sum(hash(col::varchar))` per column plus a whole-row hash count;
|
|
135
|
+
- generator-added columns (`__loaded_at` provenance) are expected additions: list them explicitly as accepted differences, do not let them mask a real one.
|
|
136
|
+
|
|
137
|
+
**Proof-data standard.** The proof is only as strong as the data it runs over. For a dedup-shaped conversion (silver, and any shape whose rule treats keys specially) the data must contain the rows the shape exists to handle: duplicate keys - including the cases the `dedup_order` tie-break decides - **null keys**, and boundary rows (the first and last row of each group, empty strings, earliest and latest timestamps). A proof run on null-free data is insufficient evidence for a swap: a shape that retains null-key rows and one that drops them produce identical output on data that has none, so the proof cannot tell them apart. If the available data cannot supply such rows, record the proof as partial in Step 7 and name the cases it did not cover. A model that cannot be built twice against identical data (snapshots and other stateful resources) is **not** converted by this pathway - it stays registered and hand-written.
|
|
138
|
+
|
|
139
|
+
## Step 7 - Swap and record
|
|
140
|
+
|
|
141
|
+
Swap only on a pass. In the real project: remove the hand-written model file, run `loom generate --write` (the Step 6 properties-document decision applies to this generate too), `dbt build`, and `loom coverage`. If the model carried an enforced contract or attached tests, declare `contract_enforced` in its Loom metadata, per the Step 1 flags, and confirm the model's attached tests still run against the generated model and cover the intent. Generated data tests are additional where the generator emits them; **staging emits none**, so for a staging conversion the kept schema-YAML tests - or the carried ones - are the whole test story. The model's registration was already deleted at promote time (Step 5); the instance is its inventory record now. A re-run of `loom convert` reports the model as controlled.
|
|
142
|
+
|
|
143
|
+
**Record the outcome** in the commit message: model, generator, the equivalence evidence (the static proof record, or row counts and checksums), any accepted differences, and - for a partial proof - the cases it did not cover.
|
|
144
|
+
|
|
145
|
+
## After adoption - living on Loom
|
|
146
|
+
|
|
147
|
+
- **Adding a test to a generated model.** Generic (data) tests are declared in the instance's `generic_tests` and generate into the model's properties document; `loom create-new data-test <model> --test <name> [--column <col>]` prints the declaration (for a hand-written model it prints the dbt-YAML form instead). `loom create-new unit-test <model>` prints a unit-test declaration the same way. A test whose arguments Loom cannot know is printed with a `__TODO__` sentinel the validator rejects - an unfinished declaration can never validate silently. Remember the one refused combination: `generic_tests` on a model that declares `skip_properties_document`.
|
|
148
|
+
- **New hand-written models** enter the inventory as `unknown_to_loom` at the next assessment; convert drafts for them, register keeps them out - the same fork as Step 2/Step 3, taken per model as the project evolves.
|
|
149
|
+
- **Re-assessment is cheap and derived.** Readiness is recomputed every run from the drafts' current content, so completing a draft's holes, refreshing the manifest, or deleting a registration shows up as a readiness transition in the next report rather than as stale stored state.
|
|
150
|
+
|
|
151
|
+
## What not to do
|
|
152
|
+
|
|
153
|
+
- Do not force a fit. A `no_fit` with named signals is a successful assessment outcome: bespoke models stay hand-written and registered. The pathway's value is the inventory plus the provable conversions, not a conversion count.
|
|
154
|
+
- Do not call an authoring draft a fit, or its target generator a match. An authoring draft is a starting instance with the transform left as the author's work; treating the destination as a shape claim would recreate, for the majority of models, exactly the false-fit failure the fit rules were tightened against. If the target generator cannot express the transform, the draft stays a draft.
|
|
155
|
+
- Do not delete Loom's generated properties document after every `loom generate` to keep your own schema YAML - declare `skip_properties_document` on the instance; that is what the opt-out is for. And do not pair the opt-out with `generic_tests` carriage; validation refuses the pair because the carriage has nowhere to land.
|
|
156
|
+
- Do not treat an evidence hole as an authoring hole. An authoring hole is yours to fill; an evidence hole names something Loom could not verify - the remedy is a current manifest or a corrected declaration, after which the same draft re-derives clean.
|
|
157
|
+
- Do not let an agent apply `--force`, delete hand-written files, or skip the equivalence build. Those are the practitioner's acts.
|
|
158
|
+
- Do not convert a flagged model (contract/Mesh/tests) in the same pass as an unflagged one; flags mean coordination, and mixing them hides the coordination in routine diffs.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
name: customer_order_counts
|
|
2
|
+
classification: mart
|
|
3
|
+
description: Number of orders per customer, hand-written over the generated hub and staging models.
|
|
4
|
+
owner: analytics
|
|
5
|
+
tags: [orders, reporting]
|
|
6
|
+
upstream: Customer hub and order staging
|
|
7
|
+
depends_on:
|
|
8
|
+
- hub_customer
|
|
9
|
+
- stg_orders
|
|
10
|
+
keys:
|
|
11
|
+
- customer_id
|
|
12
|
+
business_key: customer_id
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
pattern: staging
|
|
2
|
+
instances:
|
|
3
|
+
- name: stg_customers
|
|
4
|
+
params:
|
|
5
|
+
description: Customer staging from raw seed data.
|
|
6
|
+
source_ref:
|
|
7
|
+
name: raw
|
|
8
|
+
table: customers
|
|
9
|
+
columns:
|
|
10
|
+
- source: id
|
|
11
|
+
alias: customer_id
|
|
12
|
+
- source: customer_name
|
|
13
|
+
alias: customer_name
|
|
14
|
+
- source: customer_email
|
|
15
|
+
alias: customer_email
|
|
16
|
+
|
|
17
|
+
- name: stg_orders
|
|
18
|
+
params:
|
|
19
|
+
description: Order staging from raw seed data.
|
|
20
|
+
source_ref:
|
|
21
|
+
name: raw
|
|
22
|
+
table: orders
|
|
23
|
+
columns:
|
|
24
|
+
- source: id
|
|
25
|
+
alias: order_id
|
|
26
|
+
- source: customer_id
|
|
27
|
+
alias: customer_id
|
|
28
|
+
- source: order_date
|
|
29
|
+
alias: order_date
|