dbt-testpilot 0.1.0__tar.gz

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.
@@ -0,0 +1,10 @@
1
+ # Copy this file to .env and fill in your keys. NEVER commit .env.
2
+
3
+ # Which LLM lane to use: gemini | groq | ollama
4
+ LLM_PROVIDER=gemini
5
+
6
+ # Google AI Studio (Gemini) — https://aistudio.google.com (primary)
7
+ GEMINI_API_KEY=
8
+
9
+ # Groq — https://console.groq.com (backup lane)
10
+ GROQ_API_KEY=
@@ -0,0 +1,39 @@
1
+ # Secrets — never commit
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ # Python
6
+ .venv/
7
+ venv/
8
+ __pycache__/
9
+ *.py[cod]
10
+ *.egg-info/
11
+ build/
12
+ dist/
13
+
14
+ # dbt build output
15
+ target/
16
+ dbt_packages/
17
+ logs/
18
+
19
+ # DuckDB
20
+ *.duckdb
21
+ *.duckdb.wal
22
+
23
+ # Tooling
24
+ .ruff_cache/
25
+ .pytest_cache/
26
+ .mypy_cache/
27
+
28
+ # macOS
29
+ .DS_Store
30
+
31
+ # Local sandbox — external sample project, not part of dbt-testpilot
32
+ jaffle_shop_duckdb/
33
+
34
+ # Stray build wheels
35
+ *.whl
36
+
37
+ # Generated profiler / proposer output
38
+ profiles.json
39
+ proposals.json
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kushal Mishra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbt-testpilot
3
+ Version: 0.1.0
4
+ Summary: Scan a dbt project, profile the real data, and propose (and write) the dbt tests you're missing — with a human approve step.
5
+ Project-URL: Homepage, https://github.com/orgkushal/dbt-testpilot
6
+ Project-URL: Repository, https://github.com/orgkushal/dbt-testpilot
7
+ Project-URL: Issues, https://github.com/orgkushal/dbt-testpilot/issues
8
+ Author: Kushal Mishra
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: analytics-engineering,data-engineering,data-quality,dbt,duckdb,testing
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: duckdb>=1.5
24
+ Requires-Dist: ruamel-yaml>=0.18
25
+ Provides-Extra: llm
26
+ Requires-Dist: google-genai>=1.7; extra == 'llm'
27
+ Requires-Dist: groq>=1.5; extra == 'llm'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # dbt-testpilot
31
+
32
+ > Scans a dbt project, profiles the **real data**, and proposes the dbt tests you're missing — `not_null`, `unique`, `accepted_values`, `relationships`, and custom — with a human approve step.
33
+
34
+ **Status:** Part 0 (setup), Week 1 (profiler), Week 2 (proposals), and Week 3 (write + run) are complete. Not yet published to PyPI.
35
+ Part of a small **dbt reliability toolkit** (alongside a SQL→dbt converter).
36
+
37
+ Analysts chronically under-test their data. dbt-testpilot reads your dbt project's metadata, profiles the actual tables in DuckDB, and suggests the tests you're missing — you stay in control and approve what gets written. The design is **heuristics-first, LLM-augmented**: obvious tests come from deterministic rules; the LLM adds rationale, cross-table relationships, and custom tests.
38
+
39
+ ## Contents
40
+
41
+ - [What it does](#what-it-does)
42
+ - [How it works](#how-it-works)
43
+ - [Requirements](#requirements)
44
+ - [Week 0 — environment setup](#week-0--environment-setup)
45
+ - [Week 1 — profiling your data](#week-1--profiling-your-data)
46
+ - [Week 2 — propose the missing tests](#week-2--propose-the-missing-tests)
47
+ - [Week 3 — write tests + run](#week-3--write-tests--run)
48
+ - [Project layout](#project-layout)
49
+ - [Roadmap](#roadmap)
50
+ - [Troubleshooting](#troubleshooting)
51
+ - [License](#license)
52
+
53
+ ## What it does
54
+
55
+ | Stage | Capability | Status |
56
+ |---|---|---|
57
+ | Week 1 | Profile every model's data — nulls, cardinality, ranges, value lists, existing tests | **Done** |
58
+ | Week 2 | Propose the *missing* tests — heuristics + optional LLM, as validated JSON | **Done** |
59
+ | Week 3 | Write approved tests to `schema.yml` (human approve) and run `dbt test` | **Done** |
60
+ | Week 4 | `pip install dbt-testpilot` | Planned |
61
+
62
+ ## How it works
63
+
64
+ 1. **Read** the dbt project's `manifest.json` / `catalog.json` (from `target/`). *(done)*
65
+ 2. **Profile** each model in DuckDB — row counts, null %, cardinality, ranges, value patterns. *(done)*
66
+ 3. **Propose** tests: deterministic heuristics first, LLM-augmented for rationale, relationships, and custom tests (strict JSON). *(done)*
67
+ 4. **Approve** — you review; approved tests are written into the model's `schema.yml`. *(done)*
68
+ 5. **Run** — `dbt test` executes them. *(done)*
69
+
70
+ ## Requirements
71
+
72
+ - macOS (Apple Silicon) or Linux
73
+ - **Python 3.12** (dbt Core supports 3.10–3.13; 3.12 is the safe middle)
74
+ - **DuckDB** (Python package `duckdb`); **`ruamel.yaml`** for round-trip `schema.yml` edits (Week 3)
75
+ - A dbt project to point at — this repo uses **jaffle_shop_duckdb** as its sandbox
76
+ - A **Gemini** and/or **Groq** API key (only needed from Week 2)
77
+
78
+ ---
79
+
80
+ ## Week 0 — environment setup
81
+
82
+ One-time setup on your machine. No account here needs a credit card.
83
+
84
+ ### 1. Create accounts
85
+
86
+ - **GitHub** — enable two-factor auth.
87
+ - **Google AI Studio** (https://aistudio.google.com) → *Get API key* → save it (primary LLM: Gemini).
88
+ - **Groq** (https://console.groq.com) → create an API key (fast backup lane).
89
+ - *(PyPI / TestPyPI come later, for Week 4.)*
90
+
91
+ ### 2. Install tools (macOS / Apple Silicon)
92
+
93
+ ```bash
94
+ # Homebrew (skip if installed) — then follow its PATH note for /opt/homebrew
95
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
96
+
97
+ brew install git uv duckdb
98
+ git config --global user.name "Your Name"
99
+ git config --global user.email "you@example.com" # match GitHub
100
+ uv python install 3.12
101
+ ```
102
+
103
+ ### 3. Create the project and a virtual environment
104
+
105
+ ```bash
106
+ uv init dbt-testpilot && cd dbt-testpilot # or: git clone <this repo> && cd dbt-testpilot
107
+ uv venv --python 3.12 && source .venv/bin/activate
108
+ ```
109
+
110
+ ### 4. Install dbt — read this (common gotcha)
111
+
112
+ ```bash
113
+ python -m pip install --upgrade pip wheel setuptools
114
+ pip install dbt-core dbt-duckdb # do NOT run `pip install dbt`
115
+ dbt --version # expect a 1.x core + the duckdb adapter
116
+ ```
117
+
118
+ > Plain `pip install dbt` installs the newer Rust-based **Fusion / platform CLI**, which shadows `dbt-core` on your PATH. For a stable, artifact-friendly setup, install the Python **dbt Core v1** line plus `dbt-duckdb`, inside the venv.
119
+
120
+ ### 5. Clone the sandbox and build it
121
+
122
+ ```bash
123
+ git clone https://github.com/dbt-labs/jaffle_shop_duckdb.git
124
+ cd jaffle_shop_duckdb
125
+ # follow that repo's README to set up, then:
126
+ dbt build # creates target/manifest.json, target/catalog.json, jaffle_shop.duckdb
127
+ cd ..
128
+ ```
129
+
130
+ ### 6. Configure secrets
131
+
132
+ ```bash
133
+ cp .env.example .env # then edit .env and paste your keys
134
+ ```
135
+
136
+ `.env` holds `LLM_PROVIDER` (`gemini` | `groq` | `ollama`), `GEMINI_API_KEY`, and `GROQ_API_KEY`. It is gitignored — **never commit it**.
137
+
138
+ ### 7. Verify your LLM keys
139
+
140
+ ```bash
141
+ pip install google-genai groq
142
+ python check_llm.py # uses LLM_PROVIDER from .env; `python check_llm.py groq` forces a lane
143
+ ```
144
+
145
+ Expect `[ok] gemini responded: '...key OK'`.
146
+
147
+ **Week 0 is done when:** `dbt build` succeeds on jaffle_shop and `check_llm.py` prints `[ok]`.
148
+
149
+ ---
150
+
151
+ ## Week 1 — profiling your data
152
+
153
+ The profiler is the `dbt_testpilot` Python package in this repo.
154
+
155
+ ### Run it
156
+
157
+ ```bash
158
+ source .venv/bin/activate
159
+ pip install duckdb
160
+ python -m dbt_testpilot profile --project-dir jaffle_shop_duckdb --out profiles.json
161
+ ```
162
+
163
+ | Flag | Meaning |
164
+ |---|---|
165
+ | `--project-dir` | dbt project directory (must contain `target/` and the `.duckdb` file). Default `.` |
166
+ | `--db` | Explicit path to the DuckDB file (auto-detected from `--project-dir` if omitted) |
167
+ | `--out` | Write per-model profiles to this JSON file |
168
+ | `--max-values` | Max distinct values captured for low-cardinality columns (default 25) |
169
+
170
+ ### What you get
171
+
172
+ **Console** — per model, each column with type, null %, cardinality, `UNIQUE` / `NOT NULL` flags, min/max range, low-cardinality value lists, and any tests it already has.
173
+
174
+ **`profiles.json`** — the machine-readable input for Week 2. Each column carries:
175
+
176
+ `name`, `data_type`, `null_count`, `null_pct`, `distinct_count`, `distinct_pct`, `is_unique`, `min`, `max`, `top_values` (for low-cardinality columns), and `existing_tests`.
177
+
178
+ ### Example output
179
+
180
+ ```
181
+ == main.customers == (rows: 100)
182
+ customer_id INTEGER nulls 0.0% distinct 100 (100%) [UNIQUE, NOT NULL] range[1..100] tests:unique,not_null
183
+ first_order DATE nulls 38.0% distinct 46 (46%) range[2018-01-01..2018-04-07]
184
+
185
+ == main.orders == (rows: 99)
186
+ status VARCHAR nulls 0.0% distinct 5 (5%) [NOT NULL] tests:accepted_values
187
+ values: completed(67), placed(13), shipped(13), returned(4), return_pending(2)
188
+ ```
189
+
190
+ ### How it works (architecture)
191
+
192
+ | Module | Responsibility |
193
+ |---|---|
194
+ | `artifacts.py` | Parse `manifest.json` (model list + already-existing tests) and `catalog.json` (declared column types) |
195
+ | `profiler.py` | Read the **actual** columns/types from DuckDB's `information_schema`, then one aggregate pass per table for row count, null %, cardinality, and min/max; grab value lists for low-cardinality columns |
196
+ | `report.py` | Render each profile as a JSON-ready dict and a readable console view |
197
+ | `cli.py` / `__main__.py` | The `profile` command |
198
+
199
+ Two design choices worth knowing: columns are read from the **database itself** (source of truth, not stale metadata), and **existing tests are captured** so later steps propose only the tests you're *missing*.
200
+
201
+ ---
202
+
203
+ ## Week 2 — propose the missing tests
204
+
205
+ `propose` reads the `profiles.json` from Week 1 and suggests the dbt tests each model is missing. It runs in two layers: a deterministic **heuristic** base (no API key, works offline) and an optional **LLM** layer (`--llm`) that adds rationale, relationships, and custom tests. Tests a column already has are always skipped.
206
+
207
+ ### Run it
208
+
209
+ ```bash
210
+ source .venv/bin/activate
211
+
212
+ # heuristics only — no key needed
213
+ python -m dbt_testpilot propose --profiles profiles.json --out proposals.json
214
+
215
+ # heuristics + LLM augmentation (reads LLM_PROVIDER + key from .env)
216
+ python -m dbt_testpilot propose --profiles profiles.json --out proposals.json --llm
217
+ ```
218
+
219
+ | Flag | Meaning |
220
+ |---|---|
221
+ | `--profiles` | Path to the `profiles.json` produced by `profile` (default `profiles.json`) |
222
+ | `--out` | Write the proposals to this JSON file |
223
+ | `--llm` | Also query the LLM and merge its proposals |
224
+ | `--provider` | `gemini` or `groq` (default: `LLM_PROVIDER` from `.env`) |
225
+ | `--env` | Path to the `.env` holding your API keys (default `.env`) |
226
+
227
+ ### The heuristic rules (deterministic, no key)
228
+
229
+ | Test | Proposed when |
230
+ |---|---|
231
+ | `not_null` | the column has **zero nulls** across the table |
232
+ | `unique` | distinct count **equals the non-null count** (a real key) |
233
+ | `accepted_values` | a **categorical** column (text/boolean) with ≤ 15 distinct values — the observed values become the allowed set |
234
+ | `relationships` | a `*_id` column that is **not** this table's own key but **is** unique in another model (a foreign-key guess) |
235
+
236
+ Confidence is `high` for clear cases and `medium` for softer guesses (relationships, nullable uniques).
237
+
238
+ ### The LLM layer (`--llm`)
239
+
240
+ Each model's profile is sent to Gemini/Groq **one model at a time** (small prompts, friendly to free-tier token limits) with an instruction to return **only** the missing tests as strict JSON. Every returned item is validated against the proposal schema — the column must actually exist, `accepted_values` must include `values`, `relationships` must include `to`/`field` — and anything malformed or hallucinated is dropped. If a call fails, that model degrades to heuristics-only instead of erroring. The LLM can also suggest **custom** tests (a named check plus a rationale).
241
+
242
+ ### What you get
243
+
244
+ **Console** — per model, each proposed test with its column, source (`H`euristic / `L`LM), confidence, arguments, and a one-line rationale.
245
+
246
+ **`proposals.json`** — grouped by model; each proposal carries `model`, `column`, `test`, `arguments`, `rationale`, `source` (`heuristic` | `llm`), and `confidence`.
247
+
248
+ ### Example output (heuristics on jaffle_shop)
249
+
250
+ ```
251
+ == main.orders == (2 proposed)
252
+ + not_null order_date [H/high] 0 nulls across 99 rows
253
+ + not_null status [H/high] 0 nulls across 99 rows
254
+
255
+ == main.stg_payments == (4 proposed)
256
+ + not_null order_id [H/high] 0 nulls across 113 rows
257
+ + relationships order_id [H/medium] -> ref('orders').order_id
258
+ + not_null payment_method [H/high]
259
+ + not_null amount [H/high]
260
+ ```
261
+
262
+ On jaffle_shop the heuristics alone produce **14 proposals** (12 `not_null`, 2 `relationships`) and correctly skip every column that already has a `unique` / `accepted_values` / `relationships` test.
263
+
264
+ ### How it works (architecture)
265
+
266
+ | Module | Responsibility |
267
+ |---|---|
268
+ | `proposals.py` | `Proposal` schema, the deterministic heuristic rules, LLM-output validation, and merge/dedupe (drops anything already tested) |
269
+ | `llm.py` | Per-model prompt, Gemini/Groq call in JSON mode, parse + validate, graceful per-model fallback |
270
+
271
+ The approved proposals feed **Week 3**, which writes them into each model's `schema.yml` and runs `dbt test`.
272
+
273
+ ---
274
+
275
+ ## Week 3 — write tests + run
276
+
277
+ `apply` closes the loop: review the proposals, write the ones you approve into the models' `schema.yml`, and run `dbt test`. **Nothing is written without your OK** — approval is interactive by default.
278
+
279
+ ### Run it
280
+
281
+ ```bash
282
+ source .venv/bin/activate
283
+ pip install ruamel.yaml
284
+
285
+ # review each proposal interactively, then write the approved ones
286
+ python -m dbt_testpilot apply --proposals proposals.json --project-dir jaffle_shop_duckdb
287
+
288
+ # preview only (writes nothing)
289
+ python -m dbt_testpilot apply --proposals proposals.json --project-dir jaffle_shop_duckdb --dry-run
290
+
291
+ # accept all and run dbt test in one go
292
+ python -m dbt_testpilot apply --proposals proposals.json --project-dir jaffle_shop_duckdb --yes --run
293
+ ```
294
+
295
+ | Flag | Meaning |
296
+ |---|---|
297
+ | `--proposals` | Proposals JSON from `propose` (default `proposals.json`) |
298
+ | `--project-dir` | dbt project whose `schema.yml` files get updated |
299
+ | `--yes` | Approve all (non-interactive) |
300
+ | `--min-confidence` | Only consider proposals at/above `low` / `medium` / `high` |
301
+ | `--dry-run` | Show what would be written; change nothing |
302
+ | `--include-custom` | Generate vetted macros for supported custom tests (value ≥ 0, not-future) and write them |
303
+ | `--run` | Run `dbt test` after writing |
304
+
305
+ ### The approve/reject flow
306
+
307
+ For each proposal you see the model, column, test, arguments, and rationale, then choose **[y]es / [n]o / [a]ll remaining / [q]uit**. Approved tests are merged into the model's `schema.yml` — located via the manifest's `patch_path` — using **round-trip YAML**, so your existing descriptions, comments, ordering, and formatting are preserved. It matches the file's existing key (`tests:` or `data_tests:`) and uses dbt's `arguments:` wrapper for tests that take arguments. Missing columns are created; duplicate tests are skipped.
308
+
309
+ ### Built-in vs custom tests
310
+
311
+ Only dbt's built-in generic tests (`not_null`, `unique`, `accepted_values`, `relationships`) are written by default, so `dbt test` runs green immediately.
312
+
313
+ The LLM also proposes **custom** tests (e.g. `positive_amount`, `date_in_past_or_present`). dbt runs a test named `foo` via a macro `test_foo`, which won't exist for these — so by default they're skipped with a note. With `--include-custom`, dbt-testpilot **generates vetted macros** into `macros/dbt_testpilot/` for the patterns it recognises (value ≥ 0; date not in the future) and writes those tests. Genuinely bespoke ones (multi-column or regex logic) are still skipped for you to implement — auto-writing SQL we can't guarantee would risk false confidence. On jaffle_shop this turns 9 of the 13 LLM suggestions into passing tests (`dbt test` → 45/45).
314
+
315
+ ### End-to-end result (jaffle_shop)
316
+
317
+ Approving the built-in proposals added 16 tests, and `dbt test` passed clean:
318
+
319
+ ```
320
+ Done. PASS=36 WARN=0 ERROR=0 SKIP=0 TOTAL=36
321
+ ```
322
+
323
+ That's 20 pre-existing + 16 generated — including `not_null` on `orders.order_date`, `relationships` from `stg_orders.customer_id` → `customers`, and more. With `--include-custom`, the 9 supported custom tests get generated macros too, taking the suite to **45/45** (bespoke tests skipped).
324
+
325
+ ### How it works (architecture)
326
+
327
+ | Module | Responsibility |
328
+ |---|---|
329
+ | `yaml_writer.py` | Locate each model's `schema.yml` (manifest `patch_path`) and merge approved tests into the right column via round-trip YAML — no clobbering |
330
+ | `apply.py` | Interactive approve/reject, built-in vs custom filtering, then optionally shell out to `dbt test` |
331
+ | `macrogen.py` | Generate vetted generic-test macros for supported custom patterns (value ≥ 0, not-future) |
332
+
333
+ That completes the loop — **scan → review → approve → tests running**.
334
+
335
+ ---
336
+
337
+ ## Project layout
338
+
339
+ ```
340
+ dbt-testpilot/
341
+ ├─ dbt_testpilot/ # the tool (Python package)
342
+ │ ├─ __init__.py
343
+ │ ├─ __main__.py # python -m dbt_testpilot
344
+ │ ├─ artifacts.py # parse manifest.json + catalog.json
345
+ │ ├─ profiler.py # DuckDB profiling (Week 1)
346
+ │ ├─ proposals.py # heuristic proposals + schema/validation (Week 2)
347
+ │ ├─ llm.py # LLM proposals, strict JSON (Week 2)
348
+ │ ├─ yaml_writer.py # merge approved tests into schema.yml (Week 3)
349
+ │ ├─ apply.py # approve/reject flow + run dbt test (Week 3)
350
+ │ ├─ macrogen.py # generate macros for supported custom tests (Week 3.5)
351
+ │ ├─ report.py # JSON + console rendering
352
+ │ └─ cli.py # profile + propose + apply commands
353
+ ├─ jaffle_shop_duckdb/ # sample dbt project (sandbox) — gitignored
354
+ ├─ check_llm.py # Week 0: one-shot LLM key verifier
355
+ ├─ dbt-testpilot_Roadmap.md # the 4-week plan
356
+ ├─ docs/progress-report.md # detailed build log
357
+ ├─ README.md
358
+ ├─ pyproject.toml, uv.lock # uv project scaffolding
359
+ ├─ .env / .env.example / .gitignore
360
+ └─ profiles.json # generated profiler output — gitignored
361
+ ```
362
+
363
+ ## Roadmap
364
+
365
+ - [x] **Part 0** — accounts, installs, sandbox, secrets, repo hygiene
366
+ - [x] **Week 1** — data profiler (`manifest`/`catalog` + DuckDB stats → `profiles.json`)
367
+ - [x] **Week 2** — propose missing tests (heuristics + optional LLM, validated JSON)
368
+ - [x] **Week 3** — write approved tests to `schema.yml` + `dbt test`
369
+ - [ ] **Week 4** — package and publish (`pip install dbt-testpilot`)
370
+
371
+ Full details in [`docs/progress-report.md`](docs/progress-report.md).
372
+
373
+ ## Troubleshooting
374
+
375
+ - **`no manifest.json under .../target`** — run `dbt build` (and `dbt docs generate` for `catalog.json`) in the dbt project first.
376
+ - **`catalog.json` missing** — `dbt build` alone doesn't create it; run `dbt docs generate`.
377
+ - **`SDK not installed` from `check_llm.py`** — `pip install google-genai groq`.
378
+ - **`No module named duckdb`** — `pip install duckdb` inside the active venv.
379
+ - **`No module named 'ruamel'`** — `pip install ruamel.yaml` (needed by `apply`).
380
+ - **`apply` skipped my LLM tests** — custom tests are skipped by default. Add `--include-custom` to auto-generate vetted macros for supported patterns (value ≥ 0, not-future) and write those; genuinely bespoke ones still need a hand-written macro.
381
+ - **Free-tier limits change** — provider is read from `.env`, so switching Gemini ⇄ Groq ⇄ Ollama is a one-line edit.
382
+
383
+ ## License
384
+
385
+ MIT (planned).