<!-- AUTO-GENERATED — do not edit this file directly.
     Source of truth: conformance/suite/rules/deprecation.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 -->

# Backwards-Compatibility / Deprecation Rules (B-series)

**7 rules** · Checker: `suite.checks.deprecation` (AST-based)

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

```python
# conformance: ignore[B001] intentional: migration deferred to next sprint
```

**Rule-id stability (non-migration policy):** rule ids are a permanent public contract —
each is exposed in the SARIF `help_uri` and referenced by inline `# conformance:
ignore[...]` suppressions across the fleet.  An id therefore **never migrates and never
changes**, even if a future domain series (S/B/T/A/…) later subsumes the same topic.
When a domain series takes over an area, the rule is retired in place (kept documented,
no longer firing) and the new rule gets a fresh id — the original id is never reused or
reassigned.

| ID | Name | Tier | Scope | Category | Autofixable | Since |
|---|---|---|---|---|---|---|
| [B001](#b001) | `DeprecatedSdkSymbolUsage` | `warn` | `app` | `deprecated-symbol-usage` | — | 0.5.0 |
| [B002](#b002) | `MalformedDeprecationNotice` | `warn` | `sdk` | `deprecation-hygiene` | — | 0.5.0 |
| [B003](#b003) | `OverdueDeprecationRemoval` | `warn` | `sdk` | `deprecation-hygiene` | — | 0.5.0 |
| [B004](#b004) | `UnmarkedDeprecationClaim` | `warn` | `sdk` | `deprecation-hygiene` | — | 0.5.0 |
| [B005](#b005) | `NonAdditiveContractChange` | `block` | `both` | `contract-backwards-compatibility` | — | 0.7.0 |
| [B006](#b006) | `StaleContractLedger` | `block` | `both` | `contract-backwards-compatibility` | — | 0.7.0 |
| [B007](#b007) | `DaftOnlyDataframeApiUsage` | `warn` | `app` | `daft-removal` | — | 0.18.0 |

---

## B001 — `DeprecatedSdkSymbolUsage` {#b001}

**Tier:** `warn` · **Scope:** `app` · **Category:** `deprecated-symbol-usage` · **Autofixable:** — · **Since:** 0.5.0

> Imports, subclasses, or calls an SDK symbol the SDK has marked deprecated

**Rationale:** A symbol the SDK has marked deprecated is on a removal path: code that imports,
subclasses, or calls it will break at the next major SDK bump. Surfacing that
consumption now — while the deprecation notice still carries the SDK's own migration
guidance — turns every future SDK deprecation into a fleet-wide nudge with zero per-app
configuration (BLDX-1418). The deprecated set is a committed manifest generated from SDK
source, so apps need neither the SDK source nor any local list. WARN because the
migration changes call shapes (signatures, return types) and needs human judgement, not
a blind swap.

Flags app consumption of any symbol recorded in the deprecated-symbol manifest the SDK
ships with this conformance package (BLDX-1418).  Four surfaces are matched,
name-anchored within an `application_sdk` import context:

* importing a deprecated class/function (`from application_sdk.x import Foo`); *
subclassing a deprecated base (`class MyExtractor(BaseMetadataExtractor)`); * calling a
deprecated method by attribute (`obj.upload_to_atlan(...)`); * reading a deprecated enum
member (`DataframeType.daft`), which the   SDK marks with a `__deprecated_members__`
mapping in the enum's   class body — the convention that exists because `@deprecated`
cannot attach to a member.  Matching is module-aware through the enum   class, so an
app's own same-named enum is never flagged.

The finding carries the SDK's migration guidance from the deprecation notice so the fix
is concrete.  Complements E013 `LegacyAtlanErrorRaise` (which owns the `raise
AtlanError` site); B001 owns import / construct / subclass, so the two never
double-report.

Coverage limits (intentional, low-false-positive at WARN): deprecated *parameters* /
*modes* are out of scope; method matching is attribute-name-anchored (a same-named
method on an unrelated object is a known false-positive risk); re-export aliasing can
produce false negatives.  All are suppressible with `# conformance: ignore[B001]`.

---

## B002 — `MalformedDeprecationNotice` {#b002}

**Tier:** `warn` · **Scope:** `sdk` · **Category:** `deprecation-hygiene` · **Autofixable:** — · **Since:** 0.5.0

> A deprecation notice must name both a migration target and a removal version

**Rationale:** A deprecation notice exists to migrate callers off a symbol before it is removed. To do
that it must answer two questions: what to use instead, and by when the symbol
disappears. A notice missing either is a dead end — callers see 'deprecated' with no
path forward, and B001 cannot carry a migration hint to the fleet. Enforcing 'names a
replacement AND a removal version' on the SDK's own notices keeps the whole deprecation
pipeline actionable. WARN: the required wording is a guided edit, not mechanical.

Flags a `@deprecated(...)` or `DeprecationWarning` notice in SDK source whose message
does not name *both*:

* a **migration target** — what to use instead (`use X` / `Use X instead` / `Migrate to
Y`); and * a **removal version** — when it goes away (`will be removed in v4.0`).

Both are required so every deprecation is self-describing and B001 can propagate
concrete guidance to consumers.  Only symbol-level markers are checked (decorator, or a
class emitting `DeprecationWarning` from `__init__` / `__init_subclass__`).

---

## B003 — `OverdueDeprecationRemoval` {#b003}

**Tier:** `warn` · **Scope:** `sdk` · **Category:** `deprecation-hygiene` · **Autofixable:** — · **Since:** 0.5.0

> A deprecation's stated removal version has already been reached by the SDK

**Rationale:** A deprecation that promises 'removed in v4.0' is a commitment. If the SDK is already at
or past that version and the symbol is still present, the deprecation has silently
become permanent: dead-weight code, a broken promise to callers, and a removal that will
now surprise people whenever it finally lands. Comparing each notice's stated removal
version against the SDK's own [project].version makes overdue removals visible the
moment they expire. Detect-only: removing a public symbol (or re-scheduling it) is a
human decision routed to residue, never an automated edit.

Flags a marked deprecation whose `removed in vX` version is <= the SDK's current
`[project].version`.  The symbol was promised gone by now but is still present.  The fix
— removing the public symbol or pushing the removal version out — is a human call, so
this is detect-only and routes to residue in the remediation loop.  When the SDK version
cannot be determined the check is skipped (overdue-ness is undecidable).

---

## B004 — `UnmarkedDeprecationClaim` {#b004}

**Tier:** `warn` · **Scope:** `sdk` · **Category:** `deprecation-hygiene` · **Autofixable:** — · **Since:** 0.5.0

> A symbol's docstring claims deprecation but it carries no @deprecated / DeprecationWarning marker

**Rationale:** A docstring that says 'Deprecated: use X instead' communicates intent to a human reader
but enforces nothing: no runtime DeprecationWarning, no @deprecated, and — critically —
nothing the manifest can pick up, so B001 never warns the fleet. The claim and the
mechanism have drifted apart. Flagging a deprecation claim that lacks a real marker
closes that gap so every stated deprecation is also a detectable one. Detect-only: which
marker to add (decorator vs warning) is a small design choice for the author, routed to
residue with a concrete suggestion.

Flags a function / method / class whose docstring opens with `Deprecated` (or carries a
`.. deprecated::` directive) but has no machine-readable marker — no `@deprecated`
decorator and no `DeprecationWarning` emitted from `__init__` / `__init_subclass__`.
Such a claim is invisible to consumers and to B001.  Detect-only: the remediation loop
routes it to residue with a suggestion to add a marker.

Scoped to real symbol docstrings (not, e.g., a field literally named `deprecated`),
biased toward low false positives at WARN.

---

## B005 — `NonAdditiveContractChange` {#b005}

**Tier:** `block` · **Scope:** `both` · **Category:** `contract-backwards-compatibility` · **Autofixable:** — · **Since:** 0.7.0

> An entrypoint contract field was removed or had its type changed

**Rationale:** Entrypoint contract fields are a serialization promise to every deployed consumer.
Removing a field or changing its type silently corrupts payloads that already encode the
old shape — the damage is invisible until runtime. The only valid operations on an
existing entrypoint field are: mark it deprecated (keep it, discourage use), mark it
sunset (keep it, stop consuming), or leave it unchanged. A rename must be expressed as
deprecate/sunset the old field + add a new field. The committed
contract_schema.lock.json ledger (append-only, regenerated in-PR) provides the baseline
so the check is single-checkout and offline. BLOCK from day 0: backwards-compat is a
property that must already hold; there is no warn-first window.

Fires when a ledger entry for an entrypoint contract field is either:

* **absent from the live contract** — the field was removed (a rename is   caught here:
the old name is gone); or * **present with a different canonical type** — the field's
annotation   changed (e.g. `str` → `int`, or `Optional[str]` → `str`).

Type comparison uses canonical normalized strings: `Optional[X]` and `X | None` are
equivalent, as are `List[X]` and `list[X]`, so purely syntactic rewrites do not trigger
a false positive.

Field resolution walks the full base-class chain, not just the contract's own body: a
field inherited from an in-repo base class, or from an SDK-provided mixin such as
`PublishInputMixin`, is tracked exactly like one declared directly on the contract.
Composing a contract from a mixin (the documented pattern) does not require redeclaring
the mixin's fields to stay ledger-protected.

Only entrypoint contracts are gated — Input/Output classes bound to an
`@entrypoint`-decorated method or an `App.run()` method.  `@task` boundary contracts are
explicitly excluded: tasks are internal and may evolve with breaking changes.

The ledger is append-only and machine-generated, so regeneration can only *add* — it can
never launder a removal.  To retire a field: mark it `deprecated` or `sunset` in the Pkl
widget definition, regenerate the contract, run `gen-contract-ledger` to record the new
status, and commit the updated ledger in the same PR.

---

## B006 — `StaleContractLedger` {#b006}

**Tier:** `block` · **Scope:** `both` · **Category:** `contract-backwards-compatibility` · **Autofixable:** — · **Since:** 0.7.0

> An entrypoint contract field is missing from contract_schema.lock.json

**Rationale:** B005 can only guard removals and type changes against the committed ledger. If a new
entrypoint field is not recorded in the ledger, B005 has a blind spot: the next PR that
removes it will see no ledger entry and pass silently. B006 closes that gap — a live
field not in the ledger means the ledger was not regenerated after the field was added.
Because the generator is append-only (it can never delete entries or change a recorded
type), regeneration is always safe: it can only add. BLOCK because a stale ledger
defeats the backwards-compat guarantee.

Fires when a live entrypoint contract field has no corresponding entry in
`contract_schema.lock.json`.  This means the ledger was not regenerated after the field
was introduced. This includes fields introduced by inheritance — a field inherited from
an in-repo base class or an SDK-provided mixin (e.g. `PublishInputMixin`) is just as
ledger-tracked as one declared directly on the contract, so adopting a new mixin can
also trigger this on the fields it contributes.

Fix: run `uv run atlan-application-sdk-conformance gen-contract-ledger` and commit the
updated ledger in the same PR as the contract change.

The generator is append-only — it appends new live fields and refreshes `status` from
source but never deletes an entry or rewrites a recorded `type`.  Regenerating after a
removal will therefore *not* launder the removal: B005 will still fire against the
persisted ledger entry.

Suppression: `# conformance: ignore[B006] <reason>` on the field line. Only appropriate
when the contract is pre-deployment (no consumers) and the ledger will be regenerated
before the first deploy.

---

## B007 — `DaftOnlyDataframeApiUsage` {#b007}

**Tier:** `warn` · **Scope:** `app` · **Category:** `daft-removal` · **Autofixable:** — · **Since:** 0.18.0

> Calls a daft-only DataFrame API (count_rows/to_pylist/.names) — dead on the daft-less SDK runtime

**Rationale:** The SDK's daft transformer engine is gone and its readers return pandas DataFrames, so
daft-only DataFrame APIs — count_rows(), to_pylist(), the .names property — raise
AttributeError on the frames apps actually receive. Locks crossed the 3.22 line via
automated upgrades without anything exercising the real pipeline, so these calls sat as
latent production breakage on main (a document-store connector hit all of them in fleet
testing). These are third-party daft APIs, not SDK symbols, so the generated
deprecated-symbol manifest (B001) cannot carry them — this rule encodes them directly.
DataframeType.daft is deliberately NOT here: it is the SDK's own symbol and now carries
a __deprecated_members__ marker, so B001 reports it from the generated manifest like
every other SDK deprecation. WARN because matching is heuristic (attribute-name-anchored
with pyarrow-receiver exemptions) and the pandas migration changes call shapes.

Flags daft-only DataFrame API usage in apps that consume the SDK (files importing
`application_sdk`), where SDK >= 3.22 readers return **pandas** frames and the `[daft]`
extra installs nothing:

* `frame.count_rows()` — daft-only; pandas uses `len(frame)`; * `frame.to_pylist()` —
daft-only on reader frames; pandas uses   `frame.to_dict("records")`.  Receivers that
are demonstrably   pyarrow Tables (bound from `pa.Table.from_*` / `pa.table` /
`.to_arrow_table()`) are exempt — `pyarrow.Table.to_pylist()`   is a real API; *
`frame.names` — daft-only; pandas uses `frame.columns`.  Only   simple-variable
receivers are matched (`df.schema.names` /   `df.index.names` are legitimate
pyarrow/pandas chains and are   not flagged).

`DataframeType.daft` is **not** matched here.  It is the SDK's own symbol and was only
hand-coded into this rule because nothing marked it; it now carries a
`__deprecated_members__` entry and rides the generated manifest, so **B001** reports it
— module-aware, and with the SDK's own migration text.  A second copy here would
double-report one line and reopen the drift the manifest's byte-gate prevents.

The failure class is latent: imports succeed and mocked unit tests pass; the
AttributeError appears only when a real reader frame flows through the pipeline.  Fleet
remediation guidance: do not fix these one CI cycle at a time — run the app's transforms
locally against synthetic raw data (a template harness) and migrate every call in one
pass.

Matching is attribute-name-anchored (the B001 posture): a same-named method on an
unrelated object is a known false-positive risk, accepted at WARN.  Suppress with `#
conformance: ignore[B007] <reason>` where the receiver is genuinely not an SDK reader
frame.

---
