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.
Files changed (74) hide show
  1. loom/.agents/skills/loom-adoption/SKILL.md +399 -0
  2. loom/.agents/skills/loom-adoption/references/adoption-playbook.md +158 -0
  3. loom/.agents/skills/loom-adoption/references/first-project/local/registrations/customer_order_counts.yml +12 -0
  4. loom/.agents/skills/loom-adoption/references/first-project/metadata/instances/hubs.yml +7 -0
  5. loom/.agents/skills/loom-adoption/references/first-project/metadata/instances/staging.yml +29 -0
  6. loom/.agents/skills/loom-adoption/references/first-project/metadata/patterns/current_hub.yml +8 -0
  7. loom/.agents/skills/loom-adoption/references/first-project/metadata/patterns/staging.yml +8 -0
  8. loom/.agents/skills/loom-adoption/references/first-project/models/sources.yml +25 -0
  9. loom/.agents/skills/loom-adoption/references/metadata-reference.md +347 -0
  10. loom/.agents/skills/loom-ask/SKILL.md +241 -0
  11. loom/.agents/skills/loom-authoring/SKILL.md +125 -0
  12. loom/.agents/skills/loom-authoring/references/metadata-reference.md +347 -0
  13. loom/.agents/skills/loom-bootstrap/SKILL.md +136 -0
  14. loom/.agents/skills/loom-bootstrap/references/first-project/local/registrations/customer_order_counts.yml +12 -0
  15. loom/.agents/skills/loom-bootstrap/references/first-project/metadata/instances/hubs.yml +7 -0
  16. loom/.agents/skills/loom-bootstrap/references/first-project/metadata/instances/staging.yml +29 -0
  17. loom/.agents/skills/loom-bootstrap/references/first-project/metadata/patterns/current_hub.yml +8 -0
  18. loom/.agents/skills/loom-bootstrap/references/first-project/metadata/patterns/staging.yml +8 -0
  19. loom/.agents/skills/loom-bootstrap/references/first-project/models/sources.yml +25 -0
  20. loom/.agents/skills/loom-core/SKILL.md +260 -0
  21. loom/.agents/skills/loom-core/references/commands.md +378 -0
  22. loom/.agents/skills/loom-core/references/terminology.md +88 -0
  23. loom/.agents/skills/loom-maintenance/SKILL.md +100 -0
  24. loom/__init__.py +27 -0
  25. loom/__main__.py +5 -0
  26. loom/check.py +411 -0
  27. loom/cli.py +3865 -0
  28. loom/contracts.py +723 -0
  29. loom/declared_generic_tests.py +319 -0
  30. loom/declared_unit_tests.py +387 -0
  31. loom/dialects.py +545 -0
  32. loom/equivalence.py +490 -0
  33. loom/errors.py +2 -0
  34. loom/generator_registry.py +129 -0
  35. loom/inventory.py +1021 -0
  36. loom/inventory_corpus.py +1182 -0
  37. loom/inventory_patterns.py +371 -0
  38. loom/inventory_records.py +2676 -0
  39. loom/inventory_relationships.py +1559 -0
  40. loom/manifest.py +597 -0
  41. loom/migration_assessment.py +5794 -0
  42. loom/migration_plan.py +327 -0
  43. loom/output_columns.py +624 -0
  44. loom/output_layout.py +172 -0
  45. loom/project_files.py +260 -0
  46. loom/properties.py +308 -0
  47. loom/renderers/__init__.py +53 -0
  48. loom/renderers/base.py +491 -0
  49. loom/renderers/bridge.py +115 -0
  50. loom/renderers/bronze.py +104 -0
  51. loom/renderers/dimension.py +163 -0
  52. loom/renderers/fact.py +211 -0
  53. loom/renderers/gold.py +284 -0
  54. loom/renderers/hub.py +304 -0
  55. loom/renderers/link.py +351 -0
  56. loom/renderers/mart.py +92 -0
  57. loom/renderers/normalized_table.py +118 -0
  58. loom/renderers/obt.py +116 -0
  59. loom/renderers/pit.py +107 -0
  60. loom/renderers/reference.py +220 -0
  61. loom/renderers/satellite.py +692 -0
  62. loom/renderers/scd2_dimension.py +341 -0
  63. loom/renderers/silver.py +204 -0
  64. loom/renderers/staging.py +217 -0
  65. loom/renderers/view.py +87 -0
  66. loom/reporting.py +2862 -0
  67. loom/selection.py +1544 -0
  68. loom/similarity.py +452 -0
  69. loom/source_structure.py +481 -0
  70. loom_dbt-0.1.0.dist-info/METADATA +192 -0
  71. loom_dbt-0.1.0.dist-info/RECORD +74 -0
  72. loom_dbt-0.1.0.dist-info/WHEEL +4 -0
  73. loom_dbt-0.1.0.dist-info/entry_points.txt +3 -0
  74. 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,7 @@
1
+ pattern: current_hub
2
+ instances:
3
+ - name: hub_customer
4
+ params:
5
+ description: Stable customer identity table.
6
+ input_models: [stg_customers]
7
+ 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