Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The playbook contains contradictory commands and overstates what local comparisons and row-count parity prove.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds guidance for validating dbt migrations against local production-backed data.
Changes:
- Adds a migration-validation skill and profile.
- Clarifies incremental/full-refresh guidance.
- Registers the new skill in agent configuration.
File summaries
| File | Description |
|---|---|
skills/data/ol-dbt-migration-validation/SKILL.md |
Adds the validation playbook. |
skills/data/ol-dbt-local-dev/SKILL.md |
Updates local-development rules. |
agent-config.toml |
Registers the skill and migration profile. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 5
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
blarghmatey
left a comment
There was a problem hiding this comment.
Looks good, but worth addressing copilot's comments before merging.
Copilot review on #2659 found five overclaims. All five are valid. - The incremental trap was framed as "an unchanged key set makes the run a no-op". dbt does execute the model SQL; what varies is which rows the model's own is_incremental() predicate reselects. Reframed around the predicate, with the two real failure modes (excluded rows keep old-code values; a predicate that matches everything yields a true no-op) and the SCD2 consequence that dim_course_run appends a second generation rather than replacing the first. Stopped asserting a root cause for #2403's 0.11s no-op that was never established — the lesson is not to reason from the predicate at all. - "Both sides read the identical polluted source and the duplication cancels" is false exactly where a #2072 migration re-points ref(), which is every migration PR. override_ref resolves each unbuilt ref() to its own glue__ view with independent __dbt_tmp pollution, and joins/filters mean the two do not offset. Added: build the divergent upstreams locally, or label the result unverified source noise rather than accepting/rejecting on the registration artifact the section warns about. - Step 3 used `dbt build`, contradicting the cautious-test section below it and risking an unmaterialized comparison side when a test fails. Now `dbt run`. - "Row-count delta 0 means the grain is intact" — dropped rows offset by duplicates net to zero too. Now count parity, with a count(*) = count(distinct key) check on both relations. - Step 6's distinct-pair assertion cancels duplication but not rows a polluted view is missing; scoped accordingly. Also propagated the incremental correction to ol-dbt-local-dev, including its two worked examples, which both rebuilt without --full-refresh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran the corrections against dbt instead of leaving them derivedThe five fixes in eba58e6 were reasoned from reading The incremental claim — the original skill was empirically wrongBuilt three purpose-made incremental models (isolated
Rows 1 and 2 have an identical unchanged key set and opposite outcomes. That falsifies "an incremental run whose key set is unchanged is a no-op" directly, and confirms your "delete+insert can replace an existing key when the model emits it." What separates them is only the predicate — which is what the text now says. Row 3 confirms the SCD2 behaviour I added: the prior row is expired and retained, the new derivation appended with a fresh And the genuine no-op: re-running
|
| view | rows | distinct pk | ratio |
|---|---|---|---|
dim_ocw_resource |
179,863 | 2,908 | 61.85x |
dim_course_run |
10,884 | 8,508 | 1.28x |
dim_course |
4,321 | 4,071 | 1.06x |
| 24 others | — | — | 1.00x |
dim_discussion_topic, dim_video |
— | — | 404, pointer rot |
A 1.00x view and a 61.85x view in the same layer and the same registration. Duplication can only cancel between identical views — which is precisely what a migration that re-points ref() does not have. Your objection was correct and the old wording would have licensed accepting or rejecting a migration on that artifact.
Incidentally, the view the skill cited for 31x now 404s — the __dbt_tmp path was cleaned up since 2026-09-09. That is the pointer-rot half of the same claim, observed live.
Also re-measured
Pollution table five days on: 651/667 non-raw views (97.6%), every layer but staging identical to the 2026-09-09 figures — it is the steady state, as claimed.
Scope
The grain fix (count parity ≠ grain integrity) is arithmetic, not a dbt behaviour; confirmed with a 5-row counterexample where the delta is 0 while one key is dropped and another fanned out.
I did not rebuild the real dim_course_run or any production-mirror model — all dbt writes went to a throwaway verify schema, now dropped. So this verifies the semantics the skill asserts, not a re-run of #2403 itself.
There was a problem hiding this comment.
🟡 Changes recommended
The documented commands contain invocation errors and omit the old divergent dependency path required for a controlled comparison.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
skills/data/ol-dbt-migration-validation/SKILL.md:158
- Repository local-development commands invoke dbt through
uv run(docs/LOCAL_DEV_QUICK_REF.md:17-41). Using baredbthere can select an unrelated global executable or fail in a normal unactivated uv checkout, undermining the reproducibility of the acceptance procedure.
skills/data/ol-dbt-migration-validation/SKILL.md:262 - As with the build step, run dbt through the repository's uv environment; otherwise the validation may use a different dbt/adapter version from the one locked by this project or fail when the virtual environment is not activated (
docs/LOCAL_DEV_QUICK_REF.md:32-35).
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
…ld, and the generation-count claim Second Copilot round on #2659. All three findings valid; each verified before fixing. The documented `ol-dbt run --select dim_user_old dim_user` cannot work. `--select` is declared `str | None` (commands/run.py), so the second model binds to the SUBCOMMAND positional: `Invalid value "dim_user" for SUBCOMMAND. Choose from: "build", "run", "test"` -- reproduced, it never reaches dbt. Quoted it, and noted the asymmetry that makes this easy to get wrong: `ol-dbt diff`'s -k/--exclude-columns are `list[str]` and do accept repeated values, while `run --select` does not. The canonical step-3 command contradicted the requirement this PR added in the header section. It selected the new dimensional upstream but not the old path's, so `<model>_pre` still resolved its divergent ref to an independently polluted Glue view -- the uncontrolled comparison the header exists to prevent. Now selects both divergent ancestor paths, prefixed by a ref-diff to find them, with measured selector costs (`+dim_course_run` = 53 models, `1+` = 7). Verified dbt accepts mixed plain and graph-operator selectors in one --select. "Divide to get the generation count" was wrong. The ratio is average key multiplicity; it would equal a generation count only if every snapshot held the same key set, and these snapshots drop rows as well as duplicate them. Own measurements make the point: 1.28x and 61.85x are not counts of anything. Reframed as a pollution smell, with an explicit warning not to divide by it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The playbook has unreliable ref discovery, incorrect key guidance, and insufficient protection against shared DuckDB races.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
skills/data/ol-dbt-migration-validation/SKILL.md:359
- This recommendation is reversed:
ol-dbt diff's per-column comparison joins on the supplied key, so a non-unique key (or nullable scalar key) is exactly what can fan out or mispair rows and corrupt the mismatch rate. Conversely, the top-level “Rows without an exact match” count comes fromcompare_relations, where the key is used only for ordering (src/ol_dbt_cli/ol_dbt_cli/commands/diff.py:415-425,450-480). If the key is not proven, skip the keyed diff and rely on the keyless multiset check in step 5(b).
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
…n PRs The two existing dbt skills drive the tools (`ol-dbt-local-dev` for register/build, `ol-dbt-fast-validation` for validate/impact/diff) but neither answers the question a migration PR actually turns on: is the data the same? This adds the acceptance procedure, derived from validating #2403. The load-bearing part is which local numbers are trustworthy. A fresh register of staging/intermediate/dimensional on 2026-09-09 still put 624 of 640 non-raw Glue views on `__dbt_tmp` metadata locations, which return duplicated and partially-missing rows while the build succeeds. So an absolute row count or fill rate read through a `glue__` view is not evidence, and several have already been quoted in PR bodies as though they were. A difference between two locally built sides of the SAME registration is evidence, because the pollution cancels. The skill is organised around that distinction: register once, build both sides in one invocation, compare. Also records three traps that each cost real time: - `--full-refresh` is mandatory when the model under test is incremental. #2403's own documented test command omits it, and `dim_course_run` therefore merged in 0.11s without re-deriving the column under test. This contradicts `ol-dbt-local-dev`'s "prefer incremental" rule, which is right for iterating and wrong for validating; the new skill says so explicitly rather than silently disagreeing. - The per-column multiset diff needs no join key at all and localises a difference to one column in a single pass. - A column unpopulated on both sides is unverified, not passing. Registers the skill in agent-config.toml and adds a `migration` profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… test-selection traps Follow-up to the initial draft, from validating PR #2403 end to end. Amends `ol-dbt-local-dev`'s rule rather than leaving the new skill to contradict it. The old wording — "reserve --full-refresh for when the incremental state is stale or wrong" — cannot fire, because the failure mode it needs to catch is a state that is perfectly valid and merely does not reflect your new code. An incremental run whose key set is unchanged is a no-op that still reports OK, so a changed expression is never re-evaluated and you read the old values believing they are new. #2403's own documented test command hit exactly this: dim_course_run merged in 0.11s and never re-derived the column under test, for two months. The rule is now framed as iterating vs concluding, which is the distinction that actually decides it. Adds two traps neither existing skill covers: - ~/.ol-dbt/local.duckdb is shared by every worktree and session on the machine. A concurrent session rebuilt dim_course_run from main mid-validation and semester went from 4,513/4,513 populated to 12/4,513 with no warning from either side. This is the second independent reason to materialize both comparison sides in ONE invocation: the #2403 result survived only because both marts were already physical tables. - dbt defaults to --indirect-selection=eager, which pulls in relationships_* tests owned by other models and compares your local build against production Glue views. Measured 20 tests eager vs 11 cautious on #2403's two models; the 9 difference were all cross-model, including a 42.7M-row tfact_grade scan and every failure the PR body documented as expected. Also records why expected-failure counts do not belong in a PR body — #2403 documented 87/74/13 and the same tests produced 861/178/150 plus an undocumented fourth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot review on #2659 found five overclaims. All five are valid. - The incremental trap was framed as "an unchanged key set makes the run a no-op". dbt does execute the model SQL; what varies is which rows the model's own is_incremental() predicate reselects. Reframed around the predicate, with the two real failure modes (excluded rows keep old-code values; a predicate that matches everything yields a true no-op) and the SCD2 consequence that dim_course_run appends a second generation rather than replacing the first. Stopped asserting a root cause for #2403's 0.11s no-op that was never established — the lesson is not to reason from the predicate at all. - "Both sides read the identical polluted source and the duplication cancels" is false exactly where a #2072 migration re-points ref(), which is every migration PR. override_ref resolves each unbuilt ref() to its own glue__ view with independent __dbt_tmp pollution, and joins/filters mean the two do not offset. Added: build the divergent upstreams locally, or label the result unverified source noise rather than accepting/rejecting on the registration artifact the section warns about. - Step 3 used `dbt build`, contradicting the cautious-test section below it and risking an unmaterialized comparison side when a test fails. Now `dbt run`. - "Row-count delta 0 means the grain is intact" — dropped rows offset by duplicates net to zero too. Now count parity, with a count(*) = count(distinct key) check on both relations. - Step 6's distinct-pair assertion cancels duplication but not rows a polluted view is missing; scoped accordingly. Also propagated the incremental correction to ol-dbt-local-dev, including its two worked examples, which both rebuilt without --full-refresh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ions Ran the corrected claims against dbt 1.12.4 on dev_local rather than leaving them derived from reading the source. All held. Two results were worth recording in the skill. __dbt_tmp duplication is per-view and heterogeneous. Across all 29 registered dim_ views in one registration: dim_ocw_resource 179,863 rows for 2,908 distinct pks (61.85x), dim_course_run 1.28x, dim_course 1.06x, 24 others at 1.00x, and two 404ing on pointer rot. A 1.00x and a 61.85x view in the same layer and the same registration is the empirical reason the cancellation rule has to be scoped to shared inputs -- identical views cancel, different views cannot. Also re-measured the pollution table five days on: 651/667 non-raw (97.6%), every layer but staging identical, confirming it is the steady state. The dbt build skip is intermittent here, which is worse than consistent. dbt_project.yml sets +error_if: ">10", so a test with <=10 failing rows only WARNs and the downstream model still builds. Measured: 4 failing rows -> WARN, downstream built; 24 -> FAIL, SKIP relation. So `dbt build` appears to work until a test crosses the threshold, and then a comparison side silently does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ld, and the generation-count claim Second Copilot round on #2659. All three findings valid; each verified before fixing. The documented `ol-dbt run --select dim_user_old dim_user` cannot work. `--select` is declared `str | None` (commands/run.py), so the second model binds to the SUBCOMMAND positional: `Invalid value "dim_user" for SUBCOMMAND. Choose from: "build", "run", "test"` -- reproduced, it never reaches dbt. Quoted it, and noted the asymmetry that makes this easy to get wrong: `ol-dbt diff`'s -k/--exclude-columns are `list[str]` and do accept repeated values, while `run --select` does not. The canonical step-3 command contradicted the requirement this PR added in the header section. It selected the new dimensional upstream but not the old path's, so `<model>_pre` still resolved its divergent ref to an independently polluted Glue view -- the uncontrolled comparison the header exists to prevent. Now selects both divergent ancestor paths, prefixed by a ref-diff to find them, with measured selector costs (`+dim_course_run` = 53 models, `1+` = 7). Verified dbt accepts mixed plain and graph-operator selectors in one --select. "Divide to get the generation count" was wrong. The ratio is average key multiplicity; it would equal a generation count only if every snapshot held the same key set, and these snapshots drop rows as well as duplicate them. Own measurements make the point: 1.28x and 61.85x are not counts of anything. Reframed as a pollution smell, with an explicit warning not to divide by it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… SQL that cannot parse Self-review pass before the next review round. Nine findings, all verified by running them. Runnability. Every `dbt` and `ol-dbt` command in both skills is broken when another dbt is earlier on PATH. `ol-dbt` shells out to bare "dbt" (commands/run.py: cmd = ["dbt", subcommand, ...]) resolved through PATH, not from the venv it lives in; a ~/Library/Python/3.9/bin/dbt (1.8.1) shadowed the project's 1.12.4 here and every dev_local command died with "Could not find adapter type duckdb!" -- an error that names the adapter, not the PATH. Added a prerequisite section to ol-dbt-local-dev with the diagnosis, the `which -a dbt` check and the `uv run --frozen` fix (--frozen so running a command cannot rewrite uv.lock), a pointer to it from migration-validation, and switched that skill's two dbt commands to `uv run --frozen dbt`. SQL that cannot parse. Both step-5 blocks alias `count(*) rows`, and `rows` is a reserved word in DuckDB -- the target these queries exist to run against. Neither has ever run as written. Now `as n_rows`, with a note. Also documented that a composite `count(distinct ...)` must be parenthesised as `(a, b)`; the bare form is a binder error. Verified all four blocks parse. Consistency. Step 1 said to register only the layers the model reads, while step 3 now suggests `+<upstream>`, whose leaves read source() and need raw registered -- called that out. The step-3 test command still selected `<dimensional_model>`, which stopped matching the run command when that was fixed last round; realigned, since drift there means testing a different set than you compared. `-k <key cols>` invited the space-separated form step 4 says is broken; now `-k <a,b,c>`. Smaller: "a fresh register of all three layers" described a seven-layer table; the view cited for 31x has since flipped to 404, which is the churn claim observed, so noted; dropped a duplicated sentence; and my own agent-config.toml realignment was one column out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…is tuned an order of magnitude too loose ol-dbt-local-dev told you to register, and `list-sources` warns when the registry is ">1 day old". Both understate the problem. `register` stores the Iceberg metadata_location Glue reports at that instant, and for a dbt-built table Glue routinely points the canonical name at a __dbt_tmp directory that the next production materialization swaps and deletes. Measured while validating #2403: a registry refreshed 60 MINUTES earlier failed with HTTP 404 reading the metadata JSON for int__micromasters__dedp_proctored_exam_grades, killing two mart models mid-validation. Re-registering only the intermediate layer (41 pointers moved, 118 unchanged) fixed it. Re-registering the same three layers 26 hours later moved another 336 pointers — 184 staging, 108 intermediate, 44 dimensional — which is the churn rate the one-day threshold is failing to track. The 404 is the lucky shape. When the __dbt_tmp directory still exists but holds a mid-build snapshot, the view returns duplicated or partial rows and nothing fails at all: a clean run and wrong numbers, which is the dangerous case when those numbers are about to be quoted in a PR. So the guidance is unconditional re-registration immediately before a build, not a reaction to a visible error. #2660 stopped __dbt_tmp tables being registered as sources in their own right but deliberately scoped out canonical names pointing at __dbt_tmp locations (621/636 dbt-built tables at last count); that remains tracked separately. Until it lands, freshness is the caller's responsibility, so the skill now says so. Verified the three documented register commands run verbatim from a clean worktree (0 errors each) and pre-commit passes on the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0abde18 to
2b1dff1
Compare
…ical table is immune Fourth Copilot round. Both findings valid, both verified before fixing. The ref-discovery command I added last round only matched single-quoted ref(). Measured: 13 double-quoted occurrences across 5 models, THREE OF THEM IN reporting/ -- the layer #2072 migrates. On reporting/data_detail_problems.sql the shipped pattern returns zero matches, so the diff shows no divergent refs, the reader omits the upstreams, and the comparison is uncontrolled. It fails silently and in the direction that looks clean, which is the worst shape for this particular command. Now a char-class pattern (portable -- a backreference errors under ugrep) that also strips quotes and the ref() wrapper, so it compares bare model names and re-quoting a ref between versions cannot read as a false divergence. Verified verbatim: finds the double-quoted refs, still surfaces the real divergence on the ac99b17 -> main dim_course_run case, clean across 60 models. "Table-materialized output is immune once written" contradicted this section's own opening sentence, which says another session's dbt run will overwrite your tables. It will. A physical table is immune to POINTER ROT, not to another dbt run. The #2403 marts survived because the concurrent session rebuilt dim_course_run, their upstream, and never selected the marts -- targeting, not immunity. Replaced with that distinction plus the operational requirement: confirm exclusive use of dev_local and measure immediately, since one invocation narrows the window but is not atomic. Added real isolation as the option when a result will be published -- HOME=/tmp/iso-validation redirects dev_local to a private DuckDB file (verified; the 19GB warehouse is untouched), at the cost of a full re-register since that database starts empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2b1dff1 (re-register before every build) and this skill's step 1 ("register once, then do not touch the registry") read as opposite advice in two skills that reference each other. They are not opposites -- they constrain different variables -- but nothing said so, and following them in sequence gives no way to tell. Freshness vs stability. 2b1dff1 minimizes registration AGE: pointers rot within the hour, and the silent form returns duplicated or partial rows with no error. Step 1 keeps the registration CONSTANT across a comparison, since re-registering between the two sides makes every difference drift rather than code. Both hold simultaneously exactly because step 3 builds both sides in one invocation -- register immediately before it. Where they genuinely conflicted, 2b1dff1 is right and step 1 was wrong. "Do not touch the registry" forbids the safe action in the rebuild case: the invariant that matters is that both sides of a comparison share one registration, not that you never re-register. Worse, a validation building +<upstream> ancestor trees can easily outlast the hour, so the old rule had you concluding off a registry its own skill says is rotting. Rule is now: register immediately before the invocation that builds both sides; never between the two sides; if you rebuild, re-register and rebuild BOTH. Also dropped "pointer churn is roughly 90% of a layer per day". It entered with the original commit, has no measurement recorded anywhere, and 2b1dff1's numbers contradict it -- 65% staging, 68% intermediate, 79% dimensional over 26 hours. Replaced with those, plus the 60-minute 404 that is the figure actually motivating the rule. Cross-referenced both directions so the two skills visibly agree, and updated the Rules list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… number From a SUPPRESSED Copilot comment (review body, never a thread, so the thread-based workflow never surfaced it). Verified in the macros and then by experiment; Copilot is right and step 4 said the opposite. Step 4 claimed a weak key inflates "rows without an exact match" and that you should therefore trust the per-column mismatch rate instead. Both halves are wrong, and they are wrong in the direction that matters: - compare_column_values emits `full outer join b_query on a_query.<pk> = b_query.<pk>` (audit_helper compare_column_values.sql:42), so the PER-COLUMN RATE is the key-sensitive figure. - compare_queries computes unmatched rows with EXCEPT on full row content; primary_key appears only in an `order by` (compare_queries.sql:102). The UNMATCHED COUNT does not depend on the key at all. Sample rows come from the same EXCEPT branch, so they are key-independent too. Measured 2026-09-15 to be sure: two byte-identical 4-row tables whose key repeated once reported `0 unmatched row-side(s)` (correct) alongside `val: 33.33% (2 rows)` — pure 2x2 fan-out on the duplicated key. The old guidance would have you report a 33% regression on identical data, in exactly the situation it exists to help with: an unproven key. Replaced with a table of which figure depends on the key, the measurement, and the right fallback: if the key is not proven, do not run the keyed diff at all -- use the keyless multiset check in 5(b). Also corrected the attribution of the 17,782/20,908 case, which was source duplication (the 16.7x documented elsewhere in this skill), not join noise from a guessed key, and cross-referenced the tracked NULL-key task. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Key discovery misses nested mart schemas, and registration failures can leave validation using stale inputs.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
…obbing for schema YAML Fifth Copilot round, three findings, all valid and all verified in the code before fixing. REGISTER REPORTS SUCCESS WITH FAILED TABLES. `ol-dbt local register` catches per-table failures, counts them, prints `✗ Errors: N`, and still exits 0 -- there is no raise or non-zero exit on that path in local_dev.py. A table that fails to re-register keeps its PREVIOUS view, so the pointer you re-registered to refresh can still be the stale one, and an unattended run proceeds against it. That silently defeats the freshness rule this PR just added. Step 1 now loops the three layers and gates the build on `✗ Errors: 0` (verified against real register output, and the summary string confirmed by a --dry-run); ol-dbt-local-dev gets the same warning at its register block. THE SCHEMA-YAML GLOB DOES NOT RECURSE. `models/**/_<area>__models.yml` matches ONE directory level in bash without globstar, while 31 of 35 model schema files sit two or three deep -- so it expands to nothing for most marts, which are the primary migration scope. It happens to work in zsh, which is what makes it a good trap. Compounding it, the filename convention is not uniform: dim_course_run's schema is _dim_course_run.yml, so no glob shape finds both. Replaced with `dbt ls --output-keys patch_path`, which is authoritative for either shape; verified verbatim on one model of each. While there: the skill named only dbt_expectations.expect_compound_columns_to_be_unique, but dim_course_run -- its own motivating model -- uses dbt_utils.unique_combination_of_columns (19 occurrences across 9 files vs 231 across 32). Both are now named, with dim_course_run's actual key spelled out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Several documented commands have path, registration, and control-flow errors that can invalidate or prevent validation.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
skills/data/ol-dbt-migration-validation/SKILL.md:210
- These arguments are model basenames, but the files created in step 2 live under
src/ol_dbt/models/...; at this point the procedure has not changed into that directory. Following the steps from the repository root makes bothgrepcalls fail and the two empty outputs appear identical, silently omitting all divergent upstreams. Pass the actual paths created in step 2.
skills/data/ol-dbt-local-dev/SKILL.md:27 - The new prerequisite says every command below must use
uv run --frozen, but most subsequent copyable examples still invoke bareol-dbt(includingrunanddiff, which invoke dbt). Those are exactly the commands that can pick up the shadowing global executable this section warns about. Either update every example or explicitly require activating.venvbefore following the bare-command examples.
skills/data/ol-dbt-migration-validation/SKILL.md:180 - A full ancestor tree does not necessarily terminate at raw sources.
override_source.sql:36-53derives the Glue database from each source's declared schema, and this repository hasdimensionalandreportingsources as well. Register every source layer reached by the selected graph; otherwise a valid+<upstream>build can still fail because the required non-raw Glue view was never registered.
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
…istration gate actually gate Sixth Copilot round: three threads plus three suppressed comments in the review body. All six valid, all verified before fixing. THE GATE DID NOT GATE. Last round's registration check used `break`, which only leaves the loop -- the snippet then exits 0 and an unattended run prints STOP and builds anyway. Now a script (shebang, `set -euo pipefail`) that exits 1, with a note that pasting it interactively will close the shell. NO WORKING DIRECTORY WORKED. The procedure mixed repo-root paths (step 2's git show) with a project-dir shell (step 3's cd), so two discovery commands had no directory where they worked: from the root `dbt ls` cannot find dbt_project.yml, from src/ol_dbt the patch_path join doubles the prefix, and the ref-diff took basenames for files that live under src/ol_dbt/models/. Both failure modes are silent and look clean -- two failed greps produce two empty outputs that diff calls identical, which reads as "no divergent refs". Stated the working directory once (repo root), anchored dbt with --project-dir, and made the ref-diff take the paths step 2 actually creates. I shipped that key-discovery command last round having tested a DIFFERENT one: I ran `grep "$yml"` from src/ol_dbt and committed `grep "src/ol_dbt/$yml"`. Every changed snippet this round was run verbatim from the repo root before committing. A `where` CLAUSE SCOPES WHAT A UNIQUENESS TEST PROVES. dim_course_run's is `where: "is_current = true"`, so the key is proven for current rows only, while an SCD2 relation accumulates several expired rows per business key all sharing is_current = false. Demonstrated: two changes to one key give 6 rows / 5 distinct keys unfiltered -- reads as a fan-out, is ordinary history -- and 3/3 with the predicate applied. Also: ancestor trees do not necessarily bottom out in raw (this project declares dimensional and reporting sources too), so the note now says register every source layer the selection reaches, with a command to list them. And ol-dbt-local-dev's prerequisite told readers to use `uv run --frozen` while every example below it was bare; it now says to activate the venv once (verified: which dbt then resolves to the repo's 1.12.4), with the per-command prefix as the alternative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed the three suppressed comments from this round's review body as well, in the same commit (8d30b83) — noting them here since they have no threads to reply on:
DBT_PROFILES_DIR=src/ol_dbt uv run --frozen dbt ls --project-dir src/ol_dbt \\
--select +<upstream> --resource-type source -t dev_local | grep '^source:'
Worth recording that two of this round's six findings were commands I had shipped without running as written — including one where I tested |
There was a problem hiding this comment.
🟡 Changes recommended
Several documented commands either fail from the required working directory or do not enforce the stated test and filtering constraints.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
skills/data/ol-dbt-migration-validation/SKILL.md:437
- This grep scans every uniqueness test in the shared schema file rather than the selected model's YAML block. Files such as
_marts__combined__models.ymlcontain many models and many unrelated uniqueness tests, so this output does not identify which key belongs to<model>and can lead directly to the weak-key fan-out warned about below. Scope extraction to the selected model block (and include its complete test config/where) or instruct the reader to inspect that block directly.
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced (auto)
Note
Copilot is running an experiment and ran this review at Balanced.
…rior fixes Seventh Copilot round. All four valid; three are regressions from the previous two rounds' fixes, which is the more useful signal. The dbt test command was a 52x own-goal. Round 5 told the reader to keep the test selection identical to the build selection, but the build deliberately includes `+<upstream>` ancestor trees -- so repeating it selects every ancestor's own tests, and --indirect-selection=cautious cannot exclude tests belonging to models you explicitly named. Measured: `--select dim_course_run` runs 7 tests under cautious, `--select "dim_course_run +dim_course_run"` runs 365. In the section headed "Test only your own models". Now selects only the two comparison models, with the reasoning corrected: the two selections are supposed to differ. Step 3 still cd'd into src/ol_dbt, contradicting the repo-root contract round 6 added at the top of the same procedure -- and the cd persists in an interactive shell, so every later root-relative command resolved under src/ol_dbt/src/ol_dbt. Now --project-dir like everything else. `ol-dbt diff` cannot express a `where` clause, so round 6's instruction to carry the uniqueness test's predicate into the keyed diff was unfollowable: the only filter-adjacent flag is --limit, which caps printed sample rows. On a conditionally proven key that is exactly the weak-key fan-out documented two sections earlier. Step 5(d) now says to skip it when the test has a `where`, and notes nothing is lost -- 5(b) needs no key and step 6 is hand-written SQL, so both take the predicate directly. Older text: the uniqueness grep scanned the whole schema file. _marts__combined__models.yml declares 11 models and 14 uniqueness tests, so it identified that a key exists, not which one was yours. Replaced with an awk that prints only the selected model's block. All four commands run verbatim from the repo root before committing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d one invocation style Reviewed the whole diff the way the last seven rounds have, looking for the patterns that keep recurring: a mandate with no implementation, a command that depends on an unstated context, two styles in one file. WRONG BASELINE. Step 2 took the `_pre` copy from `origin/main`, but main moves while a PR is open -- measured on this branch: 3 commits touching 7 model files since divergence. So `origin/main:<model>.sql` can hand you someone else's edit to the same model, and the diff then reports their change as part of your migration. Now `git merge-base HEAD origin/main`. Also `<layer>` rather than a hardcoded `marts/`, since reporting migrations live under models/reporting/. THE `where` MANDATE HAD NO IMPLEMENTATION. Last round I added "carry the test's predicate into the grain check, the fill-rate comparison and the diff" -- and Copilot caught that `ol-dbt diff` cannot express it. The same gap was still open in the SQL: five blocks read <pre> and none had anywhere to put a predicate. Step 5(a) and step 6 now take `<pred>` explicitly, with one line telling you to carry it through the rest. Step 6 also aliased a composite key wrong: `<key> as k` labels only the last column. Harmless (EXCEPT compares full rows) but misleading, so the template now lists the key columns bare with a note. The registration gate hardcoded three layers while the section above it says to register every source layer the selection reaches; it now says to adjust the list. ONE INVOCATION STYLE. ol-dbt-local-dev's prerequisite says to activate the venv once, then used three styles below it: bare, `uv run --frozen`, and bare again. All examples are bare now, matching the prerequisite; the only remaining `uv run --frozen` is the prose describing it as the alternative. Every changed snippet run verbatim: both SQL blocks parse with a predicate applied, and the merge-base extraction returns the 337-line file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both from a real end-to-end run of this procedure against #2686 in another session — the first time it has been exercised on a live migration rather than reasoned about. Verified both here before writing. THE +<upstream> INSTRUCTION IS UNFOLLOWABLE FOR THE COMMONEST TARGET. `+dim_course_run` / `+dim_course` cannot build on dev_local: dim_course_run.sql:191 calls `regexp_like` raw instead of through the cross-db macro, and that is a Trino builtin DuckDB lacks (confirmed in the file; tracked as tk-t1-unblock-local-validation-...-20b1b3). The #2686 run measured ERROR=3 SKIP=5 with the model under test among the skipped, leaving only the _pre side. New subsection: reconstruct the needed slice from buildable ancestors, state that this verifies the derivation rather than the built relation, and do NOT substitute a Glue view for the unbuildable ancestor — that is the uncontrolled divergent-path case. VERIFY FROM run_results.json, NOT THE LOG. A selected model can be skipped while the command looks fine. Two mechanisms, both reproduced here: `| tail` hides the per-model ERROR/SKIP lines behind dbt's deprecation summary, and `| tail` also masks the exit status — bare run exits 1, piped exits 0, piped under `set -o pipefail` exits 1. Noted that ${PIPESTATUS[0]} is a bash-ism and empty in zsh, where it is ${pipestatus[1]}. One correction to the reported finding: dbt itself does exit 1 when a model errors — I could not reproduce "dbt run exited 0". The 0 comes from the pipe, so the fix is pipefail plus the artifact check rather than anything about dbt's exit behaviour. The artifact snippet is scoped to {error, fail, skipped, runtime error} rather than != success, because test results use pass/warn and the naive form reports every passing test as a failure. Verified both ways: none on a real test-run artifact, and it catches error+skipped on a model run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd the stale-registration evidence CORRECTION to d6806b0. I wrote that `+dim_course_run` fails because dim_course_run.sql:191 calls `regexp_like` raw. That is false on current main: PR #2658 dispatched it and merged 2026-09-11, and the dispatched form is present in this branch's merge-base too. I read the raw version out of the main checkout, which is 35 commits behind origin/main (153a76e vs bb2bcfc) — I verified the claim against a stale tree, which is the same class of mistake as testing a different command from the one I shipped. The real blockers are three ANCESTORS carrying Trino-only JSON SQL, per tk-three-trino-only-json-models-block-building-dim--59c2db and confirmed against origin/main: stg__edxorg__api__course.sql:20 and stg__mitxpro__app__postgres__cms_certificatepage.sql:17 (`json_query(... with array wrapper)` wrapped in json_parse and cast to array), and int__mitxpro__coursesfaculty.sql:9 (`cast(json_parse(...) as array (json))`). The cascade is what makes it total: those error, int__edxorg__mitx_courseruns and int__mitxpro__courses skip, and since the dims `union all` every platform one broken branch skips the whole dim. Measured on #2686: PASS=48 ERROR=3 SKIP=5 TOTAL=56. Also noted #2658 explicitly so nobody hunts for the regexp_like that is already gone. Added the gate the #2686 run argued for, which is sharper than what I had: both comparison sides must appear with status: success in run_results.json before any number is measured -- not "the run finished", not "no errors scrolled past". Added the evidence for step 1's stability rule, which is the best argument in the file for it: on #2686 a staging layer registered hours earlier read 4,513 rows where a fresh registration read 4,996, and that 483-row gap was published and retracted. It was convincing because the stale side was internally consistent — the _pre model and the built dim both came from it, so the diff was a clean 0/0. A stale registration does not look stale, it looks like agreement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd state the scope up front Folds in what two manual validation runs on #2403 and #2686 learned but the skill did not yet say. CAUTIOUS DROPS THE PR'S OWN TEST. `--indirect-selection=cautious` includes a test only when EVERY model it references is selected, so a new singular test the PR itself adds — referencing the changed model plus something outside the selection — is silently excluded. On #2403 that was the PR's own new test, the one test most worth seeing. Switched the recommendation to `buildable`, which relaxes exactly that condition and still excludes the eager relationships noise. Re-measured both modes rather than trusting the July figures: on dim_course_run eager 17 / buildable 8 / cautious 8, and the single test buildable adds is the course_fk -> dim_course relationship. RUN THE TESTS BEFORE THE DIFF, with the reason: a named failing test is more diagnostic than a 20k-row mismatch report, and a broken model makes a MATCH uninterpretable — you cannot distinguish agreement from two identically-wrong sides. WHAT TO EXPECT was missing entirely — the skill said which figures to trust but never what the report looks like or what a pass is. Step 5(d) now says to diff EVERY changed model with a per-model verdict written down first: unaffected -> MATCH/exit 0; intentionally changed -> MISMATCH/exit 1 where the SHAPE is the assertion (row delta 0, only the intended columns, the intended direction). So a MISMATCH is not a failure and a MATCH is not automatically a pass. Includes the #2403 numbers that turned an asserted claim into a measured one (semester 12 -> 99 non-null over 87 re-versioned rows, body had said "~100"), and the --exclude-columns effective_date,end_date that SCD2 models need or the report is unreadable. Also rewrote the frontmatter description and the opening paragraph, which undersold the skill as "the acceptance procedure" when it is the whole register/build/test/diff workflow. Anyone scanning the description could not tell whether it covered the higher-level sequence or just the diff command. Two stale spots caught by grepping for what the change touched: "test separately, cautiously" now misread as the mode name, and a "runs 7 tests" figure that re-measurement put at 8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps found by checking the skill against everything the #2403 and #2686 manual runs produced, rather than against the review findings. NO NEGATIVE CONTROL. The skill said what a clean result looks like but never how to prove the comparison can fail. A MATCH is unfalsifiable on its own — it is indistinguishable from a diff that compared nothing. Added the recipe (replace the column under test with a literal in _pre, rebuild that model only, rerun) with the #2403 evidence that it does fail loudly: perturbing semester gave `mismatch — 17888 unmatched row-side(s), 1 column value mismatch(es)`, exit 1, rows listed. Also says to put the one-liner in the PR body, since a reviewer cannot otherwise distinguish the two. THE SHARED DUCKDB CAN STOP YOU, not just mislead you. The section covered staleness and clobbering but not the hard failure: with another session holding the file, ol-dbt diff dies on `_duckdb.IOException: Could not set lock on file ... Conflicting lock is held` — 5 retries at ~20s on #2403. Called out as contention rather than a defect in the change under review, so nobody reports it as one. Renumbered that list's intro from "Two consequences" to "Three", caught by grepping what the edit touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Several documented checks can silently pass invalid comparisons or fail when executed as instructed.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (7)
skills/data/ol-dbt-migration-validation/SKILL.md:354
- This checker does not implement the gate described below. It prints
NOT OK: nonewhen the selector matched no comparison models, and it always exits 0 even whenbadis non-empty. Explicitly require both expected model names to havesuccessstatus and exit nonzero otherwise.
import json; d=json.load(open('src/ol_dbt/target/run_results.json'))
BAD={'error','fail','skipped','runtime error'}
bad=[(r['status'], r['unique_id'].split('.')[-1]) for r in d['results'] if r['status'] in BAD]
print('NOT OK:', bad or 'none')
"
skills/data/ol-dbt-migration-validation/SKILL.md:556
- The key-discovery pipeline cannot find a single-column key: its grep only emits compound-test syntax, while the following rule requires locating
uniqueplusnot_null. For example,models/dimensional/_dim_date.yml:11-15declaresdate_keyusing those two generic tests, and this command produces no key evidence for it. Include column names and the generic test names in the scoped output.
awk -v m="<model>" '$0=="- name: "m {f=1; print; next} f && /^- name: / {exit} f' \
"src/ol_dbt/$yml" \
| grep -nE 'unique_combination_of_columns|expect_compound_columns_to_be_unique|combination_of_columns|column_list|where:'
skills/data/ol-dbt-migration-validation/SKILL.md:665
- This query omits the
<pred>that lines 649-650 require. For a conditionally unique model such asdim_course_run, it therefore compares historical rows too, so the reported per-column differences do not describe the same population as the grain and mapping checks.
select count(*) from (select "<col>" from <pre> except all select "<col>" from <new>);
select count(*) from (select "<col>" from <new> except all select "<col>" from <pre>);
**skills/data/ol-dbt-migration-validation/SKILL.md:692**
* The fill-rate example also drops the required `<pred>`. This makes its denominator include rows outside the population where the selected key is proven, contradicting lines 582-591 and potentially hiding a current-row fill-rate regression among SCD2 history.
select 'pre' as side, count() as n_rows,
count("") as non_null, round(100.0count("")/count(),2) as pct
from
union all select 'new', count(), count(""), round(100.0count("")/count(),2) from ;**skills/data/ol-dbt-migration-validation/SKILL.md:742** * These two checks do not cover what a row-level diff reports. Per-column multisets lose key-to-value association, and step 6 checks only `<changed_col>`; for example, swapping an untouched value between two keys leaves every multiset, row count, and changed-column mapping unchanged. Add a filtered full-row `EXCEPT ALL` comparison (excluding nondeterministic columns), or filtered wrapper models, before claiming the keyed diff can be skipped without loss.You lose nothing by skipping it. Step 5(b)'s multiset diff needs no key at all, and
step 6's mapping assertion is hand-written SQL — both take awheredirectly, so
together they cover what 5(d) would have told you, on a key you can actually
defend. Reach forol-dbt diffwhen the key is proven across the whole relation.**skills/data/ol-dbt-migration-validation/SKILL.md:751** * The procedure mandates repository-root execution, but this bare `dbt run` has neither `--project-dir` nor `DBT_PROFILES_DIR`; from the root it fails because there is no root `dbt_project.yml`. Anchor the negative-control command like the earlier dbt commands.uv run --frozen dbt run -t dev_local --full-refresh --select "_pre"
**skills/data/ol-dbt-migration-validation/SKILL.md:251** * Calling a snapshot an alternative conflicts with this procedure's same-registration requirement. If the current model was built under an older registration and the edited model follows step 1 by re-registering, the frozen baseline and new build use different inputs, so the diff can attribute source drift to code. Specify the fresh-register/build/snapshot/no-reregister sequence, or restrict this alternative to non-acceptance iteration.
ol-dbt local snapshot <model> --as <model>_baselineis the alternative when
the old code is not in git (an uncommitted edit) — it freezes the current build
as a plain table, immune to pointer rot. Then diff with--old-raw. Prefer the
_premodel file when the old code is a commit away, which for a migration PR
it always is.- **Files reviewed:** 3/3 changed files - **Comments generated:** 4 - **Review effort level:** Balanced (auto) </details> > [!NOTE] > Copilot is running an experiment and ran this review at Balanced.
| [profiles.migration] | ||
| skills = [ | ||
| "ol-dbt-local-dev", | ||
| "ol-dbt-fast-validation", |
| grep -q '✗ Errors: 0' "/tmp/reg_$db.log" || { | ||
| echo "STOP: $db registration had errors — do not build" >&2 | ||
| exit 1 | ||
| } |
| **Read the `✗ Errors:` line — a non-zero count does not fail the command.** | ||
| `register` catches per-table failures, prints the tally, and still exits 0. A table | ||
| that fails to re-register keeps its previous view, so a run that *looks* successful | ||
| can leave you on exactly the stale pointer you were trying to replace. Treat | ||
| `✗ Errors: 0` as the success condition, not the exit status. |
| ol-dbt local snapshot my_model --as my_model_baseline # materialize a frozen copy | ||
| # ...edit the SQL... | ||
| ol-dbt run --select my_model | ||
| ol-dbt run --select my_model --full-refresh | ||
| ol-dbt diff --old my_model_baseline --old-raw --new my_model --primary-key my_model_pk | ||
| ``` | ||
| The snapshot is frozen, but the rebuild is not: without `--full-refresh` an | ||
| incremental `my_model` may leave the edited rows untouched, and the diff then | ||
| reports "no change" for a change that simply never ran. |
What are the relevant tickets?
Supports epic #2072 (dimensional-layer mart/reporting migration). Related: #2407 /
DBT_WAREHOUSE_CI_QA_SPEC.md, which specs the automated CI side of the same problem.Description (What does it do?)
Adds
skills/data/ol-dbt-migration-validation, and corrects one rule in the existingol-dbt-local-devskill that turned out to be actively misleading.Why a third dbt skill. The two we have drive the tools:
ol-dbt-local-dev(register, build, snapshot) andol-dbt-fast-validation(validate/impact/diff). Between them they answer "does it compile, and what breaks downstream?". Neither answers the question a migration PR actually turns on — "is the data the same?" — which is an experimental-design problem, not a tooling one.DBT_WAREHOUSE_CI_QA_SPEC.mdcovers the CI automation but is explicitly not a per-PR playbook for a human.Derived by validating #2403 (
marts__micromasters_dedp_exam_grades→dim_course_run) end to end, so every number in it was measured rather than reasoned about.The load-bearing content is which local numbers count as evidence.
ol-dbt local registerstores whatevermetadata_locationGlue reports, and for a dbt-materialized Iceberg table that is a__dbt_tmp-<uuid>path — an artifact of dbt's create-temp-then-swap. Re-measured immediately after a fresh register of staging + intermediate + dimensional on 2026-09-09:__dbt_tmpRegistering does not clear it — this is the steady state, not a race. Such a view either 404s (loud) or silently returns the temp table's accumulated snapshots: duplicated and partially missing rows, measured at 31x on
int__mitxonline__proctored_exam_grades. So an absolute row count or fill-rate percentage read through aglue__view is not evidence, and some have already been quoted in PR bodies as though they were. A difference between two locally built sides of the same registration is evidence, because the pollution cancels. The whole skill is organised around that distinction: register once, build both sides in one invocation, compare.Three traps it records, each of which cost real time.
--full-refreshis mandatory when the model under test is incremental — and this is theol-dbt-local-devrule being amended. The old wording said to "reserve--full-refreshfor when the incremental state is stale or wrong", which can never fire: the failure mode is a state that is perfectly valid and merely does not reflect your new code. An incremental run whose key set is unchanged is a no-op that still reportsOK, so a changed expression is never re-evaluated and you read the old values believing they are new. feat(#2088): migrate marts__micromasters_dedp_exam_grades to dim_course_run #2403's own documented test command hit exactly this —dim_course_runmerged in 0.11s and never re-derived the column under test, for two months. Reframed as iterating vs concluding, which is the distinction that actually decides it.~/.ol-dbt/local.duckdbis shared by every worktree and session on the machine. Mid-validation, a concurrent session rebuiltdim_course_runfrommainandsemesterwent from 4,513/4,513 populated to 12/4,513, with no warning from either side. This is a second, independent reason to materialize both comparison sides in one invocation — the feat(#2088): migrate marts__micromasters_dedp_exam_grades to dim_course_run #2403 result survived only because both marts were already physical tables.dbt defaults to
--indirect-selection=eager, which selectsrelationships_*tests owned by other models and compares your local build against production Glue views. Measured on feat(#2088): migrate marts__micromasters_dedp_exam_grades to dim_course_run #2403's two changed models: 20 tests undereager, 12 underbuildable, 11 undercautious— and the 9 thateageradds were all cross-model, including a 42.7M-rowtfact_gradescan and every failure the PR body had been documenting as expected noise. The skill also records why expected-failure counts should never go in a PR body: feat(#2088): migrate marts__micromasters_dedp_exam_grades to dim_course_run #2403 documented 87/74/13 orphans and the same tests produced 861/178/150 plus an undocumented fourth failure, because the numbers depend on when you registered, not on the change.Also included: the per-column multiset diff (localises a difference to one column, needs no join key), accepting on the distinct functional mapping rather than the whole row, and the rule that a column empty on both sides is unverified, not passing — which is exactly how
passing_gradelooked clean on an earlier #2403 diff while being just as broken assemester.Registers the skill in
agent-config.tomland adds amigrationprofile alongside the existingdbtone.How can this be tested?
No code changes — two Markdown files and a TOML registration. Nothing executes.
Verify the registration is well-formed:
uv tool install 'agent-config-kit[cli]' agent-kit validate agent-config.toml agent-kit apply agent-config.toml --profile migration --dry-runFrontmatter conforms to the Agent Skills spec —
namematches the directory (ol-dbt-migration-validation),descriptionstates what it covers and when to trigger,licenseandmetadata.categorymatch the two sibling skills.To sanity-check the central factual claim yourself, register any dbt-built layer and count how many views point at a temp location:
And to reproduce the test-selection numbers without building anything:
Additional Context
The amendment to
ol-dbt-local-devis the part most worth a careful read. That skill is @tmacey's (#2425) and Rachel extended it in #2469; I have changed one of its## Rulesbullets and added a second. I could have left the new skill to disagree with it, but a reader hitting two skills that contradict each other on--full-refreshis worse off than before, and the old rule is the direct cause of #2403's instructions verifying nothing since July. Happy to split that hunk out if you would rather it landed separately.Deliberate overlap. The new skill repeats
ol-dbt-fast-validation's primary-key guidance instead of only linking to it. A reader mid-migration should not have to hop skills for the one step that silently invalidates the entire comparison if done wrong (a non-unique key pairs rows many-to-many and reports join artifacts as mismatches — one measured case read 17,782 unmatched of 20,908 rows, all artifact).This documents a limitation rather than waiting on it. The
__dbt_tmproot cause is tracked internally and is not fixed; when it is, the skill's step 5 can assert considerably more and the table above becomes historical. It names that dependency inline so the next reader knows which parts expire.🤖 Generated with Claude Code