|
| 1 | +# REVIEW.md — datapilot-cli |
| 2 | + |
| 3 | +Python CLI (`altimate-datapilot-cli` on PyPI) that parses dbt `manifest.json`/`catalog.json` |
| 4 | +artifacts across dbt manifest schema versions (vendored `dbt-artifacts-parser`, v1–v12) and runs |
| 5 | +data-quality "insight" checks over dbt projects, usable standalone or as a pre-commit hook. |
| 6 | + |
| 7 | +## Goal: catch what CI cannot |
| 8 | + |
| 9 | +CI runs formatting/linting/tests. You exist for correctness bugs in dbt-manifest parsing across |
| 10 | +schema versions, node-shape assumptions, and API/CLI behavior that no linter can see. If a diff is |
| 11 | +clean and doesn't touch any of the areas below, say `lgtm` — don't invent nits. |
| 12 | + |
| 13 | +## Don't duplicate CI — never flag |
| 14 | + |
| 15 | +This repo's `pre-commit` + `tox` pipeline (ruff, black) already enforces, so never re-flag: |
| 16 | +- Formatting (black) or import ordering (`ruff` rule `I`/isort). |
| 17 | +- Unused imports/variables, bare `except`, raise-from style (`RUF`, `RSE`), bugbear patterns (`B`), |
| 18 | + comprehension style (`C4`), pathlib usage (`PTH`), quote style (`Q`), pytest style (`PT`). |
| 19 | +- Naive `datetime` construction — ruff's `DTZ` rule already catches this. |
| 20 | +- Basic `flake8-bandit` (`S`) security smells (e.g. `assert` in non-test code, `eval`/`exec`). |
| 21 | +- Line length (140 cols, ruff-enforced). |
| 22 | +- Whether tests exist/pass — `tox` runs the suite; that's CI's job, not yours. |
| 23 | +Pre-existing issues in code the diff doesn't touch are out of scope — only review changed lines |
| 24 | +and their direct call sites. |
| 25 | + |
| 26 | +## Evidence standard — no speculation |
| 27 | + |
| 28 | +Before flagging a manifest/catalog-parsing issue, trace which manifest version(s) the changed code |
| 29 | +actually runs against and check the vendored schema in |
| 30 | +`src/vendor/dbt_artifacts_parser/parsers/manifest/manifest_vN.py` for that version — don't assume |
| 31 | +a field exists just because it's on `ManifestV12`. For CLI/API-client changes, trace whether the |
| 32 | +call site actually surfaces failures to the user before claiming error handling is missing. |
| 33 | + |
| 34 | +## Severity calibration |
| 35 | + |
| 36 | +- **Critical**: will raise/crash on a real dbt manifest version this tool claims to support (e.g. |
| 37 | + missing field, wrong required-ness, wrong checksum shape), or silently drops/misreports an |
| 38 | + insight-check result. |
| 39 | +- **Warning**: works today but is version-fragile, dialect-fragile, or duplicates logic that will |
| 40 | + drift (see focus areas below); or an API failure that's swallowed instead of surfaced to the CLI |
| 41 | + user. |
| 42 | +- **Nit**: naming, magic strings that have an existing constant, minor duplication. |
| 43 | + |
| 44 | +## Focus areas — bug classes this repo actually ships |
| 45 | + |
| 46 | +1. **Pydantic `extra="forbid"` on manifest/catalog wrapper models** (root cause of #92, #48, and |
| 47 | + related fixes). New dbt manifest versions add fields the `Altimate*` wrapper models don't know |
| 48 | + about yet; a wrapper `BaseModel` with `extra="forbid"` raises `ValidationError` and crashes |
| 49 | + parsing the moment dbt ships a minor version bump. Flag any new/edited model in |
| 50 | + `schemas/manifest.py`-style files whose `model_config` sets `extra="forbid"` (or omits it while |
| 51 | + inheriting a forbidding base) instead of `extra="allow"`. |
| 52 | + |
| 53 | +2. **Fields marked required on node wrapper models** (#98, #95, #96 — `identifier`, |
| 54 | + `resource_type` made optional after crashing on `Disabled*`/unit-test nodes). A field required |
| 55 | + on the "normal" node case often isn't populated on disabled nodes, unit-test nodes, or older |
| 56 | + manifest versions. Flag any new required (non-`Optional`) field added to an `Altimate*` node |
| 57 | + model; verify it's actually present across disabled/unit-test/seed/source variants, not just |
| 58 | + the common model case. |
| 59 | + |
| 60 | +3. **Version-specific field access without a version guard** (e.g. `deferred` not present on v12 |
| 61 | + `RPCNode`; behavior tied to a specific dbt/artifact-parser version). Flag direct attribute |
| 62 | + access on a vendored `manifest_vN` type inside code paths meant to be version-agnostic; verify |
| 63 | + against the specific `manifest_vN.py` the code targets. |
| 64 | + |
| 65 | +4. **Checksum/hash shape assumptions** (#94 — `checksum` is a dict on some node types, a |
| 66 | + string/object on others). Flag `.checksum.name` / `.checksum.checksum`-style access without |
| 67 | + confirming that node type has that structure in that manifest version. |
| 68 | + |
| 69 | +5. **Catalog/manifest lookups assumed non-`None`** ("catalog nodes can be `None`", catalog can |
| 70 | + have fewer columns than the manifest). `--catalog-path` is optional per the CLI, and |
| 71 | + `dbt docs generate` output can be stale relative to the manifest. Flag catalog/manifest |
| 72 | + `.nodes[x]` / `.columns[y]` access without a `None`/`KeyError` guard in insight-check code. |
| 73 | + |
| 74 | +6. **Mutable default arguments** (explicitly flagged in review — "list is mutable, weird things can |
| 75 | + happen" — not reliably caught by this repo's ruff config). Flag `def f(x=[])`, `def f(x={})`, or |
| 76 | + a list/dict class attribute mutated in place across calls instead of copied per-instance. |
| 77 | + |
| 78 | +7. **Hardcoded materialization/dialect assumptions in insight checks** ("dialects may have |
| 79 | + multiple materializations and it might flag something we don't want to flag", "use the VIEW |
| 80 | + constant, we have it somewhere"). Flag magic materialization strings (`"view"`, `"table"`, |
| 81 | + `"incremental"`) instead of the shared constant, and any check that assumes one dialect's |
| 82 | + materialization vocabulary applies to all configured warehouses. |
| 83 | + |
| 84 | +8. **Duplicated logic across check/insight classes** ("put this in the base `DBTCheck` class so |
| 85 | + it's not repeated", "make it a reusable util"). Flag near-identical blocks copy-pasted across |
| 86 | + files under the insights/checks package instead of factored onto a shared base class or util. |
| 87 | + |
| 88 | +9. **API-client failures not surfaced to the CLI user** ("should handle exceptions and not-200s |
| 89 | + nicely", "get the response code and log it", "return the status so we inform the user what |
| 90 | + happened"). The API client intentionally doesn't raise on non-2xx (see Known-intentional) — the |
| 91 | + bug class is the CLI layer silently swallowing that failure instead of reporting it to the |
| 92 | + user. Flag call sites that ignore or only debug-log a non-2xx/error API response. |
| 93 | + |
| 94 | +10. **Hardcoded environment-specific defaults** ("needs to be injectable from the CLI/environment, |
| 95 | + not hardcoded"). Flag new hardcoded endpoint/host/instance defaults added to config or client |
| 96 | + code that aren't also exposed as a CLI flag or env var override. |
| 97 | + |
| 98 | +11. **Enum/resource-type completeness across manifest versions** ("how is it mapping between the |
| 99 | + enums", "not sure all types have labels"). `AltimateResourceType`/`AltimateAccess`-style enums |
| 100 | + must track new dbt node/resource types introduced by an artifact-parser version bump. Flag a |
| 101 | + vendored-parser bump or new node type handled without a corresponding Altimate enum member. |
| 102 | + |
| 103 | +12. **Release/version-bump workflow trigger conditions** (`.bumpversion.cfg`, `bump-version.yml`, |
| 104 | + `tag-release.yml` — "won't this always release a tag? should be manual"). Flag changes to |
| 105 | + release-workflow `on:` triggers; verify a routine merge to main can't unintentionally fire a |
| 106 | + tag/release. |
| 107 | + |
| 108 | +## Repo invariants & landmines |
| 109 | + |
| 110 | +- `Manifest = Union[ManifestV1, ..., ManifestV12]` (`schemas/manifest.py`) — code that pattern- |
| 111 | + matches or accesses fields without checking manifest version breaks on the boundary versions. |
| 112 | +- `src/vendor/dbt_artifacts_parser` is a deliberately vendored/forked copy (see fix history: "use |
| 113 | + vendored dbt-artifacts-parser", "use forked dbt-artifacts-parser") — it exists because upstream |
| 114 | + didn't support something needed here; don't "clean up" divergence from upstream as if it were |
| 115 | + drift. |
| 116 | +- Two distinct run modes with different data completeness: a full CLI run (`datapilot dbt |
| 117 | + project-health`) vs. the pre-commit hook, which partially generates manifest/catalog from only |
| 118 | + changed files. Manifest-loading changes need to work under both. |
| 119 | +- Minimum supported Python is 3.7 per README — avoid relying on syntax/stdlib features unavailable |
| 120 | + there unless the actual tox/CI matrix has been checked. |
| 121 | +- `--catalog-path` is optional; code paths must degrade gracefully (skip catalog-dependent |
| 122 | + insights) rather than assume catalog data is present. |
| 123 | + |
| 124 | +## Known-intentional — don't flag |
| 125 | + |
| 126 | +- `APIClient.get()` not raising on non-2xx responses — deliberate design ("safer as it passes |
| 127 | + things through," per review). Only flag if the *caller* drops the failure silently (focus area |
| 128 | + 9), not the client's non-raising behavior itself. |
| 129 | +- `print()` used for CLI-facing output — confirmed intentional ("this is the intended output, not |
| 130 | + really logging"). Don't suggest converting these to `logger` calls. |
| 131 | +- The manifest wrapper not modeling unit-test nodes in certain paths — confirmed as not a real gap |
| 132 | + where raised ("there's no unit_test/UnitTest reference in the wrapper code" — deliberate scope). |
| 133 | + |
| 134 | +## High-blast-radius — never auto-edit |
| 135 | + |
| 136 | +- `src/vendor/dbt_artifacts_parser/**` — vendored upstream parser; changes must be deliberate forks |
| 137 | + with a stated reason, not incidental "fixes." |
| 138 | +- `.bumpversion.cfg`, `bump-version.yml`, `tag-release.yml`, `release.sh`, `setup.py` — PyPI release |
| 139 | + plumbing for `altimate-datapilot-cli`. |
| 140 | +- `pyproject.toml`, `tox.ini`, `.pre-commit-config.yaml` — changing these changes what's enforced |
| 141 | + repo-wide, not just this PR. |
| 142 | +- `CHANGELOG.rst`, `AUTHORS.rst` — maintained by release tooling, not hand-edited in feature PRs. |
| 143 | + |
| 144 | +## Comment style |
| 145 | + |
| 146 | +- Point at exact lines; one finding per comment; most-severe first. |
| 147 | +- Phrase as suggestions the author can accept or reject, not mandates — link a `diff` snippet if a |
| 148 | + concrete fix is short. |
| 149 | +- Skip deep review of trivial diffs (README/CHANGELOG-only, formatting-only, dependency bumps with |
| 150 | + no vendored-parser impact). |
0 commit comments