<!-- AUTO-GENERATED — do not edit this file directly.
     Source of truth: conformance/suite/rules/dependency.py
     To regenerate:  uv run atlan-application-sdk-conformance gen-rule-docs
     To check CI staleness: uv run atlan-application-sdk-conformance gen-rule-docs --check -->

# Dependency Rules (D-series)

**10 rules** · Checker: `suite.checks.dependency_conformance` (TOML-based, static)

Suppress a finding on the violating line or the line directly above it:

```python
# conformance: ignore[D002] intentional: pinned to pre-release for hotfix
```

| ID | Name | Tier | Scope | Category | Autofixable | Since |
|---|---|---|---|---|---|---|
| [D001](#d001) | `UnpinnedSdkDependency` | `block` | `app` | `dependency-pinning` | yes | 0.4.0 |
| [D002](#d002) | `RedeclaredSdkManagedDependency` | `warn` | `app` | `dependency-pinning` | yes | 0.4.0 |
| [D003](#d003) | `UnusedDependency` | `warn` | `both` | `dependency-hygiene` | — | 0.5.0 |
| [D004](#d004) | `RedeclaredSdkManagedDependencyInGroups` | `warn` | `app` | `dependency-pinning` | yes | 0.5.0 |
| [D005](#d005) | `UnknownSdkExtra` | `warn` | `app` | `dependency-pinning` | — | 0.5.0 |
| [D006](#d006) | `IncompatibleRequiresPython` | `warn` | `app` | `python-version` | yes | 0.5.0 |
| [D007](#d007) | `NonStandardBuildBackend` | `warn` | `app` | `build-system` | yes | 0.5.0 |
| [D008](#d008) | `WeakenedTypeChecking` | `warn` | `app` | `tooling-baseline` | yes | 0.5.0 |
| [D009](#d009) | `RemoteDaprComponentFetch` | `block` | `app` | `dapr-components` | yes | 0.12.0 |
| [D010](#d010) | `QueryTransformerWithoutDuckdb` | `warn` | `app` | `runtime-dependencies` | — | 0.18.0 |

---

## D001 — `UnpinnedSdkDependency` {#d001}

**Tier:** `block` · **Scope:** `app` · **Category:** `dependency-pinning` · **Autofixable:** yes · **Since:** 0.4.0

> Application SDK dependency is missing or its version specifier is not bounded on both ends

**Rationale:** An unbounded specifier lets an automated tool (Renovate) or a manual bump pull in a
future SDK major without review. The SDK's versioning discipline only holds if every app
has a bound that stops automatic upgrades past the reviewed point.

Every app must declare `atlan-application-sdk` in `[project.dependencies]` with a
version specifier that has both a lower bound (`>=` or `==`) and an upper bound (`<` or
a compatible-release `~=` form). Unbounded specifiers let an automated SDK upgrade pull
in a future major version without review, defeating the fleet-wide gate.  Apps shipping
the SDK are also exempt — packages whose `[project].name` starts with
`atlan-application-sdk` are skipped entirely.

---

## D002 — `RedeclaredSdkManagedDependency` {#d002}

**Tier:** `warn` · **Scope:** `app` · **Category:** `dependency-pinning` · **Autofixable:** yes · **Since:** 0.4.0

> Dependency redeclared in the app's pyproject.toml is already managed by the SDK

**Rationale:** When an app redeclares a package the SDK already pins, the resolver may pick the app's
specifier over the SDK's, yielding a version never validated against the SDK. This
causes resolver conflicts during upgrades and forces touching every app that holds a
duplicate when the SDK pin changes.

Packages pinned by `atlan-application-sdk` (its core `[project.dependencies]`) must not
be redeclared in the app's `[project.dependencies]` or any
`[project.optional-dependencies.*]` array.  Redeclaring a managed pin lets the app
silently override the SDK's contract, causing resolver conflicts and drift across the
fleet during automated SDK upgrades.  The SDK's managed set is read at check time via
`importlib.metadata.requires('atlan-application-sdk')`; if the SDK is not importable in
the runtime environment, this rule is skipped silently.

---

## D003 — `UnusedDependency` {#d003}

**Tier:** `warn` · **Scope:** `both` · **Category:** `dependency-hygiene` · **Autofixable:** — · **Since:** 0.5.0

> A package declared in [project.dependencies] is never imported in source

**Rationale:** A package declared in core dependencies but never imported is either dead weight that
slows resolution and widens the supply-chain/CVE surface, or it was meant to live
elsewhere (a test/dev group). Surfacing it turns the recurring manual question during a
version bump — 'is this even used?' — into a deterministic, reviewable signal. It stays
advisory (WARN, no autofix) because a dependency can be loaded dynamically, via an entry
point/plugin, or run as a server (e.g. uvicorn) without an explicit import.

Every package in the repo's core `[project.dependencies]` should be imported somewhere
in the shipped source.  This rule maps each declared distribution to the import name(s)
it provides and flags any whose modules never appear in an `import`/`from` statement
across the repo's Python sources (tests, build, and dot-directories are excluded — a
runtime dependency used *only* under `tests/` is itself a finding, because it belongs in
a test group, not core dependencies).  The finding is advisory: before removing, confirm
the dependency is not imported dynamically (via `importlib`), pulled in by an entry
point or plugin, or required by a framework/server it is never directly imported by.
Only core `[project.dependencies]` is analysed — optional-dependency extras and
dependency groups routinely carry tools and plugins that are legitimately never
imported.  A dependency that cannot be resolved in the analysis environment is skipped
(and reported), never flagged.  **Operating note:** resolution maps a distribution to
its import name(s) via installed package metadata, so the analysed repo's dependencies
must be importable in the running interpreter — run `uv sync` first.  In an isolated
runner (e.g. `uvx atlan-application-sdk-conformance detect --series D`) no dependency is
installed, so every one is skipped to stderr and the rule reports nothing; that is an
unresolved environment, not a clean repo.  The conformance CI runs the D-series leg in a
synced environment for this reason.  See BLDX-1462.

---

## D004 — `RedeclaredSdkManagedDependencyInGroups` {#d004}

**Tier:** `warn` · **Scope:** `app` · **Category:** `dependency-pinning` · **Autofixable:** yes · **Since:** 0.5.0

> SDK-managed dependency redeclared in a [dependency-groups] table

**Rationale:** D002 only covers [project.dependencies] and the optional-dependencies arrays; an
SDK-managed package re-pinned in a PEP 735 [dependency-groups] table escapes it. A
dev/test group that re-pins a package the SDK already manages drifts from the SDK's
validated dev environment and must be touched on every SDK bump.

Packages pinned by `atlan-application-sdk` must not be redeclared in the app's PEP 735
`[dependency-groups.*]` tables (dev/test groups).  This is the coverage gap left by
D002, which scans only `[project.dependencies]` and `[project.optional-dependencies.*]`.
Pull SDK-managed dev/test tooling in via `atlan-application-sdk[tests]` rather than
re-pinning it.  The managed set is read via
`importlib.metadata.requires('atlan-application-sdk')`; if the SDK is not importable,
this rule is skipped silently. Cite: BLDX-1410.

---

## D005 — `UnknownSdkExtra` {#d005}

**Tier:** `warn` · **Scope:** `app` · **Category:** `dependency-pinning` · **Autofixable:** — · **Since:** 0.5.0

> Reference to an atlan-application-sdk extra the SDK does not publish

**Rationale:** uv silently drops an unknown extra, so a typo like `atlan-application-sdk[dapr]` (no
such extra) installs nothing for that extra and the missing dependencies surface only at
runtime. Validating the reference against the SDK's published extras catches the
silent-failure at build time.

Every `atlan-application-sdk[extra]` reference must name an extra the SDK actually
publishes (its `Provides-Extra` metadata).  An unknown extra is silently dropped by uv,
so its dependencies are never installed and the failure appears only at runtime.  The
published set is read from installed metadata; if the SDK is not importable, this rule
is skipped silently.  The fix (map a typo to the intended extra) is judgment, so
findings route to residue rather than auto-fix.  Cite: BLDX-1410.

---

## D006 — `IncompatibleRequiresPython` {#d006}

**Tier:** `warn` · **Scope:** `app` · **Category:** `python-version` · **Autofixable:** yes · **Since:** 0.5.0

> App requires-python lower bound is below the SDK's minimum supported Python version

**Rationale:** An app whose requires-python lower bound is below the SDK's claims to support an
interpreter the SDK does not. Installs on that Python resolve a degraded or broken
dependency set, and the mismatch surfaces only at runtime on the oldest supported
environment — exactly where it is hardest to catch in review.

The app's `[project].requires-python` lower bound must be at least the SDK's minimum
supported Python (`>=3.11`). A lower floor lets the app be installed on a Python the SDK
never validated against, where transitive resolution and runtime behaviour are
unsupported. Apps that omit `requires-python` or set a bound at or above the SDK's floor
are unaffected. The SDK's floor is a drift-guarded constant in the checker, not read
from installed metadata, so this rule needs no resolved environment. Cite: BLDX-1410.

---

## D007 — `NonStandardBuildBackend` {#d007}

**Tier:** `warn` · **Scope:** `app` · **Category:** `build-system` · **Autofixable:** yes · **Since:** 0.5.0

> Build backend is not Hatchling

**Rationale:** Atlan apps standardise on Hatchling so build behaviour, wheel layout, and the managed CI
build steps are uniform across the fleet. A setuptools/poetry-core backend diverges from
that baseline and from the bootstrapped build-and-publish workflow, making fleet-wide
build changes per-app instead of uniform.

`[build-system].build-backend` must be `hatchling.build`.  Atlan's app fleet
standardises on Hatchling so the managed build-and-publish workflow and wheel layout are
uniform; a different backend diverges from that baseline.  A pyproject with no
`build-backend` key is not flagged.  Cite: BLDX-1410.

---

## D008 — `WeakenedTypeChecking` {#d008}

**Tier:** `warn` · **Scope:** `app` · **Category:** `tooling-baseline` · **Autofixable:** yes · **Since:** 0.5.0

> pyright typeCheckingMode is weaker than the SDK baseline 'standard'

**Rationale:** The SDK's typed contracts only protect an app whose type checker actually runs at the
SDK's level. A typeCheckingMode of 'off' or 'basic' lets type regressions against SDK
APIs pass app CI unnoticed, defeating the point of the typed surface.

`[tool.pyright].typeCheckingMode` must not be weaker than the SDK baseline `standard` —
`off` and `basic` are flagged; `standard` and `strict` pass.  A weakened mode lets type
regressions against the SDK's typed APIs slip through app CI.  A pyproject that does not
set `typeCheckingMode` is not flagged; blanket `reportX = false` overrides are out of
scope (they can be legitimate).  Cite: BLDX-1410.

---

## D009 — `RemoteDaprComponentFetch` {#d009}

**Tier:** `block` · **Scope:** `app` · **Category:** `dapr-components` · **Autofixable:** yes · **Since:** 0.12.0

> A poe task fetches Dapr component YAMLs from GitHub instead of the installed application-sdk wheel

**Rationale:** Fetching Dapr component YAMLs from raw.githubusercontent.com or the GitHub contents API
at build time hits GitHub's unauthenticated rate limit under CI concurrency, turning
routine builds into flaky 429s across the fleet. The hardcoded SDK ref these fetches pin
to also drifts from whatever application-sdk version is actually locked in the app's own
uv.lock. The installed SDK wheel already bundles these files at
application_sdk/components/, so the network round-trip is both fragile and redundant.

No `[tool.poe.tasks.*]` entry (in either the shorthand `task.shell = "..."` form or the
full `[tool.poe.tasks.task]` table form) may reference `raw.githubusercontent.com` or
`api.github.com` for `atlanhq/application-sdk`. Dapr component YAMLs are bundled inside
the `atlan-application-sdk` wheel at `application_sdk/components/` — copy them from
there instead, e.g. `shutil.copytree(pathlib.Path(application_sdk.__file__).parent /
'components', 'components', dirs_exist_ok=True)`. This requires application-sdk to
already be installed into the venv before the task runs (true both locally and in the
Docker build, where `uv sync` precedes `poe download-components`). Inline suppression:
`# conformance: ignore[D009] <reason>` on the line above the offending entry.

---

## D010 — `QueryTransformerWithoutDuckdb` {#d010}

**Tier:** `warn` · **Scope:** `app` · **Category:** `runtime-dependencies` · **Autofixable:** — · **Since:** 0.18.0

> App imports the SDK query transformer but duckdb is not resolved (no [sql]/[incremental] extra, no direct dependency)

**Rationale:** The SDK's query transformer (application_sdk.transformers.query, the transform_metadata
path) executes its transform SQL through DuckDBConnectionManager, and duckdb ships only
in the SDK's [sql] and [incremental] extras — never in core. An app that imports the
query transformer on a plain atlan-application-sdk pin, with no extra at all, hits a
guaranteed runtime ImportError ('duckdb is required for DuckDBConnectionManager') in
EVERY transform — latent until the first real pipeline run, because imports alone
succeed and mocked unit tests pass. The population that surfaced this was a different,
SDK-owned shape: apps pinned to the deprecated [daft] extra, which resolved empty over
3.22–3.27 (observed live on main for a document-store connector in fleet testing after
an automated upgrade crossed the 3.22 line). That half is fixed at the root — [daft]
aliases [sql] again from 3.28.0 — so this rule now covers the no-extras case it always
also covered. Statically checkable: transformer-usage scan + lockfile/pyproject scan.

An app whose source imports the SDK query transformer
(`application_sdk.transformers.query` — the `transform_metadata` /
`QueryBasedTransformer` path) must be able to import `duckdb` at runtime: the
transformer executes its transform SQL through `DuckDBConnectionManager`, which raises
`ImportError: duckdb is required for DuckDBConnectionManager` when the package is
absent.

`duckdb` is provided by the SDK's `[sql]` and `[incremental]` extras only — never by the
core dependency set.  So the shape this rule describes is an app that imports the
transformer on a **plain** `atlan-application-sdk` pin, with no extra at all: the
failure is silent until the first real end-to-end transform, because imports succeed and
unit tests that mock the transformer pass.

**Not the `[daft]` case — that one was ours.**  Over SDK 3.22–3.27 the deprecated
`[daft]` extra resolved to nothing, so apps that were following the SDK's own
deprecation note were broken by an automated upgrade crossing the 3.22 line.  That is
fixed at the root: from 3.28.0 `[daft]` aliases `[sql]` again, and a version bump alone
resolves `duckdb` for every such app with no repo-side change.  If this rule fires on an
app pinned to `[daft]`, upgrade the SDK rather than editing the app's extras.

Resolution order of the check:

* with a parseable `uv.lock` present, `duckdb` must be reachable   from the app's OWN
production dependencies — the lock is walked   from the app's `[[package]]` entry along
`dependencies` and the   `optional-dependencies` groups an incoming extra activates.
`uv.lock` is a *universal* resolution graph covering dev groups   and every extra, so
duckdb merely appearing somewhere in it does   NOT mean a default `uv sync --no-dev`
installs it;   `[package.dev-dependencies]` is deliberately not traversed; * without a
usable lock, the app's `pyproject.toml` must declare   `duckdb` directly or reference
`atlan-application-sdk` with a   `sql` or `incremental` extra — in `[project]
dependencies`   specifically.  Dependency groups and optional-dependency arrays do   not
count: they are not installed by default.

**Remediation:** change the SDK reference to `atlan-application-sdk[sql]` (or
`[incremental]` for the incremental analytics stack) in `[project.dependencies]` and
relock (`uv lock`).  That is the fix.

Declaring `duckdb` directly is a discouraged fallback, not a co-equal option: it clears
the finding, but it duplicates a pin the SDK's extras already manage, so the app now
owns a version range it has to keep in step with the SDK's by hand.  Reach for it only
where the extra genuinely cannot be used.

This is a WARN (per the new-rule tier policy), but unlike most WARN findings it
indicates a *guaranteed* runtime failure — treat it as an error.

---
